""" 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")