I'd like to PAUSE the animation in Scenekit AR scene view. It should freeze the frame but it returns to the first frame if I set .paused = true or player.stop()
node.enumerateHierarchy({ node, stop in
for key in node.animationKeys {
if let player = node.animationPlayer(forKey: key) {
// Whether I try .paused
player.paused = true
// Or call stop()
player.stop()
}
}
})
Is there some options or actions should be taken?
Related
I'm trying to create an animation in my app when a particular action happens which will essentially make the background of a given element change colour and back x number of times to create a kind of 'pulse' effect. The application itself is quite large, but I've managed to re-create the issue in a very basic app.
So the ContentView is as follows:
struct ContentView: View {
struct Constants {
static let animationDuration = 1.0
static let backgroundAlpha: CGFloat = 0.6
}
#State var isAnimating = false
#ObservedObject var viewModel = ContentViewViewModel()
private let animation = Animation.easeInOut(duration: Constants.animationDuration).repeatCount(6, autoreverses: false)
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Button(action: {
animate()
}) {
Text("Button")
.foregroundColor(Color.white)
}
}
.background(isAnimating ? Color.red : Color.blue)
.onReceive(viewModel.$shouldAnimate, perform: { _ in
if viewModel.shouldAnimate {
withAnimation(self.animation, {
self.isAnimating.toggle()
})
}
})
}
func animate() {
self.viewModel.isNew = true
}
}
And then my viewModel is:
import Combine
import SwiftUI
class ContentViewViewModel: ObservableObject {
#Published var shouldAnimate = false
#Published var isNew = false
var cancellables = Set<AnyCancellable>()
init() {
$isNew
.sink { result in
if result {
self.shouldAnimate = true
}
}
.store(in: &cancellables)
}
}
So the logic I am following is that when the button is tapped, we set 'isNew' to true. This in turn is a publisher which, when set to true, sets 'shouldAnimate' to true. In the ContentView, when shouldAnimate is received and is true, we toggle the background colour of the VStack x number of times.
The reason I am using this 'shouldAnimate' published property is because in the actual app, there are several different actions which may need to trigger the animation, and so it feels simpler to have this tied to one variable which we can listen for in the ContentView.
So in the code above, we should be toggling the isAnimating bool 6 times. So, we start with false then toggle as follows:
1: true, 2: false, 3: true, 4: false, 5: true, 6: false
So I would expect to end up on false and therefore have the background white. However, this is what I am getting:
I tried changing the repeatCount (in case I was misunderstanding how the count works):
private let animation = Animation.easeInOut(duration: Constants.animationDuration).repeatCount(7, autoreverses: false)
And I get the following:
No matter the count, I always end on true.
Update:
I have now managed to get the effect I am looking for by using the following loop:
for i in 0...5 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i), execute: {
withAnimation(self.animation, {
self.isAnimating.toggle()
})
})
}
Not sure this is the best way to go though....
To understand what is going on, it would help to understand CALayer property animations.
When you define an animation the system captures the state of a Layer and watches for changes in the animatable properties of that layer. It records property changes for playback during the animation. To present the animation, it create a copy of the layer in its initial state (the presentationLayer). It then substitutes the copy in place of the actual layers on screen and runs the animation by manipulating the animatable properties of the presentation layer.
I this case, when you begin the animation, the system watches what happens to the CALayer that backs your view and captures the changes to any animatable properties (in this case the background color). It then creates a presentationLayer and replays those property changes repeatedly. It's not running your code repeatedly - it's changing the properties of the presentation Layer.
In other words the animation the system knows the layer's background color property should toggle back and forth because of the example you set in your animation block, but the animation toggles the background color back and forth without running your code again.
I have an animated-vector drawable.
I want this animated vector to be animated in loop while this image is showing.
Cannot find a good solution for this.
val image = animatedVectorResource(R.drawable.no_devices_animated)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = image.painterFor(atEnd),
"image",
Modifier.width(150.dp).clickable {
atEnd = !atEnd
},
contentScale = ContentScale.Fit)
When I tap on the image it is animating but then stops. This is kind of an infinite progress.
Leaving my solution here (using compose 1.2.0-alpha07).
Add the dependencies in your build.gradle
dependencies {
implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.animation:animation-graphics:$compose_version"
}
And do the following:
#ExperimentalAnimationGraphicsApi
#Composable
fun AnimatedVectorDrawableAnim() {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.avd_anim)
var atEnd by remember { mutableStateOf(false) }
// This state is necessary to control start/stop animation
var isRunning by remember { mutableStateOf(true) }
// The coroutine scope is necessary to launch the coroutine
// in response to the click event
val scope = rememberCoroutineScope()
// This function is called when the component is first launched
// and lately when the button is pressed
suspend fun runAnimation() {
while (isRunning) {
delay(1000) // set here your delay between animations
atEnd = !atEnd
}
}
// This is necessary just if you want to run the animation when the
// component is displayed. Otherwise, you can remove it.
LaunchedEffect(image) {
runAnimation()
}
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
null,
Modifier
.size(150.dp)
.clickable {
isRunning = !isRunning // start/stop animation
if (isRunning) // run the animation if isRunning is true.
scope.launch {
runAnimation()
}
},
contentScale = ContentScale.Fit,
colorFilter = ColorFilter.tint(Color.Red)
)
}
Case you need to repeat the animation from the start, the only way I find was create two vector drawables like ic_start and ic_end using the information declared in the animated vector drawable and do the following:
// this is the vector resource of the start point of the animation
val painter = rememberVectorPainter(
image = ImageVector.vectorResource(R.drawable.ic_start)
)
val animatedPainter = rememberAnimatedVectorPainter(
animatedImageVector = AnimatedImageVector.animatedVectorResource(R.drawable.avd_anim),
atEnd = !atEnd
)
Image(
painter = if (atEnd) painter else animatedPainter,
...
)
So, when the animated vector is at the end position, the static image is drawn. After the delay, the animation is played again. If you need a continuous repetition, set the delay as the same duration of the animation.
Here's the result:
Infinite loop without static images solution:
Best working with animated vector drawable that is a loop.
First add the dependencies in your build.gradle
dependencies {
implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.animation:animation-graphics:$compose_version"
}
Animated loop loader
#OptIn(ExperimentalAnimationGraphicsApi::class)
#Composable
fun Loader() {
val image = AnimatedImageVector.animatedVectorResource(id = R.drawable.loader_cycle)
var atEnd by remember {
mutableStateOf(false)
}
val painterFirst = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
val painterSecond = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = !atEnd)
val isRunning by remember { mutableStateOf(true) }
suspend fun runAnimation() {
while (isRunning) {
atEnd = !atEnd
delay(image.totalDuration.toLong())
}
}
LaunchedEffect(image) {
runAnimation()
}
Image(
painter = if (atEnd) painterFirst else painterSecond,
contentDescription = null,
)
}
Right now I don't see any option to restart painter to start all over again - you need to rewind it. So in this solution we create two of those painter. One of them is starting with !atEnd. Each time atEnd is changed both of them do their work but we display only one animating from start to end. The other one is silently rewinding the animation.
According to https://developer.android.com/jetpack/compose/resources#animated-vector-drawables
val image = AnimatedImageVector.animatedVectorResource(R.drawable.animated_vector)
val atEnd by remember { mutableStateOf(false) }
Icon(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = null
)
To make the animation loop infinitely:
val image = AnimatedImageVector.animatedVectorResource(id = vectorResId)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd),
contentDescription = null,
)
LaunchedEffect(Unit) {
while (true) {
delay(animDuration)
atEnd = !atEnd
}
}
I tried to implement a loading spinner using this approach, but encountered some issues.
I had an issue with the proposed solution, where the animation reverses back to the beginning. To prevent this, as #nglauber suggested, I tried switching to a static vector when the animation ends. Unfortunately, that wasn't smooth as the animation would have to wait for the delay before restarting.
This was the workaround that I used.
#OptIn(ExperimentalAnimationGraphicsApi::class)
#Composable
fun rememberAnimatedVectorPainterCompat(image: AnimatedImageVector, atEnd: Boolean): Painter {
val animatedPainter = rememberAnimatedVectorPainter(image, atEnd)
val animatedPainter2 = rememberAnimatedVectorPainter(image, !atEnd)
return if (atEnd) {
animatedPainter
} else {
animatedPainter2
}
}
Im having trouble transitioning to pre determined location on a different scene. (for example when Mario goes into the tunnel he returns to the original scene right where he left off) I was able to code a way to get to the next scene but node does not appear were I would like it to.
this is my code to transition to second Scene
func didBeginContact(contact: SKPhysicsContact) {
_ = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
if (contact.bodyA.categoryBitMask == BodyType.cup.rawValue && contact.bodyB.categoryBitMask == BodyType.ball.rawValue) {
touchmain()
} else if (contact.bodyA.categoryBitMask == BodyType.ball.rawValue && contact.bodyB.categoryBitMask == BodyType.cup.rawValue) {
touchmain()
}
}
func touchmain() {
let second = GameScene(fileNamed:"GameScene")
second?.scaleMode = .AspectFill
self.view?.presentScene(second!, transition: SKTransition.fadeWithDuration(0.5))
}
I would really appreciate it if you guys can help a young developer out. much love!
There are many ways you could go about this. The general gist of it is, just remember "Mario's" location before you leave the scene and pass it along to a property in the next scene. (or you could use delegation to get the info)
If the scenario is that he starts in SceneA(main level) transfers to SceneB(small money tunnel) and then returns to SceneA(main level) you could pass the location to SceneB and have a property in SceneB that stores where Mario was. Then when you transfer back to SceneA just pass the property back to a property in SceneA, and position Mario and SceneA accordingly
func touchmain() {
let marioPos: CGPoint = Mario.position
let second = GameScene(fileNamed:"GameScene")
second.startingPos = marioPos
second?.scaleMode = .AspectFill
self.view?.presentScene(second!, transition: SKTransition.fadeWithDuration(0.5))
}
You can use the userData field on SKScene to remember the position:
Somewhere in the first scene init, do :
userData = [String : NSObject]()
Also, in your first scene, we want to override didMove(to:SKView) to set mario position based on your userdata :
override func didMove(to:SKView)
{
super.didMove(to:to)
if let position = self.userData["position"]
{
mario.position = position
}
}
We then want to actually assign the user data when transitioning away from the scene:
func touchmain() {
if let userData = self.userData
{
userData["marioPosition"] = mario.position
}
if let second = GameScene(fileNamed:"GameScene")
{
second.scaleMode = self.scaleMode //I assume we do not want to change scaleMode between scenes
second.userData = userData
self.view?.presentScene(second, transition: SKTransition.fadeWithDuration(0.5))
}
else
{
print("Error creating Second scene")
}
}
When we are coming back, we want to transfer the userData back to the first screen. This is where the didMove that I mentioned earlier comes into play, this will set the position of mario to previous location.
... I do not see this code, so try and apply it to how ever you are doing it
func touchBackToFirst() {
if let first = GameScene(fileNamed:"GameScene")
{
first.scaleMode = self.scaleMode
first.userData = userData
self.view?.presentScene(first, transition: SKTransition.fadeWithDuration(0.5))
}
else
{
print("Error creating Firstscene")
}
}
Can I call a function(one that will make another object visible/invisible) on a specific animation frame or time? I would like to have arrows describe the movement of the animation at certain times during the animation. While I can just make them visible when I start the animation and make them invisible when the animation stops, I would like to specify ranges inside the animation to do this
playPatientAnim: function (anim, callback) {
var pending = 1;
var me = this;
var finish = callback ? function () {
if (pending && !--pending) {
callback.call(me, anim);
}
} : null;
me.currentPatient.skinned.forEach(function (mesh) {
mesh.animations.forEach(function(anim){
anim.stop();
});
});
me.currentPatient.skinned.forEach(function (mesh) {
var animation = mesh.animations[anim];
animation.stop();
if (animation) {
pending++;
animation.onComplete = finish;
animation.play();
}
});
if (finish) {
finish();
}
}
You can make a mesh visible or invisible ( mesh.visible = false; //or true ). To change visibility at certain time you could use timestamp:
new Date().getTime() and calculate how you want to do the sequence of your animation.
I am using the ValueAnimator to make one row in my list pulse from dark blue to light blue finitely. However, I need to check for a boolean when the rows load and when it gets set to false I need the view to go back to its original non-pulsing state. What is the best way of doing this?
My code is as follows -
if(isHighlighted(post)) {
String titleText = title.getText().toString();
title.setText(titleText);
title.setTextColor(Color.WHITE);
timeStamp.setTextColor(Color.WHITE);
highLighterStartColor = resources.getColor( R.color.active_blue );
highLighterEndColor = resources.getColor( R.color.teal );
ValueAnimator va = ObjectAnimator.ofInt(view, "backgroundColor", highLighterStartColor, highLighterEndColor);
if(va != null) {
va.setDuration(750);
va.setEvaluator(new ArgbEvaluator());
va.setRepeatCount(ValueAnimator.INFINITE);
va.setRepeatMode(ValueAnimator.REVERSE);
va.start();
}
} else {
title.setTextAppearance(activity.getApplicationContext(), R.style.medium_text);
timeStamp.setTextAppearance(activity.getApplicationContext(), R.style.timestamp_text);
}