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