~unity-team/+junk/dashboard-playground

« back to all changes in this revision

Viewing changes to Components/Carousel.qml

  • Committer: Michał Sawicz
  • Date: 2013-06-05 22:03:08 UTC
  • Revision ID: michal.sawicz@canonical.com-20130605220308-yny8fv3futtr04fg
Inital unity8 commit.

Previous history can be found at https://code.launchpad.net/~unity-team/unity/phablet

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 */
 
16
 
 
17
import QtQuick 2.0
 
18
import Ubuntu.Components 0.1
 
19
import "carousel.js" as CarouselJS
 
20
 
 
21
Item {
 
22
    id: carousel
 
23
 
 
24
    property Component itemComponent
 
25
    property var model
 
26
    property alias minimumTileWidth: flickable.minimumTileWidth
 
27
    property alias pathItemCount: flickable.pathItemCount
 
28
    property alias tileAspectRatio: flickable.tileAspectRatio
 
29
    property int cacheBuffer: 0
 
30
    property real selectedItemScaleFactor: 1.1
 
31
 
 
32
    signal clicked(int index, var delegateItem, real itemY)
 
33
 
 
34
    implicitHeight: flickable.tileHeight * selectedItemScaleFactor
 
35
 
 
36
    /* TODO: evaluate if the component could be more efficient with a ListView,
 
37
             using this technique https://bugreports.qt-project.org/browse/QTBUG-29173 */
 
38
 
 
39
    Flickable {
 
40
        id: flickable
 
41
 
 
42
        property real minimumTileWidth: 0
 
43
        property real newContentX: -1
 
44
        property real pathItemCount: referenceWidth / referenceTileWidth
 
45
        property real tileAspectRatio: 1
 
46
 
 
47
        /* The positioning and scaling of the items in the carousel is based on the variable
 
48
           'continuousIndex', a continuous real variable between [0, 'carousel.model.count'],
 
49
           roughly representing the index of the item that is prioritised over the others.
 
50
           'continuousIndex' is not linear, but is weighted depending on if it is close
 
51
           to the beginning of the content (beginning phase), in the middle (middle phase)
 
52
           or at the end (end phase).
 
53
           Each tile is scaled and transformed in proportion to the difference between
 
54
           its own index and continuousIndex.
 
55
           To efficiently calculate continuousIndex, we have these values:
 
56
            - 'gapToMiddlePhase' gap in pixels between beginning and middle phase
 
57
            - 'gapToEndPhase' gap in pixels between middle and end phase
 
58
            - 'kGapEnd' constant used to calculate 'continuousIndex' in end phase
 
59
            - 'kMiddleIndex' constant used to calculate 'continuousIndex' in middle phase
 
60
            - 'kXBeginningEnd' constant used to calculate 'continuousIndex' in beginning and end phase. */
 
61
 
 
62
        readonly property real gapToMiddlePhase: Math.min(width / 2 - tileWidth / 2, (contentWidth - width) / 2)
 
63
        readonly property real gapToEndPhase: contentWidth - width - gapToMiddlePhase
 
64
        readonly property real kGapEnd: kMiddleIndex * (1 - gapToEndPhase / gapToMiddlePhase)
 
65
        readonly property real kMiddleIndex: (width / 2) / tileWidth - 0.5
 
66
        readonly property real kXBeginningEnd: 1 / tileWidth + kMiddleIndex / gapToMiddlePhase
 
67
        readonly property real realPathItemCount: Math.min(width / tileWidth, pathItemCount)
 
68
        readonly property real referenceGapToMiddlePhase: width / 2 - tileWidth / 2
 
69
        readonly property real referencePathItemCount: referenceWidth / referenceTileWidth
 
70
        readonly property real referenceWidth: 848
 
71
        readonly property real referenceTileWidth: 175
 
72
        readonly property real scaleFactor: tileWidth / referenceTileWidth
 
73
        readonly property real tileWidth: Math.max(width / pathItemCount, minimumTileWidth)
 
74
        readonly property real tileHeight: tileWidth / tileAspectRatio
 
75
        readonly property real translationXViewFactor: 0.2 * (referenceGapToMiddlePhase / gapToMiddlePhase)
 
76
        readonly property real verticalMargin: (parent.height - tileHeight) / 2
 
77
        readonly property real visibleTilesScaleFactor: realPathItemCount / referencePathItemCount
 
78
 
 
79
        anchors {
 
80
            fill: parent
 
81
            topMargin: verticalMargin
 
82
            bottomMargin: verticalMargin
 
83
        }
 
84
        contentWidth: view.width
 
85
        contentHeight: height
 
86
        boundsBehavior: Flickable.StopAtBounds
 
87
        flickDeceleration: Math.max(1500 * Math.pow(width / referenceWidth, 1.5), 1500) // 1500 is platform default
 
88
        maximumFlickVelocity: Math.max(2500 * Math.pow(width / referenceWidth, 1.5), 2500) // 2500 is platform default
 
89
 
 
90
        function itemClicked(index, delegateItem) {
 
91
            var x = CarouselJS.getXFromContinuousIndex(index,
 
92
                                                       width,
 
93
                                                       contentWidth,
 
94
                                                       tileWidth,
 
95
                                                       gapToMiddlePhase,
 
96
                                                       gapToEndPhase)
 
97
 
 
98
            if (Math.abs(x - contentX) < 1) {
 
99
                /* We're clicking the selected item and
 
100
                   we're in the neighbourhood of radius 1 pixel from it.
 
101
                   Let's emit the clicked signal. */
 
102
                carousel.clicked(index, delegateItem, delegateItem.y)
 
103
                return
 
104
            }
 
105
 
 
106
            stepAnimation.stop()
 
107
            newContentXAnimation.stop()
 
108
 
 
109
            newContentX = x
 
110
            newContentXAnimation.start()
 
111
        }
 
112
 
 
113
        onMovementStarted: {
 
114
            stepAnimation.stop()
 
115
            newContentXAnimation.stop()
 
116
            newContentX = -1
 
117
        }
 
118
        onMovementEnded: {
 
119
            if (contentX > 0 && contentX < contentWidth - width)
 
120
                stepAnimation.start()
 
121
        }
 
122
 
 
123
        SmoothedAnimation {
 
124
            id: stepAnimation
 
125
 
 
126
            target: flickable
 
127
            property: "contentX"
 
128
            from: flickable.contentX
 
129
            to: CarouselJS.getXFromContinuousIndex(view.selectedIndex,
 
130
                                                   flickable.width,
 
131
                                                   flickable.contentWidth,
 
132
                                                   flickable.tileWidth,
 
133
                                                   flickable.gapToMiddlePhase,
 
134
                                                   flickable.gapToEndPhase)
 
135
            duration: 450
 
136
            velocity: 200
 
137
            easing.type: Easing.InOutQuad
 
138
        }
 
139
 
 
140
        SequentialAnimation {
 
141
            id: newContentXAnimation
 
142
 
 
143
            NumberAnimation {
 
144
                target: flickable
 
145
                property: "contentX"
 
146
                from: flickable.contentX
 
147
                to: flickable.newContentX
 
148
                duration: 300
 
149
                easing.type: Easing.InOutQuad
 
150
            }
 
151
            ScriptAction {
 
152
                script: flickable.newContentX = -1
 
153
            }
 
154
        }
 
155
 
 
156
        Row {
 
157
            id: view
 
158
 
 
159
            readonly property int selectedIndex: Math.round(continuousIndex)
 
160
            readonly property real continuousIndex: CarouselJS.getContinuousIndex(flickable.contentX,
 
161
                                                                                  flickable.tileWidth,
 
162
                                                                                  flickable.gapToMiddlePhase,
 
163
                                                                                  flickable.gapToEndPhase,
 
164
                                                                                  flickable.kGapEnd,
 
165
                                                                                  flickable.kMiddleIndex,
 
166
                                                                                  flickable.kXBeginningEnd)
 
167
 
 
168
            height: parent.height
 
169
            anchors.verticalCenter: parent.verticalCenter
 
170
 
 
171
            transform: Translate {
 
172
                x: CarouselJS.getViewTranslation(flickable.contentX,
 
173
                                                 flickable.tileWidth,
 
174
                                                 flickable.gapToMiddlePhase,
 
175
                                                 flickable.gapToEndPhase,
 
176
                                                 flickable.translationXViewFactor)
 
177
            }
 
178
 
 
179
            Repeater {
 
180
                id: repeater
 
181
 
 
182
                model: carousel.model
 
183
 
 
184
                Loader {
 
185
                    property bool explicitlyScaled: explicitScaleFactor == carousel.selectedItemScaleFactor
 
186
                    property real explicitScaleFactor: explicitScale ? carousel.selectedItemScaleFactor : 1.0
 
187
                    readonly property bool explicitScale: (!flickable.moving ||
 
188
                                                           flickable.contentX <= 0 ||
 
189
                                                           flickable.contentX >= flickable.contentWidth - flickable.width) &&
 
190
                                                          flickable.newContentX < 0 &&
 
191
                                                          index === view.selectedIndex
 
192
                    readonly property real cachedTiles: flickable.realPathItemCount + carousel.cacheBuffer / flickable.tileWidth
 
193
                    readonly property real distance: view.continuousIndex - index
 
194
                    readonly property real itemTranslationScale: CarouselJS.getItemScale(0.5,
 
195
                                                                                         (index + 0.5), // good approximation of scale while changing selected item
 
196
                                                                                         repeater.count,
 
197
                                                                                         flickable.visibleTilesScaleFactor)
 
198
                    readonly property real itemScale: CarouselJS.getItemScale(distance,
 
199
                                                                              view.continuousIndex,
 
200
                                                                              repeater.count,
 
201
                                                                              flickable.visibleTilesScaleFactor)
 
202
                    readonly property real translationFactor: (flickable.tileWidth * 3) / flickable.scaleFactor
 
203
                    readonly property real translationX: index === view.selectedIndex ? 0 :
 
204
                                                         CarouselJS.getItemTranslation(distance,
 
205
                                                                                       itemScale,
 
206
                                                                                       itemTranslationScale,
 
207
                                                                                       translationFactor)
 
208
 
 
209
                    width: flickable.tileWidth
 
210
                    height: flickable.tileHeight
 
211
                    scale: itemScale * explicitScaleFactor
 
212
                    opacity: scale > 0.02 ? 1 : 0
 
213
                    sourceComponent: z > 0 ? itemComponent : undefined
 
214
                    z: cachedTiles - Math.abs(index - view.selectedIndex)
 
215
 
 
216
                    transform: Translate {
 
217
                        x: translationX * flickable.scaleFactor
 
218
                    }
 
219
 
 
220
                    Behavior on explicitScaleFactor {
 
221
                        SequentialAnimation {
 
222
                            ScriptAction {
 
223
                                script: if (!explicitScale)
 
224
                                            explicitlyScaled = false
 
225
                            }
 
226
                            NumberAnimation {
 
227
                                duration: explicitScaleFactor === 1.0 ? 250 : 150
 
228
                                easing.type: Easing.InOutQuad
 
229
                            }
 
230
                            ScriptAction {
 
231
                                script: if (explicitScale)
 
232
                                            explicitlyScaled = true
 
233
                            }
 
234
                        }
 
235
                    }
 
236
 
 
237
                    onLoaded: {
 
238
                        item.explicitlyScaled = Qt.binding(function() { return explicitlyScaled; })
 
239
                        item.model = Qt.binding(function() { return model; })
 
240
                    }
 
241
 
 
242
                    MouseArea {
 
243
                        id: mouseArea
 
244
 
 
245
                        anchors.fill: parent
 
246
 
 
247
                        onClicked: flickable.itemClicked(index, item)
 
248
                    }
 
249
                }
 
250
            }
 
251
        }
 
252
    }
 
253
}