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