diff --git a/kivymd/uix/scrollview.py b/kivymd/uix/scrollview.py index 80a48f9cb..d46e97e67 100644 --- a/kivymd/uix/scrollview.py +++ b/kivymd/uix/scrollview.py @@ -34,21 +34,93 @@ __all__ = ("MDScrollView",) +import math from kivy.effects.scroll import ScrollEffect from kivy.uix.scrollview import ScrollView +from kivy.properties import NumericProperty, ListProperty +from kivy.graphics import Scale, PushMatrix, PopMatrix +from kivy.animation import Animation from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior -class MDScrollViewEffect(ScrollEffect): +class StretchOverScrollStencil(ScrollEffect): """ Material Design overscorll effect. If you need any documentation please look at :class:`~kivy.effects.dampedscrolleffect`. """ - MAXIMUM_STRETCH = 1.07 - + # Android constants + minimum_absorbed_velocity = 100 + maximum_velocity = 10000 + stretch_intensity = 0.016 + exponential_scalar = math.e / 0.33 + scroll_friction = 0.015 + + # Velocity normilzer + normlizer = 4.5e5 + + scroll_view = None + scroll_scale = None + + def clamp(self, value, min_val=0, max_val=0): + return min(max(value, min_val), max_val) + + def __init__(self, *arg, **kwargs): + super().__init__(*arg, **kwargs) + self.friction = self.scroll_friction + + def on_value(self, stencil, scroll_distance): + super().on_value(stencil, scroll_distance) + if self.target_widget: + if not all([self.scroll_view, self.scroll_scale]): + self.scroll_view = self.target_widget.parent + self.scroll_scale = self.scroll_view._internal_scale + self.apply_transform() + + def set_scale_origin(self): + self.scroll_scale.origin = [ + 0 if self.scroll_view.scroll_x <= 0.5 else self.scroll_view.width, + 0 if self.scroll_view.scroll_y <= 0.5 else self.scroll_view.height, + ] + + def absorb_impact(self): + sanitized_velocity = self.clamp( + abs(self.velocity), 1, self.maximum_velocity + ) + new_scale = self.scroll_scale.y + sanitized_velocity / (self.normlizer) + if new_scale <= 1: + return + + init_anim = Animation( + y=new_scale, + d=(sanitized_velocity * 2) / 1e6, + t="in_out_circ", + ) + init_anim.bind( + on_complete=lambda *_: Animation(y=1, d=0.2, t="in_out_circ").start( + self.scroll_scale + ) + ) + init_anim.start(self.scroll_scale) + + def on_overscroll(self, widget, value): + if self.scroll_scale and abs(value) > 0.0 and self.velocity == 0: + self.scroll_scale.y = 1 + self.stretch_intensity * ( + 1 - math.exp(-abs(value) * self.exponential_scalar) + ) + + def apply_transform(self): + if self.scroll_view.scroll_y in [1, 0]: + self.set_scale_origin() + if abs(self.velocity) > self.minimum_absorbed_velocity: + self.absorb_impact() + + +# class StretchOverScrollShader(ScrollEffect): +# TODO: implement it if required + class MDScrollView(DeclarativeBehavior, BackgroundColorBehavior, ScrollView): """ @@ -61,6 +133,27 @@ class MDScrollView(DeclarativeBehavior, BackgroundColorBehavior, ScrollView): classes documentation. """ + _internal_scale = None | Scale + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.effect_cls = MDScrollViewEffect + with self.canvas.before: + PushMatrix() + self._internal_scale = Scale() + with self.canvas.after: + PopMatrix() + self.effect_cls = StretchOverScrollStencil + + def _normalize_scale(self): + if self._internal_scale.y > 1: + anim = Animation(y=1, d=0.1, t="in_out_circ").start( + self._internal_scale + ) + + def on_touch_up(self, touch): + self._normalize_scale() + super().on_touch_up(touch) + + def on_touch_move(self, touch): + self._normalize_scale() + super().on_touch_move(touch) diff --git a/main.py b/main.py new file mode 100644 index 000000000..23b1c5943 --- /dev/null +++ b/main.py @@ -0,0 +1,31 @@ +# ./packages/flutter/lib/src/widgets/overscroll_indicator.dart +# 820 +from kivy.metrics import dp +from kivy.uix.button import Button + +from kivymd.app import MDApp +from kivymd.uix.scrollview import MDScrollView +from kivymd.uix.boxlayout import MDBoxLayout + + +class Example(MDApp): + def build(self): + scroll_view = MDScrollView() + self.main_box = MDBoxLayout(orientation="vertical") + self.main_box.adaptive_height = True + scroll_view.add_widget(self.main_box) + return scroll_view + + def on_start(self): + super().on_start() + for i in range(1, 50): + self.main_box.add_widget( + Button( + text=f"Item {i}", + size_hint_y=None, + height=dp(50), + background_color=[1 if i % 2 == 0 else 0]*3 + [1], + ) + ) + +Example().run()