319 lines
10 KiB
Python
319 lines
10 KiB
Python
'''
|
|
Touch Ripple
|
|
============
|
|
|
|
.. versionadded:: 1.10.1
|
|
|
|
.. warning::
|
|
This code is still experimental, and its API is subject to change in a
|
|
future version.
|
|
|
|
This module contains `mixin <https://en.wikipedia.org/wiki/Mixin>`_ classes
|
|
to add a touch ripple visual effect known from `Google Material Design
|
|
<https://en.wikipedia.org/wiki/Material_Design>_` to widgets.
|
|
|
|
For an overview of behaviors, please refer to the :mod:`~kivy.uix.behaviors`
|
|
documentation.
|
|
|
|
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleBehavior` provides
|
|
rendering the ripple animation.
|
|
|
|
The class :class:`~kivy.uix.behaviors.touchripple.TouchRippleButtonBehavior`
|
|
basically provides the same functionality as
|
|
:class:`~kivy.uix.behaviors.button.ButtonBehavior` but rendering the ripple
|
|
animation instead of default press/release visualization.
|
|
'''
|
|
from kivy.animation import Animation
|
|
from kivy.clock import Clock
|
|
from kivy.graphics import CanvasBase, Color, Ellipse, ScissorPush, ScissorPop
|
|
from kivy.properties import BooleanProperty, ListProperty, NumericProperty, \
|
|
ObjectProperty, StringProperty
|
|
from kivy.uix.relativelayout import RelativeLayout
|
|
|
|
|
|
__all__ = (
|
|
'TouchRippleBehavior',
|
|
'TouchRippleButtonBehavior'
|
|
)
|
|
|
|
|
|
class TouchRippleBehavior(object):
|
|
'''Touch ripple behavior.
|
|
|
|
Supposed to be used as mixin on widget classes.
|
|
|
|
Ripple behavior does not trigger automatically, concrete implementation
|
|
needs to call :func:`ripple_show` respective :func:`ripple_fade` manually.
|
|
|
|
Example
|
|
-------
|
|
|
|
Here we create a Label which renders the touch ripple animation on
|
|
interaction::
|
|
|
|
class RippleLabel(TouchRippleBehavior, Label):
|
|
|
|
def __init__(self, **kwargs):
|
|
super(RippleLabel, self).__init__(**kwargs)
|
|
|
|
def on_touch_down(self, touch):
|
|
collide_point = self.collide_point(touch.x, touch.y)
|
|
if collide_point:
|
|
touch.grab(self)
|
|
self.ripple_show(touch)
|
|
return True
|
|
return False
|
|
|
|
def on_touch_up(self, touch):
|
|
if touch.grab_current is self:
|
|
touch.ungrab(self)
|
|
self.ripple_fade()
|
|
return True
|
|
return False
|
|
'''
|
|
|
|
ripple_rad_default = NumericProperty(10)
|
|
'''Default radius the animation starts from.
|
|
|
|
:attr:`ripple_rad_default` is a :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `10`.
|
|
'''
|
|
|
|
ripple_duration_in = NumericProperty(.5)
|
|
'''Animation duration taken to show the overlay.
|
|
|
|
:attr:`ripple_duration_in` is a :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0.5`.
|
|
'''
|
|
|
|
ripple_duration_out = NumericProperty(.2)
|
|
'''Animation duration taken to fade the overlay.
|
|
|
|
:attr:`ripple_duration_out` is a :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0.2`.
|
|
'''
|
|
|
|
ripple_fade_from_alpha = NumericProperty(.5)
|
|
'''Alpha channel for ripple color the animation starts with.
|
|
|
|
:attr:`ripple_fade_from_alpha` is a
|
|
:class:`~kivy.properties.NumericProperty` and defaults to `0.5`.
|
|
'''
|
|
|
|
ripple_fade_to_alpha = NumericProperty(.8)
|
|
'''Alpha channel for ripple color the animation targets to.
|
|
|
|
:attr:`ripple_fade_to_alpha` is a :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0.8`.
|
|
'''
|
|
|
|
ripple_scale = NumericProperty(2.)
|
|
'''Max scale of the animation overlay calculated from max(width/height) of
|
|
the decorated widget.
|
|
|
|
:attr:`ripple_scale` is a :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `2.0`.
|
|
'''
|
|
|
|
ripple_func_in = StringProperty('in_cubic')
|
|
'''Animation callback for showing the overlay.
|
|
|
|
:attr:`ripple_func_in` is a :class:`~kivy.properties.StringProperty`
|
|
and defaults to `in_cubic`.
|
|
'''
|
|
|
|
ripple_func_out = StringProperty('out_quad')
|
|
'''Animation callback for hiding the overlay.
|
|
|
|
:attr:`ripple_func_out` is a :class:`~kivy.properties.StringProperty`
|
|
and defaults to `out_quad`.
|
|
'''
|
|
|
|
ripple_rad = NumericProperty(10)
|
|
ripple_pos = ListProperty([0, 0])
|
|
ripple_color = ListProperty((1., 1., 1., .5))
|
|
|
|
def __init__(self, **kwargs):
|
|
super(TouchRippleBehavior, self).__init__(**kwargs)
|
|
self.ripple_pane = CanvasBase()
|
|
self.canvas.add(self.ripple_pane)
|
|
self.bind(
|
|
ripple_color=self._ripple_set_color,
|
|
ripple_pos=self._ripple_set_ellipse,
|
|
ripple_rad=self._ripple_set_ellipse
|
|
)
|
|
self.ripple_ellipse = None
|
|
self.ripple_col_instruction = None
|
|
|
|
def ripple_show(self, touch):
|
|
'''Begin ripple animation on current widget.
|
|
|
|
Expects touch event as argument.
|
|
'''
|
|
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
|
|
self._ripple_reset_pane()
|
|
x, y = self.to_window(*self.pos)
|
|
width, height = self.size
|
|
if isinstance(self, RelativeLayout):
|
|
self.ripple_pos = ripple_pos = (touch.x - x, touch.y - y)
|
|
else:
|
|
self.ripple_pos = ripple_pos = (touch.x, touch.y)
|
|
rc = self.ripple_color
|
|
ripple_rad = self.ripple_rad
|
|
self.ripple_color = [rc[0], rc[1], rc[2], self.ripple_fade_from_alpha]
|
|
with self.ripple_pane:
|
|
ScissorPush(
|
|
x=int(round(x)),
|
|
y=int(round(y)),
|
|
width=int(round(width)),
|
|
height=int(round(height))
|
|
)
|
|
self.ripple_col_instruction = Color(rgba=self.ripple_color)
|
|
self.ripple_ellipse = Ellipse(
|
|
size=(ripple_rad, ripple_rad),
|
|
pos=(
|
|
ripple_pos[0] - ripple_rad / 2.,
|
|
ripple_pos[1] - ripple_rad / 2.
|
|
)
|
|
)
|
|
ScissorPop()
|
|
anim = Animation(
|
|
ripple_rad=max(width, height) * self.ripple_scale,
|
|
t=self.ripple_func_in,
|
|
ripple_color=[rc[0], rc[1], rc[2], self.ripple_fade_to_alpha],
|
|
duration=self.ripple_duration_in
|
|
)
|
|
anim.start(self)
|
|
|
|
def ripple_fade(self):
|
|
'''Finish ripple animation on current widget.
|
|
'''
|
|
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
|
|
width, height = self.size
|
|
rc = self.ripple_color
|
|
duration = self.ripple_duration_out
|
|
anim = Animation(
|
|
ripple_rad=max(width, height) * self.ripple_scale,
|
|
ripple_color=[rc[0], rc[1], rc[2], 0.],
|
|
t=self.ripple_func_out,
|
|
duration=duration
|
|
)
|
|
anim.bind(on_complete=self._ripple_anim_complete)
|
|
anim.start(self)
|
|
|
|
def _ripple_set_ellipse(self, instance, value):
|
|
ellipse = self.ripple_ellipse
|
|
if not ellipse:
|
|
return
|
|
ripple_pos = self.ripple_pos
|
|
ripple_rad = self.ripple_rad
|
|
ellipse.size = (ripple_rad, ripple_rad)
|
|
ellipse.pos = (
|
|
ripple_pos[0] - ripple_rad / 2.,
|
|
ripple_pos[1] - ripple_rad / 2.
|
|
)
|
|
|
|
def _ripple_set_color(self, instance, value):
|
|
if not self.ripple_col_instruction:
|
|
return
|
|
self.ripple_col_instruction.rgba = value
|
|
|
|
def _ripple_anim_complete(self, anim, instance):
|
|
self._ripple_reset_pane()
|
|
|
|
def _ripple_reset_pane(self):
|
|
self.ripple_rad = self.ripple_rad_default
|
|
self.ripple_pane.clear()
|
|
|
|
|
|
class TouchRippleButtonBehavior(TouchRippleBehavior):
|
|
'''
|
|
This `mixin <https://en.wikipedia.org/wiki/Mixin>`_ class provides
|
|
a similar behavior to :class:`~kivy.uix.behaviors.button.ButtonBehavior`
|
|
but provides touch ripple animation instead of button pressed/released as
|
|
visual effect.
|
|
|
|
:Events:
|
|
`on_press`
|
|
Fired when the button is pressed.
|
|
`on_release`
|
|
Fired when the button is released (i.e. the touch/click that
|
|
pressed the button goes away).
|
|
'''
|
|
|
|
last_touch = ObjectProperty(None)
|
|
'''Contains the last relevant touch received by the Button. This can
|
|
be used in `on_press` or `on_release` in order to know which touch
|
|
dispatched the event.
|
|
|
|
:attr:`last_touch` is a :class:`~kivy.properties.ObjectProperty` and
|
|
defaults to `None`.
|
|
'''
|
|
|
|
always_release = BooleanProperty(False)
|
|
'''This determines whether or not the widget fires an `on_release` event if
|
|
the touch_up is outside the widget.
|
|
|
|
:attr:`always_release` is a :class:`~kivy.properties.BooleanProperty` and
|
|
defaults to `False`.
|
|
'''
|
|
|
|
def __init__(self, **kwargs):
|
|
self.register_event_type('on_press')
|
|
self.register_event_type('on_release')
|
|
super(TouchRippleButtonBehavior, self).__init__(**kwargs)
|
|
|
|
def on_touch_down(self, touch):
|
|
if super(TouchRippleButtonBehavior, self).on_touch_down(touch):
|
|
return True
|
|
if touch.is_mouse_scrolling:
|
|
return False
|
|
if not self.collide_point(touch.x, touch.y):
|
|
return False
|
|
if self in touch.ud:
|
|
return False
|
|
touch.grab(self)
|
|
touch.ud[self] = True
|
|
self.last_touch = touch
|
|
self.ripple_show(touch)
|
|
self.dispatch('on_press')
|
|
return True
|
|
|
|
def on_touch_move(self, touch):
|
|
if touch.grab_current is self:
|
|
return True
|
|
if super(TouchRippleButtonBehavior, self).on_touch_move(touch):
|
|
return True
|
|
return self in touch.ud
|
|
|
|
def on_touch_up(self, touch):
|
|
if touch.grab_current is not self:
|
|
return super(TouchRippleButtonBehavior, self).on_touch_up(touch)
|
|
assert(self in touch.ud)
|
|
touch.ungrab(self)
|
|
self.last_touch = touch
|
|
if self.disabled:
|
|
return
|
|
self.ripple_fade()
|
|
if not self.always_release and not self.collide_point(*touch.pos):
|
|
return
|
|
|
|
# defer on_release until ripple_fade has completed
|
|
def defer_release(dt):
|
|
self.dispatch('on_release')
|
|
Clock.schedule_once(defer_release, self.ripple_duration_out)
|
|
return True
|
|
|
|
def on_disabled(self, instance, value):
|
|
# ensure ripple animation completes if disabled gets set to True
|
|
if value:
|
|
self.ripple_fade()
|
|
return super(TouchRippleButtonBehavior, self).on_disabled(
|
|
instance, value)
|
|
|
|
def on_press(self):
|
|
pass
|
|
|
|
def on_release(self):
|
|
pass
|