fix: smooth the bubble's open and close
This commit is contained in:
1 parent
77ae883b98
commit
d1a590bb50
2 files changed
+100
-52
No files matched your search
+92
-48
@@ -4,9 +4,10 @@
|
|||||||
// opens up, shows the face while it looks, and closes again.
|
// opens up, shows the face while it looks, and closes again.
|
||||||
//
|
//
|
||||||
// The choreography is Glance's: entering, the island slides in first and grows
|
// The choreography is Glance's: entering, the island slides in first and grows
|
||||||
// a moment later; leaving, it shrinks first and slides away after. Growing is
|
// a moment later; leaving, it shrinks first and slides away as it finishes.
|
||||||
// a spring that overshoots a little, shrinking does not. While it scans the
|
// Growing overshoots a little, shrinking does not, and both end on time rather
|
||||||
// content breathes, so it reads as looking rather than stuck.
|
// than creeping in. The content grows and shrinks with the island. While it
|
||||||
|
// scans the content breathes, so it reads as looking rather than stuck.
|
||||||
//
|
//
|
||||||
// "full" is the open island with the face in it. "minimal" is a small pill
|
// "full" is the open island with the face in it. "minimal" is a small pill
|
||||||
// with a lock on one side and the face on the other.
|
// with a lock on one side and the face on the other.
|
||||||
@@ -28,9 +29,13 @@ Item {
|
|||||||
|
|
||||||
property bool positioned: false
|
property bool positioned: false
|
||||||
property bool expanded: false
|
property bool expanded: false
|
||||||
|
// Which way it is going. Set before the change it is about, so the
|
||||||
|
// animations below have the matching curve by the time they start.
|
||||||
|
property bool opening: false
|
||||||
|
|
||||||
function choreograph() {
|
function choreograph() {
|
||||||
if (wantOpen) {
|
if (wantOpen) {
|
||||||
|
opening = true
|
||||||
slideOut.stop()
|
slideOut.stop()
|
||||||
closeDone.stop()
|
closeDone.stop()
|
||||||
positioned = true
|
positioned = true
|
||||||
@@ -38,6 +43,7 @@ Item {
|
|||||||
expandLater.restart()
|
expandLater.restart()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
opening = false
|
||||||
expandLater.stop()
|
expandLater.stop()
|
||||||
expanded = false
|
expanded = false
|
||||||
slideOut.restart()
|
slideOut.restart()
|
||||||
@@ -61,16 +67,24 @@ Item {
|
|||||||
}
|
}
|
||||||
Timer {
|
Timer {
|
||||||
id: closeDone
|
id: closeDone
|
||||||
interval: Theme.slideDuration + 60
|
interval: Theme.slideOutDuration + 60
|
||||||
onTriggered: if (root.bubble) root.bubble.closed()
|
onTriggered: if (root.bubble) root.bubble.closed()
|
||||||
}
|
}
|
||||||
|
|
||||||
// What the face shows, from the phase.
|
// The phase on screen. Closing keeps the last one, so the tick does not
|
||||||
readonly property string glyphMode: phase === "success" ? "success"
|
// turn back into a face on its way out.
|
||||||
: phase === "failure" ? "failure"
|
property string shownPhase: "idle"
|
||||||
: phase === "lockout" ? "lockout"
|
onPhaseChanged: {
|
||||||
: phase === "scanning" ? (bubble.faceSeen ? "tracking" : "scanning")
|
if (phase !== "hidden") {
|
||||||
: "idle"
|
shownPhase = phase
|
||||||
|
}
|
||||||
|
if (phase === "failure" && minimal) {
|
||||||
|
pillShake.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the face shows.
|
||||||
|
readonly property string glyphMode: shownPhase === "scanning" ? (bubble.faceSeen ? "tracking" : "scanning") : shownPhase
|
||||||
|
|
||||||
// -- the breathing while it scans
|
// -- the breathing while it scans
|
||||||
property real pulse: 0
|
property real pulse: 0
|
||||||
@@ -96,7 +110,6 @@ Item {
|
|||||||
|
|
||||||
// -- the minimal pill shakes as a whole; the full island shakes its face
|
// -- the minimal pill shakes as a whole; the full island shakes its face
|
||||||
property real shake: 0
|
property real shake: 0
|
||||||
onPhaseChanged: if (phase === "failure" && minimal) pillShake.restart()
|
|
||||||
SequentialAnimation {
|
SequentialAnimation {
|
||||||
id: pillShake
|
id: pillShake
|
||||||
NumberAnimation { target: root; property: "shake"; to: -10; duration: 55; easing.type: Easing.OutQuad }
|
NumberAnimation { target: root; property: "shake"; to: -10; duration: 55; easing.type: Easing.OutQuad }
|
||||||
@@ -111,22 +124,45 @@ Item {
|
|||||||
|
|
||||||
readonly property real targetWidth: root.expanded ? (root.minimal ? Theme.minimalWidth : Theme.openWidth) : Theme.closedWidth
|
readonly property real targetWidth: root.expanded ? (root.minimal ? Theme.minimalWidth : Theme.openWidth) : Theme.closedWidth
|
||||||
readonly property real targetHeight: root.expanded ? (root.minimal ? Theme.minimalHeight : Theme.openHeight) : Theme.closedHeight
|
readonly property real targetHeight: root.expanded ? (root.minimal ? Theme.minimalHeight : Theme.openHeight) : Theme.closedHeight
|
||||||
|
// How far open it is, for the content that grows with it.
|
||||||
|
readonly property real fit: Math.min(width / Theme.openWidth, height / Theme.openHeight)
|
||||||
|
|
||||||
width: targetWidth
|
width: targetWidth
|
||||||
height: targetHeight
|
height: targetHeight
|
||||||
// Growing overshoots a little; shrinking settles without bouncing.
|
// Growing overshoots sideways more than down, so the bottom edge does
|
||||||
Behavior on width { SpringAnimation { spring: root.expanded ? 3.4 : 6; damping: root.expanded ? 0.28 : 0.9; epsilon: 0.25 } }
|
// not sag.
|
||||||
Behavior on height { SpringAnimation { spring: root.expanded ? 3.4 : 6; damping: root.expanded ? 0.28 : 0.9; epsilon: 0.25 } }
|
Behavior on width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root.opening ? Theme.growDuration : Theme.shrinkDuration
|
||||||
|
easing.type: root.opening ? Easing.OutBack : Easing.OutCubic
|
||||||
|
easing.overshoot: 1.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root.opening ? Theme.growDuration : Theme.shrinkDuration
|
||||||
|
easing.type: root.opening ? Easing.OutBack : Easing.OutCubic
|
||||||
|
easing.overshoot: 0.9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
x: (root.width - width) / 2 + root.shake
|
x: (root.width - width) / 2 + root.shake
|
||||||
y: root.positioned ? Theme.topGap : -height - 30
|
// Off screen by a fixed amount. A target that followed the shrinking
|
||||||
Behavior on y { NumberAnimation { duration: Theme.slideDuration; easing.type: Easing.OutCubic } }
|
// height would restart the slide every frame and hold it back.
|
||||||
|
y: root.positioned ? Theme.topGap : -Theme.closedHeight - 20
|
||||||
|
Behavior on y {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root.opening ? Theme.slideInDuration : Theme.slideOutDuration
|
||||||
|
easing.type: root.opening ? Easing.OutCubic : Easing.InCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: shape
|
id: shape
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Theme.panel
|
color: Theme.panel
|
||||||
radius: root.expanded && !root.minimal ? Math.min(Theme.openRadius, height / 2) : height / 2
|
// From size alone, so it cannot jump when the island turns.
|
||||||
|
radius: root.minimal ? height / 2 : Math.min(Theme.openRadius, height / 2)
|
||||||
|
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
@@ -139,41 +175,49 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- full: the face, and a line of text under it
|
// -- full: the face, and a line of text under it. Laid out at the
|
||||||
|
// open size and scaled with the island, so it never pokes out.
|
||||||
Item {
|
Item {
|
||||||
id: full
|
id: full
|
||||||
anchors.fill: parent
|
width: Theme.openWidth
|
||||||
|
height: Theme.openHeight
|
||||||
|
anchors.centerIn: parent
|
||||||
visible: !root.minimal
|
visible: !root.minimal
|
||||||
opacity: root.expanded ? 1 - 0.35 * root.pulse : 0
|
scale: island.fit
|
||||||
scale: root.expanded ? 1 - 0.03 * root.pulse : 0.3
|
opacity: root.expanded ? 1 : 0
|
||||||
Behavior on opacity { enabled: !pulseAnimation.running; NumberAnimation { duration: 220 } }
|
Behavior on opacity { NumberAnimation { duration: root.opening ? 260 : 120 } }
|
||||||
Behavior on scale { enabled: !pulseAnimation.running; NumberAnimation { duration: 260; easing.type: Easing.OutCubic } }
|
|
||||||
|
|
||||||
readonly property bool hasMessage: root.bubble && root.bubble.message.length > 0
|
readonly property bool hasMessage: root.bubble && root.bubble.message.length > 0
|
||||||
|
|
||||||
FaceGlyph {
|
Item {
|
||||||
id: glyph
|
anchors.fill: parent
|
||||||
width: 100
|
opacity: 1 - 0.35 * root.pulse
|
||||||
height: 100
|
scale: 1 - 0.03 * root.pulse
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
y: full.hasMessage ? 28 : 40
|
|
||||||
Behavior on y { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
|
|
||||||
mode: root.glyphMode
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
FaceGlyph {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
id: glyph
|
||||||
anchors.bottom: parent.bottom
|
width: 100
|
||||||
anchors.bottomMargin: 20
|
height: 100
|
||||||
width: parent.width - 32
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
y: full.hasMessage ? 28 : 40
|
||||||
elide: Text.ElideRight
|
Behavior on y { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
|
||||||
text: root.bubble ? root.bubble.message : ""
|
mode: root.glyphMode
|
||||||
color: root.phase === "failure" || root.phase === "lockout" ? Theme.textDetail : Theme.textSecondary
|
}
|
||||||
font.pixelSize: 13
|
|
||||||
font.weight: Font.Medium
|
Text {
|
||||||
opacity: full.hasMessage ? 1 : 0
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 20
|
||||||
|
width: parent.width - 32
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
text: root.bubble ? root.bubble.message : ""
|
||||||
|
color: root.shownPhase === "failure" || root.shownPhase === "lockout" ? Theme.textDetail : Theme.textSecondary
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.weight: Font.Medium
|
||||||
|
opacity: full.hasMessage ? 1 : 0
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,15 +227,15 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: root.minimal
|
visible: root.minimal
|
||||||
opacity: root.expanded ? 1 : 0
|
opacity: root.expanded ? 1 : 0
|
||||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
Behavior on opacity { NumberAnimation { duration: root.opening ? 200 : 120 } }
|
||||||
|
|
||||||
LockGlyph {
|
LockGlyph {
|
||||||
width: 18
|
width: 18
|
||||||
height: 18
|
height: 18
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
x: 16
|
x: 16
|
||||||
open: root.phase === "success"
|
open: root.shownPhase === "success"
|
||||||
color: root.phase === "failure" || root.phase === "lockout" ? Theme.failure : Theme.textPrimary
|
color: root.shownPhase === "failure" || root.shownPhase === "lockout" ? Theme.failure : Theme.textPrimary
|
||||||
}
|
}
|
||||||
|
|
||||||
FaceGlyph {
|
FaceGlyph {
|
||||||
|
|||||||
@@ -34,8 +34,12 @@ QtObject {
|
|||||||
readonly property int minimalHeight: 40
|
readonly property int minimalHeight: 40
|
||||||
readonly property int topGap: 6
|
readonly property int topGap: 6
|
||||||
|
|
||||||
// Enter: slide down, then grow. Leave: shrink, then slide up.
|
// Enter: slide down, then grow with a small overshoot. Leave: shrink
|
||||||
readonly property int slideDuration: 250
|
// without one, and slide up while it finishes.
|
||||||
readonly property int expandDelay: 160
|
readonly property int slideInDuration: 280
|
||||||
readonly property int slideOutDelay: 180
|
readonly property int expandDelay: 150
|
||||||
|
readonly property int growDuration: 460
|
||||||
|
readonly property int shrinkDuration: 280
|
||||||
|
readonly property int slideOutDelay: 150
|
||||||
|
readonly property int slideOutDuration: 240
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user