on db5e51c help sign in
unity8/Components/Carousel.qml
284 lines 15 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 /// The component to be used as delegate. This component has to be derived from BaseCarouselDelegate
28 property Component itemComponent
29 /// Model for the Carousel, which has to be a model usable by a ListView
30 property alias model: listView.model
31 /// A minimal width of a tile can be set here. Per default a best fit will be calculated
32 property alias minimumTileWidth: listView.minimumTileWidth
33 /// Sets the number of tiles that are visible
34 property alias pathItemCount: listView.pathItemCount
35 /// Aspect ratio of the tiles width/height
36 property alias tileAspectRatio: listView.tileAspectRatio
37 /// Used to cache some delegates for performance reasons. See the ListView documentation for details
38 property alias cacheBuffer: listView.cacheBuffer
39 /// Width of the "draw buffer" in pixel. The drawBuffer is an additional area at start/end where
40 /// items drawn, even if it is not in the visible area.
41 /// cacheBuffer controls only the to retain delegates outside the visible area (and is used on top of the drawBuffer)
42 /// see https://bugreports.qt-project.org/browse/QTBUG-29173
43 property int drawBuffer: width / pathItemCount // an "ok" value - but values used from the listView cause loops
44 /// The selected item can be shown in a different size controlled by selectedItemScaleFactor
45 property real selectedItemScaleFactor: 1.1
47 /// Emitted when the user clicked on an item
48 /// @param index is the index of the clicked item
49 /// @param delegateItem is the clicked component/delegate itself
50 /// @param itemY is y of the clicked delegate
51 signal clicked(int index, var delegateItem, real itemY)
53 implicitHeight: listView.tileHeight * selectedItemScaleFactor
55 /* Basic idea behind the carousel effect is to move the items of the delegates (compacting /stuffing them).
56 One drawback is, that more delegates have to be drawn than usually. As some items are moved from the
57 invisible to the visible area. Setting the cacheBuffer does not fix this.
58 See https://bugreports.qt-project.org/browse/QTBUG-29173
59 Therefore the ListView has negative left and right anchors margins, and in addition a header
60 and footer item to compensate that.
62 The scaling of the items is controlled by the variable continuousIndex, described below. */
63 ListView {
64 id: listView
66 property real minimumTileWidth: 0
67 property real newContentX: -1
68 property real pathItemCount: referenceWidth / referenceTileWidth
69 property real tileAspectRatio: 1
71 /* The positioning and scaling of the items in the carousel is based on the variable
72 'continuousIndex', a continuous real variable between [0, 'carousel.model.count'],
73 roughly representing the index of the item that is prioritised over the others.
74 'continuousIndex' is not linear, but is weighted depending on if it is close
75 to the beginning of the content (beginning phase), in the middle (middle phase)
76 or at the end (end phase).
77 Each tile is scaled and transformed in proportion to the difference between
78 its own index and continuousIndex.
79 To efficiently calculate continuousIndex, we have these values:
80 - 'gapToMiddlePhase' gap in pixels between beginning and middle phase
81 - 'gapToEndPhase' gap in pixels between middle and end phase
82 - 'kGapEnd' constant used to calculate 'continuousIndex' in end phase
83 - 'kMiddleIndex' constant used to calculate 'continuousIndex' in middle phase
84 - 'kXBeginningEnd' constant used to calculate 'continuousIndex' in beginning and end phase
85 - 'realContentWidth' the width of all the delegates only (without header/footer)
86 - 'realContentX' the 'contentX' of the listview ignoring the 'drawBuffer'
87 - 'realWidth' the 'width' of the listview, as it is used as component. */
89 readonly property real gapToMiddlePhase: Math.min(realWidth / 2 - tileWidth / 2, (realContentWidth - realWidth) / 2)
90 readonly property real gapToEndPhase: realContentWidth - realWidth - gapToMiddlePhase
91 readonly property real kGapEnd: kMiddleIndex * (1 - gapToEndPhase / gapToMiddlePhase)
92 readonly property real kMiddleIndex: (realWidth / 2) / tileWidth - 0.5
93 readonly property real kXBeginningEnd: 1 / tileWidth + kMiddleIndex / gapToMiddlePhase
94 readonly property real maximumItemTranslation: (listView.tileWidth * 3) / listView.scaleFactor
95 readonly property real realContentWidth: contentWidth - 2 * carousel.drawBuffer
96 readonly property real realContentX: contentX + carousel.drawBuffer
97 readonly property real realPathItemCount: Math.min(realWidth / tileWidth, pathItemCount)
98 readonly property real realWidth: carousel.width
99 readonly property real referenceGapToMiddlePhase: realWidth / 2 - tileWidth / 2
100 readonly property real referencePathItemCount: referenceWidth / referenceTileWidth
101 readonly property real referenceWidth: 848
102 readonly property real referenceTileWidth: 175
103 readonly property real scaleFactor: tileWidth / referenceTileWidth
104 readonly property real tileWidth: Math.max(realWidth / pathItemCount, minimumTileWidth)
105 readonly property real tileHeight: tileWidth / tileAspectRatio
106 readonly property real translationXViewFactor: 0.2 * (referenceGapToMiddlePhase / gapToMiddlePhase)
107 readonly property real verticalMargin: (parent.height - tileHeight) / 2
108 readonly property real visibleTilesScaleFactor: realPathItemCount / referencePathItemCount
110 anchors {
111 fill: parent
112 topMargin: verticalMargin
113 bottomMargin: verticalMargin
114 // extending the "drawing area"
115 leftMargin: -carousel.drawBuffer
116 rightMargin: -carousel.drawBuffer
117 }
119 /* The header and footer help to "extend" the area, the listview draws items.
120 This together with anchors.leftMargin and anchors.rightMargin. */
121 header: Item {
122 width: carousel.drawBuffer
123 height: listView.tileHeight
124 }
125 footer: Item {
126 width: carousel.drawBuffer
127 height: listView.tileHeight
128 }
130 boundsBehavior: Flickable.StopAtBounds
131 cacheBuffer: carousel.cacheBuffer
132 flickDeceleration: Math.max(1500 * Math.pow(realWidth / referenceWidth, 1.5), 1500) // 1500 is platform default
133 maximumFlickVelocity: Math.max(2500 * Math.pow(realWidth / referenceWidth, 1.5), 2500) // 2500 is platform default
134 orientation: ListView.Horizontal
136 function itemClicked(index, delegateItem) {
137 var x = CarouselJS.getXFromContinuousIndex(index,
138 realWidth,
139 realContentWidth,
140 tileWidth,
141 gapToMiddlePhase,
142 gapToEndPhase,
143 carousel.drawBuffer)
145 if (Math.abs(x - contentX) < 1) {
146 /* We're clicking the selected item and
147 we're in the neighbourhood of radius 1 pixel from it.
148 Let's emit the clicked signal. */
149 carousel.clicked(index, delegateItem, delegateItem.y)
150 return
151 }
153 stepAnimation.stop()
154 newContentXAnimation.stop()
156 newContentX = x
157 newContentXAnimation.start()
158 }
160 onMovementStarted: {
161 stepAnimation.stop()
162 newContentXAnimation.stop()
163 newContentX = -1
164 }
165 onMovementEnded: {
166 if (realContentX > 0 && realContentX < realContentWidth - realWidth)
167 stepAnimation.start()
168 }
170 SmoothedAnimation {
171 id: stepAnimation
173 target: listView
174 property: "contentX"
175 to: CarouselJS.getXFromContinuousIndex(listView.selectedIndex,
176 listView.realWidth,
177 listView.realContentWidth,
178 listView.tileWidth,
179 listView.gapToMiddlePhase,
180 listView.gapToEndPhase,
181 carousel.drawBuffer)
182 duration: 450
183 velocity: 200
184 easing.type: Easing.InOutQuad
185 }
187 SequentialAnimation {
188 id: newContentXAnimation
190 NumberAnimation {
191 target: listView
192 property: "contentX"
193 from: listView.contentX
194 to: listView.newContentX
195 duration: 300
196 easing.type: Easing.InOutQuad
197 }
198 ScriptAction {
199 script: listView.newContentX = -1
200 }
201 }
203 readonly property int selectedIndex: Math.round(continuousIndex)
204 readonly property real continuousIndex: CarouselJS.getContinuousIndex(listView.realContentX,
205 listView.tileWidth,
206 listView.gapToMiddlePhase,
207 listView.gapToEndPhase,
208 listView.kGapEnd,
209 listView.kMiddleIndex,
210 listView.kXBeginningEnd)
212 property real viewTranslation: CarouselJS.getViewTranslation(listView.realContentX,
213 listView.tileWidth,
214 listView.gapToMiddlePhase,
215 listView.gapToEndPhase,
216 listView.translationXViewFactor)
218 delegate: Loader {
219 property bool explicitlyScaled: explicitScaleFactor == carousel.selectedItemScaleFactor
220 property real explicitScaleFactor: explicitScale ? carousel.selectedItemScaleFactor : 1.0
221 readonly property bool explicitScale: (!listView.moving ||
222 listView.realContentX <= 0 ||
223 listView.realContentX >= listView.realContentWidth - listView.realWidth) &&
224 listView.newContentX < 0 &&
225 index === listView.selectedIndex
226 readonly property real cachedTiles: listView.realPathItemCount + carousel.drawBuffer / listView.tileWidth
227 readonly property real distance: listView.continuousIndex - index
228 readonly property real itemTranslationScale: CarouselJS.getItemScale(0.5,
229 (index + 0.5), // good approximation of scale while changing selected item
230 listView.count,
231 listView.visibleTilesScaleFactor)
232 readonly property real itemScale: CarouselJS.getItemScale(distance,
233 listView.continuousIndex,
234 listView.count,
235 listView.visibleTilesScaleFactor)
236 readonly property real translationX: CarouselJS.getItemTranslation(index,
237 listView.selectedIndex,
238 distance,
239 itemScale,
240 itemTranslationScale,
241 listView.maximumItemTranslation)
243 width: listView.tileWidth
244 height: listView.tileHeight
245 scale: itemScale * explicitScaleFactor
246 sourceComponent: itemComponent
247 z: cachedTiles - Math.abs(index - listView.selectedIndex)
249 transform: Translate {
250 x: listView.viewTranslation + translationX * listView.scaleFactor
251 }
253 Behavior on explicitScaleFactor {
254 SequentialAnimation {
255 ScriptAction {
256 script: if (!explicitScale)
257 explicitlyScaled = false
258 }
259 NumberAnimation {
260 duration: explicitScaleFactor === 1.0 ? 250 : 150
261 easing.type: Easing.InOutQuad
262 }
263 ScriptAction {
264 script: if (explicitScale)
265 explicitlyScaled = true
266 }
267 }
268 }
270 onLoaded: {
271 item.explicitlyScaled = Qt.binding(function() { return explicitlyScaled; })
272 item.model = Qt.binding(function() { return model; })
273 }
275 MouseArea {
276 id: mouseArea
278 anchors.fill: parent
280 onClicked: listView.itemClicked(index, item)
281 }
282 }
283 }