2 * Copyright (C) 2013 Canonical, Ltd.
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.
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.
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/>.
18 import Ubuntu.Components 0.1
19 import "carousel.js" as CarouselJS
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).
27 clip: true // Don't leak horizontally to other dashes
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
55 implicitHeight: listView.tileHeight * selectedItemScaleFactor
56 opacity: listView.highlightIndex === -1 ? 1 : 0.6
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.
65 The scaling of the items is controlled by the variable continuousIndex, described below. */
68 objectName: "listView"
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
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. */
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
118 topMargin: verticalMargin
119 bottomMargin: verticalMargin
120 // extending the "drawing area"
121 leftMargin: -carousel.drawBuffer
122 rightMargin: -carousel.drawBuffer
125 /* The header and footer help to "extend" the area, the listview draws items.
126 This together with anchors.leftMargin and anchors.rightMargin. */
128 width: carousel.drawBuffer
129 height: listView.tileHeight
132 width: carousel.drawBuffer
133 height: listView.tileHeight
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
142 function getXFromContinuousIndex(index) {
143 return CarouselJS.getXFromContinuousIndex(index,
152 function itemClicked(index, delegateItem) {
153 listView.currentIndex = index
154 var x = getXFromContinuousIndex(index);
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()
165 newContentXAnimation.stop()
168 newContentXAnimation.start()
171 function itemPressAndHold(index, delegateItem) {
172 var x = getXFromContinuousIndex(index);
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();
182 stepAnimation.stop();
183 newContentXAnimation.stop();
186 newContentXAnimation.start();
189 onHighlightIndexChanged: {
190 if (highlightIndex != -1) {
191 itemClicked(highlightIndex)
197 newContentXAnimation.stop()
198 newContentX = disabledNewContentX
201 if (realContentX > 0)
202 stepAnimation.start()
207 objectName: "stepAnimation"
211 to: listView.getXFromContinuousIndex(listView.selectedIndex)
214 easing.type: Easing.InOutQuad
217 SequentialAnimation {
218 id: newContentXAnimation
223 from: listView.contentX
224 to: listView.newContentX
226 easing.type: Easing.InOutQuad
229 script: listView.newContentX = listView.disabledNewContentX
233 readonly property int selectedIndex: Math.round(continuousIndex)
234 readonly property real continuousIndex: CarouselJS.getContinuousIndex(listView.realContentX,
236 listView.gapToMiddlePhase,
237 listView.gapToEndPhase,
239 listView.kMiddleIndex,
240 listView.kXBeginningEnd)
242 property real viewTranslation: CarouselJS.getViewTranslation(listView.realContentX,
244 listView.gapToMiddlePhase,
245 listView.gapToEndPhase,
246 listView.translationXViewFactor)
248 delegate: tileWidth > 0 && tileHeight > 0 ? loaderComponent : undefined
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
266 listView.visibleTilesScaleFactor)
267 readonly property real itemScale: CarouselJS.getItemScale(distance,
268 listView.continuousIndex,
270 listView.visibleTilesScaleFactor)
271 readonly property real translationX: CarouselJS.getItemTranslation(index,
272 listView.selectedIndex,
275 itemTranslationScale,
276 listView.maximumItemTranslation)
278 readonly property real xTransform: listView.viewTranslation + translationX * listView.scaleFactor
279 readonly property real center: x - listView.contentX + xTransform - drawBuffer + (width/2)
281 width: listView.tileWidth
282 height: listView.tileHeight
283 scale: itemScale * explicitScaleFactor
284 sourceComponent: itemComponent
285 z: cachedTiles - Math.abs(index - listView.selectedIndex)
287 transform: Translate {
291 Behavior on explicitScaleFactor {
292 SequentialAnimation {
294 script: if (!explicitScale)
295 explicitlyScaled = false
298 duration: explicitScaleFactor === 1.0 ? 250 : 150
299 easing.type: Easing.InOutQuad
302 script: if (explicitScale)
303 explicitlyScaled = true
309 item.explicitlyScaled = Qt.binding(function() { return explicitlyScaled; });
310 item.index = Qt.binding(function() { return index; });
311 item.model = Qt.binding(function() { return model; });
320 listView.itemClicked(index, item)
324 listView.itemPressAndHold(index, item)