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