[gnome-shell/wip/swarm: 10/14] appDisplay: Animate AllView and FrequentView
- From: Carlos Soriano <csoriano src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell/wip/swarm: 10/14] appDisplay: Animate AllView and FrequentView
- Date: Sat, 30 Aug 2014 11:59:09 +0000 (UTC)
commit d0c260f8ced8a827496de4ec5f47e11b3347a7cb
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 Müllner for the debugging work
https://bugzilla.gnome.org/show_bug.cgi?id=734726
js/ui/appDisplay.js | 122 +++++++++++++++++++++++++++++++++++++++++++-
js/ui/iconGrid.js | 137 +++++++++++++++++++++++++++++++++++++++++++++++--
js/ui/viewSelector.js | 28 ++++++++--
3 files changed, 275 insertions(+), 12 deletions(-)
---
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index e1edf43..c80f1bc 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,34 @@ 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,
+ onComplete: Lang.bind(this, function() {
+ // Restore opacity for the case where
+ // animate is not called the next time to
+ // show the view
+ if (animationDirection == IconGrid.AnimationDirection.OUT)
+ this._controls.opacity = 255;
+ })
+ });
+ 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 8f69f5b..a2170da 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 different
* 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]