VolumeOSD/NotificationPopup: Animation Adjustments

This merge request adjust animations in the VolumeOSD and NotificationPopup to bring consistency and to make it feel better when swiping up a notification.
This commit is contained in:
Micah Stanley 2025-04-30 03:50:22 +00:00
parent 0f1c0d86e7
commit d39bdbedb8
4 changed files with 130 additions and 61 deletions

View file

@ -33,6 +33,9 @@ Item {
property real aboveNotificationFullOffset: 0 property real aboveNotificationFullOffset: 0
property int aboveNotificationHeight: 0 property int aboveNotificationHeight: 0
// set to true when notification is swiped up by user
property bool closedWithSwipe: false
// the drag offset on the current popup notification - used to position notification when stacked underneath // the drag offset on the current popup notification - used to position notification when stacked underneath
property real currentDragOffset: { property real currentDragOffset: {
let current = popupNotifications.currentPopupIndex == notificationPopup.popupIndex; let current = popupNotifications.currentPopupIndex == notificationPopup.popupIndex;
@ -257,7 +260,7 @@ Item {
if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") { if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") {
notificationItem.offset = openOffset; notificationItem.offset = openOffset;
notificationItem.scale = 0.75; notificationItem.scale = 0.75;
notificationItem.opacity = 0.0; notificationItem.popupOpacity = 0.0;
} }
notificationItem.state = "inDrawerClosed"; notificationItem.state = "inDrawerClosed";
notificationPopup.removeKeyboardFocus(); notificationPopup.removeKeyboardFocus();
@ -268,7 +271,7 @@ Item {
if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") { if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") {
notificationItem.offset = openOffset; notificationItem.offset = openOffset;
notificationItem.scale = 0.75; notificationItem.scale = 0.75;
notificationItem.opacity = 0.0; notificationItem.popupOpacity = 0.0;
} }
notificationItem.state = "open"; notificationItem.state = "open";
notificationPopup.removeKeyboardFocus(); notificationPopup.removeKeyboardFocus();
@ -353,29 +356,53 @@ Item {
property real offset: closedOffset property real offset: closedOffset
property real scale: 1.0 property real scale: 1.0
property real drawerScale: 1 - Math.max(Math.min(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 3), 1) * 0.075 property real popupOpacity: 1.0 // controls the opacity of the notification popup when outside the popup drawer
property real drawerScale: {
if (notificationPopup.popupDrawerOpened) {
return 0; // when popup drawer is opened, reset scale to 0
}
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
// clamp the index value to avoid scaling too much with animations
let indexClamped = Math.max(Math.min(index, 2), 0);
return indexClamped * 0.075;
}
property real drawerAddedOffset: {
if (notificationPopup.popupDrawerOpened) {
return 0; // when popup drawer is opened, reset any added height to 0
}
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
// clamp the index value to avoid moving too much with animations
let indexClamped = Math.max(Math.min(index, 2), -1);
return Kirigami.Units.gridUnit * 0.5 * indexClamped;
}
property real drawerOpacity: {
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
if (index > 2 && !notificationPopup.popupDrawerOpened) {
return 0; // make this popup invisible if it is below 3 other popups
} else {
return 1; // when popup drawer is opened, reset opacity to 1
}
}
Behavior on drawerScale { Behavior on drawerScale {
NumberAnimation { NumberAnimation {
duration: Kirigami.Units.veryLongDuration duration: Kirigami.Units.veryLongDuration * 1.25
easing.type: Easing.OutExpo easing.type: Easing.OutQuint
} }
} }
property real drawerAddedOffset: Kirigami.Units.gridUnit * 0.5 * Math.max(Math.min(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 3), 1)
Behavior on drawerAddedOffset { Behavior on drawerAddedOffset {
NumberAnimation { NumberAnimation {
duration: Kirigami.Units.veryLongDuration duration: Kirigami.Units.veryLongDuration * 1.25
easing.type: Easing.OutExpo easing.type: Easing.OutQuint
} }
} }
property real drawerOpacity: (Math.max(Math.min(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 3), 1) > 2) ? 0 : 1
Behavior on drawerOpacity { Behavior on drawerOpacity {
NumberAnimation { NumberAnimation {
duration: Kirigami.Units.veryLongDuration duration: Kirigami.Units.veryLongDuration * 1.25
easing.type: Easing.OutExpo easing.type: Easing.OutQuint
} }
} }
opacity: 1.0 opacity: Math.min(popupOpacity, drawerOpacity)
state: "" state: ""
@ -389,7 +416,7 @@ Item {
target: notificationItem; scale: 1.0 target: notificationItem; scale: 1.0
} }
PropertyChanges { PropertyChanges {
target: notificationItem; opacity: 1.0 target: notificationItem; popupOpacity: 1.0
} }
}, },
State { State {
@ -401,7 +428,7 @@ Item {
target: notificationItem; scale: 1.0 target: notificationItem; scale: 1.0
} }
PropertyChanges { PropertyChanges {
target: notificationItem; opacity: 1.0 target: notificationItem; popupOpacity: 1.0
} }
}, },
State { State {
@ -413,46 +440,87 @@ Item {
target: notificationItem; scale: 0.75 target: notificationItem; scale: 0.75
} }
PropertyChanges { PropertyChanges {
target: notificationItem; opacity: 0.0 target: notificationItem; popupOpacity: 0.0
} }
}, },
State { State {
name: "inDrawerClosed" name: "inDrawerClosed"
PropertyChanges { PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset + (notificationPopup.popupDrawerOpened ? 0 : drawerAddedOffset) target: notificationItem; offset: notificationPopup.openOffset
} }
PropertyChanges { PropertyChanges {
target: notificationItem; scale: notificationPopup.popupDrawerOpened ? 1 : drawerScale target: notificationItem; scale: 1
} }
PropertyChanges { PropertyChanges {
target: notificationItem; opacity: notificationPopup.popupDrawerOpened ? 1 : drawerOpacity target: notificationItem; popupOpacity: 1
} }
} }
] ]
readonly property int notificationEasing: {
// check whether the popup is the current one or above it
let topPopup = popupNotifications.currentPopupIndex >= notificationPopup.popupIndex;
// check whether the popup has any popups below it
let popupBelow = notificationPopup.popupCount - notificationPopup.popupIndex > 1;
let popupOpening = notificationItem.state == "open" || notificationItem.state == "inDrawerClosed";
let popupClosing = notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale"
if (notificationPopup.closedWithSwipe || (topPopup && popupClosing && popupBelow)) {
// set the easing type to linear when closed with a swipe or if a popup is below when closing
// as to make sure the popup feels like it is keeping it's momentum
return Easing.Linear;
} else if (popupOpening) {
// set the easing type to 'Out' when opening so the popup will have a gentle landing
return Easing.OutQuint;
} else {
// if above conditions fail, set the easing type to 'In' so the popup will build up speed for it's exit
return Easing.InQuint;
}
}
readonly property real notificationDuration: {
// check whether the popup is the current one or above it
let topPopup = popupNotifications.currentPopupIndex >= notificationPopup.popupIndex;
// check whether the popup has any popups below it
let popupBelow = notificationPopup.popupCount - notificationPopup.popupIndex > 1;
let popupClosing = notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale"
if (notificationPopup.closedWithSwipe || (topPopup && popupClosing && popupBelow)) {
// make sure the speed it faster when closed with a swipe or if there is a popup below when closing
// as to make sure the speed feels comparable with the easing type is set to linear
return Kirigami.Units.veryLongDuration * 0.5;
} else {
return Kirigami.Units.veryLongDuration * 1.25;
}
}
transitions: Transition { transitions: Transition {
SequentialAnimation { SequentialAnimation {
ParallelAnimation { ParallelAnimation {
PropertyAnimation { PropertyAnimation {
properties: "offset"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5 properties: "offset"
easing.type: notificationItem.notificationEasing
duration: notificationItem.notificationDuration
} }
PropertyAnimation { PropertyAnimation {
properties: "scale"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5 properties: "scale"
easing.type: notificationItem.notificationEasing
duration: notificationItem.notificationDuration
} }
PropertyAnimation { PropertyAnimation {
properties: "opacity"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5 properties: "popupOpacity"
easing.type: notificationItem.notificationEasing
duration: notificationItem.notificationDuration
} }
} }
ScriptAction { ScriptAction {
script: { script: {
if (notificationItem.state == "open") { if (notificationItem.state == "open") {
preventDismissTimeout = false; notificationPopup.preventDismissTimeout = false;
notificationPopup.updateTouchArea(); notificationPopup.updateTouchArea();
} else if (notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale") { } else if (notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale") {
preventDismissTimeout = true; notificationPopup.preventDismissTimeout = true;
if (dismissTimeout) { if (notificationPopup.dismissTimeout) {
notificationPopup.dismissClicked(); notificationPopup.dismissClicked();
} else if (!isActionDrawerOpen) { } else if (!notificationPopup.isActionDrawerOpen) {
notificationPopup.expired(); notificationPopup.expired();
} }
} }
@ -465,15 +533,15 @@ Item {
Scale { Scale {
origin.x: Math.round(notificationPopup.popupWidth / 2) origin.x: Math.round(notificationPopup.popupWidth / 2)
origin.y: notificationPopup.scaleOriginY origin.y: notificationPopup.scaleOriginY
xScale: notificationItem.scale xScale: notificationItem.scale - notificationItem.drawerScale
yScale: notificationItem.scale yScale: notificationItem.scale - notificationItem.drawerScale
} }
] ]
} }
transform: [ transform: [
Translate { Translate {
y: notificationItem.offset + notificationPopup.fullOffsetAn + notificationPopup.dragOffset + notificationPopup.currentDragOffset y: notificationItem.offset + notificationPopup.fullOffsetAn + notificationPopup.dragOffset + notificationPopup.currentDragOffset + notificationItem.drawerAddedOffset
} }
] ]
@ -506,12 +574,14 @@ Item {
startActive = active; startActive = active;
notificationPopup.preventDismissTimeout = true; notificationPopup.preventDismissTimeout = true;
if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) { if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) {
dragOffsetAn.running = true;
if ((lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0) || (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset)) { if ((lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0) || (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset)) {
// this code is called when the notifition is swiped or draged to the top. // this code is called when the notifition is swiped or draged to the top.
notificationPopup.closedWithSwipe = true;
notificationPopup.closePopup(popupIndex); notificationPopup.closePopup(popupIndex);
return; return;
} else if (notificationPopup.dragOffset - lastOffset > 1.0 || Kirigami.Units.gridUnit * 3 < notificationPopup.dragOffset) { }
dragOffsetAn.running = true;
if (notificationPopup.dragOffset - lastOffset > 1.0 || Kirigami.Units.gridUnit * 3 < notificationPopup.dragOffset) {
// this code is called when the notifition is swiped or draged down. // this code is called when the notifition is swiped or draged down.
} }
notificationPopup.preventDismissTimeout = (keyboardInteractivity == LayerShell.Window.KeyboardInteractivityOnDemand); notificationPopup.preventDismissTimeout = (keyboardInteractivity == LayerShell.Window.KeyboardInteractivityOnDemand);

View file

@ -74,6 +74,14 @@ Window {
value: popupDrawerOpened value: popupDrawerOpened
} }
// hide on timeout to give time to finish animations
Timer {
id: hideTimeout
interval: Kirigami.Units.veryLongDuration * 1.5
repeat: false
onTriggered: if (notifications.count == 0) notificationPopupManager.visible = false;
}
// Update the window touch region to encapsulate the notification area or the whole screen depending on the 'popupDrawerOpened' state // Update the window touch region to encapsulate the notification area or the whole screen depending on the 'popupDrawerOpened' state
function updateTouchArea() { function updateTouchArea() {
ShellUtil.setInputTransparent(notificationPopupManager, false); ShellUtil.setInputTransparent(notificationPopupManager, false);
@ -166,7 +174,7 @@ Window {
onCountChanged: { onCountChanged: {
if (count == 0) { if (count == 0) {
ShellUtil.setInputTransparent(notificationPopupManager, true); ShellUtil.setInputTransparent(notificationPopupManager, true);
notificationPopupManager.visible = false; hideTimeout.restart();
notificationPopupManager.popupDrawerOpened = false; notificationPopupManager.popupDrawerOpened = false;
fullHeight = 0; fullHeight = 0;
return; return;

View file

@ -23,17 +23,16 @@ Window {
id: window id: window
width: osd.width + 6 width: osd.width + 6
height: cards.implicitHeight + 6 height: cards.implicitHeight + 6 + cards.openOffset
onWidthChanged: if (visible) window.updateTouchRegion()
visible: false visible: false
readonly property real offsetMargins: Math.max(cards.offset, 0)
LayerShell.Window.scope: "overlay" LayerShell.Window.scope: "overlay"
LayerShell.Window.anchors: LayerShell.Window.AnchorTop LayerShell.Window.anchors: LayerShell.Window.AnchorTop
LayerShell.Window.layer: LayerShell.Window.LayerOverlay LayerShell.Window.layer: LayerShell.Window.LayerOverlay
LayerShell.Window.exclusionZone: -1 LayerShell.Window.exclusionZone: -1
LayerShell.Window.margins.top: offsetMargins
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.colorSet: Kirigami.Theme.View
@ -51,10 +50,11 @@ Window {
} }
function open() { function open() {
// set window input transparency to allow touches to pass through while the opening animation is playing
ShellUtil.setInputTransparent(window, true);
window.visible = true; window.visible = true;
cards.state = "open"; cards.state = "open";
// set window input transparency to accept touches
ShellUtil.setInputTransparent(window, false);
} }
function close() { function close() {
@ -63,6 +63,10 @@ Window {
ShellUtil.setInputTransparent(window, true); ShellUtil.setInputTransparent(window, true);
} }
function updateTouchRegion() {
ShellUtil.setInputRegion(window, Qt.rect(0, cards.openOffset, window.width, cards.implicitHeight + 6));
}
Timer { Timer {
id: hideTimer id: hideTimer
interval: 2000 interval: 2000
@ -87,7 +91,6 @@ Window {
readonly property real closedOffset: -(cards.implicitHeight + Kirigami.Units.smallSpacing) readonly property real closedOffset: -(cards.implicitHeight + Kirigami.Units.smallSpacing)
readonly property real openOffset: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3 readonly property real openOffset: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3
property real offset: closedOffset property real offset: closedOffset
property real scale: 0.95
state: "closed" state: "closed"
@ -97,18 +100,12 @@ Window {
PropertyChanges { PropertyChanges {
target: cards; offset: openOffset target: cards; offset: openOffset
} }
PropertyChanges {
target: cards; scale: 1.0
}
}, },
State { State {
name: "closed" name: "closed"
PropertyChanges { PropertyChanges {
target: cards; offset: closedOffset target: cards; offset: closedOffset
} }
PropertyChanges {
target: cards; scale: 0.95
}
} }
] ]
@ -116,16 +113,16 @@ Window {
SequentialAnimation { SequentialAnimation {
ParallelAnimation { ParallelAnimation {
PropertyAnimation { PropertyAnimation {
properties: "offset"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25 properties: "offset"; easing.type: cards.state == "open" ? Easing.OutQuint : Easing.InQuint; duration: Kirigami.Units.veryLongDuration * 1.25
}
PropertyAnimation {
properties: "scale"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25
} }
} }
ScriptAction { ScriptAction {
script: { script: {
if (cards.state == "open") { if (cards.state == "open") {
hideTimer.restart(); hideTimer.restart();
// set window input transparency to accept touches
ShellUtil.setInputTransparent(window, false);
window.updateTouchRegion();
} else { } else {
hideTimer.stop(); hideTimer.stop();
window.visible = false; window.visible = false;
@ -142,13 +139,7 @@ Window {
transform: [ transform: [
Translate { Translate {
y: cards.offset - window.offsetMargins + 1 y: cards.offset + 1
},
Scale {
origin.x: Math.round(width / 2)
origin.y: Math.round(height / 2)
xScale: cards.scale
yScale: cards.scale
} }
] ]

View file

@ -121,16 +121,16 @@ Window {
SequentialAnimation { SequentialAnimation {
ParallelAnimation { ParallelAnimation {
PropertyAnimation { PropertyAnimation {
properties: "offset"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25 properties: "offset"; easing.type: Easing.OutQuint; duration: Kirigami.Units.veryLongDuration * 1.25
} }
PropertyAnimation { PropertyAnimation {
properties: "scale"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25 properties: "scale"; easing.type: Easing.OutQuint; duration: Kirigami.Units.veryLongDuration * 1.25
} }
PropertyAnimation { PropertyAnimation {
properties: "opacity"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25 properties: "opacity"; easing.type: Easing.OutQuint; duration: Kirigami.Units.veryLongDuration * 1.25
} }
PropertyAnimation { PropertyAnimation {
properties: "color"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.25 properties: "color"; easing.type: Easing.OutQuint; duration: Kirigami.Units.veryLongDuration * 1.25
} }
} }
ScriptAction { ScriptAction {