Sideband/sbapp/kivymd/uix/sliverappbar/sliverappbar.py

498 lines
14 KiB
Python

"""
Components/SliverAppbar
=======================
.. versionadded:: 1.0.0
.. rubric:: MDSliverAppbar is a Material Design widget in KivyMD which gives
scrollable or collapsible
`MDTopAppBar <https://kivymd.readthedocs.io/en/latest/components/toolbar/>`_
.. note:: This widget is a modification of the
`silverappbar.py <https://github.com/kivymd-extensions/akivymd/blob/main/kivymd_extensions/akivymd/uix/silverappbar.py>`_ module.
Usage
-----
.. code-block:: kv
MDScreen:
MDSliverAppbar:
MDSliverAppbarHeader:
# Custom content.
...
# Custom list.
MDSliverAppbarContent:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-usage.png
:align: center
Example
-------
.. code-block:: python
from kivy.lang.builder import Builder
from kivymd.app import MDApp
from kivymd.uix.card import MDCard
KV = '''
<CardItem>
size_hint_y: None
height: "86dp"
padding: "4dp"
radius: 12
FitImage:
source: "avatar.jpg"
radius: root.radius
size_hint_x: None
width: root.height
MDBoxLayout:
orientation: "vertical"
adaptive_height: True
spacing: "6dp"
padding: "12dp", 0, 0, 0
pos_hint: {"center_y": .5}
MDLabel:
text: "Title text"
font_style: "H5"
bold: True
adaptive_height: True
MDLabel:
text: "Subtitle text"
theme_text_color: "Hint"
adaptive_height: True
MDScreen:
MDSliverAppbar:
background_color: "2d4a50"
MDSliverAppbarHeader:
MDRelativeLayout:
FitImage:
source: "bg.jpg"
MDSliverAppbarContent:
id: content
orientation: "vertical"
padding: "12dp"
spacing: "12dp"
adaptive_height: True
'''
class CardItem(MDCard):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.elevation = 3
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
def on_start(self):
for x in range(10):
self.root.ids.content.add_widget(CardItem())
Example().run()
"""
__all__ = ("MDSliverAppbar", "MDSliverAppbarHeader", "MDSliverAppbarContent")
import os
from typing import Union
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.lang.builder import Builder
from kivy.properties import (
BooleanProperty,
ColorProperty,
NumericProperty,
ObjectProperty,
VariableListProperty,
)
from kivymd import uix_path
from kivymd.theming import ThemableBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.toolbar import MDTopAppBar
with open(
os.path.join(uix_path, "sliverappbar", "sliverappbar.kv"), encoding="utf-8"
) as kv_file:
Builder.load_string(kv_file.read())
class MDSliverAppbarException(Exception):
pass
class MDSliverAppbarContent(ThemableBehavior, MDBoxLayout):
"""Implements a box for a scrollable list of custom items."""
md_bg_color = ColorProperty([0, 0, 0, 0])
"""
See :attr:`~kivymd.uix.sliverappbar.sliverappbar.MDSliverAppbar.background_color`.
:attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `[0, 0, 0, 0]`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
Clock.schedule_once(self.set_bg_color)
def set_bg_color(self, interval: Union[int, float]) -> None:
if self.md_bg_color == [0, 0, 0, 0]:
self.md_bg_color = self.theme_cls.bg_normal
class MDSliverAppbarHeader(MDBoxLayout):
pass
class MDSliverAppbar(MDBoxLayout, ThemableBehavior):
"""
MDSliverAppbar class.
See module documentation for more information.
:Events:
:attr:`on_scroll_content`
Called when the list of custom content is being scrolled.
"""
toolbar_cls = ObjectProperty()
"""
Must be an object of the :class:`~kivymd.uix.toolbar.toolbar.MDTopAppBar' class.
See :class:`~kivymd.uix.toolbar.toolbar.MDTopAppBar` class documentation
for more information.
By default, MDSliverAppbar widget uses the MDTopAppBar class with no
parameters.
.. code-block:: python
from kivy.lang.builder import Builder
from kivymd.uix.card import MDCard
from kivymd.uix.toolbar import MDTopAppBar
KV = '''
#:import SliverToolbar __main__.SliverToolbar
<CardItem>
size_hint_y: None
height: "86dp"
padding: "4dp"
radius: 12
FitImage:
source: "avatar.jpg"
radius: root.radius
size_hint_x: None
width: root.height
MDBoxLayout:
orientation: "vertical"
adaptive_height: True
spacing: "6dp"
padding: "12dp", 0, 0, 0
pos_hint: {"center_y": .5}
MDLabel:
text: "Title text"
font_style: "H5"
bold: True
adaptive_height: True
MDLabel:
text: "Subtitle text"
theme_text_color: "Hint"
adaptive_height: True
MDScreen:
MDSliverAppbar:
background_color: "2d4a50"
toolbar_cls: SliverToolbar()
MDSliverAppbarHeader:
MDRelativeLayout:
FitImage:
source: "bg.jpg"
MDSliverAppbarContent:
id: content
orientation: "vertical"
padding: "12dp"
spacing: "12dp"
adaptive_height: True
'''
class CardItem(MDCard):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.elevation = 3
class SliverToolbar(MDTopAppBar):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.shadow_color = (0, 0, 0, 0)
self.type_height = "medium"
self.headline_text = "Headline medium"
self.left_action_items = [["arrow-left", lambda x: x]]
self.right_action_items = [
["attachment", lambda x: x],
["calendar", lambda x: x],
["dots-vertical", lambda x: x],
]
class Example(MDApp):
def build(self):
self.theme_cls.material_style = "M3"
return Builder.load_string(KV)
def on_start(self):
for x in range(10):
self.root.ids.content.add_widget(CardItem())
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-toolbar-cls.gif
:align: center
:attr:`toolbar_cls` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
background_color = ColorProperty(None)
"""
Background color of toolbar in (r, g, b, a) format.
.. code-block:: kv
MDSliverAppbar:
background_color: "2d4a50"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-background-color.png
:align: center
:attr:`background_color` is an :class:`~kivy.properties.ColorProperty`
and defaults to `None`.
"""
max_height = NumericProperty(Window.height / 2)
"""
Distance from top of screen to start of custom list content.
.. code-block:: kv
MDSliverAppbar:
max_height: "200dp"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-max-height.png
:align: center
:attr:`max_height` is an :class:`~kivy.properties.NumericProperty`
and defaults to `Window.height / 2`.
"""
hide_toolbar = BooleanProperty(True)
"""
Whether to hide the toolbar when scrolling through a list
of custom content.
.. code-block:: kv
MDSliverAppbar:
hide_toolbar: False
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-hide-toolbar.gif
:align: center
MDSliverAppbar:
hide_toolbar: True
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-hide-toolbar-true.gif
:align: center
:attr:`hide_toolbar` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
radius = VariableListProperty([20], length=4)
"""
Box radius for custom item list.
.. code-block:: kv
MDSliverAppbar:
radius: 20
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-radius.png
:align: center
:attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
and defaults to `[20]`.
"""
max_opacity = NumericProperty(1)
"""
Maximum background transparency value for the
:class:`~kivymd.uix.sliverappbar.sliverappbar.MDSliverAppbarHeader` class.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-max-opacity.gif
:align: center
.. code-block:: kv
MDSliverAppbar:
max_opacity: .5
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-max-opacity-05.gif
:align: center
:attr:`max_opacity` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
_opacity = NumericProperty()
_scroll_was_moving = BooleanProperty(False)
_last_scroll_y_pos = 0.0
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.register_event_type("on_scroll_content")
def on_scroll_content(
self,
instance_sliverappbar: object = None,
value: float = 1.0,
direction: str = "up",
):
"""
Called when the list of custom content is being scrolled.
:param instance_sliverappbar: :class:`~MDSliverAppbar`
:param value: see :attr:`~kivy.uix.scrollview.ScrollView.scroll_y`
:param direction: scroll direction: 'up/down'
"""
def on_background_color(
self, instance_sliver_appbar, color_value: list
) -> None:
if self.toolbar_cls:
self.toolbar_cls.md_bg_color = color_value
def on_toolbar_cls(
self, instance_sliver_appbar, instance_toolbar_cls: MDTopAppBar
) -> None:
"""Called when a value is set to the :attr:`toolbar_cls` parameter."""
def on_toolbar_cls(*args):
# If an MDTopAppBar object is already in use, delete it
# before adding a new MDTopAppBar object.
for widget in self.ids.float_box.children:
if issubclass(widget.__class__, MDTopAppBar):
self.ids.float_box.remove_widget(widget)
# Adding a custom MDTopAppBar object.
if issubclass(instance_toolbar_cls.__class__, MDTopAppBar):
instance_toolbar_cls.pos_hint = {"top": 1}
instance_toolbar_cls.elevation = 0
self.ids.float_box.add_widget(instance_toolbar_cls)
else:
raise MDSliverAppbarException(
"The `toolbar_cls` parameter must be an object of the "
"`kivymd.uix.toolbar.MDTopAppBar class`"
)
# Schedule using for declarative style.
# Otherwise get AttributeError exception.
Clock.schedule_once(on_toolbar_cls)
def on_vbar(self) -> None:
if not self.background_color:
self.background_color = self.theme_cls.primary_color
if not self.toolbar_cls:
self.toolbar_cls = self.get_default_toolbar()
scroll_box = self.ids.scroll_box
vbar = self.ids.scroll.vbar
toolbar_percent = (self.toolbar_cls.height / scroll_box.height) * 100
current_percent = (vbar[0] + vbar[1]) * 100
percent_min = (
1 - self.max_height / scroll_box.height
) * 100 + toolbar_percent
if self._scroll_was_moving:
direction = self._get_direction_swipe(self.ids.scroll.scroll_y)
self._last_scroll_y_pos = self.ids.scroll.scroll_y
self.dispatch(
"on_scroll_content", self.ids.scroll.scroll_y, direction
)
if self.hide_toolbar:
if percent_min <= current_percent:
opacity = (current_percent - percent_min) / (100 - percent_min)
self._opacity = self.max_opacity * (1 - opacity)
self.background_color = self.background_color[0:3] + [
1 - opacity
]
self.toolbar_cls._hard_shadow_a = 1 - opacity
self.toolbar_cls._soft_shadow_a = 1 - opacity
else:
self.background_color = self.background_color[0:3] + [1]
def get_default_toolbar(self) -> MDTopAppBar:
"""Called if no value is passed for the toolbar_cls attribute."""
return MDTopAppBar(
pos_hint={"top": 1}, md_bg_color=self.background_color
)
def add_widget(self, widget, index=0, canvas=None):
if issubclass(widget.__class__, MDSliverAppbarContent):
Clock.schedule_once(lambda x: self._set_radius(widget))
self.ids.scroll_box.add_widget(widget)
elif issubclass(widget.__class__, MDSliverAppbarHeader):
self.ids.header.add_widget(widget)
else:
super().add_widget(widget, index=index, canvas=canvas)
def _set_radius(self, instance: MDSliverAppbarContent) -> None:
instance.radius = self.radius
def _get_direction_swipe(self, current_percent: float) -> str:
if self._last_scroll_y_pos > current_percent:
direction = "up"
else:
direction = "down"
return direction