Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement cross-platform animations support #255

Closed
valentinstoychev opened this issue Jun 1, 2015 · 22 comments

Comments

@valentinstoychev
Copy link

commented Jun 1, 2015

Come up with a solution for providing a way to implement smooth animations in NativeScript app.

  • for 1.2.0 - Create research and a spike based on the animations coming from native API
  • for 1.3.0 - Create a fluent public API, tests, examples and documentation.

If you are interested in this feature please add comments below with specific requirements and vote for this feature in our ideas portal.

@valentinstoychev

This comment has been minimized.

Copy link
Author

commented Jun 1, 2015

Animations is a very wide topic and I will be glad to see scenarios that we need to support first.

@emiloberg

This comment has been minimized.

Copy link

commented Jun 1, 2015

Animations are paramount to be able to create functional user experiences. As to scenarios, what kind are you looking for?

@RangerMauve

This comment has been minimized.

Copy link

commented Jun 1, 2015

I'm assuming that the stuff CSS Animations can do would be very useful. Or at the very least, being able to transition transform and opacity attributes which can be hardware accelerated.

@nemephx

This comment has been minimized.

Copy link
Contributor

commented Jun 3, 2015

For ui elements, I need translate, opacity, and color animations. Also would be great to have custom slide/fade animations for page in and out on the navigation stack.

@mdgiles

This comment has been minimized.

Copy link

commented Jun 5, 2015

+1 for Css3 animations (without javascript), and also something javascript based like jquery's animate which can apply to any numeric css property.

@valentinstoychev valentinstoychev added this to the 1.2.0 milestone Jun 9, 2015
@hshristov hshristov modified the milestones: 1.2.0, 1.2.0 (Under Review) Jun 10, 2015
@EvanWieland

This comment has been minimized.

Copy link

commented Jun 19, 2015

@RangerMauve is right, transform is huge for UI. Also, @keyframes would be a huge asset to NativeScript as well.

@hamorphis hamorphis self-assigned this Jun 29, 2015
@hamorphis

This comment has been minimized.

Copy link
Contributor

commented Jun 29, 2015

Animations Proposal

Animatable Properties

  • opacity
  • backgroundColor
  • translate
  • scale
  • rotate

Features

  • specify duration (in milliseconds)
  • specify start delay (in milliseconds)
  • specify repeat count
  • ability to cancel a running animation
  • animation finished callback
  • specify native easing curve, i.e. UIViewAnimationCurve or android.animation.TimeInterpolator

JavaScript (code-behind) API

My suggestion is to use an API that closely resembles jQuery's animate. For example:

var panel = page.getViewById("panel");
var cancellationToken = panel.animate({
    opacity: 0.25, // number between 0.0 and 0.0
    backgroundColor: "#FFFF0000", // string containing a hex or a known color name
    translate: {x:100, y:100}, // JSON object with two numbers -- x and y
    scale: {x: 2, y:2}, // JSON object with two numbers -- x and y
    rotate: 180, // number of degrees
  }, 5000, 100, 3, function(cancelled) {
    if (cancelled){
         console.log("Animation cancelled");
    }
    else {
         console.log("Animation complete");
    }
  });

Where 5000 is the duration, 100 is the delay, and 3 is the repeat count. Duration, delay, repeat count and the finished callback are all optional parameters.
You can cancel a running animation by calling cancellationToken.cancel();
So the most minimal animation code could look like this:

page.getViewById("panel").animate("opacity: 0");

This will make the panel transparent for a duration that is default for the specific platform's animations.

Keyframes

Since the native platforms do not have built-in keyframes support, keyframes could easily be achieved by simply chaining several individual animations with the same or different durations one after another. For example, here is how one could move a label to the four corners of its parent panel:

label1.animate({translate: {x:100 y:0}}, function(){
   label1.animate({translate: {x:100 y:100}}, function(){
      label1.animate({translate: {x:0 y:100}}, function(){
         label1.animate({translate: {x:0 y:0}}, function(){
            console.log("Animation finished");
         });      
      });
   });
});

CSS-like syntax

I guess that we can think of a CSS-like syntax that will read the what is written in the CSS, build the animations with the JavaScript API suggested above and then run then when the respective component is loaded in the visual tree.

Additional features

The core animation API is also public and accessible so you could do more complex things with it, such as combining multiple animations for different controls and running them together or sequentially. Here is a TypeScript example for the core animation API:

    var animations: Array<animation.Animation>;

    animations = new Array<animation.Animation>();
    animations.push({ target: button1, property: animation.Properties.translate, value: { x: -240, y: 0 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
    animations.push({ target: button1, property: animation.Properties.scale, value: { x: 0.5, y: 0.5 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
    animations.push({ target: button1, property: animation.Properties.opacity, value: 0, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });

    animations.push({ target: button2, property: animation.Properties.translate, value: { x: -240, y: 0 }, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
    animations.push({ target: button2, property: animation.Properties.scale, value: { x: 0.5, y: 0.5 }, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
    animations.push({ target: button2, property: animation.Properties.opacity, value: 0, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });

    animations.push({ target: button3, property: animation.Properties.translate, value: { x: -240, y: 0 }, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
    animations.push({ target: button3, property: animation.Properties.scale, value: { x: 0, y: 0 }, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
    animations.push({ target: button3, property: animation.Properties.opacity, value: 0, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });

    configureAnimationCurve(animations, true);

    cancelToken = animation.start(animations, vm.playSequentially, (cancelled?: boolean) => {
        if (cancelled) {
            console.log("Buttons slide out animations cancelled");
            return;
        }
        console.log("Buttons slide out animations completed!");

        animations = new Array<animation.Animation>();
        animations.push({ target: panel1, property: animation.Properties.scale, value: { x: 0, y: 0 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
        animations.push({ target: panel1, property: animation.Properties.rotate, value: 1080, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount  });
        animations.push({ target: panel1, property: animation.Properties.backgroundColor, value: new colorModule.Color("red"), duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
        configureAnimationCurve(animations, true);

        cancelToken = animation.start(animations, vm.playSequentially,(cancelled?: boolean) => {
            if (cancelled) {
                console.log("Panel animation cancelled");
                return;
            }
            console.log("Panel animation completed!");
        });
    });

The code-snippet above is taken from our animations testing app located here:
https://github.com/NativeScript/NativeScript/blob/animations/apps/animations/main-page.ts

The jQuery-like API that I am suggesting here will simply be a kind of syntactic sugar wrapper on top of the core animation API (which is also public and accessible and anyone can use it directly if he wants to). So for a very simple animation you could simply do label.animate(...). For more complex animation sets you could directly use the core animation API located in the animation module here:

https://github.com/NativeScript/NativeScript/tree/animations/ui/animation

Any suggestions are welcome.

Here is a link to the Android application I am testing with:
https://www.dropbox.com/s/xig9ln2hvw51qmk/Animations.Android.zip?dl=0

Here is a link to the iOS application I am testing with:

https://www.dropbox.com/s/s0uaagkmv4wor7o/Animations.iOS.zip?dl=0

@hdeshev

This comment has been minimized.

Copy link
Contributor

commented Jun 29, 2015

var cancellationToken = panel.animate({
    opacity: 0.25, // number between 0.0 and 0.0
    backgroundColor: "#FFFF0000", // string containing a hex or a known color name
    translate: {x:100, y:100}, // JSON object with two numbers -- x and y
    scale: {x: 2, y:2}, // JSON object with two numbers -- x and y
    rotate: 180, // number of degrees
  }, 5000, 100, 3, function(cancelled) {
    if (cancelled){
         console.log("Animation cancelled");
    }
    else {
         console.log("Animation complete");
    }
  });

Where 5000 is the duration, 100 is the delay, and 3 is the repeat count.

Why not move duration, delay, and repeatCount on the options object? It looks more readable to me:

var cancellationToken = panel.animate({
    opacity: 0.25, // number between 0.0 and 0.0
    backgroundColor: "#FFFF0000", // string containing a hex or a known color name
    translate: {x:100, y:100}, // JSON object with two numbers -- x and y
    scale: {x: 2, y:2}, // JSON object with two numbers -- x and y
    rotate: 180, // number of degrees.

    duration: 5000,
    delay: 100,
    repeat: 3
  }, function(cancelled) {
    if (cancelled){
         console.log("Animation cancelled");
    }
    else {
         console.log("Animation complete");
    }
  });

The jQuery API seems to allow both a number as a second parameter and a full-blown options map:

$( "#clickme" ).click(function() {
  $( "#book" ).animate({
    opacity: 0.25,
    left: "+=50",
    height: "toggle"
  }, 5000, function() {
    // Animation complete.
  });
});

vs.

$( "#clickme" ).click(function() {
  $( "#book" ).animate({
    width: "toggle",
    height: "toggle"
  }, {
    duration: 5000,
    specialEasing: {
      width: "linear",
      height: "easeOutBounce"
    },
    complete: function() {
      $( this ).after( "<div>Animation complete.</div>" );
    }
  });
});
@emiloberg

This comment has been minimized.

Copy link

commented Jun 29, 2015

First of all; Great work!

Imho animate should really return a promise instead of taking a callback function (or if you want to: return a promise if no callback function is set). jQuery has the .promise() method (e.g. $('div').hide().promise() but I can't really see any benefits of doing it that way, compared to always return a promise) Also, other functions of {N} returns Promises, such as the alert.

That way we could have this syntax for "keyframing"

label1
    .animate({translate: {x:100 y:0}})
    .animate({translate: {x:100 y:100}})
    .animate({translate: {x:0 y:100}})

...and this syntax for creating animations. Note how I've added the duration, delay and repeat properties to the parameters object.

panel
    .animate({
        opacity: 0.25, 
        duration: 5000, 
        delay: 100, 
        repeat: 3
    })
    .catch(function (cancelled) {
        console.log("Animation cancelled");
    })
    .then(function () {
        console.log("Animation complete");
    })

Small notes:

  • Will translate opacity remove tap click events? Like css' pointer-events: none;. In HTML/CSS we've the problem that even a transparent element still catches user input such as clicks, and we therefor need to remove the pointer-events.
  • How about repeat infinitely with keyword infinite (just as css animation-iteration-count: infinite)
  • Small typo in your comment, should say 0.1 and 1.0

opacity: 0.25, // number between 0.0 and 0.0

@hamorphis

This comment has been minimized.

Copy link
Contributor

commented Jun 29, 2015

@emiloberg

This is simply a fluent API. We can do that without promises. We can make the animate method return some kind of hollow animation set abstraction object which also has the animate method and will simply "gather" the information that you pass with each call. It should also have a start method to run the chained animations. Something like

label1.animate(...).animate(...).animate(...).playSequentially() or label1.animate(...).animate(...).animate(...).playTogether().

I have already implemented such functionality here:
https://github.com/NativeScript/NativeScript/blob/animations/ui/animation/animation.d.ts

export function start(animations: Array<Animation>, playSequentially: boolean, finishedCallback?: (cancelled?: boolean) => void): Cancelable;
}

So behind the scenes when someone calls the animate method of this not-yet-existing animation set abstraction, the method will append one animation to an internal animation array and return 'this' so the animate method can be called again. Finally, when someone calls playSequentially/playTogether we will call the above start function.

We can add the duration, delay, repeat count and curve to the options JSON. It is a matter of choice. jQuery has the duration as a separate parameter.

Will translate opacity remove tap click events? Like css' pointer-events: none;. In HTML/CSS we've the problem that even a transparent element still catches user input such as clicks, and we therefor need to remove the pointer-events.
The opacity animation simply sets UIView.alpha = 0 and android.view.View.setAlpha(0); We don't do anything additional besides that.
How about repeat infinitely with keyword infinite (just as css animation-iteration-count: infinite)

I will write the infinite loop count this down as feature suggestion.

@hamorphis

This comment has been minimized.

Copy link
Contributor

commented Jun 29, 2015

Here is a link to the Android application I am testing with:
https://www.dropbox.com/s/xig9ln2hvw51qmk/Animations.Android.zip?dl=0

Here is a link to the iOS application I am testing with:

https://www.dropbox.com/s/s0uaagkmv4wor7o/Animations.iOS.zip?dl=0

@ligaz

This comment has been minimized.

Copy link
Contributor

commented Jun 29, 2015

I also think it will be good if we return a Promise from the start instead of taking a function as last parameter. We can even implement one of the early drafts of the cancellation spec: https://github.com/promises-aplus/cancellation-spec

@ligaz

This comment has been minimized.

Copy link
Contributor

commented Jun 29, 2015

Is there a need to combine more complex animations when some of the items will be played sequentially and some in parallel? Currently the API supports only sequential composition.

@valentinstoychev

This comment has been minimized.

Copy link
Author

commented Jun 29, 2015

@ligaz Yes, I think so. Angular 2.0 animations syntax has this support and we will create a future limitation if we don't support it. We want to support Angular 2.0 animations at some point.

@tjvantoll

This comment has been minimized.

Copy link
Contributor

commented Jun 29, 2015

I'm a big fan of the jQuery-like API, and I agree with @hdeshev and @emiloberg that you should be able to optionally place the animation config—duration and such—in the options object.

Random observation, but is there a need to configure repeat? jQuery doesn't have it, and I can't think of a practical use case.

Anyways, it seems a lot of the discussion is about the return value of animate(). @hamorphis could you provide a snippet of how to use the current return value of animate(), because I don't see one in this thread so far. Ideally I'd like to see us to say as consistent with the web animations spec as much as reasonably possible, which would mean returning an object you can set an onfinish property on or call cancel() on.

@hamorphis

This comment has been minimized.

Copy link
Contributor

commented Jun 30, 2015

@ligaz
"Currently the API supports only sequential composition."

That is not true. The API supports both sequential and parallel execution of multiple animations. Here is the definition of the start function from the animations module:

export function start(animations: Array<Animation>, playSequentially: boolean, finishedCallback?: (cancelled?: boolean) => void): Cancelable;
}

Here is how I do it in Android:

    var animatorSet = new android.animation.AnimatorSet();
    if (playSequentially) {
        animatorSet.playSequentially(nativeArray);
    }
    else {
        animatorSet.playTogether(nativeArray);
    }

In iOS, based on the playSequentially parameter value, I create a bunch of iOS animation functions and chain them differently depending on the parameter:

        var nextAnimationCallback: Function;
        var animationDelegate: AnimationDelegateImpl;
        if (index === animations.length - 1) {
            // This is the last animation, so tell it to call the master finishedCallback when done.
            animationDelegate = AnimationDelegateImpl.new().initWithFinishedCallback(finishedCallback);
        }
        else {
            nextAnimationCallback = createiOSAnimation(animations, index + 1, playSequentially, finishedCallback);
            // If animations are to be played sequentially, tell it to start the next animation when done. 
            // If played together, all individual animations will call the master finishedCallback, which increments a counter every time it is called.
            animationDelegate = AnimationDelegateImpl.new().initWithFinishedCallback(playSequentially ? nextAnimationCallback : finishedCallback);
        }
@hamorphis

This comment has been minimized.

Copy link
Contributor

commented Jun 30, 2015

@tjvantoll
" @hamorphis could you provide a snippet of how to use the current return value of animate()"

There is no animate method yet. This is why I started this discussion. We need input from many people. We need to decide what the public API will be first, and then I will implement it. What we currently have is the so called core animation API, which is a low-level .NET-style API. Given that, we can build any kind of fluent public API on top of it. This discussion is foe that -- we are trying to decide what kind of public API to build on top of the existing one, which by the way is fully functional. My point is that I don't want to create a public API which people will dislike -- so I am waiting for input here.

@hamorphis

This comment has been minimized.

Copy link
Contributor

commented Jun 30, 2015

For everyone involved in this discussion, I think that it would be great to take a look at what is happening behind the scenes when we reach the Android or iOS runtimes and tell then to animate something. After all, we are developing animations for Android and iOS and we have to have in mind what limitations there are. We depend on this functionality, i.e. we are not developing a random animation API for a random platform. You get my point.

Please, take a look at these:

https://github.com/NativeScript/NativeScript/blob/animations/ui/animation/animation.d.ts
https://github.com/NativeScript/NativeScript/blob/animations/ui/animation/animation.android.ts
https://github.com/NativeScript/NativeScript/blob/animations/ui/animation/animation.ios.ts

@hamorphis hamorphis modified the milestones: 1.3.0, 1.2.0 Jul 6, 2015
@heldrida

This comment has been minimized.

Copy link

commented Jul 29, 2015

I found about NativeScript, I haven't tested yet because I can't seem to find a good example with a cool UI, transitions, animations, nice fonts, etc.

This topic is interesting, so my suggestion is to follow this model API: http://greensock.com/gsap

@hamorphis hamorphis added feature done and removed ready for test labels Jul 31, 2015
@glebmachine

This comment has been minimized.

Copy link

commented Aug 21, 2016

@heldrida About greensock:

There is no reason to copy greensock API.
Better - add gsap support and develop few additional plugins to it, to ensure compatibility.

@lock

This comment has been minimized.

Copy link

commented Aug 29, 2019

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked and limited conversation to collaborators Aug 29, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
You can’t perform that action at this time.