1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-07 06:12:08 -04:00

dms: Material Animation Refactor

- Thanks Google for Material 3 Expressive stuffs
- Thanks Caelestia shell for pushing qml limits to showcase the blueprint
This commit is contained in:
purian23
2026-02-08 20:24:37 -05:00
parent d775974a90
commit 37cc4ab197
16 changed files with 442 additions and 34 deletions

View File

@@ -0,0 +1,9 @@
import QtQuick
import qs.Common
// Reusable NumberAnimation wrapper
NumberAnimation {
duration: Theme.expressiveDurations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standard
}

View File

@@ -0,0 +1,9 @@
import QtQuick
import qs.Common
// Reusable ColorAnimation wrapper
ColorAnimation {
duration: Theme.expressiveDurations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standard
}

View File

@@ -0,0 +1,60 @@
pragma Singleton
import QtQuick
import qs.Common
// Reusable ListView/GridView transitions
QtObject {
id: root
readonly property Transition add: Transition {
ParallelAnimation {
DankAnim {
property: "opacity"
from: 0
to: 1
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
DankAnim {
property: "scale"
from: 0.92
to: 1
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
}
readonly property Transition remove: Transition {
ParallelAnimation {
DankAnim {
property: "opacity"
to: 0
duration: Theme.expressiveDurations.fast
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
}
DankAnim {
property: "scale"
to: 0.92
duration: Theme.expressiveDurations.fast
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
}
}
}
readonly property Transition displaced: Transition {
DankAnim {
properties: "x,y"
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
readonly property Transition move: Transition {
DankAnim {
properties: "x,y"
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
}

View File

@@ -154,10 +154,14 @@ Singleton {
property bool nightModeEnabled: false
property int animationSpeed: SettingsData.AnimationSpeed.Short
property int customAnimationDuration: 500
property bool syncComponentAnimationSpeeds: true
onSyncComponentAnimationSpeedsChanged: saveSettings()
property int popoutAnimationSpeed: SettingsData.AnimationSpeed.Short
property int popoutCustomAnimationDuration: 150
property int modalAnimationSpeed: SettingsData.AnimationSpeed.Short
property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true
onEnableRippleEffectsChanged: saveSettings()
property string wallpaperFillMode: "Fill"
property bool blurredWallpaperLayer: false
property bool blurWallpaperOnOverview: false

View File

@@ -769,6 +769,9 @@ Singleton {
readonly property int popoutAnimationDuration: {
if (typeof SettingsData === "undefined")
return 150;
if (SettingsData.syncComponentAnimationSpeeds) {
return Math.min(currentAnimationBaseDuration, 1000);
}
const presetMap = [0, 150, 300, 500];
if (SettingsData.popoutAnimationSpeed === SettingsData.AnimationSpeed.Custom)
return SettingsData.popoutCustomAnimationDuration;
@@ -778,6 +781,9 @@ Singleton {
readonly property int modalAnimationDuration: {
if (typeof SettingsData === "undefined")
return 150;
if (SettingsData.syncComponentAnimationSpeeds) {
return Math.min(currentAnimationBaseDuration, 1000);
}
const presetMap = [0, 150, 300, 500];
if (SettingsData.modalAnimationSpeed === SettingsData.AnimationSpeed.Custom)
return SettingsData.modalCustomAnimationDuration;

View File

@@ -40,10 +40,12 @@ var SPEC = {
nightModeEnabled: { def: false },
animationSpeed: { def: 1 },
customAnimationDuration: { def: 500 },
syncComponentAnimationSpeeds: { def: true },
popoutAnimationSpeed: { def: 1 },
popoutCustomAnimationDuration: { def: 150 },
modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true },
wallpaperFillMode: { def: "Fill" },
blurredWallpaperLayer: { def: false },
blurWallpaperOnOverview: { def: false },

View File

@@ -284,9 +284,8 @@ Item {
Behavior on opacity {
enabled: root.animationsEnabled
NumberAnimation {
DankAnim {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
@@ -332,27 +331,24 @@ Item {
Behavior on animX {
enabled: root.animationsEnabled
NumberAnimation {
DankAnim {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
NumberAnimation {
DankAnim {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled
NumberAnimation {
DankAnim {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}

View File

@@ -309,9 +309,8 @@ Item {
visible: contentVisible || opacity > 0
Behavior on opacity {
NumberAnimation {
DankAnim {
duration: Theme.modalAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
@@ -345,17 +344,15 @@ Item {
transformOrigin: Item.Center
Behavior on opacity {
NumberAnimation {
DankAnim {
duration: Theme.modalAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Behavior on scale {
NumberAnimation {
DankAnim {
duration: Theme.modalAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}

View File

@@ -718,7 +718,7 @@ PanelWindow {
target: content
property: "swipeOffset"
to: isTopCenter ? -content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width)
duration: Anims.durShort
duration: Theme.shortDuration
easing.type: Easing.OutCubic
onStopped: {
NotificationService.dismissNotification(notificationData);
@@ -757,9 +757,9 @@ PanelWindow {
return isLeft ? -Anims.slidePx : Anims.slidePx;
}
to: 0
duration: Anims.durMed
duration: Theme.mediumDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: isTopCenter ? Anims.standardDecel : Anims.emphasizedDecel
easing.bezierCurve: isTopCenter ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
onStopped: {
if (!win.exiting && !win._isDestroying) {
if (isTopCenter) {
@@ -788,9 +788,9 @@ PanelWindow {
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -Anims.slidePx : Anims.slidePx;
}
duration: Anims.durShort
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
}
NumberAnimation {
@@ -798,9 +798,9 @@ PanelWindow {
property: "opacity"
from: 1
to: 0
duration: Anims.durShort
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardAccel
easing.bezierCurve: Theme.expressiveCurves.standardAccel
}
NumberAnimation {
@@ -808,9 +808,9 @@ PanelWindow {
property: "scale"
from: 1
to: 0.98
duration: Anims.durShort
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
}
}
@@ -867,9 +867,9 @@ PanelWindow {
enabled: !exiting && !_isDestroying
NumberAnimation {
duration: Anims.durShort
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
easing.bezierCurve: Theme.expressiveCurves.standardDecel
}
}
}

View File

@@ -239,10 +239,10 @@ Item {
tab: "typography"
tags: ["animation", "duration", "custom", "speed"]
settingKey: "customAnimationDuration"
text: I18n.tr("Custom Duration")
description: I18n.tr("Fine-tune animation timing in milliseconds")
text: I18n.tr("Animation Duration")
description: I18n.tr("Globally scale all animation durations")
minimum: 0
maximum: 750
maximum: 1000
value: Theme.currentAnimationBaseDuration
unit: "ms"
defaultValue: 200
@@ -269,11 +269,34 @@ Item {
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
}
SettingsToggleRow {
tab: "typography"
tags: ["animation", "sync", "popout", "modal", "global"]
settingKey: "syncComponentAnimationSpeeds"
text: I18n.tr("Sync Popouts & Modals")
description: I18n.tr("Popouts and Modals follow global Animation Speed (disable to customize independently)")
checked: SettingsData.syncComponentAnimationSpeeds
onToggled: checked => SettingsData.set("syncComponentAnimationSpeeds", checked)
Connections {
target: SettingsData
function onSyncComponentAnimationSpeedsChanged() {
}
}
}
}
SettingsCard {
tab: "typography"
tags: ["animation", "speed", "motion", "duration", "popout"]
tags: ["animation", "speed", "motion", "duration", "popout", "sync"]
title: I18n.tr("%1 Animation Speed").arg(I18n.tr("Popouts"))
settingKey: "popoutAnimationSpeed"
iconName: "open_in_new"
@@ -295,6 +318,8 @@ Item {
onSelectionChanged: (index, selected) => {
if (!selected)
return;
if (SettingsData.syncComponentAnimationSpeeds)
SettingsData.set("syncComponentAnimationSpeeds", false);
SettingsData.set("popoutAnimationSpeed", index);
}
@@ -322,11 +347,13 @@ Item {
text: I18n.tr("Custom Duration")
description: I18n.tr("%1 custom animation duration").arg(I18n.tr("Popouts"))
minimum: 0
maximum: 2000
maximum: 1000
value: Theme.popoutAnimationDuration
unit: "ms"
defaultValue: 150
onSliderValueChanged: newValue => {
if (SettingsData.syncComponentAnimationSpeeds)
SettingsData.set("syncComponentAnimationSpeeds", false);
SettingsData.set("popoutAnimationSpeed", SettingsData.AnimationSpeed.Custom);
SettingsData.set("popoutCustomAnimationDuration", newValue);
}
@@ -343,7 +370,7 @@ Item {
Connections {
target: Theme
function onPopoutAnimationDurationChanged() {
if (SettingsData.popoutAnimationSpeed === SettingsData.AnimationSpeed.Custom)
if (!SettingsData.syncComponentAnimationSpeeds && SettingsData.popoutAnimationSpeed === SettingsData.AnimationSpeed.Custom)
return;
popoutDurationSlider.value = Theme.popoutAnimationDuration;
}
@@ -353,7 +380,7 @@ Item {
SettingsCard {
tab: "typography"
tags: ["animation", "speed", "motion", "duration", "modal"]
tags: ["animation", "speed", "motion", "duration", "modal", "sync"]
title: I18n.tr("%1 Animation Speed").arg(I18n.tr("Modals"))
settingKey: "modalAnimationSpeed"
iconName: "web_asset"
@@ -375,6 +402,8 @@ Item {
onSelectionChanged: (index, selected) => {
if (!selected)
return;
if (SettingsData.syncComponentAnimationSpeeds)
SettingsData.set("syncComponentAnimationSpeeds", false);
SettingsData.set("modalAnimationSpeed", index);
}
@@ -402,11 +431,13 @@ Item {
text: I18n.tr("Custom Duration")
description: I18n.tr("%1 custom animation duration").arg(I18n.tr("Modals"))
minimum: 0
maximum: 2000
maximum: 1000
value: Theme.modalAnimationDuration
unit: "ms"
defaultValue: 150
onSliderValueChanged: newValue => {
if (SettingsData.syncComponentAnimationSpeeds)
SettingsData.set("syncComponentAnimationSpeeds", false);
SettingsData.set("modalAnimationSpeed", SettingsData.AnimationSpeed.Custom);
SettingsData.set("modalCustomAnimationDuration", newValue);
}
@@ -423,13 +454,37 @@ Item {
Connections {
target: Theme
function onModalAnimationDurationChanged() {
if (SettingsData.modalAnimationSpeed === SettingsData.AnimationSpeed.Custom)
if (!SettingsData.syncComponentAnimationSpeeds && SettingsData.modalAnimationSpeed === SettingsData.AnimationSpeed.Custom)
return;
modalDurationSlider.value = Theme.modalAnimationDuration;
}
}
}
}
SettingsCard {
tab: "typography"
tags: ["animation", "ripple", "effect", "material", "feedback"]
title: I18n.tr("Ripple Effects")
settingKey: "enableRippleEffects"
iconName: "radio_button_unchecked"
SettingsToggleRow {
tab: "typography"
tags: ["animation", "ripple", "effect", "material", "click"]
settingKey: "enableRippleEffects"
text: I18n.tr("Enable Ripple Effects")
description: I18n.tr("Show Material Design ripple animations on interactive elements")
checked: SettingsData.enableRippleEffects ?? true
onToggled: newValue => SettingsData.set("enableRippleEffects", newValue)
Connections {
target: SettingsData
function onEnableRippleEffectsChanged() {
}
}
}
}
}
}
}

View File

@@ -15,6 +15,8 @@ Rectangle {
property color textColor: Theme.buttonText
property int buttonHeight: 40
property int horizontalPadding: Theme.spacingL
property bool enableScaleAnimation: false
property bool enableRipple: false
signal clicked
@@ -23,6 +25,15 @@ Rectangle {
radius: Theme.cornerRadius
color: backgroundColor
opacity: enabled ? 1 : 0.4
scale: (enableScaleAnimation && pressed) ? 0.98 : 1.0
Behavior on scale {
enabled: enableScaleAnimation && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
DankAnim {
duration: 100
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
Rectangle {
id: stateLayer

View File

@@ -0,0 +1,130 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Widgets
ColumnLayout {
id: root
required property string title
property string description: ""
property bool expanded: false
property bool showBackground: false
property alias headerColor: headerRect.color
signal toggleRequested
spacing: Theme.spacingS
Layout.fillWidth: true
Rectangle {
id: headerRect
Layout.fillWidth: true
Layout.preferredHeight: Math.max(titleRow.implicitHeight + Theme.paddingM * 2, 48)
radius: Theme.cornerRadius
color: "transparent"
RowLayout {
id: titleRow
anchors.fill: parent
anchors.leftMargin: Theme.paddingM
anchors.rightMargin: Theme.paddingM
spacing: Theme.spacingM
StyledText {
text: root.title
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
Layout.fillWidth: true
}
DankIcon {
name: "expand_more"
size: Theme.iconSizeSmall
rotation: root.expanded ? 180 : 0
Behavior on rotation {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
DankAnim {
duration: Theme.shortDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
}
}
StateLayer {
anchors.fill: parent
onClicked: {
root.toggleRequested();
root.expanded = !root.expanded;
}
}
}
default property alias content: contentColumn.data
Item {
id: contentWrapper
Layout.fillWidth: true
Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Theme.spacingS * 2) : 0
clip: true
Behavior on Layout.preferredHeight {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
DankAnim {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
Rectangle {
id: backgroundRect
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.surfaceContainer
opacity: root.showBackground && root.expanded ? 1.0 : 0.0
visible: root.showBackground
Behavior on opacity {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
DankAnim {
duration: Theme.shortDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
}
ColumnLayout {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
y: Theme.spacingS
anchors.leftMargin: Theme.paddingM
anchors.rightMargin: Theme.paddingM
anchors.bottomMargin: Theme.spacingS
spacing: Theme.spacingS
opacity: root.expanded ? 1.0 : 0.0
Behavior on opacity {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
DankAnim {
duration: Theme.shortDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
StyledText {
id: descriptionText
Layout.fillWidth: true
Layout.topMargin: root.description !== "" ? Theme.spacingXS : 0
Layout.bottomMargin: root.description !== "" ? Theme.spacingS : 0
visible: root.description !== ""
text: root.description
color: Theme.surfaceTextSecondary
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
}
}
}
}

View File

@@ -34,6 +34,14 @@ Item {
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
antialiasing: true
Behavior on color {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
DankColorAnim {
duration: Theme.shorterDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
font.variableAxes: {
"FILL": root.fill.toFixed(1),
"GRAD": root.grade,

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
ListView {
@@ -22,6 +23,11 @@ ListView {
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
add: ListViewTransitions.add
remove: ListViewTransitions.remove
displaced: ListViewTransitions.displaced
move: ListViewTransitions.move
onMovementStarted: {
isUserScrolling = true;
vbar._scrollBarActive = true;

View File

@@ -0,0 +1,91 @@
import QtQuick
import qs.Common
// Material Design 3 ripple effect component
MouseArea {
id: root
property color rippleColor: Theme.primary
property real cornerRadius: 0
property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
property real _rippleX: 0
property real _rippleY: 0
property real _rippleRadius: 0
enabled: false
hoverEnabled: false
function trigger(x, y) {
if (!enableRipple || Theme.currentAnimationSpeed === SettingsData.AnimationSpeed.None)
return;
_rippleX = x;
_rippleY = y;
const dist = (ox, oy) => ox * ox + oy * oy;
_rippleRadius = Math.sqrt(Math.max(dist(x, y), dist(x, height - y), dist(width - x, y), dist(width - x, height - y)));
rippleAnim.restart();
}
SequentialAnimation {
id: rippleAnim
PropertyAction {
target: ripple
property: "x"
value: root._rippleX
}
PropertyAction {
target: ripple
property: "y"
value: root._rippleY
}
PropertyAction {
target: ripple
property: "opacity"
value: 0.08
}
ParallelAnimation {
DankAnim {
target: ripple
property: "implicitWidth"
from: 0
to: root._rippleRadius * 2
duration: Theme.expressiveDurations.expressiveEffects
easing.bezierCurve: Theme.expressiveCurves.standardDecel
}
DankAnim {
target: ripple
property: "implicitHeight"
from: 0
to: root._rippleRadius * 2
duration: Theme.expressiveDurations.expressiveEffects
easing.bezierCurve: Theme.expressiveCurves.standardDecel
}
}
DankAnim {
target: ripple
property: "opacity"
to: 0
duration: Theme.expressiveDurations.expressiveEffects
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
Rectangle {
id: ripple
radius: Math.min(width, height) / 2
color: root.rippleColor
opacity: 0
transform: Translate {
x: -ripple.width / 2
y: -ripple.height / 2
}
}
}

View File

@@ -9,6 +9,7 @@ MouseArea {
property real cornerRadius: parent && parent.radius !== undefined ? parent.radius : Theme.cornerRadius
property var tooltipText: null
property string tooltipSide: "bottom"
property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
readonly property real stateOpacity: disabled ? 0 : pressed ? 0.12 : containsMouse ? 0.08 : 0
@@ -16,10 +17,33 @@ MouseArea {
cursorShape: disabled ? undefined : Qt.PointingHandCursor
hoverEnabled: true
onPressed: mouse => {
if (!disabled && enableRipple) {
rippleLayer.trigger(mouse.x, mouse.y);
}
}
Rectangle {
id: stateRect
anchors.fill: parent
radius: root.cornerRadius
color: Qt.rgba(stateColor.r, stateColor.g, stateColor.b, stateOpacity)
Behavior on color {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
DankColorAnim {
duration: Theme.shorterDuration
easing.bezierCurve: Theme.expressiveCurves.standardDecel
}
}
}
DankRipple {
id: rippleLayer
anchors.fill: parent
rippleColor: root.stateColor
cornerRadius: root.cornerRadius
enableRipple: root.enableRipple
}
Timer {