diff --git a/kivymd/_version.py b/kivymd/_version.py index d503c8e5c8..5e2b80cd21 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 cc76d53005..d91c820af9 100644 --- a/kivymd/uix/scrollview.py +++ b/kivymd/uix/scrollview.py @@ -34,21 +34,120 @@ __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 + fling_friction = 1.01 + + # Duration to normalize scale + # when touch up is recieved and view is stretched + duration_normailzer = 10 + + scroll_view = None + scroll_scale = None + + scale_axis = "y" + last_touch_pos = 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): + # TODO: Fix this function + return + self.set_scale_origin() + sanitized_velocity = self.clamp( + abs(self.velocity), 1, self.maximum_velocity + ) + new_scale = 1 + min( + self.stretch_intensity + + (self.fling_friction / sanitized_velocity), + 1 + ) + init_anim = Animation( + **{self.scale_axis: new_scale}, + d=(sanitized_velocity * 4) / 1e6, + t="in_out_circ", + ) + init_anim.bind( + on_complete=lambda *_: Animation( + **{self.scale_axis: 1}, d=0.4, t="in_out_circ" + ).start(self.scroll_scale) + ) + init_anim.start(self.scroll_scale) + + def get_component(self, pos): + return pos[-1 if self.scale_axis == "y" else 1] + + 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 getattr(self.scroll_view, "do_scroll_" + self.scale_axis) + and self.velocity == 0 + ): + # sets stretch direction + self.set_scale_origin() + # Distance travelled by touch divided by size of scrollview + distance = ( + abs( + self.get_component(touch.pos) + - self.get_component(self.last_touch_pos) + ) + / self.scroll_view.height + ) + # constant scale due to distance + linear_intensity = self.stretch_intensity * distance + # Far the touch -> less it stretches + exponential_intensity = self.stretch_intensity * ( + 1 - math.exp(-distance * self.exponential_scalar) + ) + new_scale = 1 + exponential_intensity + linear_intensity + setattr(self.scroll_scale, self.scale_axis, new_scale) + + def apply_transform(self): + if getattr(self.scroll_view, "scroll_" + self.scale_axis) in [1, 0]: + if abs(self.velocity) > self.minimum_absorbed_velocity: + self.absorb_impact() class MDScrollView(DeclarativeBehavior, BackgroundColorBehavior, ScrollView): @@ -62,6 +161,43 @@ 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=(self._internal_scale.y - 1) + * self.effect_y.duration_normailzer, + ).start(self._internal_scale) + if self._internal_scale.x > 1: + anim = Animation( + x=1, + d=(self._internal_scale.y - 1) + * self.effect_x.duration_normailzer, + ).start(self._internal_scale) + + def on_touch_down(self, touch): + self.effect_x.last_touch_pos = touch.pos + self.effect_y.last_touch_pos = touch.pos + super().on_touch_down(touch) + + 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 0000000000..cbadb0f721 --- /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()