on 832d15e help sign in
unity8/Components/Carousel.qml
324 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: disabledNewContentX
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 disabledNewContentX: -carousel.drawBuffer - 1
104 readonly property real realContentWidth: contentWidth - 2 * carousel.drawBuffer
105 readonly property real realContentX: contentX + carousel.drawBuffer
106 readonly property real realPathItemCount: Math.min(realWidth / tileWidth, pathItemCount)
107 readonly property real realWidth: carousel.width
108 readonly property real referenceGapToMiddlePhase: realWidth / 2 - tileWidth / 2
109 readonly property real referencePathItemCount: referenceWidth / referenceTileWidth
110 readonly property real referenceWidth: 848
111 readonly property real referenceTileWidth: 175
112 readonly property real scaleFactor: tileWidth / referenceTileWidth
113 readonly property real tileWidth: Math.max(realWidth / pathItemCount, minimumTileWidth)
114 readonly property real tileHeight: tileWidth / tileAspectRatio
115 readonly property real translationXViewFactor: 0.2 * (referenceGapToMiddlePhase / gapToMiddlePhase)
116 readonly property real verticalMargin: (parent.height - tileHeight) / 2
117 readonly property real visibleTilesScaleFactor: realPathItemCount / referencePathItemCount
119 anchors {
120 fill: parent
121 topMargin: verticalMargin
122 bottomMargin: verticalMargin
123 // extending the "drawing area"
124 leftMargin: -carousel.drawBuffer
125 rightMargin: -carousel.drawBuffer
126 }
128 /* The header and footer help to "extend" the area, the listview draws items.
129 This together with anchors.leftMargin and anchors.rightMargin. */
130 header: Item {
131 width: carousel.drawBuffer
132 height: listView.tileHeight
133 }
134 footer: Item {
135 width: carousel.drawBuffer
136 height: listView.tileHeight
137 }
139 boundsBehavior: Flickable.StopAtBounds
140 cacheBuffer: carousel.cacheBuffer
141 flickDeceleration: Math.max(1500 * Math.pow(realWidth / referenceWidth, 1.5), 1500) // 1500 is platform default
142 maximumFlickVelocity: Math.max(2500 * Math.pow(realWidth / referenceWidth, 1.5), 2500) // 2500 is platform default
143 orientation: ListView.Horizontal
145 function itemClicked(index, delegateItem) {
146 var x = CarouselJS.getXFromContinuousIndex(index,
147 realWidth,
148 realContentWidth,
149 tileWidth,
150 gapToMiddlePhase,
151 gapToEndPhase,
152 carousel.drawBuffer)
154 if (Math.abs(x - contentX) < 1) {
155 /* We're clicking the selected item and
156 we're in the neighbourhood of radius 1 pixel from it.
157 Let's emit the clicked signal. */
158 carousel.clicked(index, delegateItem, delegateItem.y)
159 return
160 }
162 stepAnimation.stop()
163 newContentXAnimation.stop()
165 newContentX = x
166 newContentXAnimation.start()
167 }
169 function itemPressAndHold(index, delegateItem) {
170 var x = CarouselJS.getXFromContinuousIndex(index,
171 realWidth,
172 realContentWidth,
173 tileWidth,
174 gapToMiddlePhase,
175 gapToEndPhase,
176 carousel.drawBuffer);
178 if (Math.abs(x - contentX) < 1) {
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 carousel.pressAndHold(index, delegateItem, delegateItem.y);
183 return;
184 }
186 stepAnimation.stop();
187 newContentXAnimation.stop();
189 newContentX = x;
190 newContentXAnimation.start();
191 }
193 onMovementStarted: {
194 stepAnimation.stop()
195 newContentXAnimation.stop()
196 newContentX = disabledNewContentX
197 }
198 onMovementEnded: {
199 if (realContentX > 0 && realContentX < realContentWidth - realWidth)
200 stepAnimation.start()
201 }
203 SmoothedAnimation {
204 id: stepAnimation
206 target: listView
207 property: "contentX"
208 to: CarouselJS.getXFromContinuousIndex(listView.selectedIndex,
209 listView.realWidth,
210 listView.realContentWidth,
211 listView.tileWidth,
212 listView.gapToMiddlePhase,
213 listView.gapToEndPhase,
214 carousel.drawBuffer)
215 duration: 450
216 velocity: 200
217 easing.type: Easing.InOutQuad
218 }
220 SequentialAnimation {
221 id: newContentXAnimation
223 NumberAnimation {
224 target: listView
225 property: "contentX"
226 from: listView.contentX
227 to: listView.newContentX
228 duration: 300
229 easing.type: Easing.InOutQuad
230 }
231 ScriptAction {
232 script: listView.newContentX = listView.disabledNewContentX
233 }
234 }
236 readonly property int selectedIndex: Math.round(continuousIndex)
237 readonly property real continuousIndex: CarouselJS.getContinuousIndex(listView.realContentX,
238 listView.tileWidth,
239 listView.gapToMiddlePhase,
240 listView.gapToEndPhase,
241 listView.kGapEnd,
242 listView.kMiddleIndex,
243 listView.kXBeginningEnd)
245 property real viewTranslation: CarouselJS.getViewTranslation(listView.realContentX,
246 listView.tileWidth,
247 listView.gapToMiddlePhase,
248 listView.gapToEndPhase,
249 listView.translationXViewFactor)
251 delegate: Loader {
252 property bool explicitlyScaled: explicitScaleFactor == carousel.selectedItemScaleFactor
253 property real explicitScaleFactor: explicitScale ? carousel.selectedItemScaleFactor : 1.0
254 readonly property bool explicitScale: (!listView.moving ||
255 listView.realContentX <= 0 ||
256 listView.realContentX >= listView.realContentWidth - listView.realWidth) &&
257 listView.newContentX === listView.disabledNewContentX &&
258 index === listView.selectedIndex
259 readonly property real cachedTiles: listView.realPathItemCount + carousel.drawBuffer / listView.tileWidth
260 readonly property real distance: listView.continuousIndex - index
261 readonly property real itemTranslationScale: CarouselJS.getItemScale(0.5,
262 (index + 0.5), // good approximation of scale while changing selected item
263 listView.count,
264 listView.visibleTilesScaleFactor)
265 readonly property real itemScale: CarouselJS.getItemScale(distance,
266 listView.continuousIndex,
267 listView.count,
268 listView.visibleTilesScaleFactor)
269 readonly property real translationX: CarouselJS.getItemTranslation(index,
270 listView.selectedIndex,
271 distance,
272 itemScale,
273 itemTranslationScale,
274 listView.maximumItemTranslation)
276 width: listView.tileWidth
277 height: listView.tileHeight
278 scale: itemScale * explicitScaleFactor
279 sourceComponent: itemComponent
280 z: cachedTiles - Math.abs(index - listView.selectedIndex)
282 transform: Translate {
283 x: listView.viewTranslation + translationX * listView.scaleFactor
284 }
286 Behavior on explicitScaleFactor {
287 SequentialAnimation {
288 ScriptAction {
289 script: if (!explicitScale)
290 explicitlyScaled = false
291 }
292 NumberAnimation {
293 duration: explicitScaleFactor === 1.0 ? 250 : 150
294 easing.type: Easing.InOutQuad
295 }
296 ScriptAction {
297 script: if (explicitScale)
298 explicitlyScaled = true
299 }
300 }
301 }
303 onLoaded: {
304 item.explicitlyScaled = Qt.binding(function() { return explicitlyScaled; })
305 item.model = Qt.binding(function() { return model; })
306 }
308 MouseArea {
309 id: mouseArea
311 anchors.fill: parent
313 onClicked: {
314 listView.itemClicked(index, item)
315 }
317 onPressAndHold: {
318 listView.itemPressAndHold(index, item)
319 }
321 }
322 }
323 }