338 lines
10 KiB
Python
338 lines
10 KiB
Python
"""
|
|
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)
|