Unity 8
TransformedSpreadDelegate.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 
23 SpreadDelegate {
24  id: root
25 
26  // Set this to true when this tile is selected in the spread. The animation will change to bring the tile to front.
27  property bool selected: false
28  // Set this to true when another tile in the spread is selected. The animation will change to fade this tile out.
29  property bool otherSelected: false
30 
31  // The progress animates the tiles. A value > 0 makes it appear from the right edge. At 1 it reaches the end position.
32  property real progress: 0
33  // This is required to snap tile 1 during phase 1 and 2.
34  property real animatedProgress: 0
35 
36  property real startAngle: 0
37  property real endAngle: 0
38 
39  property real startScale: 1
40  property real endScale: 1
41 
42  // Specific to just one tile
43  property real tile1StartScale: startScale + .4
44  property real tile0SnapAngle: 10
45 
46  property real startDistance: units.gu(5)
47  property real endDistance: units.gu(.5)
48 
49  // Whether first app is displayed entirely vs just on the spread edge
50  property bool focusFirstApp: true
51 
52  // Whether this delegate is displayed to the user outside of the spread
53  readonly property bool isFocused: focusFirstApp && index === 0
54 
55  // Adjusted index for any spread animation calculations
56  readonly property int spreadIndex: index - (focusFirstApp ? 1 : 0)
57 
58  // How far left this delegate has been translated
59  readonly property alias xTranslate: priv.xTranslate
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  onPhaseChanged: {
78  if (root.focusFirstApp && spreadView.phase == 1) {
79  if (index === 0) {
80  priv.phase2startTranslate = priv.easingAnimation(0, spreadView.positionMarker4, 0, -spreadView.width, spreadView.positionMarker4) + spreadView.width;
81  priv.phase2startAngle = priv.easingAnimation(0, spreadView.positionMarker4, root.startAngle, root.endAngle, spreadView.positionMarker4);
82  priv.phase2startScale = priv.easingAnimation(0, spreadView.positionMarker4, root.startScale, root.endScale, spreadView.positionMarker4);
83  priv.phase2startTopMarginProgress = priv.easingAnimation(0, 1, 0, 1, spreadView.positionMarker4);
84  } else if (index == 1) {
85  // find where the main easing for Tile 1 would be when reaching phase 2
86  var phase2Progress = spreadView.positionMarker4 - spreadView.tileDistance / spreadView.width;
87  priv.phase2startTranslate = priv.easingAnimation(0, phase2Progress, 0, -spreadView.width + root.endDistance, phase2Progress);
88  priv.phase2startAngle = priv.easingAnimation(0, phase2Progress, root.startAngle, root.endAngle, phase2Progress);
89  priv.phase2startScale = priv.easingAnimation(0, phase2Progress, root.startScale, root.endScale, phase2Progress);
90  priv.phase2startTopMarginProgress = priv.easingAnimation(0, 1, 0, spreadView.positionMarker4, phase2Progress);
91  }
92  }
93  }
94  }
95 
96  QtObject {
97  id: priv
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  // Those values are needed as target values for the end of phase 1.
108  // As they are static values, lets calculate them once when entering phase 1 instead of calculating them in each animation pass.
109  property real phase2startTranslate
110  property real phase2startAngle
111  property real phase2startScale
112  property real phase2startTopMarginProgress
113 
114  function snapshot() {
115  selectedProgress = root.progress;
116  selectedXTranslate = xTranslate;
117  selectedAngle = angle;
118  selectedScale = scale;
119  selectedOpacity = opacity;
120  selectedTopMarginProgress = topMarginProgress;
121  }
122 
123  function linearAnimation(startProgress, endProgress, startValue, endValue, progress) {
124  // progress : progressDiff = value : valueDiff => value = progress * valueDiff / progressDiff
125  return (progress - startProgress) * (endValue - startValue) / (endProgress - startProgress) + startValue;
126  }
127 
128  function easingAnimation(startProgress, endProgress, startValue, endValue, progress) {
129  helperEasingCurve.progress = progress - startProgress;
130  helperEasingCurve.period = endProgress - startProgress;
131  return helperEasingCurve.value * (endValue - startValue) + startValue;
132  }
133 
134  // The following blocks handle the animation of the tile in the spread.
135  // At the beginning, each tile is attached at the right edge, outside the screen.
136  // The progress for each tile starts at 0 and it reaches its end position at a progress of 1.
137  // The first phases are handled special for the first 2 tiles. as we do the alt-tab and snapping
138  // in there. Once we reached phase 3, the animation is the same for all tiles.
139  // When a tile is selected, the animation state is snapshotted, and the spreadView is unwound.
140  // All tiles are kept in place and faded out to 0 opacity except
141  // the selected tile, which is animated from the snapshotted position to be fullscreen.
142 
143  readonly property real xTranslate: {
144  if (!spreadView.active) {
145  return 0;
146  }
147 
148  if (otherSelected) {
149  if (spreadView.phase < 2 && root.isFocused) {
150  return linearAnimation(selectedProgress, 0, selectedXTranslate,
151  selectedXTranslate - spreadView.tileDistance, root.progress);
152  }
153 
154  return selectedXTranslate;
155  }
156 
157  if (root.focusFirstApp && index === 0) {
158  if (spreadView.phase == 0) {
159  return Math.min(0, linearAnimation(0, spreadView.positionMarker2,
160  0, -spreadView.width * .25, root.animatedProgress));
161  } else if (spreadView.phase == 1){
162  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
163  -spreadView.width * .25, priv.phase2startTranslate, root.progress);
164  } else if (!priv.isSelected){ // phase 2
165  // Apply the same animation as with the rest but add spreadView.width to align it with the others.
166  return -easingCurve.value * spreadView.width + spreadView.width;
167  } else {
168  return linearAnimation(selectedProgress, 0, selectedXTranslate, 0, root.progress);
169  }
170  } else if (root.focusFirstApp && index === 1) {
171  if (spreadView.phase == 0 && !priv.isSelected) {
172  return linearAnimation(0, spreadView.positionMarker2,
173  0, -spreadView.width * spreadView.snapPosition, root.animatedProgress);
174  } else if (spreadView.phase == 1 && !priv.isSelected) {
175  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
176  -spreadView.width * spreadView.snapPosition, priv.phase2startTranslate,
177  root.progress);
178  }
179  }
180 
181  if (priv.isSelected) {
182  // Distance to left edge
183  var targetTranslate = -spreadView.width - (root.spreadIndex * root.startDistance);
184  return linearAnimation(selectedProgress, 0,
185  selectedXTranslate, targetTranslate, root.progress);
186  }
187 
188  // Fix it at the right edge...
189  var rightEdgeOffset = -(root.spreadIndex * root.startDistance);
190  // ...and use our easing to move them to the left. Stop a bit earlier for each tile
191  var animatedEndDistance = linearAnimation(0, 2, root.endDistance, 0, root.progress);
192  return -easingCurve.value * spreadView.width + (index * animatedEndDistance) + rightEdgeOffset;
193 
194  }
195 
196  readonly property real angle: {
197  if (!spreadView.active) {
198  return 0;
199  }
200 
201  if (priv.otherSelected) {
202  return priv.selectedAngle;
203  }
204  if (priv.isSelected) {
205  return linearAnimation(selectedProgress, 0, selectedAngle, 0, root.progress);
206  }
207  if (root.focusFirstApp && index === 0) {
208  if (spreadView.phase == 0) {
209  return Math.max(0, linearAnimation(0, spreadView.positionMarker2,
210  0, root.tile0SnapAngle, root.animatedProgress));
211  } else if (spreadView.phase == 1) {
212  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
213  root.tile0SnapAngle, phase2startAngle, root.progress);
214  }
215  } else if (root.focusFirstApp && index === 1) {
216  if (spreadView.phase == 0) {
217  return linearAnimation(0, spreadView.positionMarker2, root.startAngle,
218  root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
219  } else if (spreadView.phase == 1) {
220  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
221  root.startAngle * (1-spreadView.snapPosition), priv.phase2startAngle,
222  root.progress);
223  }
224  }
225  return root.startAngle - easingCurve.value * (root.startAngle - root.endAngle);
226  }
227 
228  readonly property real scale: {
229  if (!spreadView.active) {
230  return 1;
231  }
232  if (priv.otherSelected) {
233  return priv.selectedScale;
234  }
235  if (priv.isSelected) {
236  return linearAnimation(selectedProgress, 0, selectedScale, 1, root.progress);
237  }
238 
239  if (root.focusFirstApp && index === 0) {
240  if (spreadView.phase == 0) {
241  return 1;
242  } else if (spreadView.phase == 1) {
243  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
244  1, phase2startScale, root.progress);
245  }
246  } else if (root.focusFirstApp && index === 1) {
247  if (spreadView.phase == 0) {
248  var targetScale = tile1StartScale - ((tile1StartScale - 1) * spreadView.snapPosition);
249  return linearAnimation(0, spreadView.positionMarker2,
250  root.tile1StartScale, targetScale, root.animatedProgress);
251  } else if (spreadView.phase == 1) {
252  var startScale = tile1StartScale - ((tile1StartScale - 1) * spreadView.snapPosition);
253  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
254  startScale, priv.phase2startScale, root.progress);
255  }
256  }
257  return root.startScale - easingCurve.value * (root.startScale - root.endScale);
258  }
259 
260  readonly property real opacity: {
261  if (priv.otherSelected) {
262  return linearAnimation (selectedProgress, Math.max(0, selectedProgress - .5),
263  selectedOpacity, 0, root.progress);
264  }
265  if (root.isFocused) {
266  switch (spreadView.phase) {
267  case 0:
268  return linearAnimation(0, spreadView.positionMarker2, 1, .7, root.animatedProgress);
269  case 1:
270  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
271  .7, 1, root.animatedProgress);
272  }
273  }
274 
275  return 1;
276  }
277 
278  readonly property real topMarginProgress: {
279  if (priv.isSelected) {
280  return linearAnimation(selectedProgress, 0, selectedTopMarginProgress, 0, root.progress);
281  }
282 
283  if (root.focusFirstApp && index === 0) {
284  if (spreadView.phase == 0) {
285  return 0;
286  } else if (spreadView.phase == 1) {
287  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
288  0, priv.phase2startTopMarginProgress, root.progress);
289  }
290  } else if (root.focusFirstApp && index === 1) {
291  if (spreadView.phase == 0) {
292  return 0;
293  } else if (spreadView.phase == 1) {
294  return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4,
295  0, priv.phase2startTopMarginProgress, root.progress);
296  }
297  }
298 
299  return easingCurve.value;
300  }
301  }
302 
303  transform: [
304  Rotation {
305  origin { x: 0; y: spreadView.height / 2 }
306  axis { x: 0; y: 1; z: 0 }
307  angle: priv.angle
308  },
309 
310  // The next two transformations are to ensure that fullscreen and rotated
311  // windows all have the same size and position as an unrotated (0 degrees)
312  // non-fullscreen window when they're stacked on top of each other on the
313  // far left of the spread.
314  Translate {
315  y: !fullscreen && appWindowRotation === 180
316  ? priv.topMarginProgress * maximizedAppTopMargin
317  : 0
318  },
319  Scale {
320  origin {
321  x: appWindowRotation === 270 ? spreadView.width : 0
322  y: spreadView.height
323  }
324 
325  xScale: {
326  switch (appWindowRotation) {
327  case 90:
328  case 270:
329  if (fullscreen) {
330  return 1;
331  } else {
332  return 1 + priv.topMarginProgress * maximizedAppTopMargin / spreadView.width;
333  }
334  break;
335  default:
336  return 1;
337  }
338  }
339 
340  yScale: {
341  switch (appWindowRotation) {
342  case 0:
343  if (fullscreen) {
344  return 1 - priv.topMarginProgress * maximizedAppTopMargin / spreadView.height;
345  } else {
346  return 1;
347  }
348  break;
349  case 90:
350  case 270:
351  return 1 - priv.topMarginProgress * maximizedAppTopMargin / spreadView.height;
352  break;
353  default:
354  return 1;
355  }
356  }
357  },
358 
359  Scale {
360  origin { x: 0; y: spreadView.height / 2 }
361  xScale: priv.scale
362  yScale: xScale
363  },
364  Translate {
365  x: priv.xTranslate
366  }
367  ]
368 
369  opacity: spreadView.phase == 2 ? priv.opacity : 1
370 
371  Rectangle {
372  anchors.fill: parent
373  color: "black"
374  opacity: spreadView.phase < 2 ? 1 - priv.opacity : 0
375  }
376 
377  EasingCurve {
378  id: easingCurve
379  type: EasingCurve.OutSine
380  period: 1 - spreadView.positionMarker2
381  progress: root.progress
382  }
383 
384  // This is used as a calculation helper to figure values for progress other than the current one
385  // Do not bind anything to this...
386  EasingCurve {
387  id: helperEasingCurve
388  type: easingCurve.type
389  period: easingCurve.period
390  }
391 }