[gnome-shell/wip/swarm: 1/6] appDisplay: Animate AllView and FrequentView



commit 426ce895d8d6e698ee1ea536ac9e55eff5c12e12
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   |  104 +++++++++++++++++++++++++++++-
 js/ui/iconGrid.js     |  170 ++++++++++++++++++++++++++++++++++++++++---------
 js/ui/viewSelector.js |   28 ++++++--
 3 files changed, 264 insertions(+), 38 deletions(-)
---
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index e1edf43..b492065 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,51 @@ 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.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);
+                        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 +744,41 @@ 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.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);
+                        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 +897,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 6474fb8..70d038a 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;
@@ -369,32 +380,16 @@ const IconGrid = new Lang.Class({
         for (let index = 0; index < actors.length; index++)
             actors[index].opacity = 0;
 
-        // Hack:
-        // We need to delay the animation until the icons have the right
-        // size acording to updateChildrenScale. updateChildrenScale is
-        // normally called from adaptToSize, which is called inside a
-        // allocation, so it needs to be called inside a Meta.later_add
-        // BEFORE_REDRAW. That causes that in animate() we won't be sure
-        // if the items have the right size or not.
-        //
-        // To avoid that we need two things. First, expect the caller to
-        // animate() to call it inside a BEFORE_REDRAW. But that's not
-        // enough, since both animate() and updateChildrenScale() are
-        // called inside a BEFORE_REDRAW and we don't know which one
-        // will be executed first. So we need to make sure
-        // updateChildrenScale() is called first. To achieve that, we
-        // need to wait another frame here, calling the animation inside
-        // another BEFORE_REDRAW.
-        let delayedAnimation = Lang.bind(this, function() {
-            switch (animationType) {
-                case AnimationType.PULSE:
-                    this._animatePulse(actors, animationDirection);
-                    break;
-                default:
-                    log("animation doesn't exist. Icongrid won\'t work");
-            }
-        });
-        Meta.later_add(Meta.LaterType.BEFORE_REDRAW, delayedAnimation);
+        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;
+            default:
+                log("animation doesn't exist. Icongrid won\'t work");
+        }
     },
 
     _animatePulse: function(actors, animationDirection) {
@@ -442,6 +437,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);
@@ -716,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;
@@ -771,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]