Unity 8
 All Classes Functions Properties
Carousel.qml
1 /*
2  * Copyright (C) 2013 Canonical, Ltd.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU 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 General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 import QtQuick 2.0
18 import Ubuntu.Components 0.1
19 import "carousel.js" as CarouselJS
20 
24 Item {
25  id: carousel
26 
27  clip: true // Don't leak horizontally to other dashes
28 
30  property Component itemComponent
32  property alias model: listView.model
34  property alias minimumTileWidth: listView.minimumTileWidth
36  property alias pathItemCount: listView.pathItemCount
38  property alias tileAspectRatio: listView.tileAspectRatio
40  property alias cacheBuffer: listView.cacheBuffer
45  property int drawBuffer: width / pathItemCount // an "ok" value - but values used from the listView cause loops
47  property real selectedItemScaleFactor: 1.1
49  property alias highlightIndex: listView.highlightIndex
51  readonly property alias currentItem: listView.currentItem
53  readonly property alias verticalSpacing: listView.verticalMargin
54 
58  signal clicked(int index, real itemY)
59 
60 
61  signal pressAndHold(int index, real itemY)
64 
65  implicitHeight: listView.tileHeight * selectedItemScaleFactor
66  opacity: listView.highlightIndex === -1 ? 1 : 0.6
67 
68  /* Basic idea behind the carousel effect is to move the items of the delegates (compacting /stuffing them).
69  One drawback is, that more delegates have to be drawn than usually. As some items are moved from the
70  invisible to the visible area. Setting the cacheBuffer does not fix this.
71  See https://bugreports.qt-project.org/browse/QTBUG-29173
72  Therefore the ListView has negative left and right anchors margins, and in addition a header
73  and footer item to compensate that.
74 
75  The scaling of the items is controlled by the variable continuousIndex, described below. */
76  ListView {
77  id: listView
78  objectName: "listView"
79 
80  property int highlightIndex: -1
81  property real minimumTileWidth: 0
82  property real newContentX: disabledNewContentX
83  property real pathItemCount: referenceWidth / referenceTileWidth
84  property real tileAspectRatio: 1
85 
86  /* The positioning and scaling of the items in the carousel is based on the variable
87  'continuousIndex', a continuous real variable between [0, 'carousel.model.count'],
88  roughly representing the index of the item that is prioritised over the others.
89  'continuousIndex' is not linear, but is weighted depending on if it is close
90  to the beginning of the content (beginning phase), in the middle (middle phase)
91  or at the end (end phase).
92  Each tile is scaled and transformed in proportion to the difference between
93  its own index and continuousIndex.
94  To efficiently calculate continuousIndex, we have these values:
95  - 'gapToMiddlePhase' gap in pixels between beginning and middle phase
96  - 'gapToEndPhase' gap in pixels between middle and end phase
97  - 'kGapEnd' constant used to calculate 'continuousIndex' in end phase
98  - 'kMiddleIndex' constant used to calculate 'continuousIndex' in middle phase
99  - 'kXBeginningEnd' constant used to calculate 'continuousIndex' in beginning and end phase
100  - 'realContentWidth' the width of all the delegates only (without header/footer)
101  - 'realContentX' the 'contentX' of the listview ignoring the 'drawBuffer'
102  - 'realWidth' the 'width' of the listview, as it is used as component. */
103 
104  readonly property real gapToMiddlePhase: Math.min(realWidth / 2 - tileWidth / 2, (realContentWidth - realWidth) / 2)
105  readonly property real gapToEndPhase: realContentWidth - realWidth - gapToMiddlePhase
106  readonly property real kGapEnd: kMiddleIndex * (1 - gapToEndPhase / gapToMiddlePhase)
107  readonly property real kMiddleIndex: (realWidth / 2) / tileWidth - 0.5
108  readonly property real kXBeginningEnd: 1 / tileWidth + kMiddleIndex / gapToMiddlePhase
109  readonly property real maximumItemTranslation: (listView.tileWidth * 3) / listView.scaleFactor
110  readonly property real disabledNewContentX: -carousel.drawBuffer - 1
111  readonly property real realContentWidth: contentWidth - 2 * carousel.drawBuffer
112  readonly property real realContentX: contentX + carousel.drawBuffer
113  readonly property real realPathItemCount: Math.min(realWidth / tileWidth, pathItemCount)
114  readonly property real realWidth: carousel.width
115  readonly property real referenceGapToMiddlePhase: realWidth / 2 - tileWidth / 2
116  readonly property real referencePathItemCount: referenceWidth / referenceTileWidth
117  readonly property real referenceWidth: 848
118  readonly property real referenceTileWidth: 175
119  readonly property real scaleFactor: tileWidth / referenceTileWidth
120  readonly property real tileWidth: Math.max(realWidth / pathItemCount, minimumTileWidth)
121  readonly property real tileHeight: tileWidth / tileAspectRatio
122  readonly property real translationXViewFactor: 0.2 * (referenceGapToMiddlePhase / gapToMiddlePhase)
123  readonly property real verticalMargin: (parent.height - tileHeight) / 2
124  readonly property real visibleTilesScaleFactor: realPathItemCount / referencePathItemCount
125 
126  anchors {
127  fill: parent
128  topMargin: verticalMargin
129  bottomMargin: verticalMargin
130  // extending the "drawing area"
131  leftMargin: -carousel.drawBuffer
132  rightMargin: -carousel.drawBuffer
133  }
134 
135  /* The header and footer help to "extend" the area, the listview draws items.
136  This together with anchors.leftMargin and anchors.rightMargin. */
137  header: Item {
138  width: carousel.drawBuffer
139  height: listView.tileHeight
140  }
141  footer: Item {
142  width: carousel.drawBuffer
143  height: listView.tileHeight
144  }
145 
146  boundsBehavior: Flickable.DragOverBounds
147  cacheBuffer: carousel.cacheBuffer
148  flickDeceleration: Math.max(1500 * Math.pow(realWidth / referenceWidth, 1.5), 1500) // 1500 is platform default
149  maximumFlickVelocity: Math.max(2500 * Math.pow(realWidth / referenceWidth, 1.5), 2500) // 2500 is platform default
150  orientation: ListView.Horizontal
151 
152  function getXFromContinuousIndex(index) {
153  return CarouselJS.getXFromContinuousIndex(index,
154  realWidth,
155  footerItem.x,
156  tileWidth,
157  gapToMiddlePhase,
158  gapToEndPhase,
159  carousel.drawBuffer)
160  }
161 
162  function itemClicked(index, delegateItem) {
163  listView.currentIndex = index
164  var x = getXFromContinuousIndex(index);
165 
166  if (Math.abs(x - contentX) < 1 && delegateItem !== undefined) {
167  /* We're clicking the selected item and
168  we're in the neighbourhood of radius 1 pixel from it.
169  Let's emit the clicked signal. */
170  carousel.clicked(index, delegateItem.y)
171  return
172  }
173 
174  stepAnimation.stop()
175  newContentXAnimation.stop()
176 
177  newContentX = x
178  newContentXAnimation.start()
179  }
180 
181  function itemPressAndHold(index, delegateItem) {
182  var x = getXFromContinuousIndex(index);
183 
184  if (Math.abs(x - contentX) < 1 && delegateItem !== undefined) {
185  /* We're pressAndHold the selected item and
186  we're in the neighbourhood of radius 1 pixel from it.
187  Let's emit the pressAndHold signal. */
188  carousel.pressAndHold(index, delegateItem.y);
189  return;
190  }
191 
192  stepAnimation.stop();
193  newContentXAnimation.stop();
194 
195  newContentX = x;
196  newContentXAnimation.start();
197  }
198 
199  onHighlightIndexChanged: {
200  if (highlightIndex != -1) {
201  itemClicked(highlightIndex)
202  }
203  }
204 
205  onMovementStarted: {
206  stepAnimation.stop()
207  newContentXAnimation.stop()
208  newContentX = disabledNewContentX
209  }
210  onMovementEnded: {
211  if (realContentX > 0)
212  stepAnimation.start()
213  }
214 
215  SmoothedAnimation {
216  id: stepAnimation
217  objectName: "stepAnimation"
218 
219  target: listView
220  property: "contentX"
221  to: listView.getXFromContinuousIndex(listView.selectedIndex)
222  duration: 450
223  velocity: 200
224  easing.type: Easing.InOutQuad
225  }
226 
227  SequentialAnimation {
228  id: newContentXAnimation
229 
230  NumberAnimation {
231  target: listView
232  property: "contentX"
233  from: listView.contentX
234  to: listView.newContentX
235  duration: 300
236  easing.type: Easing.InOutQuad
237  }
238  ScriptAction {
239  script: listView.newContentX = listView.disabledNewContentX
240  }
241  }
242 
243  readonly property int selectedIndex: Math.round(continuousIndex)
244  readonly property real continuousIndex: CarouselJS.getContinuousIndex(listView.realContentX,
245  listView.tileWidth,
246  listView.gapToMiddlePhase,
247  listView.gapToEndPhase,
248  listView.kGapEnd,
249  listView.kMiddleIndex,
250  listView.kXBeginningEnd)
251 
252  property real viewTranslation: CarouselJS.getViewTranslation(listView.realContentX,
253  listView.tileWidth,
254  listView.gapToMiddlePhase,
255  listView.gapToEndPhase,
256  listView.translationXViewFactor)
257 
258  delegate: tileWidth > 0 && tileHeight > 0 ? loaderComponent : undefined
259 
260  Component {
261  id: loaderComponent
262 
263  Loader {
264  property bool explicitlyScaled: explicitScaleFactor == carousel.selectedItemScaleFactor
265  property real explicitScaleFactor: explicitScale ? carousel.selectedItemScaleFactor : 1.0
266  readonly property bool explicitScale: (!listView.moving ||
267  listView.realContentX <= 0 ||
268  listView.realContentX >= listView.realContentWidth - listView.realWidth) &&
269  listView.newContentX === listView.disabledNewContentX &&
270  index === listView.selectedIndex
271  readonly property real cachedTiles: listView.realPathItemCount + carousel.drawBuffer / listView.tileWidth
272  readonly property real distance: listView.continuousIndex - index
273  readonly property real itemTranslationScale: CarouselJS.getItemScale(0.5,
274  (index + 0.5), // good approximation of scale while changing selected item
275  listView.count,
276  listView.visibleTilesScaleFactor)
277  readonly property real itemScale: CarouselJS.getItemScale(distance,
278  listView.continuousIndex,
279  listView.count,
280  listView.visibleTilesScaleFactor)
281  readonly property real translationX: CarouselJS.getItemTranslation(index,
282  listView.selectedIndex,
283  distance,
284  itemScale,
285  itemTranslationScale,
286  listView.maximumItemTranslation)
287 
288  readonly property real xTransform: listView.viewTranslation + translationX * listView.scaleFactor
289  readonly property real center: x - listView.contentX + xTransform - drawBuffer + (width/2)
290 
291  width: listView.tileWidth
292  height: listView.tileHeight
293  scale: itemScale * explicitScaleFactor
294  sourceComponent: itemComponent
295  z: cachedTiles - Math.abs(index - listView.selectedIndex)
296 
297  transform: Translate {
298  x: xTransform
299  }
300 
301  Behavior on explicitScaleFactor {
302  SequentialAnimation {
303  ScriptAction {
304  script: if (!explicitScale)
305  explicitlyScaled = false
306  }
307  NumberAnimation {
308  duration: explicitScaleFactor === 1.0 ? 250 : 150
309  easing.type: Easing.InOutQuad
310  }
311  ScriptAction {
312  script: if (explicitScale)
313  explicitlyScaled = true
314  }
315  }
316  }
317 
318  onLoaded: {
319  item.explicitlyScaled = Qt.binding(function() { return explicitlyScaled; })
320  item.model = Qt.binding(function() { return model; })
321  }
322 
323  MouseArea {
324  id: mouseArea
325 
326  anchors.fill: parent
327 
328  onClicked: {
329  listView.itemClicked(index, item)
330  }
331 
332  onPressAndHold: {
333  listView.itemPressAndHold(index, item)
334  }
335  }
336  }
337  }
338  }
339 }