on 3e61bb2 help sign in
unity8/Components/Carousel.qml
339 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
66 opacity: listView.highlightIndex === -1 ? 1 : 0.6
68 /* Basic idea behind the carousel effect is to move the items of the delegates (compacting /stuffing them).
69 One drawback is, that more delegates have to be drawn than usually. As some items are moved from the
70 invisible to the visible area. Setting the cacheBuffer does not fix this.
71 See https://bugreports.qt-project.org/browse/QTBUG-29173
72 Therefore the ListView has negative left and right anchors margins, and in addition a header
73 and footer item to compensate that.
75 The scaling of the items is controlled by the variable continuousIndex, described below. */
76 ListView {
77 id: listView
79 property int highlightIndex: -1
80 property real minimumTileWidth: 0
81 property real newContentX: disabledNewContentX
82 property real pathItemCount: referenceWidth / referenceTileWidth
83 property real tileAspectRatio: 1
85 /* The positioning and scaling of the items in the carousel is based on the variable
86 'continuousIndex', a continuous real variable between [0, 'carousel.model.count'],
87 roughly representing the index of the item that is prioritised over the others.
88 'continuousIndex' is not linear, but is weighted depending on if it is close
89 to the beginning of the content (beginning phase), in the middle (middle phase)
90 or at the end (end phase).
91 Each tile is scaled and transformed in proportion to the difference between
92 its own index and continuousIndex.
93 To efficiently calculate continuousIndex, we have these values:
94 - 'gapToMiddlePhase' gap in pixels between beginning and middle phase
95 - 'gapToEndPhase' gap in pixels between middle and end phase
96 - 'kGapEnd' constant used to calculate 'continuousIndex' in end phase
97 - 'kMiddleIndex' constant used to calculate 'continuousIndex' in middle phase
98 - 'kXBeginningEnd' constant used to calculate 'continuousIndex' in beginning and end phase
99 - 'realContentWidth' the width of all the delegates only (without header/footer)
100 - 'realContentX' the 'contentX' of the listview ignoring the 'drawBuffer'
101 - 'realWidth' the 'width' of the listview, as it is used as component. */
103 readonly property real gapToMiddlePhase: Math.min(realWidth / 2 - tileWidth / 2, (realContentWidth - realWidth) / 2)
104 readonly property real gapToEndPhase: realContentWidth - realWidth - gapToMiddlePhase
105 readonly property real kGapEnd: kMiddleIndex * (1 - gapToEndPhase / gapToMiddlePhase)
106 readonly property real kMiddleIndex: (realWidth / 2) / tileWidth - 0.5
107 readonly property real kXBeginningEnd: 1 / tileWidth + kMiddleIndex / gapToMiddlePhase
108 readonly property real maximumItemTranslation: (listView.tileWidth * 3) / listView.scaleFactor
109 readonly property real disabledNewContentX: -carousel.drawBuffer - 1
110 readonly property real realContentWidth: contentWidth - 2 * carousel.drawBuffer
111 readonly property real realContentX: contentX + carousel.drawBuffer
112 readonly property real realPathItemCount: Math.min(realWidth / tileWidth, pathItemCount)
113 readonly property real realWidth: carousel.width
114 readonly property real referenceGapToMiddlePhase: realWidth / 2 - tileWidth / 2
115 readonly property real referencePathItemCount: referenceWidth / referenceTileWidth
116 readonly property real referenceWidth: 848
117 readonly property real referenceTileWidth: 175
118 readonly property real scaleFactor: tileWidth / referenceTileWidth
119 readonly property real tileWidth: Math.max(realWidth / pathItemCount, minimumTileWidth)
120 readonly property real tileHeight: tileWidth / tileAspectRatio
121 readonly property real translationXViewFactor: 0.2 * (referenceGapToMiddlePhase / gapToMiddlePhase)
122 readonly property real verticalMargin: (parent.height - tileHeight) / 2
123 readonly property real visibleTilesScaleFactor: realPathItemCount / referencePathItemCount
125 anchors {
126 fill: parent
127 topMargin: verticalMargin
128 bottomMargin: verticalMargin
129 // extending the "drawing area"
130 leftMargin: -carousel.drawBuffer
131 rightMargin: -carousel.drawBuffer
132 }
134 /* The header and footer help to "extend" the area, the listview draws items.
135 This together with anchors.leftMargin and anchors.rightMargin. */
136 header: Item {
137 width: carousel.drawBuffer
138 height: listView.tileHeight
139 }
140 footer: Item {
141 width: carousel.drawBuffer
142 height: listView.tileHeight
143 }
145 boundsBehavior: Flickable.DragOverBounds
146 cacheBuffer: carousel.cacheBuffer
147 flickDeceleration: Math.max(1500 * Math.pow(realWidth / referenceWidth, 1.5), 1500) // 1500 is platform default
148 maximumFlickVelocity: Math.max(2500 * Math.pow(realWidth / referenceWidth, 1.5), 2500) // 2500 is platform default
149 orientation: ListView.Horizontal
151 function itemClicked(index, delegateItem) {
152 listView.currentIndex = index
153 var x = CarouselJS.getXFromContinuousIndex(index,
154 realWidth,
155 realContentWidth,
156 tileWidth,
157 gapToMiddlePhase,
158 gapToEndPhase,
159 carousel.drawBuffer)
161 if (Math.abs(x - contentX) < 1 && delegateItem !== undefined) {
162 /* We're clicking the selected item and
163 we're in the neighbourhood of radius 1 pixel from it.
164 Let's emit the clicked signal. */
165 carousel.clicked(index, delegateItem.y)
166 return
167 }
169 stepAnimation.stop()
170 newContentXAnimation.stop()
172 newContentX = x
173 newContentXAnimation.start()
174 }
176 function itemPressAndHold(index, delegateItem) {
177 var x = CarouselJS.getXFromContinuousIndex(index,
178 realWidth,
179 realContentWidth,
180 tileWidth,
181 gapToMiddlePhase,
182 gapToEndPhase,
183 carousel.drawBuffer);
185 if (Math.abs(x - contentX) < 1 && delegateItem !== undefined) {
186 /* We're pressAndHold the selected item and
187 we're in the neighbourhood of radius 1 pixel from it.
188 Let's emit the pressAndHold signal. */
189 carousel.pressAndHold(index, delegateItem.y);
190 return;
191 }
193 stepAnimation.stop();
194 newContentXAnimation.stop();
196 newContentX = x;
197 newContentXAnimation.start();
198 }
200 onHighlightIndexChanged: {
201 if (highlightIndex != -1) {
202 itemClicked(highlightIndex)
203 }
204 }
206 onMovementStarted: {
207 stepAnimation.stop()
208 newContentXAnimation.stop()
209 newContentX = disabledNewContentX
210 }
211 onMovementEnded: {
212 if (realContentX > 0 && realContentX < realContentWidth - realWidth)
213 stepAnimation.start()
214 }
216 SmoothedAnimation {
217 id: stepAnimation
219 target: listView
220 property: "contentX"
221 to: CarouselJS.getXFromContinuousIndex(listView.selectedIndex,
222 listView.realWidth,
223 listView.realContentWidth,
224 listView.tileWidth,
225 listView.gapToMiddlePhase,
226 listView.gapToEndPhase,
227 carousel.drawBuffer)
228 duration: 450
229 velocity: 200
230 easing.type: Easing.InOutQuad
231 }
233 SequentialAnimation {
234 id: newContentXAnimation
236 NumberAnimation {
237 target: listView
238 property: "contentX"
239 from: listView.contentX
240 to: listView.newContentX
241 duration: 300
242 easing.type: Easing.InOutQuad
243 }
244 ScriptAction {
245 script: listView.newContentX = listView.disabledNewContentX
246 }
247 }
249 readonly property int selectedIndex: Math.round(continuousIndex)
250 readonly property real continuousIndex: CarouselJS.getContinuousIndex(listView.realContentX,
251 listView.tileWidth,
252 listView.gapToMiddlePhase,
253 listView.gapToEndPhase,
254 listView.kGapEnd,
255 listView.kMiddleIndex,
256 listView.kXBeginningEnd)
258 property real viewTranslation: CarouselJS.getViewTranslation(listView.realContentX,
259 listView.tileWidth,
260 listView.gapToMiddlePhase,
261 listView.gapToEndPhase,
262 listView.translationXViewFactor)
264 delegate: Loader {
265 property bool explicitlyScaled: explicitScaleFactor == carousel.selectedItemScaleFactor
266 property real explicitScaleFactor: explicitScale ? carousel.selectedItemScaleFactor : 1.0
267 readonly property bool explicitScale: (!listView.moving ||
268 listView.realContentX <= 0 ||
269 listView.realContentX >= listView.realContentWidth - listView.realWidth) &&
270 listView.newContentX === listView.disabledNewContentX &&
271 index === listView.selectedIndex
272 readonly property real cachedTiles: listView.realPathItemCount + carousel.drawBuffer / listView.tileWidth
273 readonly property real distance: listView.continuousIndex - index
274 readonly property real itemTranslationScale: CarouselJS.getItemScale(0.5,
275 (index + 0.5), // good approximation of scale while changing selected item
276 listView.count,
277 listView.visibleTilesScaleFactor)
278 readonly property real itemScale: CarouselJS.getItemScale(distance,
279 listView.continuousIndex,
280 listView.count,
281 listView.visibleTilesScaleFactor)
282 readonly property real translationX: CarouselJS.getItemTranslation(index,
283 listView.selectedIndex,
284 distance,
285 itemScale,
286 itemTranslationScale,
287 listView.maximumItemTranslation)
289 readonly property real xTransform: listView.viewTranslation + translationX * listView.scaleFactor
290 readonly property real center: x - listView.contentX + xTransform - drawBuffer + (width/2)
292 width: listView.tileWidth
293 height: listView.tileHeight
294 scale: itemScale * explicitScaleFactor
295 sourceComponent: itemComponent
296 z: cachedTiles - Math.abs(index - listView.selectedIndex)
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 }
336 }
337 }
338 }