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.0
20 import Utils 0.1
21 import Ubuntu.Components 0.1
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 dragOffset: 0
51 
52  dropShadow: spreadView.active ||
53  (active && model.stage == ApplicationInfoInterface.MainStage && priv.xTranslate != 0)
54 
55  onSelectedChanged: {
56  if (selected) {
57  priv.snapshot();
58  }
59  priv.isSelected = selected;
60  }
61 
62  onOtherSelectedChanged: {
63  if (otherSelected) {
64  priv.snapshot();
65  }
66  priv.otherSelected = otherSelected;
67  }
68 
69  Connections {
70  target: spreadView
71 
72  onPhaseChanged: {
73  if (spreadView.phase == 1) {
74  var phase2Progress = spreadView.positionMarker4 - (root.zIndex * spreadView.tileDistance / spreadView.width);
75  priv.phase2StartTranslate = priv.easingAnimation(0, 1, 0, -spreadView.width + (root.zIndex * root.endDistance), phase2Progress);
76  priv.phase2StartScale = priv.easingAnimation(0, 1, root.startScale, root.endScale, phase2Progress);
77  priv.phase2StartAngle = priv.easingAnimation(0, 1, root.startAngle, root.endAngle, phase2Progress);
78  priv.phase2StartTopMarginProgress = priv.easingAnimation(0, 1, 0, 1, phase2Progress);
79  }
80  }
81  }
82 
83  // Required to be an item because we're using states on it.
84  Item {
85  id: priv
86 
87  // true if this is the next tile on the stack that comes in when dragging from the right
88  property bool nextInStack: spreadView.nextZInStack == zIndex
89  // true if the next tile in the stack is the nextInStack one. This one will be moved a bit to the left
90  property bool movedActive: spreadView.nextZInStack == zIndex + 1
91  property real animatedEndDistance: linearAnimation(0, 2, root.endDistance, 0, root.progress)
92 
93  property real phase2StartTranslate
94  property real phase2StartScale
95  property real phase2StartAngle
96  property real phase2StartTopMarginProgress
97 
98  property bool isSelected: false
99  property bool otherSelected: false
100  property real selectedProgress
101  property real selectedXTranslate
102  property real selectedAngle
103  property real selectedScale
104  property real selectedOpacity
105  property real selectedTopMarginProgress
106 
107  function snapshot() {
108  selectedProgress = root.progress;
109  selectedXTranslate = xTranslate;
110  selectedAngle = angle;
111  selectedScale = priv.scale;
112  selectedOpacity = priv.opacityTransform;
113  selectedTopMarginProgress = topMarginProgress;
114  }
115 
116  // This calculates how much negative progress there can be if unwinding the spread completely
117  // the progress for each tile starts at 0 when it crosses the right edge, so the later a tile comes in,
118  // the bigger its negativeProgress can be.
119  property real negativeProgress: {
120  if (nextInStack && spreadView.phase < 2) {
121  return 0;
122  }
123  return -root.zIndex * spreadView.tileDistance / spreadView.width;
124  }
125 
126  function linearAnimation(startProgress, endProgress, startValue, endValue, progress) {
127  // progress : progressDiff = value : valueDiff => value = progress * valueDiff / progressDiff
128  return (progress - startProgress) * (endValue - startValue) / (endProgress - startProgress) + startValue;
129  }
130 
131  function easingAnimation(startProgress, endProgress, startValue, endValue, progress) {
132  helperEasingCurve.progress = progress - startProgress;
133  helperEasingCurve.period = endProgress - startProgress;
134  return helperEasingCurve.value * (endValue - startValue) + startValue;
135  }
136 
137  Behavior on xTranslate {
138  enabled: !spreadView.active &&
139  !snapAnimation.running &&
140  model.appId !== "unity8-dash" &&
141  !spreadView.sideStageDragging &&
142  spreadView.animateX
143  UbuntuNumberAnimation {
144  duration: UbuntuAnimation.FastDuration
145  }
146  }
147 
148  property real xTranslate: {
149  var newTranslate = 0;
150 
151  if (otherSelected) {
152  return priv.selectedXTranslate;
153  }
154 
155  if (isSelected) {
156  if (model.stage == ApplicationInfoInterface.MainStage) {
157  return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.width, root.progress);
158  } else {
159  return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.sideStageWidth, root.progress);
160  }
161  }
162 
163  // 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
164  // when we're only dragging the side stage in on top of a main stage app
165  var shouldMoveAway = spreadView.nextInStack >= 0 && priv.movedActive && model.stage === ApplicationInfoInterface.MainStage &&
166  ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage;
167 
168  if (active) {
169  newTranslate -= root.width
170  // Only do the hide animation for active apps in the mainstage, and not if we only drag the ss in
171  if (spreadView.phase == 0 && shouldMoveAway) {
172  newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -units.gu(4), root.animatedProgress);
173  }
174  newTranslate += root.dragOffset;
175  }
176  if (!spreadView.active && model.appId == "unity8-dash" && !root.active) {
177  newTranslate -= root.width;
178  }
179 
180  if (nextInStack && spreadView.phase == 0) {
181  if (model.stage == ApplicationInfoInterface.MainStage) {
182  if (spreadView.sideStageVisible && root.progress > 0) {
183  // Move it so it appears from behind the side stage immediately
184  newTranslate += -spreadView.sideStageWidth;
185  }
186  }
187 
188  if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
189  // This is when we only drag the side stage in, without rotation or snapping
190  newTranslate = linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth, root.progress);
191  } else {
192  newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth * spreadView.snapPosition, root.animatedProgress);
193  }
194  }
195 
196  if (spreadView.phase == 1) {
197  if (nextInStack) {
198  if (model.stage == ApplicationInfoInterface.MainStage) {
199  var startValue = -spreadView.sideStageWidth * spreadView.snapPosition + (spreadView.sideStageVisible ? -spreadView.sideStageWidth : 0);
200  newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startValue, priv.phase2StartTranslate, root.animatedProgress);
201  } else {
202  var endValue = -spreadView.width + spreadView.width * root.zIndex / 6;
203  if (!spreadView.sideStageVisible) {
204  newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth, priv.phase2StartTranslate, root.progress);
205  } else {
206  newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, -spreadView.sideStageWidth * spreadView.snapPosition, priv.phase2StartTranslate, root.animatedProgress);
207  }
208  }
209  } else if (root.active) {
210  var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
211  var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
212  var startTranslate = -root.width + (shouldMoveAway ? -units.gu(4) : 0);
213  newTranslate = linearAnimation(startProgress, endProgress, startTranslate, priv.phase2StartTranslate, root.progress);
214  } else {
215  var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
216  var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
217  newTranslate = linearAnimation(startProgress, endProgress, 0, priv.phase2StartTranslate, root.progress);
218  }
219  }
220 
221  if (spreadView.phase == 2) {
222  newTranslate = -easingCurve.value * (spreadView.width - root.zIndex * animatedEndDistance);
223  }
224 
225  return newTranslate;
226  }
227 
228  property real scale: {
229  if (!spreadView.active) {
230  return 1;
231  }
232 
233  if (otherSelected) {
234  return selectedScale;
235  }
236 
237  if (isSelected) {
238  return linearAnimation(selectedProgress, negativeProgress, selectedScale, 1, root.progress);
239  }
240 
241  if (spreadView.phase == 0) {
242  if (nextInStack) {
243  if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
244  return 1;
245  } else {
246  var targetScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
247  return linearAnimation(0, spreadView.positionMarker2, root.dragStartScale, targetScale, root.animatedProgress);
248  }
249  } else if (active) {
250  return 1;
251  } else {
252  return linearAnimation(0, spreadView.positionMarker2, root.startScale, root.endScale, root.progress);
253  }
254  }
255 
256  if (spreadView.phase == 1) {
257  if (nextInStack) {
258  var startScale = 1;
259  if (model.stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
260  startScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
261  }
262  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startScale, priv.phase2StartScale, root.animatedProgress);
263  }
264  var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
265  var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
266  return linearAnimation(startProgress, endProgress, 1, priv.phase2StartScale, root.animatedProgress);
267  }
268 
269  if (spreadView.phase == 2) {
270  return root.startScale - easingCurve.value * (root.startScale - root.endScale);
271  }
272 
273  return 1;
274  }
275 
276  property real angle: {
277  if (!spreadView.active) {
278  return 0;
279  }
280 
281  if (otherSelected) {
282  return selectedAngle;
283  }
284  if (isSelected) {
285  return linearAnimation(selectedProgress, negativeProgress, selectedAngle, 0, root.progress);
286  }
287 
288  // The tile should rotate a bit when another one comes on top, but not when only dragging the side stage in
289  var shouldMoveAway = spreadView.nextInStack >= 0 && movedActive &&
290  (ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage ||
291  model.stage == ApplicationInfoInterface.SideStage);
292 
293  if (spreadView.phase == 0) {
294  if (nextInStack) {
295  if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
296  return 0;
297  } else {
298  return linearAnimation(0, spreadView.positionMarker2, root.startAngle, root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
299  }
300  }
301  if (shouldMoveAway) {
302  return linearAnimation(0, spreadView.positionMarker2, 0, root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
303  }
304  }
305  if (spreadView.phase == 1) {
306  if (nextInStack) {
307  if (model.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
308  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, 0, priv.phase2StartAngle, root.animatedProgress);
309  } else {
310  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, root.startAngle * (1-spreadView.snapPosition), priv.phase2StartAngle, root.animatedProgress);
311  }
312  }
313  var startProgress = spreadView.positionMarker2 - (zIndex * spreadView.positionMarker2 / 2);
314  var endProgress = spreadView.positionMarker4 - (zIndex * spreadView.tileDistance / spreadView.width);
315  var startAngle = shouldMoveAway ? root.startAngle * (1-spreadView.snapPosition) : 0;
316  return linearAnimation(startProgress, endProgress, startAngle, priv.phase2StartAngle, root.progress);
317  }
318  if (spreadView.phase == 2) {
319  return root.startAngle - easingCurve.value * (root.startAngle - root.endAngle);
320  }
321 
322  return 0;
323  }
324 
325  property real opacityTransform: {
326  if (otherSelected && spreadView.phase == 2) {
327  return linearAnimation(selectedProgress, negativeProgress, selectedOpacity, 0, root.progress);
328  }
329 
330  return 1;
331  }
332 
333  property real topMarginProgress: {
334  if (priv.isSelected) {
335  return linearAnimation(selectedProgress, negativeProgress, selectedTopMarginProgress, 0, root.progress);
336  }
337  switch (spreadView.phase) {
338  case 0:
339  return 0;
340  case 1:
341  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
342  0, priv.phase2StartTopMarginProgress, root.progress);
343  }
344  return 1;
345  }
346 
347  states: [
348  State {
349  name: "sideStageDragging"; when: spreadView.sideStageDragging && root.isInSideStage
350  PropertyChanges { target: priv; xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * spreadView.sideStageDragProgress }
351  }
352  ]
353  }
354 
355  transform: [
356  Rotation {
357  origin { x: 0; y: spreadView.height / 2 }
358  axis { x: 0; y: 1; z: 0 }
359  angle: priv.angle
360  },
361  Scale {
362  origin { x: 0; y: spreadView.height / 2 }
363  xScale: priv.scale
364  yScale: xScale
365  },
366  Scale {
367  origin { x: 0; y: (spreadView.height * priv.scale) + maximizedAppTopMargin * 3 }
368  xScale: 1
369  yScale: fullscreen ? 1 - priv.topMarginProgress * maximizedAppTopMargin / spreadView.height : 1
370  },
371  Translate {
372  x: priv.xTranslate
373  }
374  ]
375  opacity: priv.opacityTransform
376 
377  UbuntuNumberAnimation {
378  id: fadeBackInAnimation
379  target: root
380  property: "opacity"
381  duration: UbuntuAnimation.SlowDuration
382  from: 0
383  to: 1
384  }
385 
386  EasingCurve {
387  id: easingCurve
388  type: EasingCurve.OutSine
389  period: 1
390  progress: root.progress
391  }
392 
393  EasingCurve {
394  id: helperEasingCurve
395  type: easingCurve.type
396  period: easingCurve.period
397  }
398 }