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