Merge lp:~ahayzen/music-app/remix-rewrite-column-flow into lp:music-app/remix
- remix-rewrite-column-flow
- Merge into remix
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Victor Thompson | ||||
Approved revision: | 738 | ||||
Merged at revision: | 735 | ||||
Proposed branch: | lp:~ahayzen/music-app/remix-rewrite-column-flow | ||||
Merge into: | lp:music-app/remix | ||||
Diff against target: |
713 lines (+412/-147) 8 files modified
MusicArtists.qml (+1/-1) MusicPlaylists.qml (+4/-4) MusicStart.qml (+1/-1) common/AlbumsPage.qml (+7/-0) common/BlurredHeader.qml (+0/-2) common/Card.qml (+4/-0) common/CardView.qml (+11/-5) common/ColumnFlow.qml (+384/-134) |
||||
To merge this branch: | bzr merge lp:~ahayzen/music-app/remix-rewrite-column-flow | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Victor Thompson | Approve | ||
Ubuntu Phone Apps Jenkins Bot | continuous-integration | Approve | |
Review via email: mp+241338@code.launchpad.net |
Commit message
* Rewrite the ColumnFlow.qml to use incubation to create objects and set offscreen cards as invisible
Description of the change
* Rewrite the ColumnFlow.qml to use incubation to create objects and set offscreen cards as invisible
This is a *complete* rewrite of the ColumnFlow, please test all instances of the CardView.
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
PASSED: Continuous integration, rev:731
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
PASSED: Continuous integration, rev:732
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
PASSED: Continuous integration, rev:733
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Victor Thompson (vthompson) wrote : | # |
2 inline comments so far.
Also, I've managed to get the Artist tab to do some bad things.
1. I've gotten this: http://
2. Flicking downward in the Artists tab is initially "sticky". If you flick, it will stop quickly when it gets to the end of the currently loaded items.
3. I can cause the app to crash very easily in the Artists tab by over scrolling
Andrew Hayzen (ahayzen) wrote : | # |
So we are going to investigate setting visible to false rather than destroying the old objects.
This should:
* Stop the thumbnails from reloading after they are destroyed
* Allow for easier implementation of adding/removing columns
* Still maintain the reduced time to initially load the thumbnails
* Simplify the code and reduce chances of corruption
Andrew Hayzen (ahayzen) wrote : | # |
I've rewritten the component so that it incubates the objects as you scroll (meaning the thumbnails appear as fast as possible). Any cards that go out of the viewport are then set invisible to allow for increased performance. Resizing the window doesn't destroy the 'live' cards and instead reanchors them to their new positions so you get an animation.
Please rereview :)
Victor Thompson (vthompson) wrote : | # |
Functionally I think this works really well. The code is looking good thus far--I have 2 minor inline comments.
Small typo, "Force any"
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
FAILED: Continuous integration, rev:737
http://
Executed test runs:
UNSTABLE: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 738. By Andrew Hayzen
-
* Fix for abstraction
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
PASSED: Continuous integration, rev:738
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Victor Thompson (vthompson) wrote : | # |
LGTM! This makes the card views pretty snappy! :)
Preview Diff
1 | === modified file 'MusicArtists.qml' |
2 | --- MusicArtists.qml 2014-11-01 23:25:42 +0000 |
3 | +++ MusicArtists.qml 2014-11-11 20:14:18 +0000 |
4 | @@ -23,7 +23,6 @@ |
5 | import Ubuntu.Components.ListItems 1.0 as ListItem |
6 | import Ubuntu.MediaScanner 0.1 |
7 | import Ubuntu.Thumbnailer 0.1 |
8 | -import QtMultimedia 5.0 |
9 | import QtQuick.LocalStorage 2.0 |
10 | import "meta-database.js" as Library |
11 | import "playlists.js" as Playlists |
12 | @@ -37,6 +36,7 @@ |
13 | |
14 | CardView { |
15 | id: artistCardView |
16 | + getter: function (i) { return {"artist": artistsModel.get(i, ArtistsModel.RoleArtist)}; } |
17 | itemWidth: units.gu(12) |
18 | model: ArtistsModel { |
19 | id: artistsModel |
20 | |
21 | === modified file 'MusicPlaylists.qml' |
22 | --- MusicPlaylists.qml 2014-10-26 16:18:12 +0000 |
23 | +++ MusicPlaylists.qml 2014-11-11 20:14:18 +0000 |
24 | @@ -68,12 +68,12 @@ |
25 | objectName: "playlistsCardView" |
26 | delegate: Card { |
27 | id: playlistCard |
28 | - coverSources: Playlists.getPlaylistCovers(name) |
29 | - primaryText: name |
30 | - secondaryText: i18n.tr("%1 song", "%1 songs", count).arg(count) |
31 | + coverSources: Playlists.getPlaylistCovers(model.name) |
32 | + primaryText: model.name |
33 | + secondaryText: i18n.tr("%1 song", "%1 songs", model.count).arg(model.count) |
34 | |
35 | onClicked: { |
36 | - albumTracksModel.filterPlaylistTracks(name) |
37 | + albumTracksModel.filterPlaylistTracks(model.name) |
38 | |
39 | var comp = Qt.createComponent("common/SongsPage.qml") |
40 | var songsPage = comp.createObject(mainPageStack, |
41 | |
42 | === modified file 'MusicStart.qml' |
43 | --- MusicStart.qml 2014-11-01 23:25:42 +0000 |
44 | +++ MusicStart.qml 2014-11-11 20:14:18 +0000 |
45 | @@ -79,7 +79,7 @@ |
46 | secondaryText: model.type === "playlist" ? i18n.tr("Playlist") : (recentAlbumSongs.status === SongsModel.Ready && recentAlbumSongs.get(0, SongsModel.RoleModelData).author != "" ? recentAlbumSongs.get(0, SongsModel.RoleModelData).author : i18n.tr("Unknown Artist")) |
47 | |
48 | onClicked: { |
49 | - if (type === "playlist") { |
50 | + if (model.type === "playlist") { |
51 | albumTracksModel.filterPlaylistTracks(model.data) |
52 | } |
53 | |
54 | |
55 | === modified file 'common/AlbumsPage.qml' |
56 | --- common/AlbumsPage.qml 2014-11-01 23:25:42 +0000 |
57 | +++ common/AlbumsPage.qml 2014-11-11 20:14:18 +0000 |
58 | @@ -39,6 +39,13 @@ |
59 | anchors { |
60 | fill: parent |
61 | } |
62 | + getter: function (i) { |
63 | + return { |
64 | + "art": artistsModel.get(i, AlbumsModel.RoleArt), |
65 | + "artist": artistsModel.get(i, AlbumsModel.RoleArtist), |
66 | + "title": artistsModel.get(i, AlbumsModel.RoleTitle), |
67 | + }; |
68 | + } |
69 | header: BlurredHeader { |
70 | rightColumn: Column { |
71 | spacing: units.gu(2) |
72 | |
73 | === modified file 'common/BlurredHeader.qml' |
74 | --- common/BlurredHeader.qml 2014-10-22 15:40:36 +0000 |
75 | +++ common/BlurredHeader.qml 2014-11-11 20:14:18 +0000 |
76 | @@ -54,7 +54,6 @@ |
77 | left: coversImage.right |
78 | leftMargin: units.gu(2) |
79 | } |
80 | - asynchronous: true |
81 | } |
82 | |
83 | Loader { |
84 | @@ -66,6 +65,5 @@ |
85 | top: coversImage.bottom |
86 | topMargin: units.gu(1) |
87 | } |
88 | - asynchronous: true |
89 | } |
90 | } |
91 | |
92 | === modified file 'common/Card.qml' |
93 | --- common/Card.qml 2014-10-21 20:11:35 +0000 |
94 | +++ common/Card.qml 2014-11-11 20:14:18 +0000 |
95 | @@ -23,6 +23,10 @@ |
96 | id: card |
97 | height: cardColumn.childrenRect.height + 2 * bg.anchors.margins |
98 | |
99 | + /* Required by ColumnFlow */ |
100 | + property int index |
101 | + property var model |
102 | + |
103 | property alias coverSources: coverGrid.covers |
104 | property alias primaryText: primaryLabel.text |
105 | property alias secondaryText: secondaryLabel.text |
106 | |
107 | === modified file 'common/CardView.qml' |
108 | --- common/CardView.qml 2014-10-28 03:32:01 +0000 |
109 | +++ common/CardView.qml 2014-11-11 20:14:18 +0000 |
110 | @@ -20,21 +20,25 @@ |
111 | |
112 | |
113 | Flickable { |
114 | + id: cardViewFlickable |
115 | anchors { |
116 | fill: parent |
117 | } |
118 | |
119 | // dont use flow.contentHeight as it is inaccurate due to height of labels |
120 | // changing as they load |
121 | - contentHeight: headerLoader.childrenRect.height + flowContainer.height |
122 | + contentHeight: headerLoader.childrenRect.height + flow.contentHeight + flowContainer.anchors.margins * 2 |
123 | contentWidth: width |
124 | |
125 | property alias count: flow.count |
126 | property alias delegate: flow.delegate |
127 | + property var getter |
128 | property alias header: headerLoader.sourceComponent |
129 | property var model: flow.model |
130 | property real itemWidth: units.gu(15) |
131 | |
132 | + onGetterChanged: flow.getter = getter // cannot use alias to set a function (must be var) |
133 | + |
134 | onVisibleChanged: { |
135 | if (visible) { // only load model once CardView is visible |
136 | flow.model = model |
137 | @@ -43,7 +47,6 @@ |
138 | |
139 | Loader { |
140 | id: headerLoader |
141 | - asynchronous: true |
142 | visible: sourceComponent !== undefined |
143 | width: parent.width |
144 | } |
145 | @@ -51,18 +54,21 @@ |
146 | Item { |
147 | id: flowContainer |
148 | anchors { |
149 | + bottom: parent.bottom |
150 | + left: parent.left |
151 | + margins: units.gu(1) |
152 | + right: parent.right |
153 | top: headerLoader.bottom |
154 | } |
155 | - height: flow.childrenRect.height + flow.anchors.margins * 2 |
156 | width: parent.width |
157 | |
158 | ColumnFlow { |
159 | id: flow |
160 | anchors { |
161 | fill: parent |
162 | - margins: units.gu(1) |
163 | } |
164 | - columns: parseInt(width / itemWidth) |
165 | + columns: parseInt(cardViewFlickable.width / itemWidth) |
166 | + flickable: cardViewFlickable |
167 | } |
168 | } |
169 | |
170 | |
171 | === modified file 'common/ColumnFlow.qml' |
172 | --- common/ColumnFlow.qml 2014-10-13 16:56:55 +0000 |
173 | +++ common/ColumnFlow.qml 2014-11-11 20:14:18 +0000 |
174 | @@ -1,7 +1,6 @@ |
175 | /* |
176 | * Copyright (C) 2014 |
177 | * Andrew Hayzen <ahayzen@gmail.com> |
178 | - * Michael Spencer <sonrisesoftware@gmail.com> |
179 | * |
180 | * This program is free software; you can redistribute it and/or modify |
181 | * it under the terms of the GNU General Public License as published by |
182 | @@ -14,147 +13,398 @@ |
183 | * |
184 | * You should have received a copy of the GNU General Public License |
185 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
186 | - * |
187 | - * Upstream location: |
188 | - * https://github.com/iBeliever/ubuntu-ui-extras/blob/master/ColumnFlow.qml |
189 | */ |
190 | |
191 | import QtQuick 2.3 |
192 | |
193 | - |
194 | Item { |
195 | id: columnFlow |
196 | property int columns: 1 |
197 | - property bool repeaterCompleted: false |
198 | - property alias count: repeater.count |
199 | - property alias model: repeater.model |
200 | - property alias delegate: repeater.delegate |
201 | + property Flickable flickable |
202 | + property var model |
203 | + property Component delegate |
204 | + |
205 | + property var getter: function (i) { return model.get(i); } // optional getter override (useful for music-app ms2 models) |
206 | + |
207 | + property int buffer: units.gu(20) |
208 | + property var columnHeights: [] |
209 | + property var columnHeightsMax: [] |
210 | + property int columnWidth: parent.width / columns |
211 | property int contentHeight: 0 |
212 | - |
213 | - onColumnsChanged: reEvalColumns() |
214 | - onModelChanged: reEvalColumns() |
215 | - onWidthChanged: updateWidths() |
216 | - |
217 | - function updateWidths() { |
218 | - if (repeaterCompleted) { |
219 | - var count = 0 |
220 | - |
221 | - //add the first <column> elements |
222 | - for (var i = 0; count < columns && i < columnFlow.children.length; i++) { |
223 | - //print(i, count) |
224 | - if (!columnFlow.children[i] || String(columnFlow.children[i]).indexOf("QQuickRepeater") == 0) |
225 | - //|| !columnFlow.children[i].visible) // CUSTOM - view is invisible at start |
226 | - continue |
227 | - |
228 | - columnFlow.children[i].width = width / columns |
229 | - |
230 | - count++ |
231 | - } |
232 | - } |
233 | - } |
234 | - |
235 | - function reEvalColumns() { |
236 | - if (columnFlow.repeaterCompleted === false) |
237 | - return |
238 | - |
239 | - if (columns === 0) { |
240 | - contentHeight = 0 |
241 | - return |
242 | - } |
243 | - |
244 | - var i, j |
245 | - var columnHeights = new Array(columns); |
246 | - var lastItem = new Array(columns) |
247 | - var lastI = -1 |
248 | - var count = 0 |
249 | - |
250 | - //add the first <column> elements |
251 | - for (i = 0; count < columns && i < columnFlow.children.length; i++) { |
252 | - // CUSTOM - ignore if has just been removed |
253 | - if (i === repeater.removeHintIndex && columnFlow.children[i] === repeater.removeHintItem) { |
254 | - continue |
255 | - } |
256 | - |
257 | - if (!columnFlow.children[i] || String(columnFlow.children[i]).indexOf("QQuickRepeater") == 0) |
258 | - //|| !columnFlow.children[i].visible) // CUSTOM - view is invisible at start |
259 | - continue |
260 | - |
261 | - lastItem[count] = i |
262 | - |
263 | - columnHeights[count] = columnFlow.children[i].height |
264 | - columnFlow.children[i].anchors.top = columnFlow.top |
265 | - columnFlow.children[i].anchors.left = (lastI === -1 ? columnFlow.left : columnFlow.children[lastI].right) |
266 | - columnFlow.children[i].anchors.right = undefined |
267 | - columnFlow.children[i].width = columnFlow.width / columns |
268 | - |
269 | - lastI = i |
270 | - count++ |
271 | - } |
272 | - |
273 | - //add the other elements |
274 | - for (i = i; i < columnFlow.children.length; i++) { |
275 | - var highestHeight = Number.MAX_VALUE |
276 | - var newColumn = 0 |
277 | - |
278 | - // CUSTOM - ignore if has just been removed |
279 | - if (i === repeater.removeHintIndex && columnFlow.children[i] === repeater.removeHintItem) { |
280 | - continue |
281 | - } |
282 | - |
283 | - if (!columnFlow.children[i] || String(columnFlow.children[i]).indexOf("QQuickRepeater") == 0) |
284 | - //|| !columnFlow.children[i].visible) // CUSTOM - view is invisible at start |
285 | - continue |
286 | - |
287 | - // find the shortest column |
288 | - for (j = 0; j < columns; j++) { |
289 | - if (columnHeights[j] !== null && columnHeights[j] < highestHeight) { |
290 | - newColumn = j |
291 | - highestHeight = columnHeights[j] |
292 | - } |
293 | - } |
294 | - |
295 | - // add the element to the shortest column |
296 | - columnFlow.children[i].anchors.top = columnFlow.children[lastItem[newColumn]].bottom |
297 | - columnFlow.children[i].anchors.left = columnFlow.children[lastItem[newColumn]].left |
298 | - columnFlow.children[i].anchors.right = columnFlow.children[lastItem[newColumn]].right |
299 | - |
300 | - lastItem[newColumn] = i |
301 | - columnHeights[newColumn] += columnFlow.children[i].height |
302 | - } |
303 | - |
304 | - var cHeight = 0 |
305 | - |
306 | - for (i = 0; i < columnHeights.length; i++) { |
307 | - if (columnHeights[i]) |
308 | - cHeight = Math.max(cHeight, columnHeights[i]) |
309 | - } |
310 | - |
311 | - contentHeight = cHeight |
312 | - updateWidths() |
313 | - } |
314 | - |
315 | - Repeater { |
316 | - id: repeater |
317 | - model: columnFlow.model |
318 | - Component.onCompleted: { |
319 | - columnFlow.repeaterCompleted = true |
320 | - columnFlow.reEvalColumns() |
321 | - } |
322 | - |
323 | - // Provide a hint of the removed item |
324 | - property int removeHintIndex: -1 // CUSTOM |
325 | - property var removeHintItem // CUSTOM |
326 | - |
327 | - onItemAdded: columnFlow.reEvalColumns() // CUSTOM - ms2 models are live |
328 | - onItemRemoved: { |
329 | - removeHintIndex = index |
330 | - removeHintItem = item |
331 | - |
332 | - columnFlow.reEvalColumns() // CUSTOM - ms2 models are live |
333 | - |
334 | - // Set back to null to allow freeing of memory |
335 | - removeHintIndex = -1 |
336 | - removeHintItem = undefined |
337 | + property int count: model === undefined ? 0 : model.count |
338 | + property var incubating: ({}) // incubating objects |
339 | + property var items: ({}) |
340 | + property var itemToColumn: ({}) // cache of the columns of indexes |
341 | + property int lastIndex: 0 // the furtherest index loaded |
342 | + property bool restoring: false // is the view restoring? |
343 | + property var restoreItems: ({}) // when rebuilding items are stored here temporarily |
344 | + |
345 | + onColumnWidthChanged: { |
346 | + if (restoring) { |
347 | + return; |
348 | + } |
349 | + else if (columns != columnHeights.length && visible) { |
350 | + // number of columns has changed so rebuild the columns |
351 | + rebuildColumns() |
352 | + } else { // column width has changed update visible items properties linked to columnWidth |
353 | + for (var column=0; column < columnHeights.length; column++) { |
354 | + for (var i in columnHeights[column]) { |
355 | + if (columnHeights[column].hasOwnProperty(i) && items.hasOwnProperty(i)) { |
356 | + items[i].width = columnWidth; |
357 | + items[i].x = column * columnWidth; |
358 | + } |
359 | + } |
360 | + } |
361 | + |
362 | + ensureItemsVisible() |
363 | + } |
364 | + } |
365 | + |
366 | + onCountChanged: { |
367 | + if (!visible) { // store changes for when visible |
368 | + if (count === 0 && lastIndex > -1) { |
369 | + lastIndex = -1; |
370 | + } else if (lastIndex > -1) { |
371 | + lastIndex = -(lastIndex + 2); // save the index to restore later |
372 | + } |
373 | + } else if (count === 0) { // likely the model is been reset so reset the view |
374 | + reset() |
375 | + } else { // likely new items in the model check if any can be shown |
376 | + append() |
377 | + } |
378 | + } |
379 | + |
380 | + onVisibleChanged: { |
381 | + if (columns != columnHeights.length && visible) { // number of columns has changed while invisible so reset |
382 | + if (!restoring) { |
383 | + rebuildColumns() |
384 | + } |
385 | + } else if (lastIndex < 0 && visible) { // restore from count change |
386 | + if (lastIndex === -1) { |
387 | + reset() |
388 | + } else { |
389 | + lastIndex = (-lastIndex) - 2 |
390 | + } |
391 | + |
392 | + append() |
393 | + } |
394 | + } |
395 | + |
396 | + // Append a new row of items if possible |
397 | + function append() |
398 | + { |
399 | + // Do not allow append to run if incubating |
400 | + if (isIncubating() || restoring) { |
401 | + return; |
402 | + } |
403 | + |
404 | + // get the columns in order |
405 | + var columnsByHeight = getColumnsByHeight(); |
406 | + var workDone = false; |
407 | + |
408 | + // check if a new item in each column is possible |
409 | + for (var i=0; i < columnsByHeight.length; i++) { |
410 | + var y = columnHeightsMax[columnsByHeight[i]]; |
411 | + |
412 | + // build new object in column if possible |
413 | + if (count > 0 && lastIndex < count && inViewport(y, 0)) { |
414 | + incubateObject(lastIndex++, columnsByHeight[i], getMaxInColumn(columnsByHeight[i]), append); |
415 | + workDone = true |
416 | + } else { |
417 | + break; |
418 | + } |
419 | + } |
420 | + |
421 | + if (!workDone) { // last iteration over append so visible ensure items are correct |
422 | + ensureItemsVisible(); |
423 | + } |
424 | + } |
425 | + |
426 | + // Cache the size of the columns for use later |
427 | + function cacheColumnHeights() |
428 | + { |
429 | + columnHeightsMax = []; |
430 | + |
431 | + for (var i=0; i < columnHeights.length; i++) { |
432 | + var sum = 0; |
433 | + |
434 | + for (var j in columnHeights[i]) { |
435 | + sum += columnHeights[i][j]; |
436 | + } |
437 | + |
438 | + columnHeightsMax.push(sum); |
439 | + } |
440 | + |
441 | + if (!restoring) { // when not restoring otherwise user will be pushed to the top of the view |
442 | + // set the height of columnFlow to max column (for flickable contentHeight) |
443 | + contentHeight = Math.max.apply(null, columnHeightsMax); |
444 | + } |
445 | + } |
446 | + |
447 | + // Recache the visible items heights (due to a change in their height) |
448 | + function cacheVisibleItemsHeights() |
449 | + { |
450 | + for (var i in items) { |
451 | + if (items.hasOwnProperty(i)) { |
452 | + columnHeights[itemToColumn[i]][i] = items[i].height; |
453 | + } |
454 | + } |
455 | + |
456 | + cacheColumnHeights(); |
457 | + } |
458 | + |
459 | + // Ensures that the correct items are visible |
460 | + function ensureItemsVisible() |
461 | + { |
462 | + for (var i in items) { |
463 | + if (items.hasOwnProperty(i)) { |
464 | + items[i].visible = inViewport(items[i].y, items[i].height) |
465 | + } |
466 | + } |
467 | + } |
468 | + |
469 | + // Return if there are incubating objects |
470 | + function isIncubating() |
471 | + { |
472 | + for (var i in incubating) { |
473 | + if (incubating.hasOwnProperty(i)) { |
474 | + return true; |
475 | + } |
476 | + } |
477 | + |
478 | + return false; |
479 | + } |
480 | + |
481 | + // Run after incubation to store new column height and call any further append/restores |
482 | + function finishIncubation(index, callback) |
483 | + { |
484 | + var obj = incubating[index].object; |
485 | + delete incubating[index]; |
486 | + |
487 | + obj.heightChanged.connect(cacheVisibleItemsHeights) // if the height changes recache |
488 | + |
489 | + // Ensure properties linked to columnWidth are correct (as width may still be changing) |
490 | + obj.x = itemToColumn[index] * columnWidth; |
491 | + obj.width = columnWidth; |
492 | + |
493 | + items[index] = obj; |
494 | + |
495 | + columnHeights[itemToColumn[index]][index] = obj.height; // ensure height is the latest |
496 | + |
497 | + if (!isIncubating()) { |
498 | + cacheColumnHeights(); |
499 | + |
500 | + // Check if there is any more work to be done (append or restore) |
501 | + callback(); |
502 | + } |
503 | + } |
504 | + |
505 | + // Force any incubation to finish |
506 | + function forceIncubationCompletion() |
507 | + { |
508 | + for (var i in incubating) { |
509 | + if (incubating.hasOwnProperty(i)) { |
510 | + incubating[i].forceCompletion() |
511 | + } |
512 | + } |
513 | + } |
514 | + |
515 | + // Get the column index in order of height |
516 | + function getColumnsByHeight() |
517 | + { |
518 | + var columnsByHeight = []; |
519 | + |
520 | + for (var i=0; i < columnHeightsMax.length; i++) { |
521 | + var min = undefined; |
522 | + var index = -1; |
523 | + |
524 | + // Find the smallest column that has not been found yet |
525 | + for (var j=0; j < columnHeightsMax.length; j++) { |
526 | + if (columnsByHeight.indexOf(j) === -1 && (min === undefined || columnHeightsMax[j] < min)) { |
527 | + min = columnHeightsMax[j]; |
528 | + index = j; |
529 | + } |
530 | + } |
531 | + |
532 | + columnsByHeight.push(index); |
533 | + } |
534 | + |
535 | + return columnsByHeight; |
536 | + } |
537 | + |
538 | + // Get the highest index for a column |
539 | + function getMaxInColumn(column) |
540 | + { |
541 | + var max; |
542 | + |
543 | + for (var i in columnHeights[column]) { |
544 | + if (columnHeights[column].hasOwnProperty(i)) { |
545 | + i = parseInt(i); |
546 | + |
547 | + if (items.hasOwnProperty(i)) { |
548 | + if (i > max || max === undefined) { |
549 | + max = i; |
550 | + } |
551 | + } |
552 | + } |
553 | + } |
554 | + |
555 | + return max; |
556 | + } |
557 | + |
558 | + // Incubate an object for creation |
559 | + function incubateObject(index, column, anchorIndex, callback) |
560 | + { |
561 | + // Load parameters to send to the object on creation |
562 | + var params = { |
563 | + "anchors.top": anchorIndex === undefined ? parent.top : items[anchorIndex].bottom, |
564 | + index: index, |
565 | + model: getter(index), |
566 | + width: columnWidth, |
567 | + x: column * columnWidth |
568 | + }; |
569 | + |
570 | + // Start incubating and cache the column |
571 | + incubating[index] = delegate.incubateObject(parent, params); |
572 | + itemToColumn[index] = column; |
573 | + |
574 | + if (incubating[index].status != Component.Ready) { |
575 | + incubating[index].onStatusChanged = function(status) { |
576 | + if (status == Component.Ready) { |
577 | + finishIncubation(index, callback) |
578 | + } |
579 | + } |
580 | + } else { |
581 | + finishIncubation(index, callback) |
582 | + } |
583 | + } |
584 | + |
585 | + // Detect if a loaded object is in the viewport with a buffer |
586 | + function inViewport(y, height) |
587 | + { |
588 | + return flickable.contentY - buffer < y + height && y < flickable.contentY + flickable.height + buffer; |
589 | + } |
590 | + |
591 | + // Number of columns has changed rebuild with live items |
592 | + function rebuildColumns() |
593 | + { |
594 | + restoring = true; |
595 | + var i; |
596 | + |
597 | + forceIncubationCompletion() |
598 | + |
599 | + columnHeights = [] |
600 | + columnHeightsMax = [] |
601 | + |
602 | + for (i=0; i < columns; i++) { |
603 | + columnHeights.push({}); |
604 | + columnHeightsMax.push(0); |
605 | + } |
606 | + |
607 | + lastIndex = 0; |
608 | + |
609 | + restoreItems = items; |
610 | + items = {}; |
611 | + |
612 | + restoreExisting() |
613 | + |
614 | + restoring = false; |
615 | + |
616 | + cacheColumnHeights(); // rebuilds contentHeight |
617 | + |
618 | + // If the columns have changed while the view was locked rerun |
619 | + if (columns != columnHeights.length && visible) { |
620 | + rebuildColumns() |
621 | + } else { |
622 | + append() // check if any new items can be added |
623 | + } |
624 | + } |
625 | + |
626 | + // Restores existing items into potentially new positions |
627 | + function restoreExisting() |
628 | + { |
629 | + var i; |
630 | + |
631 | + // get the columns in order |
632 | + var columnsByHeight = getColumnsByHeight(); |
633 | + var workDone = false; |
634 | + |
635 | + // check if a new item in each column is possible |
636 | + for (i=0; i < columnsByHeight.length; i++) { |
637 | + var column = columnsByHeight[i]; |
638 | + |
639 | + // build new object in column if possible |
640 | + if (count > 0 && lastIndex < count) { |
641 | + if (restoreItems.hasOwnProperty(lastIndex)) { |
642 | + var item = restoreItems[lastIndex]; |
643 | + var maxInColumn = getMaxInColumn(column); // get lowest item in column |
644 | + |
645 | + itemToColumn[lastIndex] = column; |
646 | + columnHeights[column][lastIndex] = item.height; // ensure height is the latest |
647 | + |
648 | + // Rebuild item properties |
649 | + item.anchors.bottom = undefined |
650 | + item.anchors.top = maxInColumn === undefined ? parent.top : items[maxInColumn].bottom; |
651 | + item.x = column * columnWidth; |
652 | + item.visible = inViewport(item.y, item.height); |
653 | + |
654 | + // Migrate item from restoreItems to items |
655 | + items[lastIndex] = item; |
656 | + delete restoreItems[lastIndex]; |
657 | + |
658 | + // set after restore as height will likely change causing cacheVisibleItemsHeights to be run |
659 | + item.width = columnWidth; |
660 | + |
661 | + cacheColumnHeights(); // ensure column heights are up to date |
662 | + |
663 | + lastIndex++; |
664 | + workDone = true; |
665 | + } |
666 | + } else { |
667 | + break; |
668 | + } |
669 | + } |
670 | + |
671 | + if (workDone) { |
672 | + restoreExisting() // if work done then check if any more is needed |
673 | + } else { |
674 | + restoreItems = {}; // ensure restoreItems is empty |
675 | + } |
676 | + } |
677 | + |
678 | + // Reset the column flow |
679 | + function reset() |
680 | + { |
681 | + forceIncubationCompletion() |
682 | + |
683 | + // Destroy any old items |
684 | + for (var j in items) { |
685 | + if (items.hasOwnProperty(j)) { |
686 | + items[j].destroy() |
687 | + } |
688 | + } |
689 | + |
690 | + // Reset and rebuild the variables |
691 | + items = ({}) |
692 | + lastIndex = 0 |
693 | + |
694 | + columnHeights = [] |
695 | + |
696 | + for (var k=0; k < columns; k++) { |
697 | + columnHeights.push({}) |
698 | + } |
699 | + |
700 | + cacheColumnHeights() |
701 | + |
702 | + contentHeight = 0 |
703 | + } |
704 | + |
705 | + Connections { |
706 | + target: flickable |
707 | + onContentYChanged: { |
708 | + append() // Append any new items (scrolling down) |
709 | + |
710 | + ensureItemsVisible() |
711 | } |
712 | } |
713 | } |
PASSED: Continuous integration, rev:730 91.189. 93.70:8080/ job/music- app-remix- ci/240/ 91.189. 93.70:8080/ job/generic- mediumtests- vivid/127 91.189. 93.70:8080/ job/generic- mediumtests- vivid/127/ artifact/ work/output/ *zip*/output. zip 91.189. 93.70:8080/ job/music- app-remix- vivid-amd64- ci/15
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild: 91.189. 93.70:8080/ job/music- app-remix- ci/240/ rebuild
http://