252 lines
7.9 KiB
Python
252 lines
7.9 KiB
Python
|
"""
|
||
|
Effects/RouletteScrollEffect
|
||
|
============================
|
||
|
|
||
|
This is a subclass of :class:`kivy.effects.ScrollEffect` that simulates the
|
||
|
motion of a roulette, or a notched wheel (think Wheel of Fortune). It is
|
||
|
primarily designed for emulating the effect of the iOS and android date pickers.
|
||
|
|
||
|
Usage
|
||
|
-----
|
||
|
|
||
|
Here's an example of using :class:`RouletteScrollEffect` for a
|
||
|
:class:`kivy.uix.scrollview.ScrollView`:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from kivy.uix.gridlayout import GridLayout
|
||
|
from kivy.uix.button import Button
|
||
|
from kivy.uix.scrollview import ScrollView
|
||
|
|
||
|
# Preparing a `GridLayout` inside a `ScrollView`.
|
||
|
layout = GridLayout(cols=1, padding=10, size_hint=(None, None), width=500)
|
||
|
layout.bind(minimum_height=layout.setter('height'))
|
||
|
|
||
|
for i in range(30):
|
||
|
btn = Button(text=str(i), size=(480, 40), size_hint=(None, None))
|
||
|
layout.add_widget(btn)
|
||
|
|
||
|
root = ScrollView(
|
||
|
size_hint=(None, None),
|
||
|
size=(500, 320),
|
||
|
pos_hint={'center_x': .5, 'center_y': .5},
|
||
|
do_scroll_x=False,
|
||
|
)
|
||
|
root.add_widget(layout)
|
||
|
|
||
|
# Preparation complete. Now add the new scroll effect.
|
||
|
root.effect_y = RouletteScrollEffect(anchor=20, interval=40)
|
||
|
runTouchApp(root)
|
||
|
|
||
|
Here the :class:`ScrollView` scrolls through a series of buttons with height
|
||
|
40. We then attached a :class:`RouletteScrollEffect` with interval 40,
|
||
|
corresponding to the button heights. This allows the scrolling to stop at
|
||
|
the same offset no matter where it stops. The :attr:`RouletteScrollEffect.anchor`
|
||
|
adjusts this offset.
|
||
|
|
||
|
Customizations
|
||
|
--------------
|
||
|
|
||
|
Other settings that can be played with include:
|
||
|
|
||
|
:attr:`RouletteScrollEffect.pull_duration`,
|
||
|
:attr:`RouletteScrollEffect.coasting_alpha`,
|
||
|
:attr:`RouletteScrollEffect.pull_back_velocity`, and
|
||
|
:attr:`RouletteScrollEffect.terminal_velocity`.
|
||
|
|
||
|
See their module documentations for details.
|
||
|
|
||
|
:class:`RouletteScrollEffect` has one event ``on_coasted_to_stop`` that
|
||
|
is fired when the roulette stops, "making a selection". It can be listened to
|
||
|
for handling or cleaning up choice making.
|
||
|
"""
|
||
|
|
||
|
from math import ceil, exp, floor
|
||
|
|
||
|
from kivy.animation import Animation
|
||
|
from kivy.effects.scroll import ScrollEffect
|
||
|
from kivy.properties import AliasProperty, NumericProperty, ObjectProperty
|
||
|
|
||
|
__all_ = ("RouletteScrollEffect",)
|
||
|
|
||
|
|
||
|
class RouletteScrollEffect(ScrollEffect):
|
||
|
"""
|
||
|
This is a subclass of :class:`kivy.effects.ScrollEffect` that simulates the
|
||
|
motion of a roulette, or a notched wheel (think Wheel of Fortune). It is
|
||
|
primarily designed for emulating the effect of the iOS and android date pickers.
|
||
|
|
||
|
.. versionadded:: 0.104.2
|
||
|
"""
|
||
|
|
||
|
__events__ = ("on_coasted_to_stop",)
|
||
|
|
||
|
drag_threshold = NumericProperty(0)
|
||
|
"""
|
||
|
Overrides :attr:`ScrollEffect.drag_threshold` to abolish drag threshold.
|
||
|
|
||
|
.. note::
|
||
|
If using this with a :class:`Roulette` or other :class:`Tickline`
|
||
|
subclasses, what matters is :attr:`Tickline.drag_threshold`, which
|
||
|
is passed to this attribute in the end.
|
||
|
|
||
|
:attr:`drag_threshold` is an :class:`~kivy.properties.NumericProperty`
|
||
|
and defaults to `0`.
|
||
|
"""
|
||
|
|
||
|
min = NumericProperty(-float("inf"))
|
||
|
max = NumericProperty(float("inf"))
|
||
|
|
||
|
interval = NumericProperty(50)
|
||
|
"""
|
||
|
The interval of the values of the "roulette".
|
||
|
|
||
|
:attr:`interval` is an :class:`~kivy.properties.NumericProperty`
|
||
|
and defaults to `50`.
|
||
|
"""
|
||
|
|
||
|
anchor = NumericProperty(0)
|
||
|
"""
|
||
|
One of the valid stopping values.
|
||
|
|
||
|
:attr:`anchor` is an :class:`~kivy.properties.NumericProperty`
|
||
|
and defaults to `0`.
|
||
|
"""
|
||
|
|
||
|
pull_duration = NumericProperty(0.2)
|
||
|
"""
|
||
|
When movement slows around a stopping value, an animation is used
|
||
|
to pull it toward the nearest value. :attr:`pull_duration` is the duration
|
||
|
used for such an animation.
|
||
|
|
||
|
:attr:`pull_duration` is an :class:`~kivy.properties.NumericProperty`
|
||
|
and defaults to `0.2`.
|
||
|
"""
|
||
|
|
||
|
coasting_alpha = NumericProperty(0.5)
|
||
|
"""
|
||
|
When within :attr:`coasting_alpha` * :attr:`interval` of the
|
||
|
next notch and velocity is below :attr:`terminal_velocity`,
|
||
|
coasting begins and will end on the next notch.
|
||
|
|
||
|
:attr:`coasting_alpha` is an :class:`~kivy.properties.NumericProperty`
|
||
|
and defaults to `0.5`.
|
||
|
"""
|
||
|
|
||
|
pull_back_velocity = NumericProperty("50sp")
|
||
|
"""
|
||
|
The velocity below which the scroll value will be drawn to the
|
||
|
*nearest* notch instead of the *next* notch in the direction travelled.
|
||
|
|
||
|
:attr:`pull_back_velocity` is an :class:`~kivy.properties.NumericProperty`
|
||
|
and defaults to `50sp`.
|
||
|
"""
|
||
|
|
||
|
_anim = ObjectProperty(None)
|
||
|
|
||
|
def get_term_vel(self):
|
||
|
return (
|
||
|
exp(self.friction)
|
||
|
* self.interval
|
||
|
* self.coasting_alpha
|
||
|
/ self.pull_duration
|
||
|
)
|
||
|
|
||
|
def set_term_vel(self, val):
|
||
|
self.pull_duration = (
|
||
|
exp(self.friction) * self.interval * self.coasting_alpha / val
|
||
|
)
|
||
|
|
||
|
terminal_velocity = AliasProperty(
|
||
|
get_term_vel,
|
||
|
set_term_vel,
|
||
|
bind=["interval", "coasting_alpha", "pull_duration", "friction"],
|
||
|
cache=True,
|
||
|
)
|
||
|
"""
|
||
|
If velocity falls between :attr:`pull_back_velocity` and
|
||
|
:attr:`terminal velocity` then the movement will start to coast
|
||
|
to the next coming stopping value.
|
||
|
|
||
|
:attr:`terminal_velocity` is computed from a set formula given
|
||
|
:attr:`interval`, :attr:`coasting_alpha`, :attr:`pull_duration`,
|
||
|
and :attr:`friction`. Setting :attr:`terminal_velocity` has the
|
||
|
effect of setting :attr:`pull_duration`.
|
||
|
"""
|
||
|
|
||
|
def start(self, val, t=None):
|
||
|
if self._anim:
|
||
|
self._anim.stop(self)
|
||
|
return ScrollEffect.start(self, val, t=t)
|
||
|
|
||
|
def on_notch(self, *args):
|
||
|
return (self.scroll - self.anchor) % self.interval == 0
|
||
|
|
||
|
def nearest_notch(self, *args):
|
||
|
interval = float(self.interval)
|
||
|
anchor = self.anchor
|
||
|
n = round((self.scroll - anchor) / interval)
|
||
|
return anchor + n * interval
|
||
|
|
||
|
def next_notch(self, *args):
|
||
|
interval = float(self.interval)
|
||
|
anchor = self.anchor
|
||
|
round_ = ceil if self.velocity > 0 else floor
|
||
|
n = round_((self.scroll - anchor) / interval)
|
||
|
return anchor + n * interval
|
||
|
|
||
|
def near_notch(self, d=0.01):
|
||
|
nearest = self.nearest_notch()
|
||
|
if abs((nearest - self.scroll) / self.interval) % 1 < d:
|
||
|
return nearest
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def near_next_notch(self, d=None):
|
||
|
d = d or self.coasting_alpha
|
||
|
next_ = self.next_notch()
|
||
|
if abs((next_ - self.scroll) / self.interval) % 1 < d:
|
||
|
return next_
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def update_velocity(self, dt):
|
||
|
if self.is_manual:
|
||
|
return
|
||
|
velocity = self.velocity
|
||
|
t_velocity = self.terminal_velocity
|
||
|
next_ = self.near_next_notch()
|
||
|
pull_back_velocity = self.pull_back_velocity
|
||
|
if pull_back_velocity < abs(velocity) < t_velocity and next_:
|
||
|
duration = abs((next_ - self.scroll) / self.velocity)
|
||
|
anim = Animation(
|
||
|
scroll=next_,
|
||
|
duration=duration,
|
||
|
)
|
||
|
self._anim = anim
|
||
|
anim.on_complete = self._coasted_to_stop
|
||
|
anim.start(self)
|
||
|
return
|
||
|
if abs(velocity) < pull_back_velocity and not self.on_notch():
|
||
|
anim = Animation(
|
||
|
scroll=self.nearest_notch(),
|
||
|
duration=self.pull_duration,
|
||
|
t="in_out_circ",
|
||
|
)
|
||
|
self._anim = anim
|
||
|
anim.on_complete = self._coasted_to_stop
|
||
|
anim.start(self)
|
||
|
else:
|
||
|
self.velocity -= self.velocity * self.friction
|
||
|
self.apply_distance(self.velocity * dt)
|
||
|
self.trigger_velocity_update()
|
||
|
|
||
|
def on_coasted_to_stop(self, *args):
|
||
|
"""
|
||
|
This event fires when the roulette has stopped, `making a selection`.
|
||
|
"""
|
||
|
|
||
|
def _coasted_to_stop(self, *args):
|
||
|
self.velocity = 0
|
||
|
self.dispatch("on_coasted_to_stop")
|