mirror of
https://github.com/markqvist/Sideband.git
synced 2024-12-28 17:09:34 -05:00
216 lines
7.0 KiB
Python
216 lines
7.0 KiB
Python
"""
|
|
Effects/StiffScrollEffect
|
|
=========================
|
|
|
|
An Effect to be used with ScrollView to prevent scrolling beyond
|
|
the bounds, but politely.
|
|
|
|
A ScrollView constructed with StiffScrollEffect,
|
|
eg. ScrollView(effect_cls=StiffScrollEffect), will get harder to
|
|
scroll as you get nearer to its edges. You can scroll all the way to
|
|
the edge if you want to, but it will take more finger-movement than
|
|
usual.
|
|
|
|
Unlike DampedScrollEffect, it is impossible to overscroll with
|
|
StiffScrollEffect. That means you cannot push the contents of the
|
|
ScrollView far enough to see what's beneath them. This is appropriate
|
|
if the ScrollView contains, eg., a background image, like a desktop
|
|
wallpaper. Overscrolling may give the impression that there is some
|
|
reason to overscroll, even if just to take a peek beneath, and that
|
|
impression may be misleading.
|
|
|
|
StiffScrollEffect was written by Zachary Spector. His other stuff is at:
|
|
https://github.com/LogicalDash/
|
|
He can be reached, and possibly hired, at:
|
|
zacharyspector@gmail.com
|
|
|
|
"""
|
|
|
|
from time import time
|
|
|
|
from kivy.animation import AnimationTransition
|
|
from kivy.effects.kinetic import KineticEffect
|
|
from kivy.properties import NumericProperty, ObjectProperty
|
|
from kivy.uix.widget import Widget
|
|
|
|
|
|
class StiffScrollEffect(KineticEffect):
|
|
drag_threshold = NumericProperty("20sp")
|
|
"""Minimum distance to travel before the movement is considered as a
|
|
drag.
|
|
|
|
:attr:`drag_threshold` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `'20sp'`.
|
|
"""
|
|
|
|
min = NumericProperty(0)
|
|
"""Minimum boundary to stop the scrolling at.
|
|
|
|
:attr:`min` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0`.
|
|
"""
|
|
|
|
max = NumericProperty(0)
|
|
"""Maximum boundary to stop the scrolling at.
|
|
|
|
:attr:`max` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0`.
|
|
"""
|
|
|
|
max_friction = NumericProperty(1)
|
|
"""How hard should it be to scroll, at the worst?
|
|
|
|
:attr:`max_friction` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `1`.
|
|
"""
|
|
|
|
body = NumericProperty(0.7)
|
|
"""Proportion of the range in which you can scroll unimpeded.
|
|
|
|
:attr:`body` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0.7`.
|
|
"""
|
|
|
|
scroll = NumericProperty(0.0)
|
|
"""Computed value for scrolling
|
|
|
|
:attr:`scroll` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0.0`.
|
|
"""
|
|
|
|
transition_min = ObjectProperty(AnimationTransition.in_cubic)
|
|
"""The AnimationTransition function to use when adjusting the friction
|
|
near the minimum end of the effect.
|
|
|
|
:attr:`transition_min` is an :class:`~kivy.properties.ObjectProperty`
|
|
and defaults to :class:`kivy.animation.AnimationTransition`.
|
|
"""
|
|
|
|
transition_max = ObjectProperty(AnimationTransition.in_cubic)
|
|
"""The AnimationTransition function to use when adjusting the friction
|
|
near the maximum end of the effect.
|
|
|
|
:attr:`transition_max` is an :class:`~kivy.properties.ObjectProperty`
|
|
and defaults to :class:`kivy.animation.AnimationTransition`.
|
|
"""
|
|
|
|
target_widget = ObjectProperty(None, allownone=True, baseclass=Widget)
|
|
"""The widget to apply the effect to.
|
|
|
|
:attr:`target_widget` is an :class:`~kivy.properties.ObjectProperty`
|
|
and defaults to ``None``.
|
|
"""
|
|
|
|
displacement = NumericProperty(0)
|
|
"""The absolute distance moved in either direction.
|
|
|
|
:attr:`displacement` is an :class:`~kivy.properties.NumericProperty`
|
|
and defaults to `0`.
|
|
"""
|
|
|
|
def __init__(self, **kwargs):
|
|
"""Set ``self.base_friction`` to the value of ``self.friction`` just
|
|
after instantiation, so that I can reset to that value later.
|
|
"""
|
|
|
|
super().__init__(**kwargs)
|
|
self.base_friction = self.friction
|
|
|
|
def update_velocity(self, dt):
|
|
"""Before actually updating my velocity, meddle with ``self.friction``
|
|
to make it appropriate to where I'm at, currently.
|
|
"""
|
|
|
|
hard_min = self.min
|
|
hard_max = self.max
|
|
if hard_min > hard_max:
|
|
hard_min, hard_max = hard_max, hard_min
|
|
|
|
margin = (1.0 - self.body) * (hard_max - hard_min)
|
|
soft_min = hard_min + margin
|
|
soft_max = hard_max - margin
|
|
|
|
if self.value < soft_min:
|
|
try:
|
|
prop = (soft_min - self.value) / (soft_min - hard_min)
|
|
self.friction = self.base_friction + abs(
|
|
self.max_friction - self.base_friction
|
|
) * self.transition_min(prop)
|
|
except ZeroDivisionError:
|
|
pass
|
|
elif self.value > soft_max:
|
|
try:
|
|
# normalize how far past soft_max I've gone as a
|
|
# proportion of the distance between soft_max and hard_max
|
|
prop = (self.value - soft_max) / (hard_max - soft_max)
|
|
self.friction = self.base_friction + abs(
|
|
self.max_friction - self.base_friction
|
|
) * self.transition_min(prop)
|
|
except ZeroDivisionError:
|
|
pass
|
|
else:
|
|
self.friction = self.base_friction
|
|
|
|
return super().update_velocity(dt)
|
|
|
|
def on_value(self, *args):
|
|
"""Prevent moving beyond my bounds, and update ``self.scroll``"""
|
|
|
|
if self.value > self.min:
|
|
self.velocity = 0
|
|
self.scroll = self.min
|
|
elif self.value < self.max:
|
|
self.velocity = 0
|
|
self.scroll = self.max
|
|
else:
|
|
self.scroll = self.value
|
|
|
|
def start(self, val, t=None):
|
|
"""Start movement with ``self.friction`` = ``self.base_friction``"""
|
|
|
|
self.is_manual = True
|
|
t = t or time()
|
|
self.velocity = self.displacement = 0
|
|
self.friction = self.base_friction
|
|
self.history = [(t, val)]
|
|
|
|
def update(self, val, t=None):
|
|
"""Reduce the impact of whatever change has been made to me, in
|
|
proportion with my current friction.
|
|
"""
|
|
|
|
t = t or time()
|
|
hard_min = self.min
|
|
hard_max = self.max
|
|
if hard_min > hard_max:
|
|
hard_min, hard_max = hard_max, hard_min
|
|
|
|
gamut = hard_max - hard_min
|
|
margin = (1.0 - self.body) * gamut
|
|
soft_min = hard_min + margin
|
|
soft_max = hard_max - margin
|
|
distance = val - self.history[-1][1]
|
|
reach = distance + self.value
|
|
|
|
if (distance < 0 and reach < soft_min) or (
|
|
distance > 0 and soft_max < reach
|
|
):
|
|
distance -= distance * self.friction
|
|
self.apply_distance(distance)
|
|
self.history.append((t, val))
|
|
|
|
if len(self.history) > self.max_history:
|
|
self.history.pop(0)
|
|
self.displacement += abs(distance)
|
|
self.trigger_velocity_update()
|
|
|
|
def stop(self, val, t=None):
|
|
"""Work out whether I've been flung."""
|
|
|
|
self.is_manual = False
|
|
self.displacement += abs(val - self.history[-1][1])
|
|
if self.displacement <= self.drag_threshold:
|
|
self.velocity = 0
|
|
|
|
return super().stop(val, t)
|