md: add ripple effect (#1892)

* Add `StateLayer` component
* Add `Ripple` component (used by StateLayer)
* use `StateLayer` in material `Button`
* use `StateLayer` in material `Item`
This commit is contained in:
Florian Blasius 2022-11-23 17:03:46 +01:00 committed by GitHub
parent 784ea30bc7
commit 0b66628fc4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 132 additions and 60 deletions

View file

@ -0,0 +1,93 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
import { md } from "md.slint";
export Ripple := Rectangle {
in property<length> ripple-x;
in property<length> ripple-y;
in property<bool> active;
in property<bool> has-effect;
in property<brush> ripple-color <=> circle.background;
circle := Rectangle {
x: root.ripple-x - width / 2;
y: root.ripple-y - width / 2;
height: width;
border-radius: width / 2;
}
states [
active when root.active && root.has-effect: {
circle.width: root.width * 2 * 1.4142;
}
]
transitions [
in active : {
animate circle.width { duration: 2s; easing: ease-out; }
}
]
}
// A touch area that also represents a visual state.
export StateLayer := TouchArea {
in property<bool> focusable;
in property<brush> selection-background;
in property<brush> ripple-color;
in property<bool> has-ripple;
in property<length> border-radius;
out property<bool> has-focus <=> fs.has-focus;
in-out property<brush> background;
in-out property<bool> checked;
forward-focus: fs;
ripple := Ripple {
width: 100%;
height: 100%;
opacity: 0;
active: root.pressed;
ripple-x: root.pressed-x;
ripple-y: root.pressed-y;
clip: true;
border-radius: root.border-radius;
background: root.background;
ripple-color: root.ripple-color;
has-effect: root.has-ripple;
animate opacity { duration: 250ms; easing: ease; }
animate background { duration: 250ms; }
}
fs := FocusScope {
width: 0px; // Do not react on clicks
enabled: root.enabled && root.focusable;
key-pressed(event) => {
if (event.text == " " || event.text == "\n") {
root.clicked();
return accept;
}
return reject;
}
}
states [
pressed when root.pressed: {
ripple.opacity: 0.12;
}
checked when root.checked: {
ripple.opacity: 1.0;
ripple.background: selection-background;
}
hover when root.has-hover: {
ripple.opacity: 0.08;
}
focused when root.has-focus: {
ripple.opacity: 0.12;
}
]
}

View file

@ -13,11 +13,13 @@ struct Color := {
on-surface-variant: color,
primary: color,
on-primary: color,
primary-ripple: color,
shadow: color,
outline: color,
outline-variant: color,
secondary-container: color,
on-secondary-container: color,
secondary-ripple: color,
}
// typo settings
@ -62,12 +64,14 @@ export global md := {
on-surface-variant: !dark-color-scheme ? #49454E : #CAC4D0,
surface-tint: !dark-color-scheme ? #6750A4 : #D0BCFF,
primary: !dark-color-scheme ? #6750A4 : #D0BCFF,
primary-ripple: !dark-color-scheme ? #D0BCFF : #6750A4,
on-primary: !dark-color-scheme ? #FFFFFF : #371E73,
shadow: #000000,
outline: !dark-color-scheme ? #79747E : #938F99,
outline-variant: !dark-color-scheme ? #C4C7C5 : #444746,
secondary-container: !dark-color-scheme ? #E8DEF8 : #4A4458,
on-secondary-container: !dark-color-scheme ? #1E192B : #E8DEF8,
secondary-ripple: !dark-color-scheme ? #fffc : #000000,
},
elevation: {

View file

@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
import { StateLayer } from "comp-state-layer.slint";
import { md } from "md.slint";
// Default button widget with Material Design Filled Button look and feel.
@ -9,9 +9,9 @@ export Button := Rectangle {
callback clicked;
in-out property<string> text <=> label.text;
out property<bool> has-focus: fs.has-focus;
out property<bool> pressed: self.enabled && touch.pressed;
in property<bool> enabled <=> touch.enabled;
out property<bool> has-focus: state-layer.has-focus;
out property<bool> pressed: self.enabled && state-layer.pressed;
in property<bool> enabled <=> state-layer.enabled;
in property<bool> checkable;
in-out property<bool> checked;
in property<image> icon;
@ -19,6 +19,7 @@ export Button := Rectangle {
accessible-label <=> label.text;
accessible-role: button;
forward-focus: state-layer;
height: 40px;
@ -32,14 +33,21 @@ export Button := Rectangle {
drop-shadow-offset-y: 1px;
}
state-layer := Rectangle {
opacity: 0;
width: 100%;
height: 100%;
state-layer := StateLayer {
has-ripple: true;
border-radius: container.border-radius;
background: md.sys.color.on-secondary-container;
animate opacity { duration: 250ms; easing: ease; }
}
ripple-color: md.sys.color.secondary-ripple;
selection-background: md.sys.color.primary;
focusable: true;
clicked => {
if (root.checkable) {
root.checked = !root.checked;
}
root.clicked();
}
}
HorizontalLayout {
padding-left: 24px;
@ -56,29 +64,9 @@ export Button := Rectangle {
color: md.sys.color.on-secondary-container;
vertical-alignment: center;
horizontal-alignment: center;
font-size: md.sys.typescale.label-large.size;
font-weight: md.sys.typescale.label-large.weight;
}
}
touch := TouchArea {
clicked => {
if (root.checkable) {
root.checked = !root.checked;
}
root.clicked();
}
}
fs := FocusScope {
width: 0px; // Do not react on clicks
enabled <=> root.enabled;
key-pressed(event) => {
if (event.text == " " || event.text == "\n") {
touch.clicked();
return accept;
}
return reject;
animate color { duration: 250ms; easing: ease; }
}
}
@ -89,16 +77,8 @@ export Button := Rectangle {
label.opacity: 0.38;
label.color: md.sys.color.on-surface;
}
pressed when touch.pressed : {
state-layer.opacity: 0.12;
}
hover when touch.has-hover : {
state-layer.opacity: 0.08;
container.drop-shadow-blur: md.sys.elevation.level1;
container.drop-shadow-color: md.sys.color.shadow;
}
focused when fs.has-focus : {
state-layer.opacity: 0.12;
checked when root.checked: {
label.color: md.sys.color.on-primary;
}
]
}

View file

@ -1,24 +1,25 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
import { StateLayer } from "comp-state-layer.slint";
import { md } from "md.slint";
// A selectable item that is used by `StandardListView` and `ComboBox`.
export Item := Rectangle {
callback clicked <=> touch.clicked;
callback clicked <=> state-layer.clicked;
property<bool> selected: false;
property<string> text <=> label.text;
in property<bool> selected;
in property<string> text;
// background: md.sys.color.background;
height: 48px;
state-layer := Rectangle {
opacity: 0;
width: 100%;
height: 100%;
state-layer := StateLayer {
checked: root.selected;
background: md.sys.color.primary;
animate opacity { duration: 250ms; easing: ease; }
selection-background: md.sys.color.secondary-container;
ripple-color: md.sys.color.primary-ripple;
has-ripple: true;
}
HorizontalLayout {
@ -26,6 +27,7 @@ export Item := Rectangle {
padding-right: 12px;
label := Text {
text: root.text;
color: md.sys.color.on-surface;
vertical-alignment: center;
// FIXME after Roboto font can be loaded
@ -35,18 +37,11 @@ export Item := Rectangle {
}
}
touch := TouchArea {}
states [
selected when selected : {
state-layer.opacity: 1;
state-layer.background: md.sys.color.secondary-container;
}
pressed when touch.pressed : {
state-layer.opacity: 0.12;
}
hover when touch.has-hover : {
state-layer.opacity: 0.08;
}
}
]
}