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
This commit is contained in:
Carlos Soriano 2014-06-17 19:10:54 +02:00
parent 9c474a635a
commit f160dda187
4 changed files with 279 additions and 29 deletions

View File

@ -34,7 +34,9 @@ const MIN_COLUMNS = 4;
const MIN_ROWS = 4; const MIN_ROWS = 4;
const INACTIVE_GRID_OPACITY = 77; 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 FOLDER_SUBICON_FRACTION = .4;
const MIN_FREQUENT_APPS_COUNT = 3; const MIN_FREQUENT_APPS_COUNT = 3;
@ -177,6 +179,38 @@ const BaseAppView = new Lang.Class({
this.selectApp(id); 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); Signals.addSignalMethods(BaseAppView.prototype);
@ -323,7 +357,7 @@ const AllView = new Lang.Class({
this._stack = new St.Widget({ layout_manager: new Clutter.BinLayout() }); this._stack = new St.Widget({ layout_manager: new Clutter.BinLayout() });
let box = new St.BoxLayout({ vertical: true }); let box = new St.BoxLayout({ vertical: true });
this._currentPage = 0; this._grid.currentPage = 0;
this._stack.add_actor(this._grid.actor); this._stack.add_actor(this._grid.actor);
this._eventBlocker = new St.Widget({ x_expand: true, y_expand: true }); this._eventBlocker = new St.Widget({ x_expand: true, y_expand: true });
this._stack.add_actor(this._eventBlocker); this._stack.add_actor(this._eventBlocker);
@ -455,14 +489,33 @@ const AllView = new Lang.Class({
this._refilterApps(); 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() { getCurrentPageY: function() {
return this._grid.getPageY(this._currentPage); return this._grid.getPageY(this._grid.currentPage);
}, },
goToPage: function(pageNumber) { goToPage: function(pageNumber) {
pageNumber = clamp(pageNumber, 0, this._grid.nPages() - 1); 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; return;
if (this._displayingPopup && this._currentPopup) if (this._displayingPopup && this._currentPopup)
this._currentPopup.popdown(); this._currentPopup.popdown();
@ -482,7 +535,7 @@ const AllView = new Lang.Class({
let time; let time;
// Only take the velocity into account on page changes, otherwise // Only take the velocity into account on page changes, otherwise
// return smoothly to the current page using the default velocity // 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); let minVelocity = totalHeight / (PAGE_SWITCH_TIME * 1000);
velocity = Math.max(minVelocity, velocity); velocity = Math.max(minVelocity, velocity);
time = (diffToPage / velocity) / 1000; time = (diffToPage / velocity) / 1000;
@ -493,9 +546,9 @@ const AllView = new Lang.Class({
// longer than PAGE_SWITCH_TIME // longer than PAGE_SWITCH_TIME
time = Math.min(time, PAGE_SWITCH_TIME); time = Math.min(time, PAGE_SWITCH_TIME);
this._currentPage = pageNumber; this._grid.currentPage = pageNumber;
Tweener.addTween(this._adjustment, Tweener.addTween(this._adjustment,
{ value: this._grid.getPageY(this._currentPage), { value: this._grid.getPageY(this._grid.currentPage),
time: time, time: time,
transition: 'easeOutQuad' }); transition: 'easeOutQuad' });
this._pageIndicators.setCurrentPage(pageNumber); this._pageIndicators.setCurrentPage(pageNumber);
@ -524,9 +577,9 @@ const AllView = new Lang.Class({
let direction = event.get_scroll_direction(); let direction = event.get_scroll_direction();
if (direction == Clutter.ScrollDirection.UP) if (direction == Clutter.ScrollDirection.UP)
this.goToPage(this._currentPage - 1); this.goToPage(this._grid.currentPage - 1);
else if (direction == Clutter.ScrollDirection.DOWN) else if (direction == Clutter.ScrollDirection.DOWN)
this.goToPage(this._currentPage + 1); this.goToPage(this._grid.currentPage + 1);
return Clutter.EVENT_STOP; return Clutter.EVENT_STOP;
}, },
@ -566,10 +619,10 @@ const AllView = new Lang.Class({
return Clutter.EVENT_STOP; return Clutter.EVENT_STOP;
if (event.get_key_symbol() == Clutter.Page_Up) { if (event.get_key_symbol() == Clutter.Page_Up) {
this.goToPage(this._currentPage - 1); this.goToPage(this._grid.currentPage - 1);
return Clutter.EVENT_STOP; return Clutter.EVENT_STOP;
} else if (event.get_key_symbol() == Clutter.Page_Down) { } else if (event.get_key_symbol() == Clutter.Page_Down) {
this.goToPage(this._currentPage + 1); this.goToPage(this._grid.currentPage + 1);
return Clutter.EVENT_STOP; return Clutter.EVENT_STOP;
} }
@ -630,7 +683,7 @@ const AllView = new Lang.Class({
if (this._availWidth != availWidth || this._availHeight != availHeight || oldNPages != this._grid.nPages()) { if (this._availWidth != availWidth || this._availHeight != availHeight || oldNPages != this._grid.nPages()) {
this._adjustment.value = 0; this._adjustment.value = 0;
this._currentPage = 0; this._grid.currentPage = 0;
Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this,
function() { function() {
this._pageIndicators.setNPages(this._grid.nPages()); this._pageIndicators.setNPages(this._grid.nPages());
@ -792,6 +845,19 @@ const AppDisplay = new Lang.Class({
let layout = new ControlsBoxLayout({ homogeneous: true }); let layout = new ControlsBoxLayout({ homogeneous: true });
this._controls = new St.Widget({ style_class: 'app-view-controls', this._controls = new St.Widget({ style_class: 'app-view-controls',
layout_manager: layout }); 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); layout.hookup_style(this._controls);
this.actor.add_actor(new St.Bin({ child: this._controls })); this.actor.add_actor(new St.Bin({ child: this._controls }));
@ -815,6 +881,29 @@ const AppDisplay = new Lang.Class({
this._updateFrequentVisibility(); 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) { _showView: function(activeIndex) {
for (let i = 0; i < this._views.length; i++) { for (let i = 0; i < this._views.length; i++) {
let actor = this._views[i].view.actor; let actor = this._views[i].view.actor;
@ -964,6 +1053,7 @@ const FolderView = new Lang.Class({
Util.ensureActorVisibleInScrollView(this.actor, actor); Util.ensureActorVisibleInScrollView(this.actor, actor);
}, },
// Overriden from BaseAppView
animate: function(animationDirection) { animate: function(animationDirection) {
this._grid.animatePulse(animationDirection); this._grid.animatePulse(animationDirection);
}, },

View File

@ -18,12 +18,16 @@ const MIN_ICON_SIZE = 16;
const EXTRA_SPACE_ANIMATION_TIME = 0.25; const EXTRA_SPACE_ANIMATION_TIME = 0.25;
const ANIMATION_TIME_IN = 0.350; 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_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 ANIMATION_BOUNCE_ICON_SCALE = 1.1;
const AnimationDirection = { const AnimationDirection = {
IN: 0 IN: 0,
OUT: 1
}; };
const BaseIcon = new Lang.Class({ 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) { _getAllocatedChildSizeAndSpacing: function(child) {
let [,, natWidth, natHeight] = child.get_preferred_size(); let [,, natWidth, natHeight] = child.get_preferred_size();
let width = Math.min(this._getHItemSize(), natWidth); let width = Math.min(this._getHItemSize(), natWidth);
@ -632,6 +748,7 @@ const PaginatedIconGrid = new Lang.Class({
_init: function(params) { _init: function(params) {
this.parent(params); this.parent(params);
this._nPages = 0; this._nPages = 0;
this.currentPage = 0;
this._rowsPerPage = 0; this._rowsPerPage = 0;
this._spaceBetweenPages = 0; this._spaceBetweenPages = 0;
this._childrenPerPage = 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) { _computePages: function (availWidthPerPage, availHeightPerPage) {
let [nColumns, usedWidth] = this._computeLayout(availWidthPerPage); let [nColumns, usedWidth] = this._computeLayout(availWidthPerPage);
let nRows; let nRows;

View File

@ -675,6 +675,10 @@ const Overview = new Lang.Class({
this.hide(); this.hide();
else else
this.show(); this.show();
},
getShowAppsButton: function() {
return this._dash.showAppsButton;
} }
}); });
Signals.addSignalMethods(Overview.prototype); Signals.addSignalMethods(Overview.prototype);

View File

@ -20,6 +20,7 @@ const ShellEntry = imports.ui.shellEntry;
const Tweener = imports.ui.tweener; const Tweener = imports.ui.tweener;
const WorkspacesView = imports.ui.workspacesView; const WorkspacesView = imports.ui.workspacesView;
const EdgeDragAction = imports.ui.edgeDragAction; const EdgeDragAction = imports.ui.edgeDragAction;
const IconGrid = imports.ui.iconGrid;
const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings'; const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
@ -301,18 +302,55 @@ const ViewSelector = new Lang.Class({
return page; 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) if (oldPage)
oldPage.hide(); oldPage.hide();
this.emit('page-empty'); this.emit('page-empty');
this._activePage.show(); this._activePage.show();
Tweener.addTween(this._activePage,
{ opacity: 255, if (this._activePage == this._appsPage && oldPage == this._workspacesPage) {
time: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME, // Restore opacity, in case we animated via _fadePageOut
transition: 'easeOutQuad' 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) { _showPage: function(page) {
@ -327,17 +365,9 @@ const ViewSelector = new Lang.Class({
this.emit('page-changed'); this.emit('page-changed');
if (oldPage) if (oldPage)
Tweener.addTween(oldPage, this._animateOut(oldPage)
{ opacity: 0,
time: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
transition: 'easeOutQuad',
onComplete: Lang.bind(this,
function() {
this._fadePageIn(oldPage);
})
});
else else
this._fadePageIn(); this._animateIn();
}, },
_a11yFocusPage: function(page) { _a11yFocusPage: function(page) {