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
20 import "Flickables" as Flickables
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).
28 clip: true // Don't leak horizontally to other dashes
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
56 implicitHeight: listView.tileHeight * selectedItemScaleFactor
57 opacity: listView.highlightIndex === -1 ? 1 : 0.6
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.
66 The scaling of the items is controlled by the variable continuousIndex, described below. */
69 objectName: "listView"
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
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. */
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
119 topMargin: verticalMargin
120 bottomMargin: verticalMargin
121 // extending the "drawing area"
122 leftMargin: -carousel.drawBuffer
123 rightMargin: -carousel.drawBuffer
126 /* The header and footer help to "extend" the area, the listview draws items.
127 This together with anchors.leftMargin and anchors.rightMargin. */
129 width: carousel.drawBuffer
130 height: listView.tileHeight
133 width: carousel.drawBuffer
134 height: listView.tileHeight
137 boundsBehavior: Flickable.DragOverBounds
138 cacheBuffer: carousel.cacheBuffer
139 orientation: ListView.Horizontal
141 function getXFromContinuousIndex(index) {
142 return CarouselJS.getXFromContinuousIndex(index,
151 function itemClicked(index, delegateItem) {
152 listView.currentIndex = index
153 var x = getXFromContinuousIndex(index);
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()
164 newContentXAnimation.stop()
167 newContentXAnimation.start()
170 function itemPressAndHold(index, delegateItem) {
171 var x = getXFromContinuousIndex(index);
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();
181 stepAnimation.stop();
182 newContentXAnimation.stop();
185 newContentXAnimation.start();
188 onHighlightIndexChanged: {
189 if (highlightIndex != -1) {
190 itemClicked(highlightIndex)
196 newContentXAnimation.stop()
197 newContentX = disabledNewContentX
200 if (realContentX > 0)
201 stepAnimation.start()
206 objectName: "stepAnimation"
210 to: listView.getXFromContinuousIndex(listView.selectedIndex)
213 easing.type: Easing.InOutQuad
216 SequentialAnimation {
217 id: newContentXAnimation
222 from: listView.contentX
223 to: listView.newContentX
225 easing.type: Easing.InOutQuad
228 script: listView.newContentX = listView.disabledNewContentX
232 readonly property int selectedIndex: Math.round(continuousIndex)
233 readonly property real continuousIndex: CarouselJS.getContinuousIndex(listView.realContentX,
235 listView.gapToMiddlePhase,
236 listView.gapToEndPhase,
238 listView.kMiddleIndex,
239 listView.kXBeginningEnd)
241 property real viewTranslation: CarouselJS.getViewTranslation(listView.realContentX,
243 listView.gapToMiddlePhase,
244 listView.gapToEndPhase,
245 listView.translationXViewFactor)
247 delegate: tileWidth > 0 && tileHeight > 0 ? loaderComponent : undefined
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
265 listView.visibleTilesScaleFactor)
266 readonly property real itemScale: CarouselJS.getItemScale(distance,
267 listView.continuousIndex,
269 listView.visibleTilesScaleFactor)
270 readonly property real translationX: CarouselJS.getItemTranslation(index,
271 listView.selectedIndex,
274 itemTranslationScale,
275 listView.maximumItemTranslation)
277 readonly property real xTransform: listView.viewTranslation + translationX * listView.scaleFactor
278 readonly property real center: x - listView.contentX + xTransform - drawBuffer + (width/2)
280 width: listView.tileWidth
281 height: listView.tileHeight
282 scale: itemScale * explicitScaleFactor
283 sourceComponent: itemComponent
284 z: cachedTiles - Math.abs(index - listView.selectedIndex)
286 transform: Translate {
290 Behavior on explicitScaleFactor {
291 SequentialAnimation {
293 script: if (!explicitScale)
294 explicitlyScaled = false
297 duration: explicitScaleFactor === 1.0 ? 250 : 150
298 easing.type: Easing.InOutQuad
301 script: if (explicitScale)
302 explicitlyScaled = true
308 item.explicitlyScaled = Qt.binding(function() { return explicitlyScaled; });
309 item.index = Qt.binding(function() { return index; });
310 item.model = Qt.binding(function() { return model; });
319 listView.itemClicked(index, item)
323 listView.itemPressAndHold(index, item)