""" Components/ScrollView ===================== .. versionadded:: 1.0.0 :class:`~kivy.uix.scrollview.ScrollView` class equivalent. It implements Material Design's overscorll effect and simplifies working with some widget properties. For example: ScrollView ---------- .. code-block:: kv ScrollView: canvas: Color: rgba: app.theme_cls.primaryColor Rectangle: pos: self.pos size: self.size MDScrollView ------------ .. code-block:: kv MDScrollView: md_bg_color: app.theme_cls.primaryColor The stretching effect --------------------- .. code-block:: python import os import sys from kivy.core.window import Window from kivy import __version__ as kv__version__ from kivy.lang import Builder from kivy.metrics import dp from kivymd.app import MDApp from kivymd import __version__ from kivymd.uix.list import ( MDListItem, MDListItemHeadlineText, MDListItemSupportingText, MDListItemLeadingIcon, ) from materialyoucolor import __version__ as mc__version__ from examples.common_app import CommonApp MAIN_KV = ''' MDScreen: md_bg_color: app.theme_cls.backgroundColor MDScrollView: do_scroll_x: False MDBoxLayout: id: main_scroll orientation: "vertical" adaptive_height: True MDBoxLayout: adaptive_height: True MDLabel: theme_font_size: "Custom" text: "OS Info" font_size: "55sp" adaptive_height: True padding: "10dp", "20dp", 0, 0 MDIconButton: icon: "menu" on_release: app.open_menu(self) pos_hint: {"center_y": .5} ''' class Example(MDApp, CommonApp): def build(self): self.theme_cls.theme_style = "Dark" return Builder.load_string(MAIN_KV) def on_start(self): info = { "Name": [ os.name, ( "microsoft" if os.name == "nt" else ("linux" if os.uname()[0] != "Darwin" else "apple") ), ], "Architecture": [os.uname().machine, "memory"], "Hostname": [os.uname().nodename, "account"], "Python Version": ["v" + sys.version, "language-python"], "Kivy Version": ["v" + kv__version__, "alpha-k-circle-outline"], "KivyMD Version": ["v" + __version__, "material-design"], "MaterialYouColor Version": ["v" + mc__version__, "invert-colors"], "Pillow Version": ["Unknown", "image"], "Working Directory": [os.getcwd(), "folder"], "Home Directory": [os.path.expanduser("~"), "folder-account"], "Environment Variables": [os.environ, "code-json"], } try: from PIL import __version__ as pil__version_ info["Pillow Version"] = ["v" + pil__version_, "image"] except Exception: pass for info_item in info: self.root.ids.main_scroll.add_widget( MDListItem( MDListItemLeadingIcon( icon=info[info_item][1], ), MDListItemHeadlineText( text=info_item, ), MDListItemSupportingText( text=str(info[info_item][0]), ), pos_hint={"center_x": .5, "center_y": .5}, ) ) Window.size = [dp(350), dp(600)] Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/stretch_over_scroll_stencil.gif :align: center """ from __future__ import annotations __all__ = ("MDScrollView", "StretchOverScrollStencil") import math from kivy.animation import Animation from kivy.effects.scroll import ScrollEffect from kivy.graphics import Color, PopMatrix, PushMatrix, Scale from kivy.uix.scrollview import ScrollView from kivymd.uix.behaviors import BackgroundColorBehavior, DeclarativeBehavior class StretchOverScrollStencil(ScrollEffect): """ Stretches the view on overscroll and absorbs velocity at start and end to convert to stretch. .. note:: This effect only works with :class:`kivymd.uix.scrollview.MDScrollView`. If you need any documentation please look at :class:`~kivy.effects.dampedscrolleffect`. """ # Android constants. minimum_absorbed_velocity = 0 maximum_velocity = 10000 stretch_intensity = 0.016 exponential_scalar = math.e / (1 / 3) scroll_friction = 0.015 # Used in `absorb_impact` but for now it's not compatible with kivy so we # using are approx value. # fling_friction = 1.01 approx_normailzer = 2e5 # Duration to normalize scale # when touch up is received 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 get_hw(self): return "height" if self.scale_axis == "y" else "width" def set_scale_origin(self): # Check if target size is small than scrollview # if yes don't stretch scroll view. if getattr(self.target_widget, self.get_hw()) < getattr( self.scroll_view, self.get_hw() ): return False 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, ] return True def absorb_impact(self): self.set_scale_origin() sanitized_velocity = self.clamp( abs(self.velocity), 1, self.maximum_velocity ) # Approx implementation. new_scale = 1 + min( (sanitized_velocity / self.approx_normailzer), 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.scroll_view.collide_point(*touch.pos) and self.is_top_or_bottom() and getattr(self.scroll_view, "do_scroll_" + self.scale_axis) and self.velocity == 0 and self.set_scale_origin() # sets stretch direction ): # 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 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=0.2, ) anim.start(self.scroll_scale) class MDScrollView(DeclarativeBehavior, BackgroundColorBehavior, ScrollView): """ An approximate implementation to Material Design's overscorll effect. For more information, see in the :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and :class:`~kivy.uix.scrollview.ScrollView` classes documentation. """ _internal_scale = None def __init__(self, *args, **kwargs): self.effect_cls = StretchOverScrollStencil super().__init__(*args, **kwargs) with self.canvas.before: Color(rgba=self.md_bg_color) 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)