[gnome-shell/wip/swarm: 116/126] appDisplay: Animate AllView and FrequentView



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

    appDisplay: Animate AllView and FrequentView

 js/ui/appDisplay.js   |  112 ++++++++++++++++++++++++++++++++++++
 js/ui/iconGrid.js     |  150 +++++++++++++++++++++++++++++++++++++++++++++++--
 js/ui/viewSelector.js |   17 +++++-
 3 files changed, 271 insertions(+), 8 deletions(-)
---
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index ca56d6c..9bef143 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -458,6 +458,58 @@ const AllView = new Lang.Class({
         this._refilterApps();
     },
 
+    animate: function(animationDirection, onCompleteOut) {
+        let dashPosition = Main.overview._dash._showAppsIcon.get_transformed_position();
+        let dashSize = Main.overview._dash._showAppsIcon.get_transformed_size();
+        let centerDashPosition = [dashPosition[0] + dashSize[0] / 2, dashPosition[1] + dashSize[1] / 2];
+        // Design decision, 1/2 of the dash icon size.
+        let dashScaledSize = [dashSize[0] / 2, dashSize[1] / 2];
+        let gridAnimationFunction = Lang.bind(this,
+            function() {
+                this._grid.animate(IconGrid.AnimationType.SPRING,
+                                   animationDirection,
+                                   { sourcePosition: centerDashPosition,
+                                     sourceSize: dashScaledSize,
+                                     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 avoid the clutter allocation loop, so we use
+                        // a call before redraw, but also we need to hide items
+                        // to not show them for a moment before the animation is
+                        // started
+                        this._grid.actor.opacity = 0;
+                        Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this,
+                            function() {
+                                this._grid.actor.opacity = 255;
+                                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);
     },
@@ -699,6 +751,47 @@ const FrequentView = new Lang.Class({
         }
     },
 
+    animate: function(animationDirection, onCompleteOut) {
+        let dashPosition = Main.overview._dash._showAppsIcon.get_transformed_position();
+        let dashSize = Main.overview._dash._showAppsIcon.get_transformed_size();
+        let centerDashPosition = [dashPosition[0] + dashSize[0] / 2, dashPosition[1] + dashSize[1] / 2];
+        // Design decision, 1/2 of the dash icon size.
+        let dashScaledSize = [dashSize[0] / 2, dashSize[1] / 2];
+        let gridAnimationFunction = Lang.bind(this, function() {
+            this._grid.animate(IconGrid.AnimationType.SPRING,
+                               animationDirection,
+                               { sourcePosition: centerDashPosition,
+                                 sourceSize: dashScaledSize });
+        });
+
+        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 avoid the clutter allocation loop, so we use
+                        // a call before redraw, but also we need to hide items
+                        // to not show them for a moment before the animation is
+                        // started
+                        this._grid.actor.opacity = 0;
+                        Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this,
+                            function() {
+                                this._grid.actor.opacity = 255;
+                                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();
@@ -817,6 +910,25 @@ const AppDisplay = new Lang.Class({
         this._updateFrequentVisibility();
     },
 
+    animate: function(animationDirection, onCompleteOut) {
+        let view = this._views[global.settings.get_uint('app-picker-view')].view;
+
+        // Animate controls opacity using swarm 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 });
+        view.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 e29f3ba..5a2d1f4 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 = {
-    BOUNCE: 0
+    BOUNCE: 0,
+    SPRING: 1
 };
 
 const AnimationDirection = {
-    IN: 0
+    IN: 0,
+    OUT: 1
 };
 
 const BaseIcon = new Lang.Class({
@@ -356,12 +361,16 @@ 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, params) {
-        let actors = this._getChildrenToAnimate();
+        params = Params.parse(params, { page: 0,
+                                        sourcePosition: null,
+                                        sourceSize: null });
+
+        let actors = this._getChildrenToAnimate(params);
         if (this._animating || actors.length == 0)
             return;
         this._animating = true;
@@ -371,6 +380,9 @@ const IconGrid = new Lang.Class({
 
         let delayedAnimation = Lang.bind(this, function() {
             switch (animationType) {
+                case AnimationType.SPRING:
+                    this._animateSpring(actors, animationDirection, params.sourcePosition, 
params.sourceSize);
+                    break;
                 case AnimationType.BOUNCE:
                     this._animateBounce(actors, animationDirection);
                     break;
@@ -415,11 +427,12 @@ const IconGrid = new Lang.Class({
                                                      scale_x: 1,
                                                      scale_y: 1,
                                                      onComplete: Lang.bind(this, function() {
-                                                        if (isLastActor)
+                                                        if (isLastActor) {
                                                             this._animating = false;
+                                                            this.emit('animation-done');
+                                                        }
                                                         actor.opacity = 255;
                                                         actorClone.destroy();
-                                                        this.emit('animation-done');
                                                     })
                                                    });
                               })
@@ -427,6 +440,113 @@ const IconGrid = new Lang.Class({
         }
     },
 
+    _animateSpring: function(actors, animationDirection, sourcePosition, sourceSize) {
+        let distances = actors.map(Lang.bind(this, function(actor) {
+            return this._distance(actor.get_transformed_position(), sourcePosition);
+        }));
+        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] = actors[index].get_transformed_size();
+            actorClone.set_size(width, height);
+            let scaleX = sourceSize[0] / width;
+            let scaleY = sourceSize[1] / height;
+
+            // Defeat onComplete anonymous function closure
+            let actor = actors[index];
+            let isLastItem = index == actors.length - 1;
+
+            let movementParams, fadeParams;
+
+            // 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
+            // or is diferent that it should be. (hint: for the AllView and FrequentView animations the
+            // sourceSize is not exactly the actor size, since we want smaller actors to be animated due to
+            // design decision)
+            let adjustedSourcePosition = [sourcePosition[0] - sourceSize[0] / 2, sourcePosition[1] - 
sourceSize[1] / 2];
+
+            if (animationDirection == AnimationDirection.IN) {
+                actorClone.opacity = 0;
+                actorClone.set_scale(scaleX, scaleY);
+
+                actorClone.set_position(adjustedSourcePosition[0], adjustedSourcePosition[1]);
+
+                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: adjustedSourcePosition[0],
+                                   y: adjustedSourcePosition[1],
+                                   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);
+        }
+    },
+
+
+    /**
+     * FIXME?: We assume the icons use full opacity.
+     **/
+    _restoreItemsOpacity: function() {
+        for (let index = 0; index < this._items.length; index++) {
+            this._items[index].actor.opacity = 255;
+        }
+    },
+
+    _distance: function(point1, point2) {
+        let x = point1[0] - point2[0];
+        let y = point1[1] - point2[1];
+        return Math.sqrt(x * x + y * y);
+    },
+
     _calculateChildBox: function(child, x, y, box) {
         let [childMinWidth, childMinHeight, childNaturalWidth, childNaturalHeight] =
              child.get_preferred_size();
@@ -697,6 +817,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 +876,20 @@ const PaginatedIconGrid = new Lang.Class({
         return Math.floor(index / this._childrenPerPage);
     },
 
+    _getChildrenInPage: function(pageNumber) {
+        let children = this._getVisibleChildren();
+
+        let firstIndex = this._childrenPerPage * pageNumber;
+        let indexOffset = 0;
+        let childrenInPage = []
+
+        while (indexOffset < this._childrenPerPage && firstIndex + indexOffset < children.length) {
+               childrenInPage.push(children[firstIndex + indexOffset]);
+               indexOffset++;
+        }
+        return childrenInPage;
+    },
+
     /**
     * openExtraSpace:
     * @sourceItem: the item for which to create extra space
diff --git a/js/ui/viewSelector.js b/js/ui/viewSelector.js
index a3d1af5..9764848 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';
 
@@ -395,11 +396,23 @@ const ViewSelector = new Lang.Class({
     _animateIn: function(page) {
         page.show();
 
-        this._fadePageIn(page);
+        if (page == this._appsPage) {
+            // Restore opacity of the page, since we could took the opacity
+            // on _fadePageIn if we didn't use swarm 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);
+        if (page == this._appsPage) {
+            this.appDisplay.animate(IconGrid.AnimationDirection.OUT,
+                                    onComplete);
+        } else {
+            this._fadePageOut(page, onComplete);
+        }
     },
 
     _hidePageAndSyncEmpty: function(page) {


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