stic
stic

Reputation: 66

Google I/O 2015 page like transition animations

recently I really like Google I/O 2015 event page, especially those transition animations between different states. I know they used Polymer for that, but I'm trying to recreate such delayed animations in Angular (1.4.1) and Angular-material and ui-router.

Basically what I want to achieve is this workflow:

  1. before state change, animate leaving components of the app
  2. leave some basic structure of the app (some basic holder containers)
  3. make state change - resolve resources (REST API call)
  4. transition to new state, with basic app structure (holders)
  5. animate entering elements (with different delays)

This is not trivial task, and ng-animate is not very helpful, but I want to use is as much as possible. One drawback is, that leaving css classes are not added, before promises are resolved, the other it, that at one moment, both - old and new state view are present on the page.

I tried to create this directive:

(function() {
    'use strict';

    angular
        .module('climbguide')
        .directive('cgAnimateElement', cgAnimateElement);

    /* @ngInject */
    function cgAnimateElement($animate, $rootScope, $state) {
        return {
            restrict:         'A',
            link:             linkFunc
        };

        function linkFunc(scope, el) {
            $animate.enter(el, el.parent());

            var cleanUp = $rootScope.$on('$stateChangeStart',
                function(event, toState, toParams) {
                    if ($rootScope.stateChangeBypass) {
                        $rootScope.stateChangeBypass = false;
                        return;
                    }
                    event.preventDefault();

                    var promise = $animate.leave(el);

                    promise.then(function() {
                        $rootScope.stateChangeBypass = true;
                        $state.go(toState.name, toParams);
                    });

                });

            scope.$on('$destroy', function() {
                cleanUp();
            });
        }
    }

})();

It basically does what I want, however for some reason it is only possible to use it one element - I assume because of the $rootScope.$on('$stateChangeStart') and later use of $state.go(toState.name, toParams);.

I also found two other solutions,

  1. angular-ui-router-in-out, which uses CSS, but there is waiting for promises to be resolved before any animation happens (the loader animation would be necessary)

  2. angular-gsapify-router, which uses javascript animations, and has the same problem as above one.

I'm still only learning angular, so I really don't know how to do this in a right way. Do you have any ideas? Thanks a lot.

P.S.: sorry for missing links to the libraries, but this is my first post to SO, so I can only post 2 links :)

Upvotes: 2

Views: 407

Answers (1)

stic
stic

Reputation: 66

Maybe it will help somebody, but I got it to work with probably dirty hack, but anyway it does, what I described in the original post.

I changed the directive so it can be re-used more times:

(function() {
    'use strict';

    angular
        .module('climbguide')
        .directive('cgAnimateElement', cgAnimateElement);

    /* @ngInject */
    function cgAnimateElement($animate, delayedRouterService) {
        return {
            restrict: 'A',
            link:     linkFunc
        };

        function linkFunc(scope, el) {

            var stateChangeBypass = false;
            $animate.enter(el, el.parent());

            // Use scope instead of $rootScope, so there is no need to de-register listener
            scope.$on('$stateChangeStart',
                function(event, toState, toParams) {
                    if (stateChangeBypass) {
                        // Resuming transition to the next state broadcasts new $stateChangeStart
                        // event, so it necessary to bypass it
                        stateChangeBypass = false;
                        return;
                    }
                    delayedRouterService.holdStateChange(event);
                    var promise = $animate.leave(el);
                    promise.then(function() {
                        stateChangeBypass = true;
                        delayedRouterService.releaseStateChange(toState, toParams);
                    });
                });
        }

    }

})();

I created service for handling state changes - preventing and resuming state changes in ui-router:

(function() {
    'use strict';

    angular
        .module('climbguide')
        .factory('delayedRouterService', delayedRouterService);

    /* @ngInject */
    function delayedRouterService($state) {

        var _runningAnimations = 0;

        /**
         * Public methods
         *
         */
        var service = {
            holdStateChange:    holdStateChange,
            releaseStateChange: releaseStateChange
        };

        return service;
        //////////////

        /**
         * Prevent state change from the first animation
         * Store the number of currently running animations
         *
         * @param event
         */
        function holdStateChange(event) {
            if (_runningAnimations === 0) {
                event.preventDefault();
            }
            _runningAnimations++;
        }

        /**
         * Remove animation from the stack after it is finished
         * Resume state transition after last animation is finished
         *
         * @param toState
         * @param toParams
         */
        function releaseStateChange(toState, toParams) {
            _runningAnimations--;

            if (_runningAnimations === 0) {
                $state.go(toState.name, toParams);
            }
        }

    }
})();

So it's possible to use it in the HTML for the element which I want to animate

<div class="main" cg-animate-element>
    ...
</div>

And the final CSS:

.main {
    &.ng-animate {
        transition: opacity, transform;
        transition-duration: 0.4s;
        transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
    }

    &.ng-enter {
        opacity: 0;
        transform: translate3d(0, 100px, 0);
    }

    &.ng-enter-active {
        opacity: 1;
        transform: translate3d(0, 0, 0);
    }

    &.ng-leave {
        opacity: 1;
        transform: translate3d(0, 0, 0);
    }

    &.ng-leave-active {
        opacity: 0;
        transform: translate3d(0, 100px, 0);
    }
}

Upvotes: 1

Related Questions