mirror of
https://github.com/markqvist/Sideband.git
synced 2024-10-01 03:15:37 -04:00
1476 lines
45 KiB
Python
Executable File
1476 lines
45 KiB
Python
Executable File
"""
|
||
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
|