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