2 * Copyright (C) 2013-2015 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 1.3
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 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
61 implicitHeight: listView.tileHeight * selectedItemScaleFactor
62 opacity: listView.highlightIndex === -1 ? 1 : 0.6
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.
71 The scaling of the items is controlled by the variable continuousIndex, described below. */
74 objectName: "listView"
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
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. */
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
124 topMargin: verticalMargin
125 bottomMargin: verticalMargin + carousel.extraBottomMargin
126 // extending the "drawing area"
127 leftMargin: -carousel.drawBuffer
128 rightMargin: -carousel.drawBuffer
131 /* The header and footer help to "extend" the area, the listview draws items.
132 This together with anchors.leftMargin and anchors.rightMargin. */
134 width: carousel.drawBuffer
135 height: listView.tileHeight
138 width: carousel.drawBuffer
139 height: listView.tileHeight
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
148 function getXFromContinuousIndex(index) {
149 return CarouselJS.getXFromContinuousIndex(index,
158 function itemClicked(index, delegateItem) {
159 listView.currentIndex = index
160 var x = getXFromContinuousIndex(index);
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()
171 newContentXAnimation.stop()
174 newContentXAnimation.start()
177 function itemPressAndHold(index, delegateItem) {
178 var x = getXFromContinuousIndex(index);
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();
188 stepAnimation.stop();
189 newContentXAnimation.stop();
192 newContentXAnimation.start();
195 onHighlightIndexChanged: {
196 if (highlightIndex != -1) {
197 itemClicked(highlightIndex)
203 newContentXAnimation.stop()
204 newContentX = disabledNewContentX
207 if (realContentX > 0)
208 stepAnimation.start()
213 objectName: "stepAnimation"
217 to: listView.getXFromContinuousIndex(listView.selectedIndex)
220 easing.type: Easing.InOutQuad
223 SequentialAnimation {
224 id: newContentXAnimation
229 from: listView.contentX
230 to: listView.newContentX
232 easing.type: Easing.InOutQuad
235 script: listView.newContentX = listView.disabledNewContentX
239 readonly property int selectedIndex: Math.round(continuousIndex)
240 readonly property real continuousIndex: CarouselJS.getContinuousIndex(listView.realContentX,
242 listView.gapToMiddlePhase,
243 listView.gapToEndPhase,
245 listView.kMiddleIndex,
246 listView.kXBeginningEnd)
248 property real viewTranslation: CarouselJS.getViewTranslation(listView.realContentX,
250 listView.gapToMiddlePhase,
251 listView.gapToEndPhase,
252 listView.translationXViewFactor)
254 delegate: tileWidth > 0 && tileHeight > 0 ? loaderComponent : null
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
272 listView.visibleTilesScaleFactor)
273 readonly property real itemScale: CarouselJS.getItemScale(distance,
274 listView.continuousIndex,
276 listView.visibleTilesScaleFactor)
277 readonly property real translationX: CarouselJS.getItemTranslation(index,
278 listView.selectedIndex,
281 itemTranslationScale,
282 listView.maximumItemTranslation)
284 readonly property real xTransform: listView.viewTranslation + translationX * listView.scaleFactor
285 readonly property real center: x - listView.contentX + xTransform - drawBuffer + (width/2)
287 width: listView.tileWidth
288 height: listView.tileHeight
289 scale: itemScale * explicitScaleFactor
290 sourceComponent: itemComponent
291 z: cachedTiles - Math.abs(index - listView.selectedIndex)
293 transform: Translate {
297 Behavior on explicitScaleFactor {
298 SequentialAnimation {
300 script: if (!explicitScale)
301 explicitlyScaled = false
304 duration: explicitScaleFactor === 1.0 ? 250 : 150
305 easing.type: Easing.InOutQuad
308 script: if (explicitScale)
309 explicitlyScaled = true
315 item.explicitlyScaled = Qt.binding(function() { return explicitlyScaled; });
316 item.index = Qt.binding(function() { return index; });
317 item.model = Qt.binding(function() { return model; });
326 listView.itemClicked(index, item)
330 listView.itemPressAndHold(index, item)