""" 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(object): """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, )