Skip to content

Commit

Permalink
(uix/scrollview): Implement material design specifications for `MDS…
Browse files Browse the repository at this point in the history
…crollView`
  • Loading branch information
T-Dynamos committed Feb 28, 2024
1 parent 4b8f456 commit 4fe3d93
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 9 deletions.
6 changes: 3 additions & 3 deletions kivymd/_version.py
Original file line number Diff line number Diff line change
@@ -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"
150 changes: 144 additions & 6 deletions kivymd/uix/scrollview.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,135 @@

__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 / (1/3)
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 view instance
scroll_scale = None # Scale instruction instance

scale_axis = "y" # axis of effect
last_touch_pos = None # used to calculate distance

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 is_top_or_bottom(self):
return getattr(self.scroll_view, "scroll_" + self.scale_axis) in [1, 0]

_should_absorb = True
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

if self.is_top_or_bottom():
if (
abs(self.velocity) > self.minimum_absorbed_velocity
and self._should_absorb # only first time when reaches top or bottom
):
self.absorb_impact()
self._should_absorb = False
else:
self._should_absorb = True

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):
self.set_scale_origin()
sanitized_velocity = self.clamp(
abs(self.velocity), 1, self.maximum_velocity
)
# Approx implementation
# Buggy statement for kivy
# math.min(_stretchIntensity + (_flingFriction / velocity), 1.0);
new_scale = 1 + min(
(sanitized_velocity / 2e5),
1/3,
)
init_anim = Animation(
**{self.scale_axis: new_scale},
d=(sanitized_velocity * 4) / 1e6,
)
init_anim.bind(on_complete=self.reset_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 self.is_top_or_bottom()
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)
print(new_scale)

def reset_scale(self, *arg):
if not self.scroll_scale:
return
_scale = getattr(self.scroll_scale, self.scale_axis)
if _scale > 1:
anim = Animation(
**{self.scale_axis: 1},
d=(_scale - 1) * self.duration_normailzer,
)
anim.start(self.scroll_scale)


class MDScrollView(DeclarativeBehavior, BackgroundColorBehavior, ScrollView):
Expand All @@ -62,6 +176,30 @@ 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 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.effect_x.reset_scale()
self.effect_y.reset_scale()
super().on_touch_up(touch)
34 changes: 34 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 4fe3d93

Please sign in to comment.