internal: better loading indicator
Abstract circular progress into own component
This commit is contained in:
parent
3e21e627e0
commit
edef1d21fb
6 changed files with 213 additions and 159 deletions
72
components/controls/CircularProgress.qml
Normal file
72
components/controls/CircularProgress.qml
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
Shape {
|
||||
id: root
|
||||
|
||||
property real value
|
||||
property int startAngle: -90
|
||||
property int strokeWidth: Appearance.padding.smaller
|
||||
property int padding: 0
|
||||
property int spacing: Appearance.spacing.small
|
||||
property color fgColour: Colours.palette.m3primary
|
||||
property color bgColour: Colours.palette.m3secondaryContainer
|
||||
|
||||
readonly property real size: Math.min(width, height)
|
||||
readonly property real arcRadius: (size - padding - strokeWidth) / 2
|
||||
readonly property real vValue: value || 1 / 360
|
||||
readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI)
|
||||
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
asynchronous: true
|
||||
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: root.bgColour
|
||||
strokeWidth: root.strokeWidth
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
startAngle: root.startAngle + 360 * root.vValue + root.gapAngle
|
||||
sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2)
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
centerX: root.size / 2
|
||||
centerY: root.size / 2
|
||||
}
|
||||
|
||||
Behavior on strokeColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: root.fgColour
|
||||
strokeWidth: root.strokeWidth
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
startAngle: root.startAngle
|
||||
sweepAngle: 360 * root.vValue
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
centerX: root.size / 2
|
||||
centerY: root.size / 2
|
||||
}
|
||||
|
||||
Behavior on strokeColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +1,146 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Shapes
|
||||
|
||||
BusyIndicator {
|
||||
id: root
|
||||
|
||||
property color fgColour: Colours.palette.m3onPrimaryContainer
|
||||
property color bgColour: Colours.palette.m3primaryContainer
|
||||
property real implicitSize: Appearance.font.size.normal * 3
|
||||
property real strokeWidth: Appearance.padding.small
|
||||
property real internalStrokeWidth: strokeWidth
|
||||
property string animState
|
||||
|
||||
padding: 0
|
||||
implicitWidth: implicitSize
|
||||
implicitHeight: implicitSize
|
||||
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
updater.completeEndProgress = 0;
|
||||
animState = "running";
|
||||
} else {
|
||||
if (animState == "running")
|
||||
animState = "completing";
|
||||
}
|
||||
}
|
||||
|
||||
states: State {
|
||||
name: "stopped"
|
||||
when: !root.running
|
||||
|
||||
PropertyChanges {
|
||||
root.opacity: 0
|
||||
root.internalStrokeWidth: root.strokeWidth / 3
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
NumberAnimation {
|
||||
properties: "opacity,internalStrokeWidth"
|
||||
duration: updater.completeEndDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
background: null
|
||||
|
||||
contentItem: Shape {
|
||||
id: shape
|
||||
contentItem: CircularProgress {
|
||||
anchors.fill: parent
|
||||
strokeWidth: root.internalStrokeWidth
|
||||
padding: root.padding
|
||||
startAngle: updater.startFraction * 360
|
||||
value: updater.endFraction - updater.startFraction
|
||||
}
|
||||
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
asynchronous: true
|
||||
Updater {
|
||||
id: updater
|
||||
}
|
||||
|
||||
RotationAnimator on rotation {
|
||||
from: 0
|
||||
to: 180
|
||||
running: root.visible && root.running
|
||||
loops: Animation.Infinite
|
||||
duration: Appearance.anim.durations.extraLarge
|
||||
easing.type: Easing.Linear
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
NumberAnimation {
|
||||
running: root.animState !== "stopped"
|
||||
loops: Animation.Infinite
|
||||
target: updater
|
||||
property: "progress"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: updater.duration
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
running: root.animState === "completing"
|
||||
target: updater
|
||||
property: "completeEndProgress"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: updater.completeEndDuration
|
||||
onFinished: {
|
||||
if (root.animState === "completing")
|
||||
root.animState = "stopped";
|
||||
}
|
||||
}
|
||||
|
||||
component Updater: QtObject {
|
||||
readonly property int duration: 5400 * Appearance.anim.durations.scale
|
||||
readonly property int expandDuration: 667 * Appearance.anim.durations.scale
|
||||
readonly property int collapseDuration: 667 * Appearance.anim.durations.scale
|
||||
readonly property int completeEndDuration: 333 * Appearance.anim.durations.scale
|
||||
readonly property int tailDegOffset: -20
|
||||
readonly property int extraDegPerCycle: 250
|
||||
readonly property int constantRotDeg: 1520
|
||||
readonly property list<int> expandDelay: [0, 1350, 2700, 4050].map(d => d * Appearance.anim.durations.scale)
|
||||
readonly property list<int> collapseDelay: [667, 2017, 3367, 4717].map(d => d * Appearance.anim.durations.scale)
|
||||
|
||||
property real progress: 0
|
||||
property real startFraction: 0
|
||||
property real endFraction: 0
|
||||
property real rotation: 0
|
||||
property real completeEndProgress: 0
|
||||
|
||||
onProgressChanged: update(progress)
|
||||
|
||||
function update(p: real): void {
|
||||
const playtime = p * duration;
|
||||
let startDeg = constantRotDeg * p + tailDegOffset;
|
||||
let endDeg = constantRotDeg * p;
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const expandFraction = getFractionInRange(playtime, expandDelay[i], expandDuration);
|
||||
endDeg += fastOutSlowIn(expandFraction) * extraDegPerCycle;
|
||||
|
||||
const collapseFraction = getFractionInRange(playtime, collapseDelay[i], collapseDuration);
|
||||
startDeg += fastOutSlowIn(collapseFraction) * extraDegPerCycle;
|
||||
}
|
||||
|
||||
// Gap closing
|
||||
startDeg += (endDeg - startDeg) * completeEndProgress;
|
||||
|
||||
startFraction = startDeg / 360;
|
||||
endFraction = endDeg / 360;
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: Math.min(root.implicitWidth, root.implicitHeight) * 0.18
|
||||
strokeColor: root.bgColour
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: shape.width / 2
|
||||
centerY: shape.height / 2
|
||||
radiusX: root.implicitWidth / 2
|
||||
radiusY: root.implicitHeight / 2
|
||||
startAngle: 0
|
||||
sweepAngle: 360
|
||||
}
|
||||
|
||||
Behavior on strokeColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
function getFractionInRange(currentTime: real, delay: int, duration: int): real {
|
||||
if (currentTime < delay)
|
||||
return 0;
|
||||
if (currentTime > delay + duration)
|
||||
return 1;
|
||||
return (currentTime - delay) / duration;
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: Math.min(root.implicitWidth, root.implicitHeight) * 0.18
|
||||
strokeColor: root.fgColour
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
function lerp(a: real, b: real, t: real): real {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
PathAngleArc {
|
||||
centerX: shape.width / 2
|
||||
centerY: shape.height / 2
|
||||
radiusX: root.implicitWidth / 2
|
||||
radiusY: root.implicitHeight / 2
|
||||
startAngle: -sweepAngle / 2
|
||||
sweepAngle: 60
|
||||
}
|
||||
function cubic(a: real, b: real, c: real, d: real, t: real): real {
|
||||
return ((1 - t) ** 3) * a + 3 * ((1 - t) ** 2) * t * b + 3 * (1 - t) * (t ** 2) * c + (t ** 3) * d;
|
||||
}
|
||||
|
||||
PathAngleArc {
|
||||
centerX: shape.width / 2
|
||||
centerY: shape.height / 2
|
||||
radiusX: root.implicitWidth / 2
|
||||
radiusY: root.implicitHeight / 2
|
||||
startAngle: 180 - sweepAngle / 2
|
||||
sweepAngle: 60
|
||||
}
|
||||
function cubicBezier(p1x: real, p1y: real, p2x: real, p2y: real, t: real): real {
|
||||
return cubic(0, p1y, p2y, 1, t);
|
||||
}
|
||||
|
||||
Behavior on strokeColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
function fastOutSlowIn(t: real): real {
|
||||
return cubicBezier(0.4, 0.0, 0.2, 1.0, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,17 +111,8 @@ ColumnLayout {
|
|||
color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0)
|
||||
|
||||
StyledBusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight
|
||||
|
||||
running: opacity > 0
|
||||
opacity: device.loading ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
anchors.fill: parent
|
||||
running: device.loading
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
|
|
|
|||
|
|
@ -105,17 +105,8 @@ ColumnLayout {
|
|||
color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0)
|
||||
|
||||
StyledBusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight
|
||||
|
||||
running: opacity > 0
|
||||
opacity: networkItem.loading ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
anchors.fill: parent
|
||||
running: networkItem.loading
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
|
|
|
|||
|
|
@ -237,23 +237,14 @@ ColumnLayout {
|
|||
id: connectBtn
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0)
|
||||
|
||||
StyledBusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight
|
||||
|
||||
running: opacity > 0
|
||||
opacity: device.loading ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
anchors.fill: parent
|
||||
running: device.loading
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.misc
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
|
@ -57,69 +58,21 @@ GridLayout {
|
|||
required property real value
|
||||
required property color colour
|
||||
|
||||
readonly property int thickness: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
|
||||
readonly property real arcRadius: (width - Appearance.padding.large * 3 - thickness) / 2
|
||||
readonly property real vValue: value || 1 / 360
|
||||
readonly property real gapAngle: ((Appearance.spacing.small + thickness) / (arcRadius || 1)) * (180 / Math.PI)
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: width
|
||||
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
Shape {
|
||||
CircularProgress {
|
||||
id: circ
|
||||
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
asynchronous: true
|
||||
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHighest, 3)
|
||||
strokeWidth: res.thickness
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
startAngle: -90 + 360 * res.vValue + res.gapAngle
|
||||
sweepAngle: 360 * (1 - res.vValue) - res.gapAngle * 2
|
||||
radiusX: res.arcRadius
|
||||
radiusY: res.arcRadius
|
||||
centerX: res.width / 2
|
||||
centerY: res.width / 2
|
||||
}
|
||||
|
||||
Behavior on strokeColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: res.colour
|
||||
strokeWidth: res.thickness
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
startAngle: -90
|
||||
sweepAngle: 360 * res.vValue
|
||||
radiusX: res.arcRadius
|
||||
radiusY: res.arcRadius
|
||||
centerX: res.width / 2
|
||||
centerY: res.width / 2
|
||||
}
|
||||
|
||||
Behavior on strokeColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
value: res.value
|
||||
padding: Appearance.padding.large * 3
|
||||
fgColour: res.colour
|
||||
bgColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 3)
|
||||
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
|
|
@ -128,7 +81,7 @@ GridLayout {
|
|||
anchors.centerIn: parent
|
||||
text: res.icon
|
||||
color: res.colour
|
||||
font.pointSize: (res.arcRadius * 0.7) || 1
|
||||
font.pointSize: (circ.arcRadius * 0.7) || 1
|
||||
font.weight: 600
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue