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



commit bf2c80c4e48ec570a10e17eb63a6c05e6f5b8afb
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   |   86 +++++++++++++++++++++++++++-
 js/ui/iconGrid.js     |  148 ++++++++++++++++++++++++++++++++++++++++++++++++-
 js/ui/viewSelector.js |   14 ++++-
 3 files changed, 241 insertions(+), 7 deletions(-)
---
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index 3944e27..8274aba 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -440,6 +440,49 @@ 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.ANIMATION_TYPE_SWARM_SPRING,
+                                   animationDirection,
+                                   { sourcePosition: centerDashPosition,
+                                     sourceSize: dashScaledSize,
+                                     page: this._currentPage })
+            });
+        if (animationDirection == IconGrid.ANIMATION_DIRECTION_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);
     },
@@ -681,6 +724,38 @@ 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.ANIMATION_TYPE_SWARM_SPRING,
+                               animationDirection,
+                               { sourcePosition: centerDashPosition,
+                                 sourceSize: dashScaledSize });
+        });
+
+        if (animationDirection == IconGrid.ANIMATION_DIRECTION_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();
@@ -799,6 +874,11 @@ const AppDisplay = new Lang.Class({
         this._updateFrequentVisibility();
     },
 
+    animate: function(animationDirection, onCompleteOut) {
+        let view = this._views[global.settings.get_uint('app-picker-view')].view;
+        view.animate(animationDirection, onCompleteOut);
+    },
+
     _showView: function(activeIndex) {
         for (let i = 0; i < this._views.length; i++) {
             let actor = this._views[i].view.actor;
@@ -947,7 +1027,7 @@ const FolderView = new Lang.Class({
     _keyFocusIn: function(actor) {
         Util.ensureActorVisibleInScrollView(this.actor, actor);
     },
-    
+
     animate: function(animationType, animationDirection, params) {
         this._grid.animate(animationType, animationDirection, params);
     },
@@ -1282,13 +1362,13 @@ const AppFolderPopup = new Lang.Class({
         this.actor.show();
 
         this._boxPointer.setArrowActor(this._source.actor);
-        // We need to hide the icons of the view until the boxpointer animation 
+        // We need to hide the icons of the view until the boxpointer animation
         // is completed so we can animate the icons after as we like withouth
         // showing them while boxpointer is animating.
         this._view.actor.opacity = 0;
         this._boxPointer.show(BoxPointer.PopupAnimation.FADE |
                               BoxPointer.PopupAnimation.SLIDE,
-                              Lang.bind(this, 
+                              Lang.bind(this,
             function() {
                 // Restore the view opacity, so now we show the icons and animate
                 // them
diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js
index e88c173..158b90f 100644
--- a/js/ui/iconGrid.js
+++ b/js/ui/iconGrid.js
@@ -10,6 +10,7 @@ const St = imports.gi.St;
 const Lang = imports.lang;
 const Params = imports.misc.params;
 const Tweener = imports.ui.tweener;
+const Main = imports.ui.main;
 
 const ICON_SIZE = 96;
 const MIN_ICON_SIZE = 16;
@@ -17,12 +18,17 @@ const MIN_ICON_SIZE = 16;
 const EXTRA_SPACE_ANIMATION_TIME = 0.25;
 
 const ANIMATION_TIME_IN = 0.400;
+const ANIMATION_TIME_OUT = 0.200;
 const ANIMATION_MAX_DELAY_FOR_ITEM = 0.250;
+const ANIMATION_MAX_DELAY_OUT_FOR_ITEM = 0.125;
+const ANIMATION_FADE_IN_TIME_FOR_ITEM = 0.100;
 
 const ANIMATION_APPEAR_ICON_SCALE = 1.1;
 
 const ANIMATION_TYPE_APPEAR = 1;
+const ANIMATION_TYPE_SWARM_SPRING = 2;
 
+const ANIMATION_DIRECTION_OUT = 0;
 const ANIMATION_DIRECTION_IN = 1;
 
 const BaseIcon = new Lang.Class({
@@ -364,6 +370,9 @@ const IconGrid = new Lang.Class({
         this._animating = true;
 
         switch (animationType) {
+            case ANIMATION_TYPE_SWARM_SPRING:
+                this._animateSwarmSpring(actors, animationDirection, params.sourcePosition, 
params.sourceSize);
+                break;
             case ANIMATION_TYPE_APPEAR:
                 this._animateAppear(actors, animationDirection);
                 break;
@@ -410,11 +419,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');
                                                     })
                                                    });
                               })
@@ -422,6 +432,118 @@ const IconGrid = new Lang.Class({
         }
     },
 
+    _animateSwarmSpring: 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++) {
+            // FIXME? Seems that putting the actors at opacity 0
+            // for animating seems like it doesn't belongs here.
+            // But works well.
+            actors[index].opacity = 0;
+
+            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 == ANIMATION_DIRECTION_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();
@@ -692,6 +814,14 @@ const PaginatedIconGrid = new Lang.Class({
         }
     },
 
+    animate: function(animationType, animationDirection, params) {
+        params = Params.parse(params, { page: 0,
+                                        sourcePosition: null,
+                                        sourceSize: null });
+
+        this._animateReal(this._getChildrenInPage(params.page), animationType, animationDirection, params);
+    },
+
     _computePages: function (availWidthPerPage, availHeightPerPage) {
         let [nColumns, usedWidth] = this._computeLayout(availWidthPerPage);
         let nRows;
@@ -747,6 +877,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 13407a5..472cef3 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';
 
@@ -230,11 +231,20 @@ const ViewSelector = new Lang.Class({
     _animateIn: function(page) {
         page.show();
 
-        this._fadePageIn(page);
+        if (page == this._appsPage) {
+            this.appDisplay.animate(IconGrid.ANIMATION_DIRECTION_IN);
+        } else {
+            this._fadePageIn(page);
+        }
     },
 
     _animateOut: function(page, onComplete) {
-        this._fadePageOut(page, onComplete);
+        if (page == this._appsPage) {
+            this.appDisplay.animate(IconGrid.ANIMATION_DIRECTION_OUT,
+                                    onComplete);
+        } else {
+            this._fadePageOut(page, onComplete);
+        }
     },
 
     _hidePageAndSyncEmpty: function(page) {


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