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