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 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 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 index of the item that should be highlighted
51 property alias highlightIndex: listView.highlightIndex
52 /// exposes the delegate of the currentItem
53 readonly property alias currentItem: listView.currentItem
54 /// exposes the distance to the next row (only one row in carousel, so it's the topMargins)
55 readonly property alias verticalSpacing: listView.verticalMargin
56 /// the width of the internal list
57 readonly property alias innerWidth: listView.width
59 implicitHeight: listView.tileHeight * selectedItemScaleFactor
60 opacity: listView.highlightIndex === -1 ? 1 : 0.6
62 /* Basic idea behind the carousel effect is to move the items of the delegates (compacting /stuffing them).
63 One drawback is, that more delegates have to be drawn than usually. As some items are moved from the
64 invisible to the visible area. Setting the cacheBuffer does not fix this.
65 See https://bugreports.qt-project.org/browse/QTBUG-29173
66 Therefore the ListView has negative left and right anchors margins, and in addition a header
67 and footer item to compensate that.
69 The scaling of the items is controlled by the variable continuousIndex, described below. */
72 objectName: "listView"
74 property int highlightIndex: -1
75 property real minimumTileWidth: 0
76 property real newContentX: disabledNewContentX
77 property real pathItemCount: referenceWidth / referenceTileWidth
78 property real tileAspectRatio: 1
80 /* The positioning and scaling of the items in the carousel is based on the variable
81 'continuousIndex', a continuous real variable between [0, 'carousel.model.count'],
82 roughly representing the index of the item that is prioritised over the others.
83 'continuousIndex' is not linear, but is weighted depending on if it is close
84 to the beginning of the content (beginning phase), in the middle (middle phase)
85 or at the end (end phase).
86 Each tile is scaled and transformed in proportion to the difference between
87 its own index and continuousIndex.
88 To efficiently calculate continuousIndex, we have these values:
89 - 'gapToMiddlePhase' gap in pixels between beginning and middle phase
90 - 'gapToEndPhase' gap in pixels between middle and end phase
91 - 'kGapEnd' constant used to calculate 'continuousIndex' in end phase
92 - 'kMiddleIndex' constant used to calculate 'continuousIndex' in middle phase
93 - 'kXBeginningEnd' constant used to calculate 'continuousIndex' in beginning and end phase
94 - 'realContentWidth' the width of all the delegates only (without header/footer)
95 - 'realContentX' the 'contentX' of the listview ignoring the 'drawBuffer'
96 - 'realWidth' the 'width' of the listview, as it is used as component. */
98 readonly property real gapToMiddlePhase: Math.min(realWidth / 2 - tileWidth / 2, (realContentWidth - realWidth) / 2)
99 readonly property real gapToEndPhase: realContentWidth - realWidth - gapToMiddlePhase
100 readonly property real kGapEnd: kMiddleIndex * (1 - gapToEndPhase / gapToMiddlePhase)
101 readonly property real kMiddleIndex: (realWidth / 2) / tileWidth - 0.5
102 readonly property real kXBeginningEnd: 1 / tileWidth + kMiddleIndex / gapToMiddlePhase
103 readonly property real maximumItemTranslation: (listView.tileWidth * 3) / listView.scaleFactor
104 readonly property real disabledNewContentX: -carousel.drawBuffer - 1
105 readonly property real realContentWidth: contentWidth - 2 * carousel.drawBuffer
106 readonly property real realContentX: contentX + carousel.drawBuffer
107 readonly property real realPathItemCount: Math.min(realWidth / tileWidth, pathItemCount)
108 readonly property real realWidth: carousel.width
109 readonly property real referenceGapToMiddlePhase: realWidth / 2 - tileWidth / 2
110 readonly property real referencePathItemCount: referenceWidth / referenceTileWidth
111 readonly property real referenceWidth: 848
112 readonly property real referenceTileWidth: 175
113 readonly property real scaleFactor: tileWidth / referenceTileWidth
114 readonly property real tileWidth: Math.max(realWidth / pathItemCount, minimumTileWidth)
115 readonly property real tileHeight: tileWidth / tileAspectRatio
116 readonly property real translationXViewFactor: 0.2 * (referenceGapToMiddlePhase / gapToMiddlePhase)
117 readonly property real verticalMargin: (parent.height - tileHeight) / 2
118 readonly property real visibleTilesScaleFactor: realPathItemCount / referencePathItemCount
122 topMargin: verticalMargin
123 bottomMargin: verticalMargin
124 // extending the "drawing area"
125 leftMargin: -carousel.drawBuffer
126 rightMargin: -carousel.drawBuffer
129 /* The header and footer help to "extend" the area, the listview draws items.
130 This together with anchors.leftMargin and anchors.rightMargin. */
132 width: carousel.drawBuffer
133 height: listView.tileHeight
136 width: carousel.drawBuffer
137 height: listView.tileHeight
140 boundsBehavior: Flickable.DragOverBounds
141 cacheBuffer: carousel.cacheBuffer
142 flickDeceleration: Math.max(1500 * Math.pow(realWidth / referenceWidth, 1.5), 1500) // 1500 is platform default
143 maximumFlickVelocity: Math.max(2500 * Math.pow(realWidth / referenceWidth, 1.5), 2500) // 2500 is platform default
144 orientation: ListView.Horizontal
146 function getXFromContinuousIndex(index) {
147 return CarouselJS.getXFromContinuousIndex(index,
156 function itemClicked(index, delegateItem) {
157 listView.currentIndex = index
158 var x = getXFromContinuousIndex(index);
160 if (Math.abs(x - contentX) < 1 && delegateItem !== undefined) {
161 /* We're clicking the selected item and
162 we're in the neighbourhood of radius 1 pixel from it.
163 Let's emit the clicked signal. */
164 delegateItem.clicked()
169 newContentXAnimation.stop()
172 newContentXAnimation.start()
175 function itemPressAndHold(index, delegateItem) {
176 var x = getXFromContinuousIndex(index);
178 if (Math.abs(x - contentX) < 1 && delegateItem !== undefined) {
179 /* We're pressAndHold the selected item and
180 we're in the neighbourhood of radius 1 pixel from it.
181 Let's emit the pressAndHold signal. */
182 delegateItem.pressAndHold();
186 stepAnimation.stop();
187 newContentXAnimation.stop();
190 newContentXAnimation.start();
193 onHighlightIndexChanged: {
194 if (highlightIndex != -1) {
195 itemClicked(highlightIndex)
201 newContentXAnimation.stop()
202 newContentX = disabledNewContentX
205 if (realContentX > 0)
206 stepAnimation.start()
211 objectName: "stepAnimation"
215 to: listView.getXFromContinuousIndex(listView.selectedIndex)
218 easing.type: Easing.InOutQuad
221 SequentialAnimation {
222 id: newContentXAnimation
227 from: listView.contentX
228 to: listView.newContentX
230 easing.type: Easing.InOutQuad
233 script: listView.newContentX = listView.disabledNewContentX
237 readonly property int selectedIndex: Math.round(continuousIndex)
238 readonly property real continuousIndex: CarouselJS.getContinuousIndex(listView.realContentX,
240 listView.gapToMiddlePhase,
241 listView.gapToEndPhase,
243 listView.kMiddleIndex,
244 listView.kXBeginningEnd)
246 property real viewTranslation: CarouselJS.getViewTranslation(listView.realContentX,
248 listView.gapToMiddlePhase,
249 listView.gapToEndPhase,
250 listView.translationXViewFactor)
252 delegate: tileWidth > 0 && tileHeight > 0 ? loaderComponent : undefined
258 property bool explicitlyScaled: explicitScaleFactor == carousel.selectedItemScaleFactor
259 property real explicitScaleFactor: explicitScale ? carousel.selectedItemScaleFactor : 1.0
260 readonly property bool explicitScale: (!listView.moving ||
261 listView.realContentX <= 0 ||
262 listView.realContentX >= listView.realContentWidth - listView.realWidth) &&
263 listView.newContentX === listView.disabledNewContentX &&
264 index === listView.selectedIndex
265 readonly property real cachedTiles: listView.realPathItemCount + carousel.drawBuffer / listView.tileWidth
266 readonly property real distance: listView.continuousIndex - index
267 readonly property real itemTranslationScale: CarouselJS.getItemScale(0.5,
268 (index + 0.5), // good approximation of scale while changing selected item
270 listView.visibleTilesScaleFactor)
271 readonly property real itemScale: CarouselJS.getItemScale(distance,
272 listView.continuousIndex,
274 listView.visibleTilesScaleFactor)
275 readonly property real translationX: CarouselJS.getItemTranslation(index,
276 listView.selectedIndex,
279 itemTranslationScale,
280 listView.maximumItemTranslation)
282 readonly property real xTransform: listView.viewTranslation + translationX * listView.scaleFactor
283 readonly property real center: x - listView.contentX + xTransform - drawBuffer + (width/2)
285 width: listView.tileWidth
286 height: listView.tileHeight
287 scale: itemScale * explicitScaleFactor
288 sourceComponent: itemComponent
289 z: cachedTiles - Math.abs(index - listView.selectedIndex)
291 transform: Translate {
295 Behavior on explicitScaleFactor {
296 SequentialAnimation {
298 script: if (!explicitScale)
299 explicitlyScaled = false
302 duration: explicitScaleFactor === 1.0 ? 250 : 150
303 easing.type: Easing.InOutQuad
306 script: if (explicitScale)
307 explicitlyScaled = true
313 item.explicitlyScaled = Qt.binding(function() { return explicitlyScaled; });
314 item.index = Qt.binding(function() { return index; });
315 item.model = Qt.binding(function() { return model; });
324 listView.itemClicked(index, item)
328 listView.itemPressAndHold(index, item)