Unity 8
TransformedTabletSpreadDelegate.qml
1 /*
2  * Copyright 2014 Canonical Ltd.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation; version 3.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU Lesser General Public License for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  *
16  * Authors: Michael Zanetti <michael.zanetti@canonical.com>
17 */
18 
19 import QtQuick 2.4
20 import Utils 0.1
21 import Ubuntu.Components 1.3
22 import Unity.Application 0.1
23 
24 SpreadDelegate {
25  id: root
26 
27  // Set this to true when this tile is selected in the spread. The animation will change to bring the tile to front.
28  property bool selected: false
29  // Set this to true when another tile in the spread is selected. The animation will change to fade this tile out.
30  property bool otherSelected: false
31  // Set this to true when this tile a currently active on either the MS or the SS.
32  property bool active: false
33 
34  property int zIndex
35  property real progress: 0
36  property real animatedProgress: 0
37 
38  property real startDistance: units.gu(5)
39  property real endDistance: units.gu(.5)
40 
41  property real startScale: 1.1
42  property real endScale: 0.7
43  property real dragStartScale: startScale + .2
44 
45  property real startAngle: 15
46  property real endAngle: 5
47 
48  property bool isInSideStage: false
49 
50  property int stage: ApplicationInfoInterface.MainStage
51 
52  property int dragOffset: 0
53  readonly property alias xTranslateAnimating: xTranslateAnimation.running
54  readonly property bool offScreen: priv.xTranslate >= 0
55 
56  dropShadow: spreadView.active ||
57  (active
58  && (root.stage == ApplicationInfoInterface.MainStage || !priv.shellIsLandscape)
59  && priv.xTranslate != 0)
60 
61  onSelectedChanged: {
62  if (selected) {
63  priv.snapshot();
64  }
65  priv.isSelected = selected;
66  }
67 
68  onOtherSelectedChanged: {
69  if (otherSelected) {
70  priv.snapshot();
71  }
72  priv.otherSelected = otherSelected;
73  }
74 
75  Connections {
76  target: spreadView
77 
78  onPhaseChanged: {
79  if (spreadView.phase == 1) {
80  var phase2Progress = spreadView.positionMarker4 - (root.zIndex * spreadView.tileDistance / spreadView.width);
81  priv.phase2StartTranslate = priv.easingAnimation(0, 1, 0, -spreadView.width + (root.zIndex * root.endDistance), phase2Progress);
82  priv.phase2StartScale = priv.easingAnimation(0, 1, root.startScale, root.endScale, phase2Progress);
83  priv.phase2StartAngle = priv.easingAnimation(0, 1, root.startAngle, root.endAngle, phase2Progress);
84  priv.phase2StartTopMarginProgress = priv.easingAnimation(0, 1, 0, 1, phase2Progress);
85  }
86  }
87  }
88 
89  // Required to be an item because we're using states on it.
90  Item {
91  id: priv
92 
93  // true if this is the next tile on the stack that comes in when dragging from the right
94  property bool nextInStack: spreadView.nextInStack == model.index
95  // true if the next tile in the stack is the nextInStack one. This one will be moved a bit to the left
96  property bool movedActive: spreadView.nextZInStack == zIndex + 1
97  property real animatedEndDistance: linearAnimation(0, 2, root.endDistance, 0, root.progress)
98 
99  property real phase2StartTranslate
100  property real phase2StartScale
101  property real phase2StartAngle
102  property real phase2StartTopMarginProgress
103 
104  property bool isSelected: false
105  property bool otherSelected: false
106  property real selectedProgress
107  property real selectedXTranslate
108  property real selectedAngle
109  property real selectedScale
110  property real selectedOpacity
111  property real selectedTopMarginProgress
112 
113  function snapshot() {
114  selectedProgress = root.progress;
115  selectedXTranslate = xTranslate;
116  selectedAngle = angle;
117  selectedScale = priv.scale;
118  selectedOpacity = priv.opacityTransform;
119  selectedTopMarginProgress = topMarginProgress;
120  }
121 
122  // This calculates how much negative progress there can be if unwinding the spread completely
123  // the progress for each tile starts at 0 when it crosses the right edge, so the later a tile comes in,
124  // the bigger its negativeProgress can be.
125  property real negativeProgress: {
126  if (nextInStack && spreadView.phase < 2) {
127  return 0;
128  }
129  return -root.zIndex * spreadView.tileDistance / spreadView.width;
130  }
131 
132  function linearAnimation(startProgress, endProgress, startValue, endValue, progress) {
133  // progress : progressDiff = value : valueDiff => value = progress * valueDiff / progressDiff
134  return (progress - startProgress) * (endValue - startValue) / (endProgress - startProgress) + startValue;
135  }
136 
137  function easingAnimation(startProgress, endProgress, startValue, endValue, progress) {
138  helperEasingCurve.progress = progress - startProgress;
139  helperEasingCurve.period = endProgress - startProgress;
140  return helperEasingCurve.value * (endValue - startValue) + startValue;
141  }
142 
143  Behavior on xTranslate {
144  enabled: !spreadView.active &&
145  !snapAnimation.running &&
146  root.application.appId !== "unity8-dash" &&
147  spreadView.animateX &&
148  !spreadView.beingResized &&
149  priv.state !== "sideStage"
150  UbuntuNumberAnimation {
151  id: xTranslateAnimation
152  duration: UbuntuAnimation.FastDuration
153  }
154  }
155 
156  property real xTranslate: {
157  var newTranslate = 0;
158 
159  // selected app or opposite stage active app.
160  if (isSelected || (otherSelected && root.active && spreadView.selectedDelegate
161  && spreadView.selectedDelegate.stage !== root.stage)) {
162  if (root.stage == ApplicationInfoInterface.MainStage) {
163  return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.width, root.progress);
164  } else {
165  return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.sideStageWidth, root.progress);
166  }
167  } else if (otherSelected) {
168  return selectedXTranslate;
169  }
170 
171  // The tile should move a bit to the left if a new one comes on top of it, but not for the Side Stage and not
172  // when we're only dragging the side stage in on top of a main stage app
173  var shouldMoveAway = spreadView.nextInStack >= 0 && priv.movedActive && root.stage === ApplicationInfoInterface.MainStage &&
174  topLevelSurfaceList.applicationAt(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage;
175 
176  if (active) {
177  newTranslate -= root.width
178  // Only do the hide animation for active apps in the mainstage, and not if we only drag the ss in
179  if (spreadView.phase == 0 && shouldMoveAway) {
180  newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -units.gu(4), root.animatedProgress);
181  }
182  newTranslate += root.dragOffset;
183  } else if (!spreadView.active && root.application.appId == "unity8-dash") {
184  newTranslate -= root.width;
185  }
186 
187  if (!spreadView.active) {
188  return newTranslate;
189  }
190 
191  var shadowOffset = units.gu(2);
192 
193  if (spreadView.phase == 0) {
194  if (nextInStack) {
195  if (root.stage == ApplicationInfoInterface.MainStage) {
196  if (spreadView.sideStageVisible && root.progress > 0) {
197  // Move it so it appears from behind the side stage immediately
198  newTranslate += -spreadView.sideStageWidth;
199  }
200  }
201 
202  if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
203  // This is when we only drag the side stage in, without rotation or snapping
204  newTranslate = linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth, root.animatedProgress);
205  } else {
206  newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth * spreadView.snapPosition, root.animatedProgress);
207  }
208  } else if (!root.active) {
209  newTranslate += shadowOffset;
210  }
211  }
212 
213  if (spreadView.phase == 1) {
214  if (nextInStack) {
215  if (root.stage == ApplicationInfoInterface.MainStage) {
216  var startValue = -spreadView.sideStageWidth * spreadView.snapPosition + (spreadView.sideStageVisible ? -spreadView.sideStageWidth : 0);
217  newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startValue, priv.phase2StartTranslate, root.animatedProgress);
218  } else {
219  var endValue = -spreadView.width + spreadView.width * root.zIndex / 6;
220  if (!spreadView.sideStageVisible) {
221  newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth, priv.phase2StartTranslate, root.animatedProgress);
222  } else {
223  newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth * spreadView.snapPosition, priv.phase2StartTranslate, root.animatedProgress);
224  }
225  }
226  } else if (root.active) {
227  var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
228  var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
229  var startTranslate = -root.width + (shouldMoveAway ? -units.gu(4) : 0);
230  newTranslate = linearAnimation(startProgress, endProgress, startTranslate, priv.phase2StartTranslate, root.animatedProgress);
231  } else {
232  var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
233  var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
234  newTranslate = linearAnimation(startProgress, endProgress, shadowOffset * Math.max(0, zIndex-2), priv.phase2StartTranslate, root.animatedProgress);
235  }
236  }
237 
238  if (spreadView.phase == 2) {
239  if (easingCurve.value == 0 && !nextInStack) {
240  newTranslate = shadowOffset;
241  } else {
242  newTranslate = -easingCurve.value * (spreadView.width - root.zIndex * animatedEndDistance);
243  }
244  }
245 
246  return newTranslate;
247  }
248 
249  property real scale: {
250  if (!spreadView.active) {
251  return 1;
252  }
253 
254  // selected app or opposite stage active app.
255  if (isSelected || (otherSelected && root.active && spreadView.selectedDelegate
256  && spreadView.selectedDelegate.stage !== root.stage)) {
257  return linearAnimation(selectedProgress, negativeProgress, selectedScale, 1, root.progress);
258  } else if (otherSelected) {
259  return selectedScale;
260  }
261 
262  if (spreadView.phase == 0) {
263  if (nextInStack) {
264  if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
265  return 1;
266  } else {
267  var targetScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
268  return linearAnimation(0, spreadView.positionMarker2, root.dragStartScale, targetScale, root.animatedProgress);
269  }
270  } else {
271  return 1;
272  }
273  }
274 
275  if (spreadView.phase == 1) {
276  if (nextInStack) {
277  var startScale = 1;
278  if (root.stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
279  startScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
280  }
281  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startScale, priv.phase2StartScale, root.animatedProgress);
282  }
283  var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
284  var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
285  return linearAnimation(startProgress, endProgress, 1, priv.phase2StartScale, root.animatedProgress);
286  }
287 
288  if (spreadView.phase == 2) {
289  return root.startScale - easingCurve.value * (root.startScale - root.endScale);
290  }
291 
292  return 1;
293  }
294 
295  property real angle: {
296  if (!spreadView.active) {
297  return 0;
298  }
299 
300  // selected app or opposite stage active app.
301  if (isSelected || (otherSelected && root.active && spreadView.selectedDelegate
302  && spreadView.selectedDelegate.stage !== root.stage)) {
303  return linearAnimation(selectedProgress, negativeProgress, selectedAngle, 0, root.progress);
304  } else if (otherSelected) {
305  return selectedAngle;
306  }
307 
308  // The tile should rotate a bit when another one comes on top, but not when only dragging the side stage in
309  var shouldMoveAway = spreadView.nextInStack == -1 ||
310  spreadView.nextInStack >= 0 && priv.movedActive &&
311  (topLevelSurfaceList.applicationAt(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage ||
312  root.stage == ApplicationInfoInterface.SideStage);
313 
314  if (spreadView.phase == 0) {
315  if (nextInStack) {
316  if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
317  return 0;
318  } else {
319  return linearAnimation(0, spreadView.positionMarker2, root.startAngle, root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
320  }
321  }
322  if (shouldMoveAway) {
323  return linearAnimation(0, spreadView.positionMarker2, 0, root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
324  }
325  }
326  if (spreadView.phase == 1) {
327  if (nextInStack) {
328  if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
329  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, 0, priv.phase2StartAngle, root.animatedProgress);
330  } else {
331  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, root.startAngle * (1-spreadView.snapPosition), priv.phase2StartAngle, root.animatedProgress);
332  }
333  }
334  var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
335  var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
336  var startAngle = shouldMoveAway ? root.startAngle * (1-spreadView.snapPosition) : 0;
337  return linearAnimation(startProgress, endProgress, startAngle, priv.phase2StartAngle, root.progress);
338  }
339  if (spreadView.phase == 2) {
340  return root.startAngle - easingCurve.value * (root.startAngle - root.endAngle);
341  }
342 
343  return 0;
344  }
345 
346  property real opacityTransform: {
347  // animate opacity for items not snapping into view.
348  if (spreadView.phase !== 2) return 1;
349  if (root.isSelected) return 1;
350 
351  if (otherSelected) {
352  if (root.active && spreadView.selectedDelegate
353  && spreadView.selectedDelegate.stage !== root.stage) {
354  return 1;
355  }
356  return linearAnimation(selectedProgress, negativeProgress, selectedOpacity, 0, root.progress);
357  }
358  return 1;
359  }
360 
361  property real topMarginProgress: {
362  // selected app or opposite stage active app.
363  if (isSelected || (otherSelected && root.active && spreadView.selectedDelegate
364  && spreadView.selectedDelegate.stage !== root.stage)) {
365  return linearAnimation(selectedProgress, negativeProgress, selectedTopMarginProgress, 0, root.progress);
366  }
367 
368  if (spreadView.phase == 0) {
369  return 0;
370  } else if (spreadView.phase == 1) {
371  if (nextInStack) {
372  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
373  0, 1, root.progress);
374  }
375  var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
376  var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
377  return linearAnimation(startProgress, endProgress, 0, 1, root.progress);
378  }
379  return 1;
380  }
381 
382  states: [
383  State {
384  name: "sideStage";
385  when: root.isInSideStage && spreadView.shiftedContentX == 0 && spreadView.phase == 0
386  PropertyChanges {
387  target: priv;
388  xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * (1-sideStage.progress)
389  }
390  }
391  ]
392  }
393 
394  transform: [
395  Rotation {
396  origin { x: 0; y: spreadView.height / 2 }
397  axis { x: 0; y: 1; z: 0 }
398  angle: priv.angle
399  },
400  Scale {
401  origin { x: 0; y: spreadView.height / 2 }
402  xScale: priv.scale
403  yScale: xScale
404  },
405  Scale {
406  origin { x: 0; y: (spreadView.height * priv.scale) + maximizedAppTopMargin * 3 }
407  xScale: 1
408  yScale: fullscreen ? 1 - priv.topMarginProgress * maximizedAppTopMargin / spreadView.height : 1
409  },
410  Translate {
411  x: priv.xTranslate
412  }
413  ]
414  opacity: priv.opacityTransform
415 
416  UbuntuNumberAnimation {
417  id: fadeBackInAnimation
418  target: root
419  property: "opacity"
420  duration: UbuntuAnimation.SlowDuration
421  from: 0
422  to: 1
423  }
424 
425  EasingCurve {
426  id: easingCurve
427  type: EasingCurve.OutSine
428  period: 1
429  progress: root.progress
430  }
431 
432  EasingCurve {
433  id: helperEasingCurve
434  type: easingCurve.type
435  period: easingCurve.period
436  }
437 }