""" Components/SliverAppbar ======================= .. versionadded:: 1.0.0 .. rubric:: MDSliverAppbar is a Material Design widget in KivyMD which gives scrollable or collapsible `MDTopAppBar `_ .. note:: This widget is a modification of the `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 = ''' 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 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