[gnome-shell/wip/swarm: 3/7] appDisplay: Animate AllView and FrequentView



commit 7823584048dbd92015b71b3d8456d6bfe5ee0f0c
Author: Carlos Soriano <carlos soriano89 gmail com>
Date:   Tue Jun 17 17:46:45 2014 +0200

    appDisplay: Animate AllView and FrequentView
    
    Following design decision, we want to animate AllView and FrequentView
    when opening and closing with a swarm spring form.
    
    This involves a few changes needed to allow that, since from some time
    now, we are animating page changes in viewSelector, using only a fade
    transition. However now we want to let appDisplay and iconGrid apply its
    own animation.
    
    For that we special case the change to and from apps page on
    viewSelector to let appDisplay to animate its own items, using and API
    on appDisplay which at the same time uses an API on iconGrid.
    
    However animating the icons in iconGrid involves a few challenges due to
    the current need of scaling icons on iconGrid on a call which uses
    BEFORE_REDRAW.  We need to call animate() after the icons are already
    scaled, that means however, calling after a BEFORE_REDRAW, so that
    means, we have to call animate() in a call using BEFORE_REDRAW which
    call a function using BEFORE_REDRAW as well, so we wait two frames to
    make sure icons are scaled correctly.
    
    Thanks Florian Mullner for the debugging work
    
    https://bugzilla.gnome.org/show_bug.cgi?id=734726

 js/ui/appDisplay.js   |  114 ++++++++++++++++++++++++++++++++++++++++-
 js/ui/iconGrid.js     |  137 +++++++++++++++++++++++++++++++++++++++++++++++--
 js/ui/viewSelector.js |   28 ++++++++--
 3 files changed, 267 insertions(+), 12 deletions(-)
---
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index e1edf43..eb6c768 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -34,7 +34,9 @@ const MIN_COLUMNS = 4;
 const MIN_ROWS = 4;
 
 const INACTIVE_GRID_OPACITY = 77;
-const INACTIVE_GRID_OPACITY_ANIMATION_TIME = 0.40;
+// This time needs to be less than IconGrid.EXTRA_SPACE_ANIMATION_TIME
+// to not clash with other animations
+const INACTIVE_GRID_OPACITY_ANIMATION_TIME = 0.24;
 const FOLDER_SUBICON_FRACTION = .4;
 
 const MIN_FREQUENT_APPS_COUNT = 3;
@@ -455,6 +457,56 @@ const AllView = new Lang.Class({
         this._refilterApps();
     },
 
+    animate: function(animationDirection, onCompleteOut) {
+        let [dashX, dashY] = Main.overview._dash._showAppsIcon.get_transformed_position();
+        let [dashWidth, dashHeight] = Main.overview._dash._showAppsIcon.get_transformed_size();
+        let [centerDashPositionX, centerDashPositionY] = [dashX + dashWidth / 2, dashY + dashHeight / 2];
+        // Design decision, 1/2 of the dash icon size.
+        let [dashScaledWidth, dashScaledHeight] = [dashWidth / 2, dashHeight / 2];
+        let gridAnimationFunction = Lang.bind(this,
+            function() {
+                this._grid.actor.opacity = 255;
+                this._grid.animate(IconGrid.AnimationType.SPRING,
+                                   animationDirection,
+                                   { sourceX: centerDashPositionX,
+                                     sourceY: centerDashPositionY,
+                                     sourceWidth: dashScaledWidth,
+                                     sourceHeight: dashScaledHeight,
+                                     page: this._currentPage });
+            });
+        if (animationDirection == IconGrid.AnimationDirection.IN) {
+            let toAnimate = this._grid.actor.connect('notify::allocation', Lang.bind(this,
+                function() {
+                    if (this._grid.actor.mapped) {
+                        this._grid.actor.disconnect(toAnimate);
+                        // We need to hide the grid temporary to not flash it
+                        // for a frame
+                        this._grid.actor.opacity = 0;
+                        Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
+                                       gridAnimationFunction);
+                    }
+                }));
+        } else {
+            let animationDoneId = this._grid.connect('animation-done', Lang.bind(this,
+                function () {
+                    this._grid.disconnect(animationDoneId);
+                    if (onCompleteOut)
+                        onCompleteOut();
+                }));
+
+            if (this._displayingPopup && this._currentPopup) {
+                this._currentPopup.popdown();
+                let spaceClosedId = this._grid.connect('space-closed', Lang.bind(this,
+                    function() {
+                        this._grid.disconnect(spaceClosedId);
+                        gridAnimationFunction();
+                    }));
+            } else {
+                gridAnimationFunction();
+            }
+        }
+    },
+
     getCurrentPageY: function() {
         return this._grid.getPageY(this._currentPage);
     },
@@ -697,6 +749,46 @@ const FrequentView = new Lang.Class({
         }
     },
 
+    animate: function(animationDirection, onCompleteOut) {
+        let [dashX, dashY] = Main.overview._dash._showAppsIcon.get_transformed_position();
+        let [dashWidth, dashHeight] = Main.overview._dash._showAppsIcon.get_transformed_size();
+        let [centerDashPositionX, centerDashPositionY] = [dashX + dashWidth / 2, dashY + dashHeight / 2];
+        // Design decision, 1/2 of the dash icon size.
+        let [dashScaledWidth, dashScaledHeight] = [dashWidth / 2, dashHeight / 2];
+        let gridAnimationFunction = Lang.bind(this,
+            function() {
+                this._grid.actor.opacity = 255;
+                this._grid.animate(IconGrid.AnimationType.SPRING,
+                                   animationDirection,
+                                   { sourceX: centerDashPositionX,
+                                     sourceY: centerDashPositionY,
+                                     sourceWidth: dashScaledWidth,
+                                     sourceHeight: dashScaledHeight});
+            });
+
+        if (animationDirection == IconGrid.AnimationDirection.IN) {
+            let toAnimate = this._grid.actor.connect('notify::allocation', Lang.bind(this,
+                function() {
+                    if (this._grid.actor.mapped) {
+                        this._grid.actor.disconnect(toAnimate);
+                        // We need to hide the grid temporary to not flash it
+                        // for a frame
+                        this._grid.actor.opacity = 0;
+                        Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
+                                       gridAnimationFunction);
+                    }
+                }));
+        } else {
+            let animationDoneId = this._grid.connect('animation-done', Lang.bind(this,
+                function () {
+                    this._grid.disconnect(animationDoneId);
+                    if (onCompleteOut)
+                        onCompleteOut();
+                }));
+            gridAnimationFunction();
+        }
+    },
+
     // Called before allocation to calculate dynamic spacing
     adaptToSize: function(width, height) {
         let box = new Clutter.ActorBox();
@@ -815,6 +907,26 @@ const AppDisplay = new Lang.Class({
         this._updateFrequentVisibility();
     },
 
+    animate: function(animationDirection, onCompleteOut) {
+        let currentView = this._views[global.settings.get_uint('app-picker-view')].view;
+
+        // Animate controls opacity using iconGrid animation time, since
+        // it will be the time the AllView or FrequentView takes to show
+        // it entirely.
+        let finalOpacity;
+        if (animationDirection == IconGrid.AnimationDirection.IN) {
+            this._controls.opacity = 0;
+            finalOpacity = 255;
+        } else {
+            finalOpacity = 0
+        }
+        Tweener.addTween(this._controls,
+                         { time: IconGrid.ANIMATION_TIME_IN,
+                           transition: 'easeInOutQuad',
+                           opacity: finalOpacity });
+        currentView.animate(animationDirection, onCompleteOut);
+    },
+
     _showView: function(activeIndex) {
         for (let i = 0; i < this._views.length; i++) {
             let actor = this._views[i].view.actor;
diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js
index a5bf413..45b35ef 100644
--- a/js/ui/iconGrid.js
+++ b/js/ui/iconGrid.js
@@ -18,16 +18,21 @@ const MIN_ICON_SIZE = 16;
 const EXTRA_SPACE_ANIMATION_TIME = 0.25;
 
 const ANIMATION_TIME_IN = 0.350;
+const ANIMATION_TIME_OUT = 1/2 * ANIMATION_TIME_IN;
 const ANIMATION_MAX_DELAY_FOR_ITEM = 2/3 * ANIMATION_TIME_IN;
+const ANIMATION_MAX_DELAY_OUT_FOR_ITEM = 2/3 * ANIMATION_TIME_OUT;
+const ANIMATION_FADE_IN_TIME_FOR_ITEM = 1/4 * ANIMATION_TIME_IN;
 
 const ANIMATION_BOUNCE_ICON_SCALE = 1.1;
 
 const AnimationType = {
-    PULSE: 0
+    PULSE: 0,
+    SPRING: 1
 };
 
 const AnimationDirection = {
-    IN: 0
+    IN: 0,
+    OUT: 1
 };
 
 const BaseIcon = new Lang.Class({
@@ -356,12 +361,18 @@ const IconGrid = new Lang.Class({
      * Intended to be override by subclasses if they need a diferent
      * set of items to be animated.
      */
-    _getChildrenToAnimate: function() {
+    _getChildrenToAnimate: function(params) {
         return this._getVisibleChildren();
     },
 
-    animate: function(animationType, animationDirection) {
-        let actors = this._getChildrenToAnimate();
+    animate: function(animationType, animationDirection, params) {
+        params = Params.parse(params, { page: 0,
+                                        sourceX: null,
+                                        sourceY: null,
+                                        sourceWidth: null,
+                                        sourceHeight: null });
+
+        let actors = this._getChildrenToAnimate(params);
         if (this._animating || actors.length == 0)
             return;
         this._animating = true;
@@ -370,6 +381,9 @@ const IconGrid = new Lang.Class({
             actors[index].opacity = 0;
 
         switch (animationType) {
+            case AnimationType.SPRING:
+                this._animateSpring(actors, animationDirection, params.sourceX, params.sourceY, 
params.sourceWidth, params.sourceHeight);
+                break;
             case AnimationType.PULSE:
                 this._animatePulse(actors, animationDirection);
                 break;
@@ -422,6 +436,107 @@ const IconGrid = new Lang.Class({
         }
     },
 
+    _animateSpring: function(actors, animationDirection, sourceX, sourceY, sourceWidth, sourceHeight) {
+        let distances = actors.map(Lang.bind(this, function(actor) {
+            let [actorX, actorY] = actor.get_transformed_position();
+            return this._distance(actorX, actorY, sourceX, sourceY);
+        }));
+        let maxDist = Math.max.apply(Math, distances);
+        let minDist = Math.min.apply(Math, distances);
+        let normalization = maxDist - minDist;
+
+        for (let index = 0; index < actors.length; index++) {
+            let actorClone = new Clutter.Clone({ source: actors[index],
+                                                 reactive: false });
+            Main.uiGroup.add_actor(actorClone);
+
+            actorClone.set_pivot_point(0.0, 0.0);
+            let [width, height,,] = this._getAllocatedChildSizeAndSpacing(actors[index]);
+            actorClone.set_size(width, height);
+            let scaleX = sourceWidth / width;
+            let scaleY = sourceHeight / height;
+            // Center the actor on the source position, expecting
+            // sourcePosition to be the center where the actor should
+            // be. In this way we avoid misaligments if the source actor
+            // size changes
+            let [adjustedSourcePositionX, adjustedSourcePositionY] = [sourceX - sourceWidth / 2, sourceY - 
sourceHeight / 2];
+
+            // Defeat onComplete anonymous function closure
+            let actor = actors[index];
+            let isLastItem = index == actors.length - 1;
+
+            let movementParams, fadeParams;
+            if (animationDirection == AnimationDirection.IN) {
+                actorClone.opacity = 0;
+                actorClone.set_scale(scaleX, scaleY);
+
+                actorClone.set_position(adjustedSourcePositionX, adjustedSourcePositionY);
+
+                let delay = (1 - (distances[index] - minDist) / normalization) * 
ANIMATION_MAX_DELAY_FOR_ITEM;
+                let [finalX, finalY]  = actors[index].get_transformed_position();
+                movementParams = { time: ANIMATION_TIME_IN,
+                                   transition: 'easeInOutQuad',
+                                   delay: delay,
+                                   x: finalX,
+                                   y: finalY,
+                                   scale_x: 1,
+                                   scale_y: 1,
+                                   onComplete: Lang.bind(this, function() {
+                                       if (isLastItem){
+                                           this._animating = false;
+                                           this.emit('animation-done');
+                                       }
+                                       actor.opacity = 255;
+                                       actorClone.destroy();
+                                   })};
+                fadeParams = { time: ANIMATION_FADE_IN_TIME_FOR_ITEM,
+                               transition: 'easeInOutQuad',
+                               delay: delay,
+                               opacity: 255 };
+            } else {
+                let [startX, startY]  = actors[index].get_transformed_position();
+                actorClone.set_position(startX, startY);
+
+                let delay = (distances[index] - minDist) / normalization * ANIMATION_MAX_DELAY_OUT_FOR_ITEM;
+                movementParams = { time: ANIMATION_TIME_OUT,
+                                   transition: 'easeInOutQuad',
+                                   delay: delay,
+                                   x: adjustedSourcePositionX,
+                                   y: adjustedSourcePositionY,
+                                   scale_x: scaleX,
+                                   scale_y: scaleY,
+                                   onComplete: Lang.bind(this, function() {
+                                       if (isLastItem) {
+                                           this._animating = false;
+                                           this.emit('animation-done');
+                                           this._restoreItemsOpacity();
+                                       }
+                                       actorClone.destroy();
+                                   })};
+                fadeParams = { time: ANIMATION_FADE_IN_TIME_FOR_ITEM,
+                               transition: 'easeInOutQuad',
+                               delay: ANIMATION_TIME_OUT + delay - ANIMATION_FADE_IN_TIME_FOR_ITEM,
+                               opacity: 0 };
+            }
+
+
+            Tweener.addTween(actorClone, movementParams);
+            Tweener.addTween(actorClone, fadeParams);
+        }
+    },
+
+    _restoreItemsOpacity: function() {
+        for (let index = 0; index < this._items.length; index++) {
+            this._items[index].actor.opacity = 255;
+        }
+    },
+
+    _distance: function(point1X, point1Y, point2X, point2Y) {
+        let x = point1X - point2X;
+        let y = point1Y - point2Y;
+        return Math.sqrt(x * x + y * y);
+    },
+
     _getAllocatedChildSizeAndSpacing: function(child) {
         let [,, natWidth, natHeight] = child.get_preferred_size();
         let width = Math.min(this._getHItemSize(), natWidth);
@@ -697,6 +812,10 @@ const PaginatedIconGrid = new Lang.Class({
         }
     },
 
+    _getChildrenToAnimate: function(params) {
+        return this._getChildrenInPage(params.page);
+    },
+
     _computePages: function (availWidthPerPage, availHeightPerPage) {
         let [nColumns, usedWidth] = this._computeLayout(availWidthPerPage);
         let nRows;
@@ -752,6 +871,14 @@ const PaginatedIconGrid = new Lang.Class({
         return Math.floor(index / this._childrenPerPage);
     },
 
+    _getChildrenInPage: function(pageNumber) {
+        let children = this._getVisibleChildren();
+        let firstIndex = this._childrenPerPage * pageNumber;
+        let lastIndex = firstIndex + this._childrenPerPage;
+
+        return children.slice(firstIndex, Math.min(lastIndex, children.length));
+    },
+
     /**
     * openExtraSpace:
     * @sourceItem: the item for which to create extra space
diff --git a/js/ui/viewSelector.js b/js/ui/viewSelector.js
index e0de436..8d38900 100644
--- a/js/ui/viewSelector.js
+++ b/js/ui/viewSelector.js
@@ -19,6 +19,7 @@ const Search = imports.ui.search;
 const ShellEntry = imports.ui.shellEntry;
 const Tweener = imports.ui.tweener;
 const WorkspacesView = imports.ui.workspacesView;
+const IconGrid = imports.ui.iconGrid;
 
 const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
 
@@ -388,14 +389,29 @@ const ViewSelector = new Lang.Class({
                          });
     },
 
-    _animateIn: function(page) {
+    _animateIn: function(page, oldPage) {
         page.show();
 
-        this._fadePageIn(page);
+        if (page == this._appsPage && oldPage == this._workspacesPage) {
+            // Restore opacity of the page, since we could took the
+            // opacity on _fadePageIn if we didn't use appDisplay custom
+            // animation to animate out.
+            page.opacity = 255;
+            this.appDisplay.animate(IconGrid.AnimationDirection.IN);
+        } else {
+            this._fadePageIn(page);
+        }
     },
 
-    _animateOut: function(page, onComplete) {
-        this._fadePageOut(page, onComplete);
+    _animateOut: function(page, newPage, onComplete) {
+        if (page == this._appsPage &&
+            newPage == this._workspacesPage &&
+            !Main.overview.animationInProgress) {
+            this.appDisplay.animate(IconGrid.AnimationDirection.OUT,
+                                    onComplete);
+        } else {
+            this._fadePageOut(page, onComplete);
+        }
     },
 
     _hidePageAndSyncEmpty: function(page) {
@@ -418,11 +434,11 @@ const ViewSelector = new Lang.Class({
         let animateActivePage = Lang.bind(this,
             function() {
                 this._hidePageAndSyncEmpty(oldPage);
-                this._animateIn(this._activePage);
+                this._animateIn(page, oldPage);
             });
 
         if (oldPage)
-            this._animateOut(oldPage, animateActivePage)
+            this._animateOut(oldPage, page, animateActivePage)
         else
             animateActivePage();
     },


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]