diff --git a/kivymd/_version.py b/kivymd/_version.py index d503c8e5c..5e2b80cd2 100644 --- a/kivymd/_version.py +++ b/kivymd/_version.py @@ -1,5 +1,5 @@ release = False __version__ = "2.0.1.dev0" -__hash__ = "43a2ce216bdf99224356e6db4106253afbe1cecb" -__short_hash__ = "43a2ce2" -__date__ = "2024-01-21" +__hash__ = "f7bde69707ac708a758a02d89f14997ee468d1ee" +__short_hash__ = "f7bde69" +__date__ = "2024-02-27" diff --git a/kivymd/uix/scrollview.py b/kivymd/uix/scrollview.py index cc76d5300..52bacc405 100644 --- a/kivymd/uix/scrollview.py +++ b/kivymd/uix/scrollview.py @@ -34,21 +34,113 @@ __all__ = ("MDScrollView",) -from kivy.effects.dampedscroll import DampedScrollEffect +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(DampedScrollEffect): +class StretchOverScrollStencil(ScrollEffect): """ - This class is simply based on DampedScrollEffect. + Material Design overscorll effect. If you need any documentation please look at :class:`~kivy.effects.dampedscrolleffect`. """ - def on_overscroll(self, instance, overscroll: int | float) -> None: - ... + # Android constants + minimum_absorbed_velocity = 0 + maximum_velocity = 10000 + + stretch_intensity = 0.016 + exponential_scalar = math.e / 0.33 + scroll_friction = 0.015 + + # Velocity normilzer + # Got from error and trial + normlizer = 2e5 + + scroll_view = None + scroll_scale = None + + scale_axis = "y" + + 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 = ( + getattr(self.scroll_scale, self.scale_axis) + + sanitized_velocity / self.normlizer + ) + init_anim = Animation( + **{self.scale_axis: new_scale}, + d=(sanitized_velocity * 2) / 1e6, + t="in_out_circ", + ) + init_anim.bind( + on_complete=lambda *_: Animation( + **{self.scale_axis: 1}, d=0.2, t="in_out_circ" + ).start(self.scroll_scale) + ) + init_anim.start(self.scroll_scale) + + def convert_overscroll(self, touch): + if ( + self.scroll_view + and self.target_widget.collide_point(*touch.pos) + and getattr(self.scroll_view, "scroll_" + self.scale_axis) in [1, 0] + and self.velocity == 0 + and getattr(self.scroll_view, "do_scroll_" + self.scale_axis) + ): + self.set_scale_origin() + distance = abs( + self.scroll_view.height + - touch.pos[-1 if self.scale_axis == "y" else 1] + ) + linear_intensity = self.stretch_intensity * distance + exponential_intensity = self.stretch_intensity * ( + 1 - math.exp(-distance * self.exponential_scalar) + ) + setattr( + self.scroll_scale, + self.scale_axis, + min( + getattr(self.scroll_scale, self.scale_axis) + + (linear_intensity / 1e4 + exponential_intensity / 1e4), + 1.078, + ), + ) + + def apply_transform(self): + if getattr(self.scroll_view, "scroll_" + self.scale_axis) in [1, 0]: + self.set_scale_origin() + if abs(self.velocity) > self.minimum_absorbed_velocity: + self.absorb_impact() class MDScrollView(DeclarativeBehavior, BackgroundColorBehavior, ScrollView): @@ -62,6 +154,35 @@ class MDScrollView(DeclarativeBehavior, BackgroundColorBehavior, ScrollView): classes documentation. """ + _internal_scale = None | Scale + def __init__(self, *args, **kwargs): + self.effect_cls = StretchOverScrollStencil + super().__init__(*args, **kwargs) - self.effect_cls = MDScrollViewEffect + with self.canvas.before: + PushMatrix() + self._internal_scale = Scale() + with self.canvas.after: + PopMatrix() + self.effect_y.scale_axis = "y" + self.effect_x.scale_axis = "x" + + 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 + ) + if self._internal_scale.x > 1: + anim = Animation(x=1, d=0.1, t="in_out_circ").start( + self._internal_scale + ) + + def on_touch_move(self, touch): + self.effect_x.convert_overscroll(touch) + self.effect_y.convert_overscroll(touch) + super().on_touch_move(touch) + + def on_touch_up(self, touch): + self._normalize_scale() + super().on_touch_up(touch) diff --git a/main.py b/main.py new file mode 100644 index 000000000..cbadb0f72 --- /dev/null +++ b/main.py @@ -0,0 +1,34 @@ +# ./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() + scroll_view.do_scroll_x = False + 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, 25): + self.main_box.add_widget( + Button( + text=f"Item {i}", + size_hint_y=None, + #size_hint_x=None, + height=dp(50), + #width=dp(400), + background_color=[1 if i % 2 == 0 else 0]*3 + [1], + ) + ) + +Example().run()