[gnome-shell] appDisplay: Animate AllView and FrequentView



commit f160dda1873fcc623813484caa1b3b1df2c0ed3d
Author: Carlos Soriano <carlos soriano89 gmail com>
Date:   Tue Jun 17 19:10:54 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.
    
    Thanks Florian Müllner for the debugging work
    
    https://bugzilla.gnome.org/show_bug.cgi?id=734726

 js/ui/appDisplay.js   |  114 +++++++++++++++++++++++++++++++++++++++-----
 js/ui/iconGrid.js     |  128 ++++++++++++++++++++++++++++++++++++++++++++++++-
 js/ui/overview.js     |    4 ++
 js/ui/viewSelector.js |   62 ++++++++++++++++++------
 4 files changed, 279 insertions(+), 29 deletions(-)
---
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index 8b00cfe..c86f5e5 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;
@@ -177,6 +179,38 @@ const BaseAppView = new Lang.Class({
                 this.selectApp(id);
             }));
         }
+    },
+
+    _doSpringAnimation: function(animationDirection) {
+        this._grid.actor.opacity = 255;
+        this._grid.animateSpring(animationDirection,
+                                 Main.overview.getShowAppsButton());
+    },
+
+    animate: function(animationDirection, onComplete) {
+        if (animationDirection == IconGrid.AnimationDirection.IN) {
+            let toAnimate = this._grid.actor.connect('notify::allocation', Lang.bind(this,
+                function() {
+                    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,
+                                   Lang.bind(this, function() {
+                                       this._doSpringAnimation(animationDirection)
+                                  }));
+                }));
+        } else {
+            this._doSpringAnimation(animationDirection);
+        }
+
+        if (onComplete) {
+            let animationDoneId = this._grid.connect('animation-done', Lang.bind(this,
+                function () {
+                    this._grid.disconnect(animationDoneId);
+                    onComplete();
+                }));
+        }
     }
 });
 Signals.addSignalMethods(BaseAppView.prototype);
@@ -323,7 +357,7 @@ const AllView = new Lang.Class({
         this._stack = new St.Widget({ layout_manager: new Clutter.BinLayout() });
         let box = new St.BoxLayout({ vertical: true });
 
-        this._currentPage = 0;
+        this._grid.currentPage = 0;
         this._stack.add_actor(this._grid.actor);
         this._eventBlocker = new St.Widget({ x_expand: true, y_expand: true });
         this._stack.add_actor(this._eventBlocker);
@@ -455,14 +489,33 @@ const AllView = new Lang.Class({
         this._refilterApps();
     },
 
+    // Overriden from BaseAppView
+    animate: function (animationDirection, onComplete) {
+        if (animationDirection == IconGrid.AnimationDirection.OUT &&
+            this._displayingPopup && this._currentPopup) {
+            this._currentPopup.popdown();
+            let spaceClosedId = this._grid.connect('space-closed', Lang.bind(this,
+                function() {
+                    this._grid.disconnect(spaceClosedId);
+                    // Given that we can't call this.parent() inside the
+                    // signal handler, call again animate which will
+                    // call the parent given that popup is already
+                    // closed.
+                    this.animate(animationDirection, onComplete);
+                }));
+        } else {
+            this.parent(animationDirection, onComplete);
+        }
+    },
+
     getCurrentPageY: function() {
-        return this._grid.getPageY(this._currentPage);
+        return this._grid.getPageY(this._grid.currentPage);
     },
 
     goToPage: function(pageNumber) {
         pageNumber = clamp(pageNumber, 0, this._grid.nPages() - 1);
 
-        if (this._currentPage == pageNumber && this._displayingPopup && this._currentPopup)
+        if (this._grid.currentPage == pageNumber && this._displayingPopup && this._currentPopup)
             return;
         if (this._displayingPopup && this._currentPopup)
             this._currentPopup.popdown();
@@ -482,7 +535,7 @@ const AllView = new Lang.Class({
         let time;
         // Only take the velocity into account on page changes, otherwise
         // return smoothly to the current page using the default velocity
-        if (this._currentPage != pageNumber) {
+        if (this._grid.currentPage != pageNumber) {
             let minVelocity = totalHeight / (PAGE_SWITCH_TIME * 1000);
             velocity = Math.max(minVelocity, velocity);
             time = (diffToPage / velocity) / 1000;
@@ -493,9 +546,9 @@ const AllView = new Lang.Class({
         // longer than PAGE_SWITCH_TIME
         time = Math.min(time, PAGE_SWITCH_TIME);
 
-        this._currentPage = pageNumber;
+        this._grid.currentPage = pageNumber;
         Tweener.addTween(this._adjustment,
-                         { value: this._grid.getPageY(this._currentPage),
+                         { value: this._grid.getPageY(this._grid.currentPage),
                            time: time,
                            transition: 'easeOutQuad' });
         this._pageIndicators.setCurrentPage(pageNumber);
@@ -524,9 +577,9 @@ const AllView = new Lang.Class({
 
         let direction = event.get_scroll_direction();
         if (direction == Clutter.ScrollDirection.UP)
-            this.goToPage(this._currentPage - 1);
+            this.goToPage(this._grid.currentPage - 1);
         else if (direction == Clutter.ScrollDirection.DOWN)
-            this.goToPage(this._currentPage + 1);
+            this.goToPage(this._grid.currentPage + 1);
 
         return Clutter.EVENT_STOP;
     },
@@ -566,10 +619,10 @@ const AllView = new Lang.Class({
             return Clutter.EVENT_STOP;
 
         if (event.get_key_symbol() == Clutter.Page_Up) {
-            this.goToPage(this._currentPage - 1);
+            this.goToPage(this._grid.currentPage - 1);
             return Clutter.EVENT_STOP;
         } else if (event.get_key_symbol() == Clutter.Page_Down) {
-            this.goToPage(this._currentPage + 1);
+            this.goToPage(this._grid.currentPage + 1);
             return Clutter.EVENT_STOP;
         }
 
@@ -630,7 +683,7 @@ const AllView = new Lang.Class({
 
         if (this._availWidth != availWidth || this._availHeight != availHeight || oldNPages != 
this._grid.nPages()) {
             this._adjustment.value = 0;
-            this._currentPage = 0;
+            this._grid.currentPage = 0;
             Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this,
                 function() {
                     this._pageIndicators.setNPages(this._grid.nPages());
@@ -792,6 +845,19 @@ const AppDisplay = new Lang.Class({
         let layout = new ControlsBoxLayout({ homogeneous: true });
         this._controls = new St.Widget({ style_class: 'app-view-controls',
                                          layout_manager: layout });
+        this._controls.connect('notify::mapped', Lang.bind(this,
+            function() {
+                // controls are faded either with their parent or
+                // explicitly in animate(); we can't know how they'll be
+                // shown next, so make sure to restore their opacity
+                // when they are hidden
+                if (this._controls.mapped)
+                  return;
+
+                Tweener.removeTweens(this._controls);
+                this._controls.opacity = 255;
+            }));
+
         layout.hookup_style(this._controls);
         this.actor.add_actor(new St.Bin({ child: this._controls }));
 
@@ -815,6 +881,29 @@ const AppDisplay = new Lang.Class({
         this._updateFrequentVisibility();
     },
 
+    animate: function(animationDirection, onComplete) {
+        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, onComplete);
+    },
+
     _showView: function(activeIndex) {
         for (let i = 0; i < this._views.length; i++) {
             let actor = this._views[i].view.actor;
@@ -964,6 +1053,7 @@ const FolderView = new Lang.Class({
         Util.ensureActorVisibleInScrollView(this.actor, actor);
     },
 
+    // Overriden from BaseAppView
     animate: function(animationDirection) {
         this._grid.animatePulse(animationDirection);
     },
diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js
index 5cd32c1..31d4ea3 100644
--- a/js/ui/iconGrid.js
+++ b/js/ui/iconGrid.js
@@ -18,12 +18,16 @@ 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 AnimationDirection = {
-    IN: 0
+    IN: 0,
+    OUT: 1
 };
 
 const BaseIcon = new Lang.Class({
@@ -420,6 +424,118 @@ const IconGrid = new Lang.Class({
         }
     },
 
+    animateSpring: function(animationDirection, sourceActor) {
+        if (this._animating)
+            return;
+
+        this._animating = true;
+
+        let actors = this._getChildrenToAnimate();
+        if (actors.length == 0) {
+            this._animationDone();
+            return;
+        }
+
+        let [sourceX, sourceY] = sourceActor.get_transformed_position();
+        let [sourceWidth, sourceHeight] = sourceActor.get_size();
+        // Get the center
+        let [sourceCenterX, sourceCenterY] = [sourceX + sourceWidth / 2, sourceY + sourceHeight / 2];
+        // Design decision, 1/2 of the source actor size.
+        let [sourceScaledWidth, sourceScaledHeight] = [sourceWidth / 2, sourceHeight / 2];
+
+        actors.forEach(function(actor) {
+            let [actorX, actorY] = actor._transformedPosition = actor.get_transformed_position();
+            let [x, y] = [actorX - sourceX, actorY - sourceY];
+            actor._distance = Math.sqrt(x * x + y * y);
+        });
+        let maxDist = actors.reduce(function(prev, cur) {
+            return Math.max(prev, cur._distance);
+        }, 0);
+        let minDist = actors.reduce(function(prev, cur) {
+            return Math.min(prev, cur._distance);
+        }, Infinity);
+        let normalization = maxDist - minDist;
+
+        for (let index = 0; index < actors.length; index++) {
+            let actor = actors[index];
+            actor.opacity = 0;
+
+            let actorClone = new Clutter.Clone({ source: actor });
+            Main.uiGroup.add_actor(actorClone);
+
+            let [width, height,,] = this._getAllocatedChildSizeAndSpacing(actor);
+            actorClone.set_size(width, height);
+            let scaleX = sourceScaledWidth / width;
+            let scaleY = sourceScaledHeight / height;
+            let [adjustedSourcePositionX, adjustedSourcePositionY] = [sourceCenterX - sourceScaledWidth / 2, 
sourceY - sourceScaledHeight / 2];
+
+            // Defeat onComplete anonymous function closure
+            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 - (actor._distance - minDist) / normalization) * ANIMATION_MAX_DELAY_FOR_ITEM;
+                let [finalX, finalY]  = actor._transformedPosition;
+                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._animationDone();
+
+                                       actor.opacity = 255;
+                                       actorClone.destroy();
+                                   })};
+                fadeParams = { time: ANIMATION_FADE_IN_TIME_FOR_ITEM,
+                               transition: 'easeInOutQuad',
+                               delay: delay,
+                               opacity: 255 };
+            } else {
+                let [startX, startY]  = actor._transformedPosition;
+                actorClone.set_position(startX, startY);
+
+                let delay = (actor._distance - 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._animationDone();
+                                           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;
+        }
+    },
+
     _getAllocatedChildSizeAndSpacing: function(child) {
         let [,, natWidth, natHeight] = child.get_preferred_size();
         let width = Math.min(this._getHItemSize(), natWidth);
@@ -632,6 +748,7 @@ const PaginatedIconGrid = new Lang.Class({
     _init: function(params) {
         this.parent(params);
         this._nPages = 0;
+        this.currentPage = 0;
         this._rowsPerPage = 0;
         this._spaceBetweenPages = 0;
         this._childrenPerPage = 0;
@@ -695,6 +812,15 @@ const PaginatedIconGrid = new Lang.Class({
         }
     },
 
+    // Overriden from IconGrid
+    _getChildrenToAnimate: function() {
+        let children = this._getVisibleChildren();
+        let firstIndex = this._childrenPerPage * this.currentPage;
+        let lastIndex = firstIndex + this._childrenPerPage;
+
+        return children.slice(firstIndex, lastIndex);
+    },
+
     _computePages: function (availWidthPerPage, availHeightPerPage) {
         let [nColumns, usedWidth] = this._computeLayout(availWidthPerPage);
         let nRows;
diff --git a/js/ui/overview.js b/js/ui/overview.js
index 2affbb2..84c070c 100644
--- a/js/ui/overview.js
+++ b/js/ui/overview.js
@@ -675,6 +675,10 @@ const Overview = new Lang.Class({
             this.hide();
         else
             this.show();
+    },
+
+    getShowAppsButton: function() {
+        return this._dash.showAppsButton;
     }
 });
 Signals.addSignalMethods(Overview.prototype);
diff --git a/js/ui/viewSelector.js b/js/ui/viewSelector.js
index 9b1c604..88ef32c 100644
--- a/js/ui/viewSelector.js
+++ b/js/ui/viewSelector.js
@@ -20,6 +20,7 @@ const ShellEntry = imports.ui.shellEntry;
 const Tweener = imports.ui.tweener;
 const WorkspacesView = imports.ui.workspacesView;
 const EdgeDragAction = imports.ui.edgeDragAction;
+const IconGrid = imports.ui.iconGrid;
 
 const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
 
@@ -301,18 +302,55 @@ const ViewSelector = new Lang.Class({
         return page;
     },
 
-    _fadePageIn: function(oldPage) {
+    _fadePageIn: function() {
+        Tweener.addTween(this._activePage,
+                         { opacity: 255,
+                           time: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
+                           transition: 'easeOutQuad'
+                         });
+    },
+
+    _fadePageOut: function(page) {
+        let oldPage = page;
+        Tweener.addTween(page,
+                         { opacity: 0,
+                           time: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
+                           transition: 'easeOutQuad',
+                           onComplete: Lang.bind(this, function() {
+                               this._animateIn(oldPage);
+                           })
+                         });
+    },
+
+    _animateIn: function(oldPage) {
         if (oldPage)
             oldPage.hide();
 
         this.emit('page-empty');
 
         this._activePage.show();
-        Tweener.addTween(this._activePage,
-            { opacity: 255,
-              time: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
-              transition: 'easeOutQuad'
-            });
+
+        if (this._activePage == this._appsPage && oldPage == this._workspacesPage) {
+            // Restore opacity, in case we animated via _fadePageOut
+            this._activePage.opacity = 255;
+            this.appDisplay.animate(IconGrid.AnimationDirection.IN);
+        } else {
+            this._fadePageIn();
+        }
+    },
+
+    _animateOut: function(page) {
+        let oldPage = page;
+        if (page == this._appsPage &&
+            this._activePage == this._workspacesPage &&
+            !Main.overview.animationInProgress) {
+            this.appDisplay.animate(IconGrid.AnimationDirection.OUT, Lang.bind(this,
+                function() {
+                    this._animateIn(oldPage)
+                }));
+        } else {
+            this._fadePageOut(page);
+        }
     },
 
     _showPage: function(page) {
@@ -327,17 +365,9 @@ const ViewSelector = new Lang.Class({
         this.emit('page-changed');
 
         if (oldPage)
-            Tweener.addTween(oldPage,
-                             { opacity: 0,
-                               time: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
-                               transition: 'easeOutQuad',
-                               onComplete: Lang.bind(this,
-                                   function() {
-                                       this._fadePageIn(oldPage);
-                                   })
-                             });
+            this._animateOut(oldPage)
         else
-            this._fadePageIn();
+            this._animateIn();
     },
 
     _a11yFocusPage: function(page) {


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