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