on bionic help sign in
unity8/qml/Components/Carousel.qml
335 lines 17 kB history raw
1/*
2 * Copyright (C) 2013-2015 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.4
18import Ubuntu.Components 1.3
19import "carousel.js" as CarouselJS
20import "."
22/*! The Carousel component presents the items of a model in a carousel view. It's similar to a
23 cover flow. But it stops at it's boundaries (therefore no PathView is used).
24 */
25Item {
26 id: carousel
28 clip: true // Don't leak horizontally to other dashes
30 /// The component to be used as delegate. This component has to be derived from BaseCarouselDelegate
31 property Component itemComponent
32 /// Model for the Carousel, which has to be a model usable by a ListView
33 property alias model: listView.model
34 /// A minimal width of a tile can be set here. Per default a best fit will be calculated
35 property alias minimumTileWidth: listView.minimumTileWidth
36 /// Sets the number of tiles that are visible
37 property alias pathItemCount: listView.pathItemCount
38 /// Aspect ratio of the tiles width/height
39 property alias tileAspectRatio: listView.tileAspectRatio
40 /// Used to cache some delegates for performance reasons. See the ListView documentation for details
41 property alias cacheBuffer: listView.cacheBuffer
42 property alias displayMarginBeginning: listView.displayMarginBeginning
43 property alias displayMarginEnd: listView.displayMarginEnd
44 /// Width of the "draw buffer" in pixel. The drawBuffer is an additional area at start/end where
45 /// items drawn, even if it is not in the visible area.
46 /// cacheBuffer controls only the to retain delegates outside the visible area (and is used on top of the drawBuffer)
47 /// see https://bugreports.qt-project.org/browse/QTBUG-29173
48 property int drawBuffer: width / pathItemCount // an "ok" value - but values used from the listView cause loops
49 /// The selected item can be shown in a different size controlled by selectedItemScaleFactor
50 property real selectedItemScaleFactor: 1.1
51 /// The extra margin at the bottom
52 property real extraBottomMargin: 0
53 /// The index of the item that should be highlighted
54 property alias highlightIndex: listView.highlightIndex
55 /// exposes the delegate of the currentItem
56 readonly property alias currentItem: listView.currentItem
57 /// exposes the distance to the next row (only one row in carousel, so it's the topMargins)
58 readonly property alias verticalSpacing: listView.verticalMargin
59 /// the width of the internal list
60 readonly property alias innerWidth: listView.width
62 implicitHeight: listView.tileHeight * selectedItemScaleFactor
63 opacity: listView.highlightIndex === -1 ? 1 : 0.6
65 /* Basic idea behind the carousel effect is to move the items of the delegates (compacting /stuffing them).
66 One drawback is, that more delegates have to be drawn than usually. As some items are moved from the
67 invisible to the visible area. Setting the cacheBuffer does not fix this.
68 See https://bugreports.qt-project.org/browse/QTBUG-29173
69 Therefore the ListView has negative left and right anchors margins, and in addition a header
70 and footer item to compensate that.
72 The scaling of the items is controlled by the variable continuousIndex, described below. */
73 ListView {
74 id: listView
75 objectName: "listView"
77 property int highlightIndex: -1
78 property real minimumTileWidth: 0
79 property real newContentX: disabledNewContentX
80 property real pathItemCount: referenceWidth / referenceTileWidth
81 property real tileAspectRatio: 1
83 /* The positioning and scaling of the items in the carousel is based on the variable
84 'continuousIndex', a continuous real variable between [0, 'carousel.model.count'],
85 roughly representing the index of the item that is prioritised over the others.
86 'continuousIndex' is not linear, but is weighted depending on if it is close
87 to the beginning of the content (beginning phase), in the middle (middle phase)
88 or at the end (end phase).
89 Each tile is scaled and transformed in proportion to the difference between
90 its own index and continuousIndex.
91 To efficiently calculate continuousIndex, we have these values:
92 - 'gapToMiddlePhase' gap in pixels between beginning and middle phase
93 - 'gapToEndPhase' gap in pixels between middle and end phase
94 - 'kGapEnd' constant used to calculate 'continuousIndex' in end phase
95 - 'kMiddleIndex' constant used to calculate 'continuousIndex' in middle phase
96 - 'kXBeginningEnd' constant used to calculate 'continuousIndex' in beginning and end phase
97 - 'realContentWidth' the width of all the delegates only (without header/footer)
98 - 'realContentX' the 'contentX' of the listview ignoring the 'drawBuffer'
99 - 'realWidth' the 'width' of the listview, as it is used as component. */
101 readonly property real gapToMiddlePhase: Math.min(realWidth / 2 - tileWidth / 2, (realContentWidth - realWidth) / 2)
102 readonly property real gapToEndPhase: realContentWidth - realWidth - gapToMiddlePhase
103 readonly property real kGapEnd: kMiddleIndex * (1 - gapToEndPhase / gapToMiddlePhase)
104 readonly property real kMiddleIndex: (realWidth / 2) / tileWidth - 0.5
105 readonly property real kXBeginningEnd: 1 / tileWidth + kMiddleIndex / gapToMiddlePhase
106 readonly property real maximumItemTranslation: (listView.tileWidth * 3) / listView.scaleFactor
107 readonly property real disabledNewContentX: -carousel.drawBuffer - 1
108 readonly property real realContentWidth: contentWidth - 2 * carousel.drawBuffer
109 readonly property real realContentX: contentX + carousel.drawBuffer
110 readonly property real realPathItemCount: Math.min(realWidth / tileWidth, pathItemCount)
111 readonly property real realWidth: carousel.width
112 readonly property real referenceGapToMiddlePhase: realWidth / 2 - tileWidth / 2
113 readonly property real referencePathItemCount: referenceWidth / referenceTileWidth
114 readonly property real referenceWidth: 848
115 readonly property real referenceTileWidth: 175
116 readonly property real scaleFactor: tileWidth / referenceTileWidth
117 readonly property real tileWidth: Math.max(realWidth / pathItemCount, minimumTileWidth)
118 readonly property real tileHeight: tileWidth / tileAspectRatio
119 readonly property real translationXViewFactor: 0.2 * (referenceGapToMiddlePhase / gapToMiddlePhase)
120 readonly property real verticalMargin: (parent.height - tileHeight - carousel.extraBottomMargin) / 2
121 readonly property real visibleTilesScaleFactor: realPathItemCount / referencePathItemCount
123 anchors {
124 fill: parent
125 topMargin: verticalMargin
126 bottomMargin: verticalMargin + carousel.extraBottomMargin
127 // extending the "drawing area"
128 leftMargin: -carousel.drawBuffer
129 rightMargin: -carousel.drawBuffer
130 }
132 /* The header and footer help to "extend" the area, the listview draws items.
133 This together with anchors.leftMargin and anchors.rightMargin. */
134 header: Item {
135 width: carousel.drawBuffer
136 height: listView.tileHeight
137 }
138 footer: Item {
139 width: carousel.drawBuffer
140 height: listView.tileHeight
141 }
143 boundsBehavior: Flickable.DragOverBounds
144 cacheBuffer: carousel.cacheBuffer
145 orientation: ListView.Horizontal
147 function getXFromContinuousIndex(index) {
148 return CarouselJS.getXFromContinuousIndex(index,
149 realWidth,
150 footerItem.x,
151 tileWidth,
152 gapToMiddlePhase,
153 gapToEndPhase,
154 carousel.drawBuffer)
155 }
157 function itemClicked(index, delegateItem) {
158 listView.currentIndex = index
159 var x = getXFromContinuousIndex(index);
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 delegateItem.clicked()
166 return
167 }
169 stepAnimation.stop()
170 newContentXAnimation.stop()
172 newContentX = x
173 newContentXAnimation.start()
174 }
176 function itemPressAndHold(index, delegateItem) {
177 var x = getXFromContinuousIndex(index);
179 if (Math.abs(x - contentX) < 1 && delegateItem !== undefined) {
180 /* We're pressAndHold the selected item and
181 we're in the neighbourhood of radius 1 pixel from it.
182 Let's emit the pressAndHold signal. */
183 delegateItem.pressAndHold();
184 return;
185 }
187 stepAnimation.stop();
188 newContentXAnimation.stop();
190 newContentX = x;
191 newContentXAnimation.start();
192 }
194 onHighlightIndexChanged: {
195 if (highlightIndex != -1) {
196 itemClicked(highlightIndex)
197 }
198 }
200 onMovementStarted: {
201 stepAnimation.stop()
202 newContentXAnimation.stop()
203 newContentX = disabledNewContentX
204 }
205 onMovementEnded: {
206 if (realContentX > 0)
207 stepAnimation.start()
208 }
210 SmoothedAnimation {
211 id: stepAnimation
212 objectName: "stepAnimation"
214 target: listView
215 property: "contentX"
216 to: listView.getXFromContinuousIndex(listView.selectedIndex)
217 duration: 450
218 velocity: 200
219 easing.type: Easing.InOutQuad
220 }
222 SequentialAnimation {
223 id: newContentXAnimation
225 NumberAnimation {
226 target: listView
227 property: "contentX"
228 from: listView.contentX
229 to: listView.newContentX
230 duration: 300
231 easing.type: Easing.InOutQuad
232 }
233 ScriptAction {
234 script: listView.newContentX = listView.disabledNewContentX
235 }
236 }
238 readonly property int selectedIndex: Math.round(continuousIndex)
239 readonly property real continuousIndex: CarouselJS.getContinuousIndex(listView.realContentX,
240 listView.tileWidth,
241 listView.gapToMiddlePhase,
242 listView.gapToEndPhase,
243 listView.kGapEnd,
244 listView.kMiddleIndex,
245 listView.kXBeginningEnd)
247 property real viewTranslation: CarouselJS.getViewTranslation(listView.realContentX,
248 listView.tileWidth,
249 listView.gapToMiddlePhase,
250 listView.gapToEndPhase,
251 listView.translationXViewFactor)
253 delegate: tileWidth > 0 && tileHeight > 0 ? loaderComponent : null
255 Component {
256 id: loaderComponent
258 Loader {
259 property bool explicitlyScaled: explicitScaleFactor == carousel.selectedItemScaleFactor
260 property real explicitScaleFactor: explicitScale ? carousel.selectedItemScaleFactor : 1.0
261 readonly property bool explicitScale: (!listView.moving ||
262 listView.realContentX <= 0 ||
263 listView.realContentX >= listView.realContentWidth - listView.realWidth) &&
264 listView.newContentX === listView.disabledNewContentX &&
265 index === listView.selectedIndex
266 readonly property real cachedTiles: listView.realPathItemCount + carousel.drawBuffer / listView.tileWidth
267 readonly property real distance: listView.continuousIndex - index
268 readonly property real itemTranslationScale: CarouselJS.getItemScale(0.5,
269 (index + 0.5), // good approximation of scale while changing selected item
270 listView.count,
271 listView.visibleTilesScaleFactor)
272 readonly property real itemScale: CarouselJS.getItemScale(distance,
273 listView.continuousIndex,
274 listView.count,
275 listView.visibleTilesScaleFactor)
276 readonly property real translationX: CarouselJS.getItemTranslation(index,
277 listView.selectedIndex,
278 distance,
279 itemScale,
280 itemTranslationScale,
281 listView.maximumItemTranslation)
283 readonly property real xTransform: listView.viewTranslation + translationX * listView.scaleFactor
284 readonly property real center: x - listView.contentX + xTransform - drawBuffer + (width/2)
286 width: listView.tileWidth
287 height: listView.tileHeight
288 scale: itemScale * explicitScaleFactor
289 sourceComponent: itemComponent
290 z: cachedTiles - Math.abs(index - listView.selectedIndex)
292 transform: Translate {
293 x: xTransform
294 }
296 Behavior on explicitScaleFactor {
297 SequentialAnimation {
298 ScriptAction {
299 script: if (!explicitScale)
300 explicitlyScaled = false
301 }
302 NumberAnimation {
303 duration: explicitScaleFactor === 1.0 ? 250 : 150
304 easing.type: Easing.InOutQuad
305 }
306 ScriptAction {
307 script: if (explicitScale)
308 explicitlyScaled = true
309 }
310 }
311 }
313 onLoaded: {
314 item.explicitlyScaled = Qt.binding(function() { return explicitlyScaled; });
315 item.index = Qt.binding(function() { return index; });
316 item.model = Qt.binding(function() { return model; });
317 }
319 MouseArea {
320 id: mouseArea
322 anchors.fill: parent
324 onClicked: {
325 listView.itemClicked(index, item)
326 }
328 onPressAndHold: {
329 listView.itemPressAndHold(index, item)
330 }
331 }
332 }
333 }
334 }