globalforest/kivymd/uix/bottomsheet.py

565 lines
15 KiB
Python
Raw Normal View History

2020-10-14 00:19:43 -04:00
"""
Components/Bottom Sheet
=======================
.. seealso::
`Material Design spec, Sheets: bottom <https://material.io/components/sheets-bottom>`_
.. rubric:: Bottom sheets are surfaces containing supplementary content that are anchored to the bottom of the screen.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet.png
:align: center
Two classes are available to you :class:`~MDListBottomSheet` and :class:`~MDGridBottomSheet`
for standard bottom sheets dialogs:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/grid-list-bottomsheets.png
:align: center
Usage :class:`~MDListBottomSheet`
=================================
.. code-block:: python
from kivy.lang import Builder
from kivymd.toast import toast
from kivymd.uix.bottomsheet import MDListBottomSheet
from kivymd.app import MDApp
KV = '''
Screen:
MDToolbar:
title: "Example BottomSheet"
pos_hint: {"top": 1}
elevation: 10
MDRaisedButton:
text: "Open list bottom sheet"
on_release: app.show_example_list_bottom_sheet()
pos_hint: {"center_x": .5, "center_y": .5}
'''
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
def callback_for_menu_items(self, *args):
toast(args[0])
def show_example_list_bottom_sheet(self):
bottom_sheet_menu = MDListBottomSheet()
for i in range(1, 11):
bottom_sheet_menu.add_item(
f"Standart Item {i}",
lambda x, y=i: self.callback_for_menu_items(
f"Standart Item {y}"
),
)
bottom_sheet_menu.open()
Example().run()
The :attr:`~MDListBottomSheet.add_item` method of the :class:`~MDListBottomSheet`
class takes the following arguments:
``text`` - element text;
``callback`` - function that will be called when clicking on an item;
There is also an optional argument ``icon``,
which will be used as an icon to the left of the item:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/icon-list-bottomsheets.png
:align: center
.. rubric:: Using the :class:`~MDGridBottomSheet` class is similar
to using the :class:`~MDListBottomSheet` class:
.. code-block:: python
from kivy.lang import Builder
from kivymd.toast import toast
from kivymd.uix.bottomsheet import MDGridBottomSheet
from kivymd.app import MDApp
KV = '''
Screen:
MDToolbar:
title: 'Example BottomSheet'
pos_hint: {"top": 1}
elevation: 10
MDRaisedButton:
text: "Open grid bottom sheet"
on_release: app.show_example_grid_bottom_sheet()
pos_hint: {"center_x": .5, "center_y": .5}
'''
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
def callback_for_menu_items(self, *args):
toast(args[0])
def show_example_grid_bottom_sheet(self):
bottom_sheet_menu = MDGridBottomSheet()
data = {
"Facebook": "facebook-box",
"YouTube": "youtube",
"Twitter": "twitter-box",
"Da Cloud": "cloud-upload",
"Camera": "camera",
}
for item in data.items():
bottom_sheet_menu.add_item(
item[0],
lambda x, y=item[0]: self.callback_for_menu_items(y),
icon_src=item[1],
)
bottom_sheet_menu.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/grid-bottomsheet.png
:align: center
.. rubric:: You can use custom content for bottom sheet dialogs:
.. code-block:: python
from kivy.lang import Builder
from kivy.factory import Factory
from kivymd.uix.bottomsheet import MDCustomBottomSheet
from kivymd.app import MDApp
KV = '''
<ItemForCustomBottomSheet@OneLineIconListItem>
on_press: app.custom_sheet.dismiss()
icon: ""
IconLeftWidget:
icon: root.icon
<ContentCustomSheet@BoxLayout>:
orientation: "vertical"
size_hint_y: None
height: "400dp"
MDToolbar:
title: 'Custom bottom sheet:'
ScrollView:
MDGridLayout:
cols: 1
adaptive_height: True
ItemForCustomBottomSheet:
icon: "page-previous"
text: "Preview"
ItemForCustomBottomSheet:
icon: "exit-to-app"
text: "Exit"
Screen:
MDToolbar:
title: 'Example BottomSheet'
pos_hint: {"top": 1}
elevation: 10
MDRaisedButton:
text: "Open custom bottom sheet"
on_release: app.show_example_custom_bottom_sheet()
pos_hint: {"center_x": .5, "center_y": .5}
'''
class Example(MDApp):
custom_sheet = None
def build(self):
return Builder.load_string(KV)
def show_example_custom_bottom_sheet(self):
self.custom_sheet = MDCustomBottomSheet(screen=Factory.ContentCustomSheet())
self.custom_sheet.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/custom-bottomsheet.png
:align: center
.. note:: When you use the :attr:`~MDCustomBottomSheet` class, you must specify
the height of the user-defined content exactly, otherwise ``dp(100)``
heights will be used for your ``ContentCustomSheet`` class:
.. code-block:: kv
<ContentCustomSheet@BoxLayout>:
orientation: "vertical"
size_hint_y: None
height: "400dp"
.. note:: The height of the bottom sheet dialog will never exceed half
the height of the screen!
"""
__all__ = (
"MDGridBottomSheet",
"GridBottomSheetItem",
"MDListBottomSheet",
"MDCustomBottomSheet",
"MDBottomSheet",
)
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.modalview import ModalView
from kivy.uix.scrollview import ScrollView
from kivymd import images_path
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import BackgroundColorBehavior
from kivymd.uix.label import MDIcon
from kivymd.uix.list import ILeftBody, OneLineIconListItem, OneLineListItem
Builder.load_string(
"""
#:import Window kivy.core.window.Window
<SheetList>:
MDGridLayout:
id: box_sheet_list
cols: 1
adaptive_height: True
padding: 0, 0, 0, "96dp"
<MDBottomSheet>
md_bg_color: root.value_transparent
_upper_padding: _upper_padding
_gl_content: _gl_content
_position_content: Window.height
MDBoxLayout:
orientation: "vertical"
padding: 0, 1, 0, 0
BsPadding:
id: _upper_padding
size_hint_y: None
height: root.height - min(root.width * 9 / 16, root._gl_content.height)
on_release: root.dismiss()
BottomSheetContent:
id: _gl_content
size_hint_y: None
cols: 1
md_bg_color: 0, 0, 0, 0
canvas:
Color:
rgba: root.theme_cls.bg_normal if not root.bg_color else root.bg_color
RoundedRectangle:
pos: self.pos
size: self.size
radius:
[
(root.radius, root.radius) if root.radius_from == "top_left" or root.radius_from == "top" else (0, 0),
(root.radius, root.radius) if root.radius_from == "top_right" or root.radius_from == "top" else (0, 0),
(root.radius, root.radius) if root.radius_from == "bottom_right" or root.radius_from == "bottom" else (0, 0),
(root.radius, root.radius) if root.radius_from == "bottom_left" or root.radius_from == "bottom" else (0, 0)
]
"""
)
class SheetList(ScrollView):
pass
class BsPadding(ButtonBehavior, FloatLayout):
pass
class BottomSheetContent(BackgroundColorBehavior, GridLayout):
pass
class MDBottomSheet(ThemableBehavior, ModalView):
background = f"{images_path}transparent.png"
"""Private attribute."""
duration_opening = NumericProperty(0.15)
"""The duration of the bottom sheet dialog opening animation.
:attr:`duration_opening` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.15`.
"""
radius = NumericProperty(25)
"""The value of the rounding of the corners of the dialog.
:attr:`radius` is an :class:`~kivy.properties.NumericProperty`
and defaults to `25`.
"""
radius_from = OptionProperty(
None,
options=[
"top_left",
"top_right",
"top",
"bottom_right",
"bottom_left",
"bottom",
],
allownone=True,
)
"""Sets which corners to cut from the dialog. Available options are:
(`"top_left"`, `"top_right"`, `"top"`, `"bottom_right"`, `"bottom_left"`, `"bottom"`).
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-radius-from.png
:align: center
:attr:`radius_from` is an :class:`~kivy.properties.OptionProperty`
and defaults to `None`.
"""
animation = BooleanProperty(False)
"""To use animation of opening of dialogue of the bottom sheet or not.
:attr:`animation` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
bg_color = ListProperty()
"""Dialog background color in ``rgba`` format.
:attr:`bg_color` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
value_transparent = ListProperty([0, 0, 0, 0.8])
"""Background transparency value when opening a dialog.
:attr:`value_transparent` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0, 0, 0.8]`.
"""
_upper_padding = ObjectProperty()
_gl_content = ObjectProperty()
_position_content = NumericProperty()
def open(self, *largs):
super().open(*largs)
def add_widget(self, widget, index=0, canvas=None):
super().add_widget(widget, index, canvas)
def on_dismiss(self):
self._gl_content.clear_widgets()
def resize_content_layout(self, content, layout, interval=0):
if not layout.ids.get("box_sheet_list"):
_layout = layout
else:
_layout = layout.ids.box_sheet_list
if _layout.height > Window.height / 2:
height = Window.height / 2
else:
height = _layout.height
if self.animation:
Animation(height=height, d=self.duration_opening).start(_layout)
Animation(height=height, d=self.duration_opening).start(content)
else:
layout.height = height
content.height = height
Builder.load_string(
"""
<ListBottomSheetIconLeft>
halign: "center"
theme_text_color: "Primary"
valign: "middle"
"""
)
class ListBottomSheetIconLeft(ILeftBody, MDIcon):
pass
class MDCustomBottomSheet(MDBottomSheet):
screen = ObjectProperty()
"""
Custom content.
:attr:`screen` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._gl_content.add_widget(self.screen)
Clock.schedule_once(
lambda x: self.resize_content_layout(self._gl_content, self.screen),
0,
)
class MDListBottomSheet(MDBottomSheet):
sheet_list = ObjectProperty()
"""
:attr:`sheet_list` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.sheet_list = SheetList(size_hint_y=None)
self._gl_content.add_widget(self.sheet_list)
Clock.schedule_once(
lambda x: self.resize_content_layout(
self._gl_content, self.sheet_list
),
0,
)
def add_item(self, text, callback, icon=None):
"""
:arg text: element text;
:arg callback: function that will be called when clicking on an item;
:arg icon: which will be used as an icon to the left of the item;
"""
if icon:
item = OneLineIconListItem(text=text, on_release=callback)
item.add_widget(ListBottomSheetIconLeft(icon=icon))
else:
item = OneLineListItem(text=text, on_release=callback)
item.bind(on_release=lambda x: self.dismiss())
self.sheet_list.ids.box_sheet_list.add_widget(item)
Builder.load_string(
"""
<GridBottomSheetItem>
orientation: "vertical"
padding: 0, dp(24), 0, 0
size_hint_y: None
size: dp(64), dp(96)
AnchorLayout:
anchoor_x: "center"
MDIconButton:
icon: root.source
user_font_size: root.icon_size
on_release: root.dispatch("on_release")
MDLabel:
font_style: "Caption"
theme_text_color: "Secondary"
text: root.caption
halign: "center"
"""
)
class GridBottomSheetItem(ButtonBehavior, BoxLayout):
source = StringProperty()
"""
Icon path if you use a local image or icon name
if you use icon names from a file ``kivymd/icon_definitions.py``.
:attr:`source` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
caption = StringProperty()
"""
Item text.
:attr:`caption` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
icon_size = StringProperty("32sp")
"""
Icon size.
:attr:`caption` is an :class:`~kivy.properties.StringProperty`
and defaults to `'32sp'`.
"""
class MDGridBottomSheet(MDBottomSheet):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.sheet_list = SheetList(size_hint_y=None)
self.sheet_list.ids.box_sheet_list.cols = 3
self.sheet_list.ids.box_sheet_list.padding = (dp(16), 0, dp(16), dp(96))
self._gl_content.add_widget(self.sheet_list)
Clock.schedule_once(
lambda x: self.resize_content_layout(
self._gl_content, self.sheet_list
),
0,
)
def add_item(self, text, callback, icon_src):
"""
:arg text: element text;
:arg callback: function that will be called when clicking on an item;
:arg icon_src: icon item;
"""
def tap_on_item(instance):
callback(instance)
self.dismiss()
item = GridBottomSheetItem(
caption=text, on_release=tap_on_item, source=icon_src
)
if len(self._gl_content.children) % 3 == 0:
self._gl_content.height += dp(96)
self.sheet_list.ids.box_sheet_list.add_widget(item)