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