Sideband/sbapp/kivymd/uix/behaviors/ripple_behavior.py
2022-10-08 17:17:59 +02:00

529 lines
16 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Behaviors/Ripple
================
.. rubric:: Classes implements a circular and rectangular ripple effects.
To create a widget with сircular ripple effect, you must create a new class
that inherits from the :class:`~CircularRippleBehavior` class.
For example, let's create an image button with a circular ripple effect:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.image import Image
from kivymd.app import MDApp
from kivymd.uix.behaviors import CircularRippleBehavior
KV = '''
MDScreen:
CircularRippleButton:
source: "data/logo/kivy-icon-256.png"
size_hint: None, None
size: "250dp", "250dp"
pos_hint: {"center_x": .5, "center_y": .5}
'''
class CircularRippleButton(CircularRippleBehavior, ButtonBehavior, Image):
def __init__(self, **kwargs):
self.ripple_scale = 0.85
super().__init__(**kwargs)
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/circular-ripple-effect.gif
:align: center
To create a widget with rectangular ripple effect, you must create a new class
that inherits from the :class:`~RectangularRippleBehavior` class:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import RectangularRippleBehavior, BackgroundColorBehavior
KV = '''
MDScreen:
RectangularRippleButton:
size_hint: None, None
size: "250dp", "50dp"
pos_hint: {"center_x": .5, "center_y": .5}
'''
class RectangularRippleButton(
RectangularRippleBehavior, ButtonBehavior, BackgroundColorBehavior
):
md_bg_color = [0, 0, 1, 1]
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/rectangular-ripple-effect.gif
:align: center
"""
__all__ = (
"CommonRipple",
"RectangularRippleBehavior",
"CircularRippleBehavior",
)
from typing import NoReturn
from kivy.animation import Animation
from kivy.graphics import (
Color,
Ellipse,
StencilPop,
StencilPush,
StencilUnUse,
StencilUse,
)
from kivy.graphics.vertex_instructions import RoundedRectangle
from kivy.properties import (
BooleanProperty,
ColorProperty,
ListProperty,
NumericProperty,
StringProperty,
)
from kivy.uix.behaviors import ToggleButtonBehavior
class CommonRipple:
"""Base class for ripple effect."""
ripple_rad_default = NumericProperty(1)
"""
The starting value of the radius of the ripple effect.
.. code-block:: kv
CircularRippleButton:
ripple_rad_default: 100
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-rad-default.gif
:align: center
:attr:`ripple_rad_default` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
ripple_color = ColorProperty(None)
"""
Ripple color in (r, g, b, a) format.
.. code-block:: kv
CircularRippleButton:
ripple_color: app.theme_cls.primary_color
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-color.gif
:align: center
:attr:`ripple_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
ripple_alpha = NumericProperty(0.5)
"""
Alpha channel values for ripple effect.
.. code-block:: kv
CircularRippleButton:
ripple_alpha: .9
ripple_color: app.theme_cls.primary_color
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-alpha.gif
:align: center
:attr:`ripple_alpha` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.5`.
"""
ripple_scale = NumericProperty(None)
"""
Ripple effect scale.
.. code-block:: kv
CircularRippleButton:
ripple_scale: .5
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-scale-05.gif
:align: center
.. code-block:: kv
CircularRippleButton:
ripple_scale: 1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-scale-1.gif
:align: center
:attr:`ripple_scale` is an :class:`~kivy.properties.NumericProperty`
and defaults to `None`.
"""
ripple_duration_in_fast = NumericProperty(0.3)
"""
Ripple duration when touching to widget.
.. code-block:: kv
CircularRippleButton:
ripple_duration_in_fast: .1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-duration-in-fast.gif
:align: center
:attr:`ripple_duration_in_fast` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.3`.
"""
ripple_duration_in_slow = NumericProperty(2)
"""
Ripple duration when long touching to widget.
.. code-block:: kv
CircularRippleButton:
ripple_duration_in_slow: 5
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-duration-in-slow.gif
:align: center
:attr:`ripple_duration_in_slow` is an :class:`~kivy.properties.NumericProperty`
and defaults to `2`.
"""
ripple_duration_out = NumericProperty(0.3)
"""
The duration of the disappearance of the wave effect.
.. code-block:: kv
CircularRippleButton:
ripple_duration_out: 5
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-duration-out.gif
:align: center
:attr:`ripple_duration_out` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.3`.
"""
ripple_canvas_after = BooleanProperty(True)
"""
The ripple effect is drawn above/below the content.
.. versionadded:: 1.0.0
.. code-block:: kv
MDIconButton:
ripple_canvas_after: True
icon: "android"
ripple_alpha: .8
ripple_color: app.theme_cls.primary_color
icon_size: "100sp"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-canvas-after-true.gif
:align: center
.. code-block:: kv
MDIconButton:
ripple_canvas_after: False
icon: "android"
ripple_alpha: .8
ripple_color: app.theme_cls.primary_color
icon_size: "100sp"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-canvas-after-false.gif
:align: center
:attr:`ripple_canvas_after` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
ripple_func_in = StringProperty("out_quad")
"""
Type of animation for ripple in effect.
:attr:`ripple_func_in` is an :class:`~kivy.properties.StringProperty`
and defaults to `'out_quad'`.
"""
ripple_func_out = StringProperty("out_quad")
"""
Type of animation for ripple out effect.
:attr:`ripple_func_out` is an :class:`~kivy.properties.StringProperty`
and defaults to `'ripple_func_out'`.
"""
_ripple_rad = NumericProperty()
_doing_ripple = BooleanProperty(False)
_finishing_ripple = BooleanProperty(False)
_fading_out = BooleanProperty(False)
_no_ripple_effect = BooleanProperty(False)
_round_rad = ListProperty([0, 0, 0, 0])
def lay_canvas_instructions(self) -> NoReturn:
raise NotImplementedError
def start_ripple(self) -> None:
if not self._doing_ripple:
self._doing_ripple = True
anim = Animation(
_ripple_rad=self.finish_rad,
t="linear",
duration=self.ripple_duration_in_slow,
)
anim.bind(on_complete=self.fade_out)
anim.start(self)
def finish_ripple(self) -> None:
if self._doing_ripple and not self._finishing_ripple:
self._finishing_ripple = True
self._doing_ripple = False
Animation.cancel_all(self, "_ripple_rad")
anim = Animation(
_ripple_rad=self.finish_rad,
t=self.ripple_func_in,
duration=self.ripple_duration_in_fast,
)
anim.bind(on_complete=self.fade_out)
anim.start(self)
def fade_out(self, *args) -> None:
rc = self.ripple_color
if not self._fading_out:
self._fading_out = True
Animation.cancel_all(self, "ripple_color")
anim = Animation(
ripple_color=[rc[0], rc[1], rc[2], 0.0],
t=self.ripple_func_out,
duration=self.ripple_duration_out,
)
anim.bind(on_complete=self.anim_complete)
anim.start(self)
def anim_complete(self, *args) -> None:
self._doing_ripple = False
self._finishing_ripple = False
self._fading_out = False
if not self.ripple_canvas_after:
canvas = self.canvas.before
else:
canvas = self.canvas.after
canvas.remove_group("circular_ripple_behavior")
canvas.remove_group("rectangular_ripple_behavior")
def on_touch_down(self, touch):
# FIXME: in fact, the output of the super method is extra.
# But without this, the list (`ScrollView`) placed in the `MDCard`
# widget will not scroll.
super().on_touch_down(touch)
if touch.is_mouse_scrolling:
return False
if not self.collide_point(touch.x, touch.y):
return False
if not self.disabled:
self.call_ripple_animation_methods(touch)
# FIXME: this check is needed for the `MDTabsLabel` object.
# With the normal `return True`, events for tabs from the `MDTabs`
# class are not processed.
# There may be problems with other widgets.
# Status: requires check.
if isinstance(self, ToggleButtonBehavior):
return super().on_touch_down(touch)
else:
return True
def call_ripple_animation_methods(self, touch) -> None:
if self._doing_ripple:
Animation.cancel_all(
self, "_ripple_rad", "ripple_color", "rect_color"
)
self.anim_complete()
self._ripple_rad = self.ripple_rad_default
self.ripple_pos = (touch.x, touch.y)
if self.ripple_color:
pass
elif hasattr(self, "theme_cls"):
self.ripple_color = self.theme_cls.ripple_color
else:
# If no theme, set Gray 300.
self.ripple_color = [
0.8784313725490196,
0.8784313725490196,
0.8784313725490196,
self.ripple_alpha,
]
self.ripple_color[3] = self.ripple_alpha
self.lay_canvas_instructions()
self.finish_rad = max(self.width, self.height) * self.ripple_scale
self.start_ripple()
def on_touch_move(self, touch, *args):
if not self.collide_point(touch.x, touch.y):
if not self._finishing_ripple and self._doing_ripple:
self.finish_ripple()
return super().on_touch_move(touch, *args)
def on_touch_up(self, touch):
if self.collide_point(touch.x, touch.y) and self._doing_ripple:
self.finish_ripple()
return super().on_touch_up(touch)
def _set_ellipse(self, instance, value):
self.ellipse.size = (self._ripple_rad, self._ripple_rad)
# Adjust ellipse pos here
def _set_color(self, instance, value):
self.col_instruction.a = value[3]
class RectangularRippleBehavior(CommonRipple):
"""Class implements a rectangular ripple effect."""
ripple_scale = NumericProperty(2.75)
"""
See :class:`~CommonRipple.ripple_scale`.
:attr:`ripple_scale` is an :class:`~kivy.properties.NumericProperty`
and defaults to `2.75`.
"""
def lay_canvas_instructions(self) -> None:
if self._no_ripple_effect:
return
with self.canvas.after if self.ripple_canvas_after else self.canvas.before:
if hasattr(self, "radius"):
if isinstance(self.radius, (float, int)):
self.radius = [
self.radius,
]
self._round_rad = self.radius
StencilPush(group="rectangular_ripple_behavior")
RoundedRectangle(
pos=self.pos,
size=self.size,
radius=self._round_rad,
group="rectangular_ripple_behavior",
)
StencilUse(group="rectangular_ripple_behavior")
self.col_instruction = Color(
rgba=self.ripple_color, group="rectangular_ripple_behavior"
)
self.ellipse = Ellipse(
size=(self._ripple_rad, self._ripple_rad),
pos=(
self.ripple_pos[0] - self._ripple_rad / 2.0,
self.ripple_pos[1] - self._ripple_rad / 2.0,
),
group="rectangular_ripple_behavior",
)
StencilUnUse(group="rectangular_ripple_behavior")
RoundedRectangle(
pos=self.pos,
size=self.size,
radius=self._round_rad,
group="rectangular_ripple_behavior",
)
StencilPop(group="rectangular_ripple_behavior")
self.bind(ripple_color=self._set_color, _ripple_rad=self._set_ellipse)
def _set_ellipse(self, instance, value):
super()._set_ellipse(instance, value)
self.ellipse.pos = (
self.ripple_pos[0] - self._ripple_rad / 2.0,
self.ripple_pos[1] - self._ripple_rad / 2.0,
)
class CircularRippleBehavior(CommonRipple):
"""Class implements a circular ripple effect."""
ripple_scale = NumericProperty(1)
"""
See :class:`~CommonRipple.ripple_scale`.
:attr:`ripple_scale` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
def lay_canvas_instructions(self) -> None:
if self._no_ripple_effect:
return
with self.canvas.after if self.ripple_canvas_after else self.canvas.before:
StencilPush(group="circular_ripple_behavior")
self.stencil = Ellipse(
size=(
self.width * self.ripple_scale,
self.height * self.ripple_scale,
),
pos=(
self.center_x - (self.width * self.ripple_scale) / 2,
self.center_y - (self.height * self.ripple_scale) / 2,
),
group="circular_ripple_behavior",
)
StencilUse(group="circular_ripple_behavior")
self.col_instruction = Color(rgba=self.ripple_color)
self.ellipse = Ellipse(
size=(self._ripple_rad, self._ripple_rad),
pos=(
self.center_x - self._ripple_rad / 2.0,
self.center_y - self._ripple_rad / 2.0,
),
group="circular_ripple_behavior",
)
StencilUnUse(group="circular_ripple_behavior")
Ellipse(
pos=self.pos, size=self.size, group="circular_ripple_behavior"
)
StencilPop(group="circular_ripple_behavior")
self.bind(
ripple_color=self._set_color, _ripple_rad=self._set_ellipse
)
def _set_ellipse(self, instance, value):
super()._set_ellipse(instance, value)
if self.ellipse.size[0] > self.width * 0.6 and not self._fading_out:
self.fade_out()
self.ellipse.pos = (
self.center_x - self._ripple_rad / 2.0,
self.center_y - self._ripple_rad / 2.0,
)