Sideband/sbapp/kivymd/uix/behaviors/elevation.py

1476 lines
45 KiB
Python
Raw Normal View History

2022-07-07 16:16:10 -04:00
"""
Behaviors/Elevation
===================
.. seealso::
`Material Design spec, Elevation <https://material.io/design/environment/elevation.html>`_
.. rubric:: Elevation is the relative distance between two surfaces along the z-axis.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/elevation-previous.png
:align: center
There are 5 classes in KivyMD that can simulate shadow:
#. :class:`~FakeRectangularElevationBehavior`
#. :class:`~FakeCircularElevationBehavior`
#. :class:`~RectangularElevationBehavior`
#. :class:`~CircularElevationBehavior`
#. :class:`~RoundedRectangularElevationBehavior`
By default, KivyMD widgets use the elevation behavior implemented in classes
:class:`~FakeRectangularElevationBehavior` and :class:`~FakeCircularElevationBehavior`
for cast shadows. These classes use the old method of rendering shadows and it
doesn't look very aesthetically pleasing. Shadows are harsh, no softness:
The :class:`~RectangularElevationBehavior`, :class:`~CircularElevationBehavior`,
:class:`~RoundedRectangularElevationBehavior` classes use the new shadow
rendering algorithm, based on textures creation using the `Pillow` library.
It looks very aesthetically pleasing and beautiful.
.. warning:: Remember that :class:`~RectangularElevationBehavior`,
:class:`~CircularElevationBehavior`, :class:`~RoundedRectangularElevationBehavior`
classes require a lot of resources from the device on which your application will run,
so you should not use these classes on mobile devices.
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.widget import Widget
from kivymd.app import MDApp
from kivymd.uix.card import MDCard
from kivymd.uix.behaviors import RectangularElevationBehavior
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
<Box@MDBoxLayout>
adaptive_size: True
orientation: "vertical"
spacing: "36dp"
<BaseShadowWidget>
size_hint: None, None
size: 100, 100
md_bg_color: 0, 0, 1, 1
elevation: 36
pos_hint: {'center_x': .5}
MDFloatLayout:
MDBoxLayout:
adaptive_size: True
pos_hint: {'center_x': .5, 'center_y': .5}
spacing: "56dp"
Box:
MDLabel:
text: "Deprecated shadow rendering"
adaptive_size: True
DeprecatedShadowWidget:
MDLabel:
text: "Doesn't require a lot of resources"
adaptive_size: True
Box:
MDLabel:
text: "New shadow rendering"
adaptive_size: True
NewShadowWidget:
MDLabel:
text: "It takes a lot of resources"
adaptive_size: True
'''
class BaseShadowWidget(Widget):
pass
class DeprecatedShadowWidget(MDCard, BaseShadowWidget):
'''Deprecated shadow rendering. Doesn't require a lot of resources.'''
class NewShadowWidget(RectangularElevationBehavior, BaseShadowWidget, MDBoxLayout):
'''New shadow rendering. It takes a lot of resources.'''
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/elevation-differential.png
:align: center
For example, let's create an button with a rectangular elevation effect:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import (
RectangularRippleBehavior,
BackgroundColorBehavior,
FakeRectangularElevationBehavior,
)
KV = '''
<RectangularElevationButton>:
size_hint: None, None
size: "250dp", "50dp"
MDScreen:
# With elevation effect
RectangularElevationButton:
pos_hint: {"center_x": .5, "center_y": .6}
elevation: 18
# Without elevation effect
RectangularElevationButton:
pos_hint: {"center_x": .5, "center_y": .4}
'''
class RectangularElevationButton(
RectangularRippleBehavior,
FakeRectangularElevationBehavior,
ButtonBehavior,
BackgroundColorBehavior,
):
md_bg_color = [0, 0, 1, 1]
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/rectangular-elevation-effect.gif
:align: center
Similarly, create a circular button:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.app import MDApp
from kivymd.uix.behaviors import (
CircularRippleBehavior,
FakeCircularElevationBehavior,
)
KV = '''
<CircularElevationButton>:
size_hint: None, None
size: "100dp", "100dp"
radius: self.size[0] / 2
md_bg_color: 0, 0, 1, 1
MDIcon:
icon: "hand-heart"
halign: "center"
valign: "center"
size: root.size
pos: root.pos
font_size: root.size[0] * .6
theme_text_color: "Custom"
text_color: [1] * 4
MDScreen:
CircularElevationButton:
pos_hint: {"center_x": .5, "center_y": .6}
elevation: 24
'''
class CircularElevationButton(
FakeCircularElevationBehavior,
CircularRippleBehavior,
ButtonBehavior,
MDBoxLayout,
):
pass
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/circular-fake-elevation.png
:align: center
Animating the elevation
-----------------------
.. code-block:: python
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.properties import ObjectProperty
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import FakeRectangularElevationBehavior, RectangularRippleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
MDFloatLayout:
ElevatedWidget:
pos_hint: {'center_x': .5, 'center_y': .5}
size_hint: None, None
size: 100, 100
md_bg_color: 0, 0, 1, 1
'''
class ElevatedWidget(
ThemableBehavior,
FakeRectangularElevationBehavior,
RectangularRippleBehavior,
ButtonBehavior,
MDBoxLayout,
):
shadow_animation = ObjectProperty()
def on_press(self, *args):
if self.shadow_animation:
Animation.cancel_all(self, "_elevation")
self.shadow_animation = Animation(_elevation=self.elevation + 10, d=0.4)
self.shadow_animation.start(self)
def on_release(self, *args):
if self.shadow_animation:
Animation.cancel_all(self, "_elevation")
self.shadow_animation = Animation(_elevation=self.elevation, d=0.1)
self.shadow_animation.start(self)
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/rectangular-elevation-animation-effect.gif
:align: center
Lighting position
-----------------
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.card import MDCard
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.behaviors import RectangularElevationBehavior
KV = '''
MDScreen:
ShadowCard:
pos_hint: {'center_x': .5, 'center_y': .5}
size_hint: None, None
size: 100, 100
shadow_pos: -10 + slider.value, -10 + slider.value
elevation: 24
md_bg_color: 1, 1, 1, 1
MDSlider:
id: slider
max: 20
size_hint_x: .6
pos_hint: {'center_x': .5, 'center_y': .3}
'''
class ShadowCard(RectangularElevationBehavior, MDBoxLayout):
pass
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-pos.gif
:align: center
"""
__all__ = (
"CommonElevationBehavior",
"RectangularElevationBehavior",
"CircularElevationBehavior",
"RoundedRectangularElevationBehavior",
"ObservableShadow",
"FakeRectangularElevationBehavior",
"FakeCircularElevationBehavior",
)
from io import BytesIO
from weakref import WeakMethod, ref
from kivy import Logger
from kivy.clock import Clock
from kivy.core.image import Image as CoreImage
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
AliasProperty,
BooleanProperty,
BoundedNumericProperty,
ListProperty,
NumericProperty,
ObjectProperty,
ReferenceListProperty,
StringProperty,
VariableListProperty,
)
from kivy.uix.widget import Widget
from PIL import Image, ImageDraw, ImageFilter
from kivymd.app import MDApp
Builder.load_string(
"""
#:import InstructionGroup kivy.graphics.instructions.InstructionGroup
<CommonElevationBehavior>
canvas.before:
# SOFT SHADOW
PushMatrix
Rotate:
angle: self.angle
origin: self._shadow_origin
Color:
group: "soft_shadow"
rgba: root.soft_shadow_cl
Rectangle:
group: "soft_shadow"
texture: self._soft_shadow_texture
size: self.soft_shadow_size
pos: self.soft_shadow_pos
PopMatrix
# HARD SHADOW
PushMatrix
Rotate:
angle: self.angle
origin: self.center
Color:
group: "hard_shadow"
rgba: root.hard_shadow_cl
Rectangle:
group: "hard_shadow"
texture: self.hard_shadow_texture
size: self.hard_shadow_size
pos: self.hard_shadow_pos
PopMatrix
Color:
group: "shadow"
a: 1
""",
filename="CommonElevationBehavior.kv",
)
class CommonElevationBehavior(Widget):
"""Common base class for rectangular and circular elevation behavior."""
elevation = BoundedNumericProperty(0, min=0, errorvalue=0)
"""
Elevation of the widget.
.. note::
Although, this value does not represent the current elevation of the
widget. :attr:`~CommonElevationBehavior._elevation` can be used to
animate the current elevation and come back using the
:attr:`~CommonElevationBehavior.elevation` property directly.
For example:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import CircularElevationBehavior, CircularRippleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
#:import Animation kivy.animation.Animation
<WidgetWithShadow>
size_hint: [None, None]
elevation: 6
animation_: None
md_bg_color: [1] * 4
on_size:
self.radius = [self.height / 2] * 4
on_press:
if self.animation_: \
self.animation_.cancel(self); \
self.animation_ = Animation(_elevation=self.elevation + 6, d=0.08); \
self.animation_.start(self)
on_release:
if self.animation_: \
self.animation_.cancel(self); \
self.animation_ = Animation(_elevation = self.elevation, d=0.08); \
self.animation_.start(self)
MDFloatLayout:
WidgetWithShadow:
size: [root.size[1] / 2] * 2
pos_hint: {"center": [0.5, 0.5]}
'''
class WidgetWithShadow(
CircularElevationBehavior,
CircularRippleBehavior,
ButtonBehavior,
MDBoxLayout,
):
def __init__(self, **kwargs):
# always set the elevation before the super().__init__ call
# self.elevation = 6
super().__init__(**kwargs)
def on_size(self, *args):
self.radius = [self.size[0] / 2]
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
:attr:`elevation` is an :class:`~kivy.properties.BoundedNumericProperty`
and defaults to `0`.
"""
# Shadow rendering properties.
# Shadow rotation memory - SHARED ACROSS OTHER CLASSES.
angle = NumericProperty(0)
"""
Angle of rotation in degrees of the current shadow.
This value is shared across different widgets.
.. note::
This value will affect both, hard and soft shadows.
Each shadow has his own origin point that's computed every time the
elevation changes.
.. warning::
Do not add `PushMatrix` inside the canvas before and add `PopMatrix`
in the next layer, this will cause visual errors, because the stack
used will clip the push and pop matrix already inside the canvas.before
canvas layer.
Incorrect:
.. code-block:: kv
<TiltedWidget>
canvas.before:
PushMatrix
[...]
canvas:
PopMatrix
Correct:
.. code-block:: kv
<TiltedWidget>
canvas.before:
PushMatrix
[...]
PopMatrix
:attr:`angle` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0`.
"""
radius = VariableListProperty([0])
"""
Radius of the corners of the shadow.
This values represents each corner of the shadow, starting from `top-left`
corner and going clockwise.
.. code-block:: python
radius = [
"top-left",
"top-right",
"bottom-right",
"bottom-left",
]
This value can be expanded thus allowing this settings to be valid:
.. code-block:: python
widget.radius=[0] # Translates to [0, 0, 0, 0]
widget.radius=[10, 3] # Translates to [10, 3, 10, 3]
widget.radius=[7.0, 8.7, 1.5, 3.0] # Translates to [7, 8, 1, 3]
.. note::
This value will affect both, hard and soft shadows.
This value only affects :class:`~RoundedRectangularElevationBehavior`
for now, but can be stored and used by custom shadow draw functions.
:attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
and defaults to `[0, 0, 0, 0]`.
"""
# Position of the shadow.
_shadow_origin_x = NumericProperty(0)
"""
Shadow origin `x` position for the rotation origin.
Managed by `_shadow_origin`.
:attr:`_shadow_origin_x` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0`.
.. note::
This property is automatically processed. by _shadow_origin.
"""
_shadow_origin_y = NumericProperty(0)
"""
Shadow origin y position for the rotation origin.
Managed by :attr:`_shadow_origin`.
:attr:`_shadow_origin_y` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0`.
.. note::
This property is automatically processed.
"""
_shadow_origin = ReferenceListProperty(_shadow_origin_x, _shadow_origin_y)
"""
Soft shadow rotation origin point.
:attr:`_shadow_origin` is an :class:`~kivy.properties.ReferenceListProperty`
and defaults to `[0, 0]`.
.. note::
This property is automatically processed and relative to the canvas center.
"""
_shadow_pos = ListProperty([0, 0]) # custom offset
"""
Soft shadow origin point.
:attr:`_shadow_pos` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0]`.
.. note::
This property is automatically processed and relative to the widget's
canvas center.
"""
shadow_pos = ListProperty([0, 0]) # bottom left corner
"""
Custom shadow origin point. If this property is set, :attr:`_shadow_pos`
will be ommited.
This property allows users to fake light source.
:attr:`shadow_pos` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0]`.
.. note::
this value overwrite the :attr:`_shadow_pos` processing.
"""
# Shadow Group shared memory
__shadow_groups = {"global": []}
shadow_group = StringProperty("global")
"""
Widget's shadow group.
By default every widget with a shadow is saved inside the memory
:attr:`__shadow_groups` as a weakref. This means that you can have multiple
light sources, one for every shadow group.
To fake a light source use :attr:`force_shadow_pos`.
:attr:`shadow_group` is an :class:`~kivy.properties.StringProperty`
and defaults to `"global"`.
"""
_elevation = BoundedNumericProperty(0, min=0, errorvalue=0)
"""
Current elevation of the widget.
.. warning::
This property is the current elevation of the widget, do not
use this property directly, instead, use :class:`~CommonElevationBehavior`
elevation.
:attr:`_elevation` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0`.
"""
# soft shadow
_soft_shadow_texture = ObjectProperty()
"""
Texture of the soft shadow texture for the canvas.
:attr:`_soft_shadow_texture` is an :class:`~kivy.core.image.Image`
and defaults to `None`.
.. note::
This property is automatically processed.
"""
soft_shadow_size = ListProperty([0, 0])
"""
Size of the soft shadow texture over the canvas.
:attr:`soft_shadow_size` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0]`.
.. note::
This property is automatically processed.
"""
soft_shadow_pos = ListProperty([0, 0])
"""
Position of the hard shadow texture over the canvas.
:attr:`soft_shadow_pos` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0]`.
.. note::
This property is automatically processed.
"""
soft_shadow_cl = ListProperty([0, 0, 0, 0.50])
"""
Color of the soft shadow.
:attr:`soft_shadow_cl` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0, 0, 0.15]`.
"""
# hard shadow
hard_shadow_texture = ObjectProperty()
"""
Texture of the hard shadow texture for the canvas.
:attr:`hard_shadow_texture` is an :class:`~kivy.core.image.Image`
and defaults to `None`.
.. note::
This property is automatically processed when elevation is changed.
"""
hard_shadow_size = ListProperty([0, 0])
"""
Size of the hard shadow texture over the canvas.
:attr:`hard_shadow_size` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0]`.
.. note::
This property is automatically processed when elevation is changed.
"""
hard_shadow_pos = ListProperty([0, 0])
"""
Position of the hard shadow texture over the canvas.
:attr:`hard_shadow_pos` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0]`.
.. note::
This property is automatically processed when elevation is changed.
"""
hard_shadow_cl = ListProperty([0, 0, 0, 0.15])
"""
Color of the hard shadow.
.. note::
:attr:`hard_shadow_cl` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0, 0, 0.15]`.
"""
# Shared property for some calculations.
# This values are used to improve the gaussain blur and avoid that
# the blur goes outside the texture.
hard_shadow_offset = BoundedNumericProperty(
2, min=0, errorhandler=lambda x: 0 if x < 0 else x
)
"""
This value sets a special offset to the shadow canvas, this offset allows a
correct draw of the canvas size. allowing the effect to correctly blur the
image in the given space.
:attr:`hard_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty`
and defaults to `2`.
"""
soft_shadow_offset = BoundedNumericProperty(
4, min=0, errorhandler=lambda x: 0 if x < 0 else x
)
"""
This value sets a special offset to the shadow canvas, this offset allows a
correct draw of the canvas size. allowing the effect to correctly blur the
image in the given space.
:attr:`soft_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty`
and defaults to `4`.
"""
draw_shadow = ObjectProperty(None)
"""
This property controls the draw call of the context.
This property is automatically set to :attr:`__draw_shadow__` inside the
`super().__init__ call.` unless the property is different of None.
To set a different drawing instruction function, set this property before the
`super(),__init__` call inside the `__init__` definition of the new class.
You can use the source for this classes as example of how to draw over
with the context:
Real time shadows:
#. :class:`~RectangularElevationBehavior`
#. :class:`~CircularElevationBehavior`
#. :class:`~RoundedRectangularElevationBehavior`
#. :class:`~ObservableShadow`
Fake shadows (d`ont use this property):
#. :class:`~FakeRectangularElevationBehavior`
#. :class:`~FakeCircularElevationBehavior`
:attr:`draw_shadow` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
.. note:: If this property is left to `None` the
:class:`~CommonElevationBehavior` will set to a function that will
raise a `NotImplementedError` inside `super().__init__`.
Follow the next example to set a new draw instruction for the class
inside `__init__`:
.. code-block:: python
class RoundedRectangularElevationBehavior(CommonElevationBehavior):
'''
Shadow class for the RoundedRectangular shadow behavior.
Controls the size and position of the shadow.
'''
def __init__(self, **kwargs):
self._draw_shadow = WeakMethod(self.__draw_shadow__)
super().__init__(**kwargs)
def __draw_shadow__(self, origin, end, context=None):
context.draw(...)
Context is a `Pillow` `ImageDraw` class. For more information check the
[Pillow official documentation](https://github.com/python-pillow/Pillow/).
"""
# All classes that uses a fake shadow shall set this value as `True`
# for performance.
_fake_elevation = BooleanProperty(False)
def __init__(self, **kwargs):
if self.draw_shadow is None:
self.draw_shadow = WeakMethod(self.__draw_shadow__)
self.prev_shadow_group = None
im = BytesIO()
Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png")
im.seek(0)
# Setting a empty image as texture, improves performance.
self._soft_shadow_texture = self.hard_shadow_texture = CoreImage(
im, ext="png"
).texture
Clock.schedule_once(self.shadow_preset, -1)
self.on_shadow_group(self, self.shadow_group)
self.bind(
pos=self._update_shadow,
size=self._update_shadow,
radius=self._update_shadow,
)
super().__init__(**kwargs)
def on_shadow_group(self, instance, value):
"""
This function controls the shadow group of the widget.
Do not use Directly to change the group. instead, use the shadow_group
:attr:`property`.
"""
groups = CommonElevationBehavior.__shadow_groups
if self.prev_shadow_group:
group = groups[self.prev_shadow_group]
for widget in group[:]:
if widget() is self:
group.remove(widget)
group = self.prev_shadow_group = self.shadow_group
if group not in groups:
groups[group] = []
r = ref(self, CommonElevationBehavior._clear_shadow_groups)
groups[group].append(r)
@staticmethod
def _clear_shadow_groups(wk):
# auto flush the element when the weak reference have been deleted
groups = CommonElevationBehavior.__shadow_groups
for group in list(groups.values()):
if not group:
break
if wk in group:
group.remove(wk)
break
def force_shadow_pos(self, shadow_pos):
"""
This property forces the shadow position in every widget inside the
widget. The argument :attr:`shadow_pos` is expected as a <class 'list'>
or <class 'tuple'>.
"""
if self.shadow_group is None:
return
group = CommonElevationBehavior.__shadow_groups[self.shadow_group]
for wk in group[:]:
widget = wk()
if widget is None:
group.remove(wk)
widget.shadow_pos = shadow_pos
del group
def update_group_property(self, property_name, value):
"""
This functions allows to change properties of every widget inside the
shadow group.
"""
if self.shadow_group is None:
return
group = CommonElevationBehavior.__shadow_groups[self.shadow_group]
for wk in group[:]:
widget = wk()
if widget is None:
group.remove(wk)
setattr(widget, property_name, value)
del group
def shadow_preset(self, *args):
"""
This function is meant to set the default configuration of the
elevation.
After a new instance is created, the elevation property will be launched
and thus this function will update the elevation if the KV lang have not
done it already.
Works similar to an `__after_init__` call inside a widget.
"""
from kivymd.uix.card import MDCard
if self.elevation is None and not issubclass(self.__class__, MDCard):
self.elevation = 10
if self._fake_elevation is False:
self._update_shadow(self, self.elevation)
self.bind(
pos=self._update_shadow,
size=self._update_shadow,
_elevation=self._update_shadow,
)
def on_elevation(self, instance, value):
"""
Elevation event that sets the current elevation value to `_elevation`.
"""
if value is not None:
self._elevation = value
def _set_soft_shadow_a(self, value):
value = 0 if value < 0 else (1 if value > 1 else value)
self.soft_shadow_cl[-1] = value
return True
def _set_hard_shadow_a(self, value):
value = 0 if value < 0 else (1 if value > 1 else value)
self.hard_shadow_cl[-1] = value
return True
def _get_soft_shadow_a(self):
return self.soft_shadow_cl[-1]
def _get_hard_shadow_a(self):
return self.hard_shadow_cl[-1]
_soft_shadow_a = AliasProperty(
_get_soft_shadow_a, _set_soft_shadow_a, bind=["soft_shadow_cl"]
)
_hard_shadow_a = AliasProperty(
_get_hard_shadow_a, _set_hard_shadow_a, bind=["hard_shadow_cl"]
)
def on_disabled(self, instance, value):
"""
This function hides the shadow when the widget is disabled.
It sets the shadow to `0`.
"""
if self.disabled is True:
self._elevation = 0
else:
self._elevation = 0 if self.elevation is None else self.elevation
self._update_shadow(self, self._elevation)
try:
super().on_disabled(instance, value)
except Exception:
pass
def _update_elevation(self, instance, value):
self._elevation = value
self._update_shadow(instance, value)
def _update_shadow_pos(self, instance, value):
if self._elevation > 0:
self.hard_shadow_pos = [
self.x - dp(self.hard_shadow_offset), # + self.shadow_pos[0],
self.y - dp(self.hard_shadow_offset), # + self.shadow_pos[1],
]
if self.shadow_pos == [0, 0]:
self.soft_shadow_pos = [
self.x
+ self._shadow_pos[0]
- self._elevation
- dp(self.soft_shadow_offset),
self.y
+ self._shadow_pos[1]
- self._elevation
- dp(self.soft_shadow_offset),
]
else:
self.soft_shadow_pos = [
self.x
+ self.shadow_pos[0]
- self._elevation
- dp(self.soft_shadow_offset),
self.y
+ self.shadow_pos[1]
- self._elevation
- dp(self.soft_shadow_offset),
]
self._shadow_origin = [
self.soft_shadow_pos[0] + self.soft_shadow_size[0] / 2,
self.soft_shadow_pos[1] + self.soft_shadow_size[1] / 2,
]
def on__shadow_pos(self, ins, val):
"""
Updates the shadow with the computed value.
Call this function every time you need to force a shadow update.
"""
self._update_shadow_pos(ins, val)
def on_shadow_pos(self, ins, val):
"""
Updates the shadow with the fixed value.
Call this function every time you need to force a shadow update.
"""
self._update_shadow_pos(ins, val)
def _update_shadow(self, instance, value):
self._update_shadow_pos(instance, value)
if self._elevation > 0 and self._fake_elevation is False:
# dynamic elevation position for the shadow
if self.shadow_pos == [0, 0]:
self._shadow_pos = [0, -self._elevation * 0.4]
# HARD Shadow
offset = int(dp(self.hard_shadow_offset))
size = [
int(self.size[0] + (offset * 2)),
int(self.size[1] + (offset * 2)),
]
im = BytesIO()
# context
img = Image.new("RGBA", tuple(size), color=(0, 0, 0, 0))
# draw context
shadow = ImageDraw.Draw(img)
self.draw_shadow()(
[offset, offset],
[
int(size[0] - 1 - offset),
int(size[1] - 1 - offset),
],
context=shadow
# context=ref(shadow)
)
img = img.filter(
ImageFilter.GaussianBlur(
radius=int(dp(1 + self.hard_shadow_offset / 3))
)
)
img.save(im, format="png")
im.seek(0)
self.hard_shadow_size = size
self.hard_shadow_texture = CoreImage(im, ext="png").texture
# soft shadow
if self.soft_shadow_cl[-1] > 0:
offset = dp(self.soft_shadow_offset)
size = [
int(self.size[0] + dp(self._elevation * 2) + (offset * 2)),
int(self.size[1] + dp(self._elevation * 2) + (offset * 2)),
# ((self._elevation)*2) + x + (offset*2)) for x in self.size
]
im = BytesIO()
img = Image.new("RGBA", tuple(size), color=((0,) * 4))
shadow = ImageDraw.Draw(img)
_offset = int(dp(self._elevation + offset))
self.draw_shadow()(
[
_offset,
_offset,
],
[int(size[0] - _offset - 1), int(size[1] - _offset - 1)],
context=shadow
# context=ref(shadow)
)
img = img.filter(
ImageFilter.GaussianBlur(radius=self._elevation // 2)
)
shadow = ImageDraw.Draw(img)
img.save(im, format="png")
im.seek(0)
self.soft_shadow_size = size
self._soft_shadow_texture = CoreImage(im, ext="png").texture
else:
im = BytesIO()
Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png")
im.seek(0)
self._soft_shadow_texture = self.hard_shadow_texture = CoreImage(
im, ext="png"
).texture
return
def _get_center(self):
center = [self.pos[0] + self.width / 2, self.pos[1] + self.height / 2]
return center
def __draw_shadow__(self, origin, end, context=None):
Logger.warning(
f"KivyMD: "
f"If you see this error, this means that either youre using "
f"`CommonElevationBehavio`r directly or your 'shader' dont have a "
f"`_draw_shadow` instruction, remember to overwrite this function"
f"to draw over the image context. Тhe figure you would like. "
f"Or your class {self.__class__.__name__} is not inherited from "
f"any of the classes {__all__}"
)
class RectangularElevationBehavior(CommonElevationBehavior):
"""
Base class for a rectangular elevation behavior.
"""
def __init__(self, **kwargs):
self.draw_shadow = WeakMethod(self.__draw_shadow__)
super().__init__(**kwargs)
def __draw_shadow__(self, origin, end, context=None):
context.rectangle(origin + end, fill=tuple([255] * 4))
class CircularElevationBehavior(CommonElevationBehavior):
"""
Base class for a circular elevation behavior.
"""
def __init__(self, **kwargs):
self.draw_shadow = WeakMethod(self.__draw_shadow__)
super().__init__(**kwargs)
def __draw_shadow__(self, origin, end, context=None):
context.ellipse(origin + end, fill=tuple([255] * 4))
class RoundedRectangularElevationBehavior(CommonElevationBehavior):
"""
Base class for rounded rectangular elevation behavior.
"""
def __init__(self, **kwargs):
self.bind(
radius=self._update_shadow,
)
self.draw_shadow = WeakMethod(self.__draw_shadow__)
super().__init__(**kwargs)
def __draw_shadow__(self, origin, end, context=None):
if self.radius == [0, 0, 0, 0]:
context.rectangle(origin + end, fill=tuple([255] * 4))
else:
radius = [x * 2 for x in self.radius]
context.pieslice(
[
origin[0],
origin[1],
origin[0] + radius[0],
origin[1] + radius[0],
],
180,
270,
fill=(255, 255, 255, 255),
)
context.pieslice(
[
end[0] - radius[1],
origin[1],
end[0],
origin[1] + radius[1],
],
270,
360,
fill=(255, 255, 255, 255),
)
context.pieslice(
[
end[0] - radius[2],
end[1] - radius[2],
end[0],
end[1],
],
0,
90,
fill=(255, 255, 255, 255),
)
context.pieslice(
[
origin[0],
end[1] - radius[3],
origin[0] + radius[3],
end[1],
],
90,
180,
fill=(255, 255, 255, 255),
)
if all((x == self.radius[0] for x in self.radius)):
radius = int(self.radius[0])
context.rectangle(
[
origin[0] + radius,
origin[1],
end[0] - radius,
end[1],
],
fill=(255,) * 4,
)
context.rectangle(
[
origin[0],
origin[1] + radius,
end[0],
end[1] - radius,
],
fill=(255,) * 4,
)
else:
radius = [
max((self.radius[0], self.radius[1])),
max((self.radius[1], self.radius[2])),
max((self.radius[2], self.radius[3])),
max((self.radius[3], self.radius[0])),
]
context.rectangle(
[
origin[0] + self.radius[0],
origin[1],
end[0] - self.radius[1],
end[1] - radius[2],
],
fill=(255,) * 4,
)
context.rectangle(
[
origin[0] + radius[3],
origin[1] + self.radius[1],
end[0],
end[1] - self.radius[2],
],
fill=(255,) * 4,
)
context.rectangle(
[
origin[0] + self.radius[3],
origin[1] + radius[0],
end[0] - self.radius[2],
end[1],
],
fill=(255,) * 4,
)
context.rectangle(
[
origin[0],
origin[1] + self.radius[0],
end[0] - radius[2],
end[1] - self.radius[3],
],
fill=(255,) * 4,
)
class ObservableShadow(CommonElevationBehavior):
"""
ObservableShadow is real time shadow render that it's intended to only
render a partial shadow of widgets based upon on the window observable
area, this is meant to improve the performance of bigger widgets.
.. warning::
This is an empty class, the name has been reserved for future use.
if you include this clas in your object, you wil get a
`NotImplementedError`.
"""
def __init__(self, **kwargs):
# self._shadow = MDApp.get_running_app().theme_cls.round_shadow
# self._fake_elevation=True
raise NotImplementedError(
"ObservableShadow:\n\t" "This class is in current development"
)
super().__init__(**kwargs)
class FakeRectangularElevationBehavior(CommonElevationBehavior):
"""
`FakeRectangularElevationBehavio`r is a shadow mockup for widgets. Improves
performance using cached images inside `kivymd.images` dir
This class cast a fake Rectangular shadow behaind the widget.
You can either use this behavior to overwrite the elevation of a prefab
widget, or use it directly inside a new widget class definition.
Use this class as follows for new widgets:
.. code-block:: python
class NewWidget(
ThemableBehavior,
FakeCircularElevationBehavior,
SpecificBackgroundColorBehavior,
# here you add the other front end classes for the widget front_end,
):
[...]
With this method each class can draw it's content in the canvas in the
correct order, avoiding some visual errors.
`FakeCircularElevationBehavior` will load prefabricated textures to
optimize loading times.
.. note:: About rounded corners:
be careful, since this behavior is a mockup and will not draw any
rounded corners.
"""
def __init__(self, **kwargs):
# self._shadow = MDApp.get_running_app().theme_cls.round_shadow
self.draw_shadow = WeakMethod(self.__draw_shadow__)
self._fake_elevation = True
self._update_shadow(self, self.elevation)
super().__init__(**kwargs)
def _update_shadow(self, *args):
if self._elevation > 0:
# Set shadow size.
ratio = self.width / (self.height if self.height != 0 else 1)
if -2 < ratio < 2:
self._shadow = MDApp.get_running_app().theme_cls.quad_shadow
width = soft_width = self.width * 1.9
height = soft_height = self.height * 1.9
elif ratio <= -2:
self._shadow = MDApp.get_running_app().theme_cls.rec_st_shadow
ratio = abs(ratio)
if ratio > 5:
ratio = ratio * 22
else:
ratio = ratio * 11.5
width = soft_width = self.width * 1.9
height = self.height + dp(ratio)
soft_height = (
self.height + dp(ratio) + dp(self._elevation) * 0.5
)
else:
self._shadow = MDApp.get_running_app().theme_cls.quad_shadow
width = soft_width = self.width * 1.8
height = soft_height = self.height * 1.8
self.soft_shadow_size = (soft_width, soft_height)
self.hard_shadow_size = (width, height)
# Set ``soft_shadow`` parameters.
center_x, center_y = self._get_center()
self.hard_shadow_pos = self.soft_shadow_pos = (
center_x - soft_width / 2,
center_y - soft_height / 2 - dp(self._elevation * 0.5),
)
# Set transparency
self._soft_shadow_a = 0.1 * 1.05**self._elevation
self._hard_shadow_a = 0.4 * 0.8**self._elevation
t = int(round(self._elevation))
if 0 < t <= 23:
self._soft_shadow_texture = (
self._hard_shadow_texture
) = self._shadow.textures[str(t)]
else:
self._soft_shadow_texture = (
self._hard_shadow_texture
) = self._shadow.textures["23"]
else:
self._soft_shadow_a = 0
self._hard_shadow_a = 0
def __draw_shadow__(self, origin, end, context=None):
pass
class FakeCircularElevationBehavior(CommonElevationBehavior):
"""
`FakeCircularElevationBehavior` is a shadow mockup for widgets. Improves
performance using cached images inside `kivymd.images` dir
This class cast a fake elliptic shadow behaind the widget.
You can either use this behavior to overwrite the elevation of a prefab
widget, or use it directly inside a new widget class definition.
Use this class as follows for new widgets:
.. code-block:: python
class NewWidget(
ThemableBehavior,
FakeCircularElevationBehavior,
SpecificBackgroundColorBehavior,
# here you add the other front end classes for the widget front_end,
):
[...]
With this method each class can draw it's content in the canvas in the
correct order, avoiding some visual errors.
`FakeCircularElevationBehavior` will load prefabricated textures to optimize
loading times.
.. note:: About rounded corners:
be careful, since this behavior is a mockup and will not draw any rounded
corners. only perfect ellipses.
"""
def __init__(self, **kwargs):
self._shadow = MDApp.get_running_app().theme_cls.round_shadow
self.draw_shadow = WeakMethod(self.__draw_shadow__)
self._fake_elevation = True
self._update_shadow(self, self.elevation)
super().__init__(**kwargs)
def _update_shadow(self, *args):
if self._elevation > 0:
# set shadow size
width = self.width * 2
height = self.height * 2
center_x, center_y = self._get_center()
x = center_x - width / 2
self.soft_shadow_size = (width, height)
self.hard_shadow_size = (width, height)
# set ``soft_shadow`` parameters
y = center_y - height / 2 - dp(0.5 * self._elevation)
self.soft_shadow_pos = (x, y)
# set ``hard_shadow`` parameters
y = center_y - height / 2 - dp(0.5 * self._elevation)
self.hard_shadow_pos = (x, y)
# shadow transparency
self._soft_shadow_a = 0.1 * 1.05**self._elevation
self._hard_shadow_a = 0.4 * 0.8**self._elevation
t = int(round(self._elevation))
if 0 < t <= 23:
if hasattr(self, "_shadow"):
self._soft_shadow_texture = (
self._hard_shadow_texture
) = self._shadow.textures[str(t)]
else:
self._soft_shadow_texture = (
self._hard_shadow_texture
) = self._shadow.textures["23"]
else:
self._soft_shadow_a = 0
self._hard_shadow_a = 0
def __draw_shadow__(self, origin, end, context=None):
pass