on 29047e4 help sign in
unity8/Components/Carousel.qml
340 lines 18 kB history raw
1/*
2 * Copyright (C) 2013 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 */
17import QtQuick 2.0
18import Ubuntu.Components 0.1
19import "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).
23 */
24Item {
25 id: carousel
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 /// Emitted when the user clicked on an item
56 /// @param index is the index of the clicked item
57 /// @param itemY is y of the clicked delegate
58 signal clicked(int index, real itemY)
60 /// Emitted when the user pressed and held on an item
61 /// @param index is the index of the held item
62 /// @param itemY is y of the held delegate
63 signal pressAndHold(int index, real itemY)
65 implicitHeight: listView.tileHeight * selectedItemScaleFactor
67 /* Basic idea behind the carousel effect is to move the items of the delegates (compacting /stuffing them).
68 One drawback is, that more delegates have to be drawn than usually. As some items are moved from the
69 invisible to the visible area. Setting the cacheBuffer does not fix this.
70 See https://bugreports.qt-project.org/browse/QTBUG-29173
71 Therefore the ListView has negative left and right anchors margins, and in addition a header
72 and footer item to compensate that.
74 The scaling of the items is controlled by the variable continuousIndex, described below. */
75 ListView {
76 id: listView
78 property real minimumTileWidth: 0
79 property real newContentX: disabledNewContentX
80 property real pathItemCount: referenceWidth / referenceTileWidth
81 property real tileAspectRatio: 1
82 property int highlightIndex: -1
84 /* The positioning and scaling of the items in the carousel is based on the variable
85 'continuousIndex', a continuous real variable between [0, 'carousel.model.count'],
86 roughly representing the index of the item that is prioritised over the others.
87 'continuousIndex' is not linear, but is weighted depending on if it is close
88 to the beginning of the content (beginning phase), in the middle (middle phase)
89 or at the end (end phase).
90 Each tile is scaled and transformed in proportion to the difference between
91 its own index and continuousIndex.
92 To efficiently calculate continuousIndex, we have these values:
93 - 'gapToMiddlePhase' gap in pixels between beginning and middle phase
94 - 'gapToEndPhase' gap in pixels between middle and end phase
95 - 'kGapEnd' constant used to calculate 'continuousIndex' in end phase
96 - 'kMiddleIndex' constant used to calculate 'continuousIndex' in middle phase
97 - 'kXBeginningEnd' constant used to calculate 'continuousIndex' in beginning and end phase
98 - 'realContentWidth' the width of all the delegates only (without header/footer)
99 - 'realContentX' the 'contentX' of the listview ignoring the 'drawBuffer'
100 - 'realWidth' the 'width' of the listview, as it is used as component. */
102 readonly property real gapToMiddlePhase: Math.min(realWidth / 2 - tileWidth / 2, (realContentWidth - realWidth) / 2)
103 readonly property real gapToEndPhase: realContentWidth - realWidth - gapToMiddlePhase
104 readonly property real kGapEnd: kMiddleIndex * (1 - gapToEndPhase / gapToMiddlePhase)
105 readonly property real kMiddleIndex: (realWidth / 2) / tileWidth - 0.5
106 readonly property real kXBeginningEnd: 1 / tileWidth + kMiddleIndex / gapToMiddlePhase
107 readonly property real maximumItemTranslation: (listView.tileWidth * 3) / listView.scaleFactor
108 readonly property real disabledNewContentX: -carousel.drawBuffer - 1
109 readonly property real realContentWidth: contentWidth - 2 * carousel.drawBuffer
110 readonly property real realContentX: contentX + carousel.drawBuffer
111 readonly property real realPathItemCount: Math.min(realWidth / tileWidth, pathItemCount)
112 readonly property real realWidth: carousel.width
113 readonly property real referenceGapToMiddlePhase: realWidth / 2 - tileWidth / 2
114 readonly property real referencePathItemCount: referenceWidth / referenceTileWidth
115 readonly property real referenceWidth: 848
116 readonly property real referenceTileWidth: 175
117 readonly property real scaleFactor: tileWidth / referenceTileWidth
118 readonly property real tileWidth: Math.max(realWidth / pathItemCount, minimumTileWidth)
119 readonly property real tileHeight: tileWidth / tileAspectRatio
120 readonly property real translationXViewFactor: 0.2 * (referenceGapToMiddlePhase / gapToMiddlePhase)
121 readonly property real verticalMargin: (parent.height - tileHeight) / 2
122 readonly property real visibleTilesScaleFactor: realPathItemCount / referencePathItemCount
124 anchors {
125 fill: parent
126 topMargin: verticalMargin
127 bottomMargin: verticalMargin
128 // extending the "drawing area"
129 leftMargin: -carousel.drawBuffer
130 rightMargin: -carousel.drawBuffer
131 }
133 /* The header and footer help to "extend" the area, the listview draws items.
134 This together with anchors.leftMargin and anchors.rightMargin. */
135 header: Item {
136 width: carousel.drawBuffer
137 height: listView.tileHeight
138 }
139 footer: Item {
140 width: carousel.drawBuffer
141 height: listView.tileHeight
142 }
144 boundsBehavior: Flickable.DragOverBounds
145 cacheBuffer: carousel.cacheBuffer
146 flickDeceleration: Math.max(1500 * Math.pow(realWidth / referenceWidth, 1.5), 1500) // 1500 is platform default
147 maximumFlickVelocity: Math.max(2500 * Math.pow(realWidth / referenceWidth, 1.5), 2500) // 2500 is platform default
148 orientation: ListView.Horizontal
150 function itemClicked(index, delegateItem) {
151 listView.currentIndex = index
152 var x = CarouselJS.getXFromContinuousIndex(index,
153 realWidth,
154 realContentWidth,
155 tileWidth,
156 gapToMiddlePhase,
157 gapToEndPhase,
158 carousel.drawBuffer)
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 carousel.clicked(index, delegateItem.y)
165 return
166 }
168 stepAnimation.stop()
169 newContentXAnimation.stop()
171 newContentX = x
172 newContentXAnimation.start()
173 }
175 function itemPressAndHold(index, delegateItem) {
176 var x = CarouselJS.getXFromContinuousIndex(index,
177 realWidth,
178 realContentWidth,
179 tileWidth,
180 gapToMiddlePhase,
181 gapToEndPhase,
182 carousel.drawBuffer);
184 if (Math.abs(x - contentX) < 1 && delegateItem !== undefined) {
185 /* We're pressAndHold the selected item and
186 we're in the neighbourhood of radius 1 pixel from it.
187 Let's emit the pressAndHold signal. */
188 carousel.pressAndHold(index, delegateItem.y);
189 return;
190 }
192 stepAnimation.stop();
193 newContentXAnimation.stop();
195 newContentX = x;
196 newContentXAnimation.start();
197 }
199 onHighlightIndexChanged: {
200 if (highlightIndex != -1) {
201 itemClicked(highlightIndex)
202 }
203 }
205 onMovementStarted: {
206 stepAnimation.stop()
207 newContentXAnimation.stop()
208 newContentX = disabledNewContentX
209 }
210 onMovementEnded: {
211 if (realContentX > 0 && realContentX < realContentWidth - realWidth)
212 stepAnimation.start()
213 }
215 SmoothedAnimation {
216 id: stepAnimation
218 target: listView
219 property: "contentX"
220 to: CarouselJS.getXFromContinuousIndex(listView.selectedIndex,
221 listView.realWidth,
222 listView.realContentWidth,
223 listView.tileWidth,
224 listView.gapToMiddlePhase,
225 listView.gapToEndPhase,
226 carousel.drawBuffer)
227 duration: 450
228 velocity: 200
229 easing.type: Easing.InOutQuad
230 }
232 SequentialAnimation {
233 id: newContentXAnimation
235 NumberAnimation {
236 target: listView
237 property: "contentX"
238 from: listView.contentX
239 to: listView.newContentX
240 duration: 300
241 easing.type: Easing.InOutQuad
242 }
243 ScriptAction {
244 script: listView.newContentX = listView.disabledNewContentX
245 }
246 }
248 readonly property int selectedIndex: Math.round(continuousIndex)
249 readonly property real continuousIndex: CarouselJS.getContinuousIndex(listView.realContentX,
250 listView.tileWidth,
251 listView.gapToMiddlePhase,
252 listView.gapToEndPhase,
253 listView.kGapEnd,
254 listView.kMiddleIndex,
255 listView.kXBeginningEnd)
257 property real viewTranslation: CarouselJS.getViewTranslation(listView.realContentX,
258 listView.tileWidth,
259 listView.gapToMiddlePhase,
260 listView.gapToEndPhase,
261 listView.translationXViewFactor)
263 delegate: Loader {
264 property bool explicitlyScaled: explicitScaleFactor == carousel.selectedItemScaleFactor
265 property real explicitScaleFactor: explicitScale ? carousel.selectedItemScaleFactor : 1.0
266 readonly property bool explicitScale: (!listView.moving ||
267 listView.realContentX <= 0 ||
268 listView.realContentX >= listView.realContentWidth - listView.realWidth) &&
269 listView.newContentX === listView.disabledNewContentX &&
270 index === listView.selectedIndex
271 readonly property real cachedTiles: listView.realPathItemCount + carousel.drawBuffer / listView.tileWidth
272 readonly property real distance: listView.continuousIndex - index
273 readonly property real itemTranslationScale: CarouselJS.getItemScale(0.5,
274 (index + 0.5), // good approximation of scale while changing selected item
275 listView.count,
276 listView.visibleTilesScaleFactor)
277 readonly property real itemScale: CarouselJS.getItemScale(distance,
278 listView.continuousIndex,
279 listView.count,
280 listView.visibleTilesScaleFactor)
281 readonly property real translationX: CarouselJS.getItemTranslation(index,
282 listView.selectedIndex,
283 distance,
284 itemScale,
285 itemTranslationScale,
286 listView.maximumItemTranslation)
288 readonly property real xTransform: listView.viewTranslation + translationX * listView.scaleFactor
289 readonly property real center: x - listView.contentX + xTransform - drawBuffer + (width/2)
291 width: listView.tileWidth
292 height: listView.tileHeight
293 scale: itemScale * explicitScaleFactor
294 sourceComponent: itemComponent
295 z: cachedTiles - Math.abs(index - listView.selectedIndex)
296 opacity: highlightIndex == -1 ? 1 : (highlightIndex == index ? 0.6 : 0.2)
298 transform: Translate {
299 x: xTransform
300 }
302 Behavior on explicitScaleFactor {
303 SequentialAnimation {
304 ScriptAction {
305 script: if (!explicitScale)
306 explicitlyScaled = false
307 }
308 NumberAnimation {
309 duration: explicitScaleFactor === 1.0 ? 250 : 150
310 easing.type: Easing.InOutQuad
311 }
312 ScriptAction {
313 script: if (explicitScale)
314 explicitlyScaled = true
315 }
316 }
317 }
319 onLoaded: {
320 item.explicitlyScaled = Qt.binding(function() { return explicitlyScaled; })
321 item.model = Qt.binding(function() { return model; })
322 }
324 MouseArea {
325 id: mouseArea
327 anchors.fill: parent
329 onClicked: {
330 listView.itemClicked(index, item)
331 }
333 onPressAndHold: {
334 listView.itemPressAndHold(index, item)
335 }
337 }
338 }
339 }