Page Navigation Transitions #811

Closed
lscown opened this Issue Sep 24, 2015 · 57 comments

Projects

None yet
@lscown
lscown commented Sep 24, 2015

Would be great to be able to customise the transitions between pages. Right now it's either animation or no animation. Both Android and iOS support custom transistions.

@emiloberg
Contributor

+1

@javorosas

+1

@bradmartin
Contributor

+1

@dpfr
dpfr commented Oct 27, 2015

+1

@N3ll N3ll added this to the 1.6 (Under Review) milestone Nov 3, 2015
@sharesoft

Being able to programmatically force a left-to-right page animation in iOS (rather than the standard right-to-left) would be really useful when you need to provide visual feedback to a user that you're returning them back up to a previous 'level' of your application.

@emiloberg
Contributor

And, to be honest, page transition animations are a great way to hide slow rendering. You get like 300 ms to play with and the user still perceives the interface as snappy. Unfortunately Androids default "fade in really fast" animations aren't that permitting.

@amjd
amjd commented Nov 14, 2015

+1
Really need this.

@jamg44
jamg44 commented Dec 8, 2015

+1

@N3ll N3ll added the S:High label Dec 9, 2015
@hamorphis hamorphis self-assigned this Dec 17, 2015
@hamorphis
Contributor

I will research whether we can integrate the built-in platform transitions in our navigation API.

@hamorphis
Contributor

In order to set the stage and gather some ideas and suggestions from the community, here is how navigation is currently performed on the two platforms.

On Android we navigate to a new page like this:

var fragmentTransaction = manager.beginTransaction();
fragmentTransaction.add(this.containerViewId, newFragment, newFragmentTag);
fragmentTransaction.setTransition(transition);
fragmentTransaction.commit();

For going back on Android we do something like this:

manager.popBackStack(backstackEntry[BACKSTACK_TAG], android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE);

On iOS the Frame has a UINavigationController and we navigate to a new page like this:

this._ios.controller.pushViewControllerAnimated(viewController, animated);

or

this._ios.controller.setViewControllersAnimated(newControllers, animated);

depending on the the navigation type.

Going back on iOS is done like this:

this._ios.controller.popToViewControllerAnimated(controller, animated);

The full-source code is located in frame.android.ts and frame.ios.ts

I am open to suggestions how to incorporate the custom transitions into our existing logic. The basic unit for configuring a navigation is the NavigationEntry so we can add some kind of transition information there which we can later use when we call the native Android and iOS navigation API's.

Let's gather different perspectives and ideas about the best and easiest way to implement such a feature.

@atanasovg
Member

I did some research on the matter in the context of the Android platform. Since API 21 there are the setEnterTransition and setExitTransition methods available on the Fragment class (http://developer.android.com/reference/android/app/Fragment.html#setEnterTransition(android.transition.Transition)). These APIs, together with the predefined set of transitions, provide powerful and slick mechanism for great animations. IMO we should stick as close as possible to the native platform animations and provide fallback mechanism for older API levels.

With regards of the API I imagine generally two approaches:

  1. What you suggest - extend the NavigationEntry object and provide enterTransition and exitTransition properties. The benefit of this approach is that one and the same Page may be animated differently depending on how it is navigated to/from. The cons however is that there is no way (as of now) to define these properties via XML.
  2. Add enterTransition and exitTransition on a per Page instance basis. When making the native navigation we may read the enterTransition property for the Page being navigated to and the exitTransition for the Page being navigated from. The major benefit of this approach is that everything will be available through XML.

Furthermore, I think that the Transition object, in general, should be able to define its opposite (or reverse) animation. This is extremely useful (and Android is actually doing it) when you specify only the enterTransition and do not want to bother with the exit one.

During my experiments, I noticed that the AnimationDefinition class does not provide the From semantic. For example, if I want to do a FadeIn animation (that is from Opacity 0 to 1) I need to first set the Opacity property of the desired view to 0 and then animate it to 1.

I imagine the following API:

<Page enterTransition="slide">
</Page>

---------
<Page>
    <Page.enterTransition>
        <SlideTransition duration="500"/>
    </Page.enterTransition>
</Page>

---------
<Page>
    <Page.enterTransition>
        <Transition duration="200">
            <AnimatedProperty name="opacity" from="0" to="1"/>
        <Transition>
    </Page.enterTransition>
</Page>

The Transition object itself should be able to:

  1. Define multiple properties to be animated
  2. Implicitly apply the initial (From) value for each animated property
  3. Work with logical units as well - such as "translateX" - [-1, 0]. This will mean that we will calculate the absolute pixels implicitly, when we know the size of the page.
  4. Know its Reverse counterpart.
@hamorphis
Contributor

@atanasovg The Animation API animates the following properties of the View class: opacity, backgroundColor, translate, scale, rotate. It was never designed to animate transitions between different pages. So I am not sure whether the page navigation transitions can and should become part of the animations API which is pretty much released and final.

@valentinstoychev
Contributor

I agree with @atanasovg - we should not start crafting our own transition logic. We should stick only to the natively available transitions and just expose them to the developers.

Also - the transitions should not be cross-platform, but platform specific.

This is the only way the app will have the UX of a native app. If we start implementing our transitions then the experience will be like in the PhoneGap app.

@atanasovg
Member

@hamorphis I am not saying transitions should be part of the Animation API. Quite the opposite - they will just internally utilize the Animations where we do custom transitions. Btw, I see no issues if we extend the existing API with some new properties/functionality, like the "from" semantic.

@hamorphis
Contributor

@atanasovg They can't internally utilize the Animation API for the reasons I stated above. The Animation API animates 5 properties of a view. Transitions between the pages are done by the platform in a completely different way.

I will provide an example. Currently, we have hard-coded the following Android transitions:

var transition = animated ? android.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN : android.app.FragmentTransaction.TRANSIT_NONE;

How can we use the Animation API to perform the code above? Which property of what view should we animate instead of writing fragmentTransaction.commit();?

My idea it to give people a chance to override the stuff that we have hard-coded and supply something different. This would be the first step. Let's do that first and then we can think about some cross-platform.

The Animation API cannot and should not do things like this._ios.controller.pushViewControllerAnimated(viewController, animated); and

var fragmentTransaction = manager.beginTransaction();
fragmentTransaction.add(this.containerViewId, newFragment, newFragmentTag);
fragmentTransaction.setTransition(transition);
fragmentTransaction.commit();
@hamorphis
Contributor

Here are some ideas for iOS. All of them seem like huge hacks though: Custom Animation for Pushing a UIViewController

And another one which looks nicer: How to change the Push and Pop animations in a navigation based app

For Push:

MainView *nextView = [[MainView alloc] init];
[UIView animateWithDuration:0.75
                         animations:^{
                             [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                             [self.navigationController pushViewController:nextView animated:NO];
                             [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.navigationController.view cache:NO];
                         }];

For Pop:

[UIView animateWithDuration:0.75
                         animations:^{
                             [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                             [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.navigationController.view cache:NO];
                         }];
[self.navigationController popViewControllerAnimated:NO];
@atanasovg
Member

Consider the following API:
http://developer.android.com/reference/android/app/FragmentTransaction.html#setCustomAnimations(int, int, int, int)

This is how, prior API 21 and Material Design, people are specifying custom fragment animations. And the XML for these animations is nothing more than what our animations are already doing:

<?xml version="1.0" encoding="utf-8"?>
<set>
  <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="x" 
    android:valueType="floatType"
    android:valueFrom="-1280"
    android:valueTo="0" 
    android:duration="500"/>
</set>

So, my idea is to allow people to describe what a transition should do and play the defined animations directly on the Page native view. Additionally, we will have predefined transitions, which will be translated to platform ones where available - for example the TRANSIT_FRAGMENT_OPEN or the Explode one.

Again - the animation API will not have anything to do with the transitions. It is the other way round:

interface AnimatedProperty{
    name: string;
    from: any;
    to: any;
}
interface Transition {
    values?: Array<AnimatedProperty>;
    duration: number;
}

We have to work with these.

Exactly - how do you imagine this to happen? Force people to write Android-specific XMLs? Or give them convenient way which will internally utilize our animation API? Do not think only within the FragmentTransaction context - we will go that path only for predefined transitions. How about the new Material Design transitions?
http://developer.android.com/reference/android/app/Fragment.html#setEnterTransition(android.transition.Transition)

@hamorphis
Contributor

If we can provide a unified cross-platform XML transition declarations which is fully supported by both platforms -- I am all for it.

@sharesoft

I don't have any knowledge of Android/iOS internals so can't offer any thoughts on how page transitions might actually be implemented. However, in terms of how they could be invoked by a {N} app, could we extend the properties currently included in the NavigationEntry object of the frame.navigate() method.

Currently, we specify:
animated: true|false

Could we extend this to:
animated: true|false|left|right|custom

Setting animated:true would invoke the defualt transition style for the platform (eg. right-to-left on iOS)

OR add a new transition option:
animated: true|false
transition: left|right|custom

@hamorphis
Contributor

I found a way to programmatically create custom transition animations for Android. Here is how. This is hard-coded dummy code which serves as a proof of concept.

  1. After I create a new FragmentTransaction, I tell it to use custom animations:
var fragmentTransaction = manager.beginTransaction();
fragmentTransaction.setCustomAnimations(-10, -20, -30, -40);
  1. Then, in the class PageFragmentBody that inherits from Fragment I override the onCreateAnimator method and plug-in my custom animations runtime:
    onCreateAnimator: function (transit: number, enter: boolean, nextAnim: number): android.animation.Animator {
        var nativeArray = java.lang.reflect.Array.newInstance(floatType, 2);
        switch (nextAnim) {
            case -10: // Enter
                nativeArray[0] = 0;
                nativeArray[1] = 1;
                break;
            case -20: // Exit
                nativeArray[0] = 1;
                nativeArray[1] = 0;
                break;
            case -30: // Pop Enter
                nativeArray[0] = 0;
                nativeArray[1] = 1;
                break;
            case -40: // Pop Exit
                nativeArray[0] = 1;
                nativeArray[1] = 0;
                break;
            default:
                return undefined;

        }
        var objectAnimator = android.animation.ObjectAnimator.ofFloat(this, "alpha", nativeArray);
        objectAnimator.setDuration(3000);
        objectAnimator.setInterpolator(new android.view.animation.AccelerateInterpolator());
        trace.write(`PageFragmentBody.onCreateAnimator(${transit}, ${enter}, ${nextAnim}): ${objectAnimator}`, trace.categories.NativeLifecycle);
        return objectAnimator;
    }

So, we now know how to do it in Android. I will start researching the iOS capabilities now.

@hamorphis
Contributor

Pushed a Proof of Concept / Spike in the https://github.com/NativeScript/NativeScript/tree/issue-811 branch. Run the perf-tests/NavigationTest app. Page transitions are animated. I animate alpha and translationX for the sake of example. For now, animation code is hard-coded since we don't have a public API invented yet. The important code is in frame.android.ts and frame.ios.ts.

Android PageFragmentBody.onCreateAnimator:

    onCreateAnimator: function (transit: number, enter: boolean, nextAnim: number): android.animation.Animator {
        var nativeAnimatorsArray = java.lang.reflect.Array.newInstance(android.animation.Animator.class, 2);

        // TranslateX
        var translationXValues = java.lang.reflect.Array.newInstance(floatType, 2);
        var screenWidth = platform.screen.mainScreen.widthPixels;
        switch (nextAnim) {
            case -10: // Enter
                translationXValues[0] = screenWidth;
                translationXValues[1] = 0;
                break;
            case -20: // Exit
                translationXValues[0] = 0;
                translationXValues[1] = -screenWidth;
                break;
            case -30: // Pop Enter
                translationXValues[0] = -screenWidth;
                translationXValues[1] = 0;
                break;
            case -40: // Pop Exit
                translationXValues[0] = 0;
                translationXValues[1] = screenWidth;
                break;
            default:
                throw new Error("Invalid transition animation ID.");
        }
        var translateXAnimator = android.animation.ObjectAnimator.ofFloat(this, "translationX", translationXValues);
        translateXAnimator.setDuration(5000);//5 seconds
        translateXAnimator.setInterpolator(new android.view.animation.DecelerateInterpolator(1.5));
        nativeAnimatorsArray[0] = translateXAnimator;

        // Alpha
        var alphaValues = java.lang.reflect.Array.newInstance(floatType, 2);
        switch (nextAnim) {
            case -10: // Enter
                alphaValues[0] = 0;
                alphaValues[1] = 1;
                break;
            case -20: // Exit
                alphaValues[0] = 1;
                alphaValues[1] = 0;
                break;
            case -30: // Pop Enter
                alphaValues[0] = 0;
                alphaValues[1] = 1;
                break;
            case -40: // Pop Exit
                alphaValues[0] = 1;
                alphaValues[1] = 0;
                break;
            default:
                throw new Error("Invalid transition animation ID.");
        }
        var alphaAnimator = android.animation.ObjectAnimator.ofFloat(this, "alpha", alphaValues);
        alphaAnimator.setDuration(5000);//5 seconds
        alphaAnimator.setInterpolator(new android.view.animation.DecelerateInterpolator(1.5));
        nativeAnimatorsArray[1] = alphaAnimator;

        var page = (<definition.BackstackEntry>this.entry).resolvedPage;
        var listener = new android.animation.Animator.AnimatorListener({
            onAnimationStart: function (animator: android.animation.Animator): void {
                trace.write(`${this.toString()} transitionAnimatorListener.onAnimationStart(${animator})`, trace.categories.Animation);
            },
            onAnimationRepeat: function (animator: android.animation.Animator): void {
                trace.write(`${this.toString()} transitionAnimatorListener.onAnimationRepeat(${animator})`, trace.categories.Animation);
            },
            onAnimationEnd: function (animator: android.animation.Animator): void {
                trace.write(`${this.toString()} transitionAnimatorListener.onAnimationEnd(${animator})`, trace.categories.Animation);
                if (nextAnim === -20 || nextAnim === -40) {
                    if (page && page.frame) {
                        trace.write(`${this.toString()} exit animation finished, removing ${page} from visual tree.`, trace.categories.NativeLifecycle);
                        page.frame._removeView(page);
                    }
                }
                animatorSet.removeAllListeners();
                listener = undefined;
            },
            onAnimationCancel: function (animator: android.animation.Animator): void {
                trace.write(`${this.toString()} transitionAnimatorListener.onAnimationCancel(${animator})`, trace.categories.Animation);
            }
        });

        var animatorSet = new android.animation.AnimatorSet();
        animatorSet.addListener(listener);
        animatorSet.playTogether(nativeAnimatorsArray);
        animatorSet.setupStartValues();

        trace.write(`PageFragmentBody.onCreateAnimator(${transit}, ${enter}, ${nextAnim}): ${animatorSet}`, trace.categories.NativeLifecycle);
        return animatorSet;
    }
});

iOS TransitionAnimator:

class TransitionAnimator extends NSObject implements UIViewControllerAnimatedTransitioning {
    public static ObjCProtocols = [UIViewControllerAnimatedTransitioning];

    private _navigationController: UINavigationController;
    private _operation: number;
    private _fromVC: UIViewController;
    private _toVC: UIViewController

    public static init(navigationController: UINavigationController, operation: number, fromVC: UIViewController, toVC: UIViewController): TransitionAnimator {
        var operationType: string;
        trace.write(`TransitionAnimator.init(${navigationController}, ${operationType}, ${fromVC}, ${toVC})`, trace.categories.NativeLifecycle);
        var impl = <TransitionAnimator>TransitionAnimator.new();
        impl._navigationController = navigationController;
        impl._operation = operation;
        impl._fromVC = fromVC;
        impl._toVC = toVC;
        return impl;
}

    public animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
        trace.write(`TransitionAnimator.animateTransition(${transitionContext})`, trace.categories.NativeLifecycle);

        var containerView = (<any>transitionContext).performSelector("containerView");
        var fromView = this._fromVC.view;
        var toView = this._toVC.view;

        fromView.alpha = 1.0;
        toView.alpha = 0.0;
        var screenWidth = platform.screen.mainScreen.widthDIPs;
        switch (this._operation) {
            case UINavigationControllerOperation.UINavigationControllerOperationPush:
                fromView.transform = CGAffineTransformIdentity;
                toView.transform = CGAffineTransformMakeTranslation(screenWidth, 0);
                containerView.insertSubviewAboveSubview(toView, fromView);
                break;
            case UINavigationControllerOperation.UINavigationControllerOperationPop:
                fromView.transform = CGAffineTransformIdentity;
                toView.transform = CGAffineTransformMakeTranslation(-screenWidth, 0);
                containerView.insertSubviewBelowSubview(toView, fromView);
                break;
        }

        var duration = this.transitionDuration(transitionContext);
        UIView.animateWithDurationAnimationsCompletion(duration,
            () => {
                UIView.setAnimationCurve(UIViewAnimationCurve.UIViewAnimationCurveEaseOut);

                this._fromVC.view.alpha = 0.0;
                this._toVC.view.alpha = 1.0;

                switch (this._operation) {
                    case UINavigationControllerOperation.UINavigationControllerOperationPush:
                        fromView.transform = CGAffineTransformMakeTranslation(-screenWidth, 0);
                        toView.transform = CGAffineTransformIdentity;
                        break;
                    case UINavigationControllerOperation.UINavigationControllerOperationPop:
                        fromView.transform = CGAffineTransformMakeTranslation(screenWidth, 0);
                        toView.transform = CGAffineTransformIdentity;
                        break;
                }
            },
            () => {
                (<any>transitionContext).performSelectorWithObject("completeTransition:", true);
            }
        ); 
    }

    public transitionDuration(transitionContext: UIViewControllerContextTransitioning): number {
        return 5;//seconds
    }
}

Now the hard part is what our public API should look like.

@hamorphis
Contributor

I will now try to prepare a small spike with the Transition abstraction proposed by @atanasovg and we can build on from there.

@atanasovg
Member

@hamorphis Excellent, the way you work-around the Android API, requiring XML IDs is very smart. I like the prototype a lot.

@hamorphis
Contributor

@atanasovg

I pushed a hard-coded card-flip animation for Android (inspired from here http://developer.android.com/training/animation/cardflip.html) and iOS. Unfortunately, on iOS you have to first assign some starting values for the views, then update the visual tree by manually adding the toView (above or below depending on push/pop) and finally animate both views simultaneously in the same animation (unlike Android where you simply return a bunch of Animators and let Android do its thing).

So this makes the iOS animation pretty custom, i.e. one that could not be easily described in some very generic XML's belonging to the two different pages. On iOS the transition has to know the two pages, i.e. fromView and toView. So it hard to declared this stuff on two separate Page instance. At least I can't think of a nice way to do so.

Check it out here: https://github.com/NativeScript/NativeScript/blob/issue-811/ui/frame/frame.ios.ts

Look for TransitionAnimator to see how coupled/custom things are in iOS. I am not sure how can we declaratively express what is going on inside the animateTransition method in a platform-agnostic way and then produce the source codes for iOS and Android at run-time.

While Android has a nice abstraction called Animator that you have to create and return, the guys from iOS simply give you a method and then let you do all kinds of crazy stuff that you want inside it. Then they make sure they call your method.

Look how simple they made it in Android: https://github.com/NativeScript/NativeScript/blob/issue-811/ui/frame/frame.android.ts

Look for the PageFragmentBody.onCreateAnimator. It is so simple.

I am perplexed :)

@hamorphis
Contributor
public animateTransition(transitionContext: UIViewControllerContextTransitioning): void {
        trace.write(`TransitionAnimator.animateTransition(${transitionContext})`, trace.categories.NativeLifecycle);

        let containerView =( <any>transitionContext).performSelector("containerView");
        let fromView = this._fromVC.view;
        let toView = this._toVC.view;
        let duration = this.transitionDuration(transitionContext);

        let rotateY = this._operation === UINavigationControllerOperation.UINavigationControllerOperationPush ? Math.PI : -Math.PI;

        // Set the from values.
        toView.alpha = 0.0;
        toView.layer.transform = CATransform3DMakeRotation(rotateY, 0.0, 1.0, 0.0);

        fromView.layer.transform = CATransform3DIdentity;
        fromView.alpha = 1.0;

        // Insert the toView in the visual tree.
        switch (this._operation) {
            case UINavigationControllerOperation.UINavigationControllerOperationPush:
                containerView.insertSubviewAboveSubview(toView, fromView);
                break;
            case UINavigationControllerOperation.UINavigationControllerOperationPop:
                containerView.insertSubviewBelowSubview(toView, fromView);
                break;
        }

        // Animate the to values together simultaneously.
        UIView.animateKeyframesWithDurationDelayOptionsAnimationsCompletion(duration, 0,
            UIViewKeyframeAnimationOptions.UIViewKeyframeAnimationOptionBeginFromCurrentState,
            () => {
                UIView.addKeyframeWithRelativeStartTimeRelativeDurationAnimations(0, 1, () => {
                    toView.layer.transform = CATransform3DIdentity;
                    fromView.layer.transform = CATransform3DMakeRotation(-rotateY, 0.0, 1.0, 0.0);
                });
                UIView.addKeyframeWithRelativeStartTimeRelativeDurationAnimations(0.5, 0.5, () => {
                    toView.alpha = 1.0;
                    fromView.alpha = 0.0;
                });
            },
            (finished: boolean) => {
                (<any>transitionContext).performSelectorWithObject("completeTransition:", finished);
            }
        );
    }
@hamorphis
Contributor

iOS provides 3 stock navigation transitions. The default one which is Slide and then you can have Flip and Curl. To replace the default transition with either Flip or Curl, we can override 4 UINavigationController methods and do the following:

    public pushViewControllerAnimated(controller: UIViewController, animated: boolean): void {
        trace.write(`UINavigationControllerImpl.pushViewControllerAnimated(${controller}, ${animated})`, trace.categories.NativeLifecycle);
        UIView.animateWithDurationAnimations(0.35, () => {
            UIView.setAnimationCurve(UIViewAnimationCurve.UIViewAnimationCurveEaseInOut);
            super.pushViewControllerAnimated(controller, false);
            //UIView.setAnimationTransitionForViewCache(UIViewAnimationTransition.UIViewAnimationTransitionFlipFromRight, this.view, false);
            UIView.setAnimationTransitionForViewCache(UIViewAnimationTransition.UIViewAnimationTransitionCurlUp, this.view, false);
        });
    }

    public setViewControllersAnimated(controllers: NSArray, animated: boolean): void {
        trace.write(`UINavigationControllerImpl.setViewControllersAnimated(${controllers}, ${animated})`, trace.categories.NativeLifecycle);
        UIView.animateWithDurationAnimations(0.35, () => {
            UIView.setAnimationCurve(UIViewAnimationCurve.UIViewAnimationCurveEaseInOut);
            super.setViewControllersAnimated(controllers, animated);
            //UIView.setAnimationTransitionForViewCache(UIViewAnimationTransition.UIViewAnimationTransitionFlipFromRight, this.view, false);
            UIView.setAnimationTransitionForViewCache(UIViewAnimationTransition.UIViewAnimationTransitionCurlUp, this.view, false);
        });
    }

    public popViewControllerAnimated(animated: boolean): UIViewController {
        trace.write(`UINavigationControllerImpl.popViewControllerAnimated(${animated})`, trace.categories.NativeLifecycle);
        UIView.animateWithDurationAnimations(0.35, () => {
            UIView.setAnimationCurve(UIViewAnimationCurve.UIViewAnimationCurveEaseInOut);
            super.popViewControllerAnimated(false);
            //UIView.setAnimationTransitionForViewCache(UIViewAnimationTransition.UIViewAnimationTransitionFlipFromLeft, this.view, false);
            UIView.setAnimationTransitionForViewCache(UIViewAnimationTransition.UIViewAnimationTransitionCurlDown, this.view, false);
        });
        return null;
    }

    public popToViewControllerAnimated(controller: UIViewController, animated: boolean): NSArray {
        trace.write(`UINavigationControllerImpl.popToViewControllerAnimated(${controller}, ${animated})`, trace.categories.NativeLifecycle);
        UIView.animateWithDurationAnimations(0.35, () => {
            UIView.setAnimationCurve(UIViewAnimationCurve.UIViewAnimationCurveEaseInOut);
            super.popToViewControllerAnimated(controller, false);
            //UIView.setAnimationTransitionForViewCache(UIViewAnimationTransition.UIViewAnimationTransitionFlipFromLeft, this.view, false);
            UIView.setAnimationTransitionForViewCache(UIViewAnimationTransition.UIViewAnimationTransitionCurlDown, this.view, false);
        });
        return null;
    }
@hamorphis
Contributor

Android offers three built-in Transitions: Slide, Fade and Explode. They are available on API Level >= 21. I pushed a hard-coded explode transition when navigating between pages.

@hamorphis
Contributor
        if (sdkVersion >= 21) {//Lollipop
            var slideIn = new (<any>android).transition.Slide((<any>android).view.Gravity.RIGHT);
            var slideOut = new (<any>android).transition.Slide((<any>android).view.Gravity.LEFT);

            var fadeIn = new (<any>android).transition.Fade((<any>android).transition.Fade.IN);
            var fadeOut = new (<any>android).transition.Fade((<any>android).transition.Fade.OUT);

            var explode = new (<any>android).transition.Explode();

            //(<any>newFragment).setEnterTransition(slideIn);
            //(<any>newFragment).setEnterTransition(fadeIn);
            (<any>newFragment).setEnterTransition(explode);

            if (this.currentPage) {
                var currentFragment = manager.findFragmentByTag(this.currentPage[TAG]);
                if (currentFragment) {

                    //(<any>currentFragment).setExitTransition(slideOut);
                    //(<any>currentFragment).setExitTransition(fadeOut);
                    (<any>currentFragment).setExitTransition(explode);
                }
            }
        }
@sitefinitysteve
Contributor

Already been discussed but, I would prefer native platform specific transitions. I am fine with saying do X on ios but Y on Android.

@maknz
maknz commented Jan 12, 2016

Platform-specific is what we'd want.

Some thoughts on possible usage:

  1. If you want the slide transition on both platforms:
<Page transition="slide">

If you want different transitions on each platform:

<Page ios:transition="curl" android:transition="explode">

If you tried to do

<Page transition="explode">

the behaviour could be that Android works as it supports the explode transition, but iOS would fall back to a default, possibly slide like it has now. Only slide would work in this fashion as it's the only cross-platform transition.

@bradmartin
Contributor

@hamorphis - how's this coming along? Need any assistance with testing? You guys think this is possible for 1.6 or more 1.7? Only asking cause this is going to be another feature that will help {N} gain a "leg up" on other frameworks and will be a great addition for making apps look a little nicer. Looking forward to it :)

@hamorphis hamorphis changed the title from Custom page navigate transitions to Page Navigation Transitions Feb 2, 2016
@hamorphis
Contributor

I pushed the transitions branch and created a pull request. Until I create a proper documentation, I have explained the basics in the pull request comments: #1473

@hamorphis hamorphis closed this in b0976bf Feb 3, 2016
@hamorphis hamorphis reopened this Feb 3, 2016
@hamorphis hamorphis closed this Feb 3, 2016
@hamorphis
Contributor

The feature has been merged into the master branch. Documentation can be found here: https://github.com/NativeScript/docs/blob/production/core-concepts/navigation.md#navigation-transitions

@hamorphis hamorphis reopened this Feb 8, 2016
@atanasovg
Member

I have some suggestions on the API:

export interface NavigationTransition {
    // named transition
    name?: string;
    // Transition instance
    instance?: Transition;
    duration?: number;
    curve?: any;
}

export interface NavigationEntry {
    ...
    transition: NavigationTransition;
    ...
}

The idea is to rename the NavigationEntry.navigationTransition property to NavigationEntry.transition which sounds more intuitive and less tautological to me.

@sitefinitysteve
Contributor

Is there any ability in the API to define the transition on each platform?
https://github.com/NativeScript/docs/blob/production/core-concepts/navigation.md#navigation-transitions

I just want to avoid lots of if statements when I need a platform specific transition, or different ones for each platform.

var transition;
if(app.ios){
  transition = {
        transition: "curlDown",
        duration: 380,
        curve: "easeIn"
    }
}else{
  transition = {
        transition: "explode",
        duration: 380,
        curve: "easeIn"
    }
}

var navigationEntry = {
    moduleName: "main-page",
    animated: true,
    navigationTransition: transition
};
topmost.navigate(navigationEntry);

Would much more prefer (maybe it's already possible)

var navigationEntry = {
    moduleName: "main-page",
    animated: true,
    navigationTransition: {
           ios:  {
                 transition: "curlDown",
                 duration: 380,
                 curve: "easeIn"
            },
           android:  {
                 transition: "explode",
                 duration: 380,
                 curve: "easeIn"
           }
    }
};
topmost.navigate(navigationEntry);
@hamorphis
Contributor

@atanasovg I agree with both suggestions. Should I start implementing them right away or do we need to gather more input from different people?

@hamorphis
Contributor

@sitefinitysteve I cannot add android and ios inside the NavigationTransition interface, but I can add two more optional properties on the NavigationEntry interface, i.e.:

        /**
         * Specifies an optional navigation transition for all platforms. If not specified, the default platform transition will be used.
         */
        transition?: NavigationTransition;

        /**
         * Specifies an optional navigation transition for iOS. If not specified, the default platform transition will be used.
         */
        transitioniOS?: NavigationTransition;

        /**
         * Specifies an optional navigation transition for iOS. If not specified, the default platform transition will be used.
         */
        transitionAndroid?: NavigationTransition;

Your code will become something like this:

        var navigationEntry = {
            moduleName: "main-page",
            animated: true,
            transitioniOS: {
                transition: "curlDown",
                duration: 380,
                curve: "easeIn"
            },
            transitionAndroid: {
                transition: "explode",
                duration: 380,
                curve: "easeIn"
            }
        };
@sitefinitysteve
Contributor

@hamorphis Whatever you wizards want to do is fine :) Anything to keep extra bloaty if statements out is great. Ternary I assume could be used too, but would get messy when you add the windows platform on, cant just do (app.ios) ? "curlDown" : "explode"

@NathanaelA
Contributor

@hamorphis, Why can't you add .android, & .ios to transitions. I know it can be done in plain old JS, is this an issue with TypeScript.
Because to be honest

   NavigationTransition: {
      transition: 'default',
      duration: 380,
      curve: 'easeIn',
      android: { transition: 'curlDown' }
      ios: { transition: 'explode' }
      windows: { curve: 'easeOut', duration: 180  }
   }

looks way nicer to read and maintain than

{
   transition: { ... } 
   transitionWindows: { ... }
   transitionIos { ... }
   transitionAndroid { ... }
}
@libsgarcia

so this has not been implemented. i was under the impression is already available to use because navigationTransition is already in the documentation: https://docs.nativescript.org/core-concepts/navigation#navigation-transitions

cant wait for this feature!

@kshep92
kshep92 commented Feb 12, 2016

I too was mistaken. I almost went crazy trying to figure out why the transition animations weren't kicking in in my code.

@hamorphis
Contributor

@NathanaelA Because we will end up with the following TypeScript interface:

    export interface NavigationTransition {
        name?: string;
        instance?: transition.Transition;
        duration?: number;
        curve?: any;
        iOS?: NavigationTransition ;
        Android?: NavigationTransition ;
    }

Which looks kind of ugly and recursive to me.

@hamorphis
Contributor

@kshep92 We will release 1.6 this week and the feature will be there.

@hamorphis
Contributor

@sitefinitysteve You will not need to write if statements. The API will be the following:

var navigationEntry = {
            moduleName: "main-page",
            animated: true,
            transitioniOS: {
                transition: "curlDown",
                duration: 380,
                curve: "easeIn"
            },
            transitionAndroid: {
                transition: "explode",
                duration: 380,
                curve: "easeIn"
            }
        };
@Emadello

Can't wait for version 1.6.0!

@vchimev vchimev added 5 - Done and removed 4 - Ready For Test labels Feb 16, 2016
@vchimev vchimev closed this Feb 16, 2016
@kshep92
kshep92 commented Feb 16, 2016

Excited!

On Tue, 16 Feb 2016, 8:10 a.m. Vasil Chimev notifications@github.com
wrote:

Closed #811 #811.


Reply to this email directly or view it on GitHub
#811 (comment).

  • Kevin
@NathanaelA
Contributor

@hamorphis; might look slightly ugly as an interface (and yeah it would be recursive if you use the same NavigationEntry). But it makes a LOT more sense as a consumer of the api, and then all the consumption code (which their is going to be a lot more of) is not horribly ugly. ;-)

    export interface NavigationTransitionOverride {
        name?: string;
        instance?: transition.Transition;
        duration?: number;
        curve?: any;
    }

    export interface NavigationTransition {
        name?: string;
        instance?: transition.Transition;
        duration?: number;
        curve?: any;
        iOS?: NavigationTransitionOverride ;
        Android?: NavigationTransitionOverride ;
        Windows?: NavigationTransitionOverride ;
    }

The question really is; should the framework make the consumers have the ugly code; or should the framework encourage consistency and deal with any ugliness itself.?

@atanasovg
Member

@NathanaelA Thanks for the proposal. I wouldn't say what @hamorphis suggests is ugly for the users to consume. Still, designing APIs always involves personal opinion and flavor, that's why we discussed your suggestion internally within the team, to gather more opinions. We all agreed that @hamorphis's way makes more sense to us. I hope you will not criticize us (not much, to say the least :)) for not willing to make changes we do not believe in.

@hamorphis
Contributor

@NathanaelA You can do everything with JavaScript. Add this convert function somewhere in your global context and use it every time you make a transition:

        function convert(myTransition: any): NavigationTransition {
            if (platform.device.os === platform.platformNames.ios && myTransition.ios) {
                return {
                    name: myTransition.ios.name || myTransition.name,
                    instance: myTransition.ios.instance || myTransition.instance,
                    duration: myTransition.ios.duration || myTransition.duration,
                    curve: myTransition.ios.curve || myTransition.curve
                };
            }

            if (platform.device.os === platform.platformNames.android && myTransition.android) {
                return {
                    name: myTransition.android.name || myTransition.name,
                    instance: myTransition.android.instance || myTransition.instance,
                    duration: myTransition.android.duration || myTransition.duration,
                    curve: myTransition.android.curve || myTransition.curve
                };
            }

            return myTransition;
        }

        topmostFrame().navigate({
            moduleName: "main-page",
            animated: true,
            transition: convert({
                name: 'default',
                duration: 380,
                curve: 'easeIn',
                android: { transition: 'explode', curve: 'easeInOut' },
                ios: { transition: 'curlDown', duration: 300 }
            })
        });

Now you have the API that you like. I hope this helps.

@NathanaelA
Contributor

@hamorphis -- I appreciate all the work you guys do. Even when you guys disagree with me; you don't have to go out of your way to make a polyfill-ish function to make it work the way I want. I do appreciate the effort; but you guys have way way better things to do with your time... 😀

@atanasovg -- I appreciate you taking the time to discuss it internally -- I think anyone who has been developing for a large amount of time will have an opinionated view. 😀 I'm not at all offended when you choose to ignore mine, and hopefully you guys are not offended when I tout my view. The {N} framework is still awesome.

@NathanaelA
Contributor

@atanasovg @valentinstoychev -- One thing to consider for a future team discussion when you have spare time, is if they(the team) finds it important or not -- is the actual framework consistency. I would be curious to know the result of this discussion; as I am pretty sure anytime I see something like this I'm going to be loudly proclaiming its code smell. And I don't really want to offend you guys. 😀

The primary reason I dislike @hamorphis solution is it is inconsistent with other parts of the framework. In the Declarative UI/XML (i.e. every single screen), we use "ios:"/ "android:"/, (& I assume "windows:") for a platform modifiers/specifics. When you start creating new modifiers, (i.e. transitionIOS) you break the internal consistency, and increase the learning curve. How many people who know and understand "ios:" as the primary platform modifier, aren't going to try doing transition: { curve: x, ios: {curve: y}}? So now since transitions has its own unique modifier, rather than using the consistent default platform modifier, their initial "default" attempt will fail to work. Instead of being able to use the "same" key modifier as I use elsewhere in the framework; I have to actually look up in the documentation how to call transitions each time (that is even assuming I remember that it has its "own" unique way of doing things). Transitions is not the only guilty party, there are other place in the framework I feel that we have messed up in the exact same way.

FWIW, I'll probably complain every single time I catch an inconsistency that will increase the learning curve and reduce the consistency of {N} -- so guys don't take any offense. 😀

@Daxito
Daxito commented Mar 1, 2016

I this documented somewhere? I can't find it, I want to know all of the possible options here.

@Daxito
Daxito commented Mar 2, 2016

Thank you! I was looking everywhere except under that option :s

@roblav96
roblav96 commented Sep 2, 2016

@hamorphis How might one go about using https://github.com/lgvalle/Material-Animations with custom transitions?

How can I get the fragments in the createAndroidAnimator method?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment