diff options
author | Daniel d'Andrada <daniel.dandrada@luxoft.com> | 2017-10-24 09:30:33 +0200 |
---|---|---|
committer | Daniel d'Andrada <daniel.dandrada@luxoft.com> | 2018-02-28 15:58:02 +0100 |
commit | d563abee402d616bdff2639c275775184069e042 (patch) | |
tree | 0959e93a7fe4379a934635e9b924464d9f32acb6 | |
parent | 9962fb888ad888b768c1f4db549d0c250d07a43c (diff) |
Improve widget resizing
- Consider all widgets while dragging a handle and not just
those immediately above and below it
- Make it follow the users finger but still snap
at the proper places both during drag and upon release
-rw-r--r-- | sysui/display/HomePage.qml | 220 | ||||
-rw-r--r-- | sysui/display/Resizer.qml | 102 | ||||
-rw-r--r-- | tests/qmltests/tst_HomePage.qml | 27 |
3 files changed, 231 insertions, 118 deletions
diff --git a/sysui/display/HomePage.qml b/sysui/display/HomePage.qml index c64d66d0..f9fb29a5 100644 --- a/sysui/display/HomePage.qml +++ b/sysui/display/HomePage.qml @@ -84,18 +84,216 @@ Item { } } + property bool resizingWidgets: false + property real startResizeDragY + property int widgetIndexAboveHandle + function onResizeHandlePressed(pos) { + startResizeDragY = pos.y; + + var i = 0; + var accumulatedHeight = 0; + while (true) { + var appInfo = root.widgetsList.get(i).appInfo; + accumulatedHeight += appInfo.heightRows * rowHeight; + if (accumulatedHeight >= pos.y) { + widgetIndexAboveHandle = i; + break; + } else { + i++; + } + } + + animateWidgetResize = false; + + // init resize heights + for (i = 0; i < repeater.count; i++) { + var delegate = repeater.itemAt(i); + delegate.heightWhenResizing = delegate.appInfo.heightRows * widgetGrid.rowHeight; + } + + resizingWidgets = true; + } + function onResizeHandleDragged(pos) { + // init resize heights + for (i = 0; i < repeater.count; i++) { + var delegate = repeater.itemAt(i); + delegate.heightWhenResizing = delegate.appInfo.heightRows * widgetGrid.rowHeight; + } + + var delta = snapToRowMultiplierIfTooClose(withinBoundsY(pos.y) - startResizeDragY); + delta = computeUsableDragDelta(delta); + + // above handle. positive delta enlarges widgets + var i = widgetIndexAboveHandle; + var remainingDelta = delta; + while (remainingDelta !== 0 && i >= 0) { + var delegate = repeater.itemAt(i); + var appInfo = delegate.appInfo; + var minHeight = appInfo.minHeightRows * rowHeight; + var targetHeight = (appInfo.heightRows * rowHeight) + remainingDelta; + + if (remainingDelta > 0) { + // no upper limit at the moment on widget height (other than grid size) + delegate.heightWhenResizing = targetHeight; + remainingDelta = 0; + } else { + if (targetHeight >= minHeight) { + delegate.heightWhenResizing = targetHeight; + remainingDelta = 0; + } else { + delegate.heightWhenResizing = minHeight; + remainingDelta -= -((appInfo.heightRows * rowHeight) - minHeight) + i--; + } + } + } + + // below handle. positive delta shrinks widgets + i = widgetIndexAboveHandle + 1; + remainingDelta = delta; + while (remainingDelta !== 0 && i < repeater.count) { + var delegate = repeater.itemAt(i); + var appInfo = delegate.appInfo; + var minHeight = appInfo.minHeightRows * rowHeight; + var targetHeight = (appInfo.heightRows * rowHeight) - remainingDelta; + + if (remainingDelta < 0) { + // no upper limit at the moment on widget height (other than grid size) + delegate.heightWhenResizing = targetHeight; + remainingDelta = 0; + } else { + if (targetHeight >= minHeight) { + delegate.heightWhenResizing = targetHeight; + remainingDelta = 0; + } else { + delegate.heightWhenResizing = minHeight; + remainingDelta -= (appInfo.heightRows * rowHeight) - minHeight; + i++; + } + } + } + } + function onResizeHandleReleased(pos) { + // apply sizes + var i; + for (i = 0; i < repeater.count; i++) { + var delegate = repeater.itemAt(i); + delegate.appInfo.heightRows = Math.round(delegate.heightWhenResizing / widgetGrid.rowHeight); + } + + animateWidgetResize = true; + resizingWidgets = false; + } + function computeUsableDragDelta(delta) { + // above handle. positive delta enlarges widgets + var i = widgetIndexAboveHandle; + var remainingDelta = delta; + var usableDeltaAbove = 0 + while (remainingDelta !== 0 && i >= 0) { + var appInfo = root.widgetsList.get(i).appInfo; + var minHeight = appInfo.minHeightRows * rowHeight; + var targetHeight = (appInfo.heightRows * rowHeight) + remainingDelta; + + if (remainingDelta > 0) { + // no upper limit at the moment on widget height (other than grid size) + usableDeltaAbove += remainingDelta; + remainingDelta = 0; + } else { + if (targetHeight >= minHeight) { + usableDeltaAbove += remainingDelta; + remainingDelta = 0; + } else { + var thisWidgetDelta = -((appInfo.heightRows * rowHeight) - minHeight); + usableDeltaAbove += thisWidgetDelta; + remainingDelta -= thisWidgetDelta; + i--; + } + } + } + + // below handle. positive delta shrinks widgets + i = widgetIndexAboveHandle + 1; + remainingDelta = usableDeltaAbove; + var usableDelta = 0 + while (remainingDelta !== 0 && i < root.widgetsList.count) { + var appInfo = root.widgetsList.get(i).appInfo; + var minHeight = appInfo.minHeightRows * rowHeight; + var targetHeight = (appInfo.heightRows * rowHeight) - remainingDelta; + + if (remainingDelta < 0) { + // no upper limit at the moment on widget height (other than grid size) + usableDelta += remainingDelta; + remainingDelta = 0; + } else { + if (targetHeight >= minHeight) { + usableDelta += remainingDelta; + remainingDelta = 0; + } else { + var thisWidgetDelta = (appInfo.heightRows * rowHeight) - minHeight; + usableDelta += thisWidgetDelta; + remainingDelta -= thisWidgetDelta; + i++; + } + } + } + return usableDelta; + } + function withinBoundsY(someY) { + return Math.max(0, Math.min(someY, height)); + } + function snapToRowMultiplierIfTooClose(delta) { + var deltaRows = delta / rowHeight; + var snapThreshold = 0.5; + + var floorDiff = deltaRows - Math.floor(deltaRows); + var ceilDiff = Math.ceil(deltaRows) - deltaRows; + + if (floorDiff < ceilDiff && floorDiff <= snapThreshold) { + //return Math.floor(deltaRows) * rowHeight; + return applyEasing(Math.floor(deltaRows)*rowHeight - snapThreshold*rowHeight, + Math.floor(deltaRows)*rowHeight + snapThreshold*rowHeight, delta); + } else if (floorDiff >= ceilDiff && ceilDiff <= snapThreshold) { + //return Math.ceil(deltaRows) * rowHeight; + return applyEasing(Math.ceil(deltaRows)*rowHeight - snapThreshold*rowHeight, + Math.ceil(deltaRows)*rowHeight + snapThreshold*rowHeight, delta); + } else { + return delta; + } + } + function applyEasing(min, max, value) { + // apply an OutInCubic easing function, ie: + // y=x^3, ranging from -1 (min) to 1 (max) + + // normalize the value to be in the [-1,1] range + var x = (((value - min) / (max - min)) * 2) - 1; + + var y = Math.pow(x, 3); + + // and map it back to its original range + return (((y + 1) / 2) * (max - min)) + min; + } + + property bool animateWidgetResize: true + Repeater { id: repeater model: root.widgetsList delegate: Column { id: repeaterDelegate width: widgetGrid.width - height: appInfo ? appInfo.heightRows * widgetGrid.rowHeight : 0 + height: { + if (widgetGrid.resizingWidgets) { + return heightWhenResizing + } else { + return appInfo ? appInfo.heightRows * widgetGrid.rowHeight : 0 + } + } + property real heightWhenResizing - Behavior on x { enabled:!moveTransition.enabled; SmoothedAnimation { easing.type: Easing.InOutQuad; duration: 270 } } - Behavior on y { enabled:!moveTransition.enabled; SmoothedAnimation { easing.type: Easing.InOutQuad; duration: 270 } } - Behavior on width { enabled:!moveTransition.enabled; SmoothedAnimation { easing.type: Easing.InOutQuad; duration: 270 } } - Behavior on height { enabled:!moveTransition.enabled; SmoothedAnimation { easing.type: Easing.InOutQuad; duration: 270 } } + Behavior on x { enabled:!moveTransition.enabled && widgetGrid.animateWidgetResize; SmoothedAnimation { easing.type: Easing.InOutQuad; duration: 270 } } + Behavior on y { enabled:!moveTransition.enabled && widgetGrid.animateWidgetResize; SmoothedAnimation { easing.type: Easing.InOutQuad; duration: 270 } } + Behavior on width { enabled:!moveTransition.enabled && widgetGrid.animateWidgetResize; SmoothedAnimation { easing.type: Easing.InOutQuad; duration: 270 } } + Behavior on height { enabled:!moveTransition.enabled && widgetGrid.animateWidgetResize; SmoothedAnimation { easing.type: Easing.InOutQuad; duration: 270 } } property alias appInfo: appWidget.appInfo readonly property int modelIndex: model.index @@ -112,7 +310,7 @@ Item { Item { id: appWidgetSlot width: repeaterDelegate.width - height: repeaterDelegate.height - resizerHandle.height + height: repeaterDelegate.height - resizeHandle.height ApplicationWidget { id: appWidget @@ -132,18 +330,18 @@ Item { } } Rectangle { - id: resizerHandle + id: resizeHandle visible: repeaterDelegate.isAtBottom ? false : true color: "grey" width: parent.width height: widgetGrid.resizerHandleHeight - Resizer { + MouseArea { anchors.fill: parent - topAppInfo: model.appInfo - bottomAppInfo: repeaterDelegate.isAtBottom ? null : repeater.model.get(model.index + 1).appInfo - grid: widgetGrid + onPressed: widgetGrid.onResizeHandlePressed(mapToItem(widgetGrid, mouseX, mouseY)) + onReleased: widgetGrid.onResizeHandleReleased(mapToItem(widgetGrid, mouseX, mouseY)) + onPositionChanged: widgetGrid.onResizeHandleDragged(mapToItem(widgetGrid, mouseX, mouseY)) } } } diff --git a/sysui/display/Resizer.qml b/sysui/display/Resizer.qml deleted file mode 100644 index 6b545175..00000000 --- a/sysui/display/Resizer.qml +++ /dev/null @@ -1,102 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2017 Pelagicore AG -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Triton IVI UI. -** -** $QT_BEGIN_LICENSE:GPL-QTAS$ -** Commercial License Usage -** Licensees holding valid commercial Qt Automotive Suite licenses may use -** this file in accordance with the commercial license agreement provided -** with the Software or, alternatively, in accordance with the terms -** contained in a written agreement between you and The Qt Company. For -** licensing terms and conditions see https://www.qt.io/terms-conditions. -** For further information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 or (at your option) any later version -** approved by the KDE Free Qt Foundation. The licenses are as published by -** the Free Software Foundation and appearing in the file LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -** SPDX-License-Identifier: GPL-3.0 -** -****************************************************************************/ - -import QtQuick 2.6 - -import utils 1.0 - -MultiPointTouchArea { - id: root - - property var topAppInfo - property var bottomAppInfo - - /* - A grid item is expected to have the following properties: - numRows: number of rows - rowHeight: height of each row - */ - property Item grid - - touchPoints: [ - TouchPoint { - id: touch1 - onXChanged: d.onTouchMoved() - onYChanged: d.onTouchMoved() - onPressedChanged: { - if (pressed) { - d.dragging = true; - - var gridPos = root.mapToItem(root.grid, touch1.x, touch1.y); - d.startGridPosY = gridPos.y - - d.startTopHeightRows = root.topAppInfo.heightRows; - - if (root.bottomAppInfo) { - d.startBottomHeightRows = root.bottomAppInfo.heightRows; - } - } else { - d.dragging = false; - } - } - } - ] - - QtObject { - id: d - - readonly property int heightIncrement: root.grid ? root.grid.rowHeight : 0 - - property bool dragging: false - - property real startGridPosY - property int startTopHeightRows - property int startBottomHeightRows - - function onTouchMoved() { - if (!d.dragging) { - return; - } - - var gridPos = root.mapToItem(root.grid, touch1.x, touch1.y); - var deltaRows = Math.round((gridPos.y - d.startGridPosY) / root.grid.rowHeight); - - var targetTopHeightRows = Math.max(d.startTopHeightRows + deltaRows, root.topAppInfo.minHeightRows); - deltaRows = targetTopHeightRows - d.startTopHeightRows; - - var targetBottomHeightRows = Math.max(d.startBottomHeightRows - deltaRows, root.bottomAppInfo.minHeightRows); - deltaRows = -targetBottomHeightRows + d.startBottomHeightRows; - - root.topAppInfo.heightRows = d.startTopHeightRows + deltaRows - root.bottomAppInfo.heightRows = d.startBottomHeightRows - deltaRows - } - } -} diff --git a/tests/qmltests/tst_HomePage.qml b/tests/qmltests/tst_HomePage.qml index f4012545..32654372 100644 --- a/tests/qmltests/tst_HomePage.qml +++ b/tests/qmltests/tst_HomePage.qml @@ -9,7 +9,7 @@ Item { height: 600 QtObject { - id: foo + id: redApp property Item window property Item loadedWindow: Rectangle { color: "red" } @@ -25,14 +25,30 @@ Item { } QtObject { - id: bar + id: greenApp + property Item window + property Item loadedWindow: Rectangle { color: "green" } + + property bool active: false + property bool canBeActive: true + + property int heightRows: 2 + property int minHeightRows: 1 + + function start() { + window = loadedWindow; + } + } + + QtObject { + id: blueApp property Item window property Item loadedWindow: Rectangle { color: "blue" } property bool active: false property bool canBeActive: true - property int heightRows: 3 + property int heightRows: 2 property int minHeightRows: 1 function start() { @@ -44,8 +60,9 @@ Item { anchors.fill: parent widgetsList: ListModel { id: listModel } Component.onCompleted: { - listModel.append({"appInfo":foo}) - listModel.append({"appInfo":bar}) + listModel.append({"appInfo":redApp}) + listModel.append({"appInfo":greenApp}) + listModel.append({"appInfo":blueApp}) } } |