Skip to content

Commit

Permalink
Merge pull request #1694 from kivymd/material_motion
Browse files Browse the repository at this point in the history
chore: add custom animation transition
  • Loading branch information
T-Dynamos authored May 27, 2024
2 parents f283a8b + 8080789 commit 7971b36
Show file tree
Hide file tree
Showing 5 changed files with 324 additions and 5 deletions.
5 changes: 2 additions & 3 deletions examples/md_axis_transition.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@
text:root.subtext
font_style:"Label"
role:"large"
theme_text_color:"Custom"
text_color:app.theme_cls.surfaceContainerLowestColor[:-1] + [0.5]
text_color:app.theme_cls.onSurfaceVariantColor
<SettingsScreen@MDScreen>:
name:"main"
Expand Down Expand Up @@ -96,7 +95,7 @@
font_style:"Body"
role:"large"
theme_text_color:"Custom"
text_color:app.theme_cls.surfaceContainerLowestColor[:-1] + [0.5]
text_color:app.theme_cls.onSurfaceVariantColor
Image:
size_hint_y:1
source:app.image_path
Expand Down
77 changes: 77 additions & 0 deletions examples/md_transitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from kivy.lang import Builder
from kivy.animation import Animation
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
from kivy.metrics import dp
from kivy.properties import ListProperty

from kivymd.app import MDApp


class AnimBox(BoxLayout):
obj_pos = ListProperty([0, 0])


UI = """
<AnimBox>:
transition:"in_out_bounce"
size_hint_y:None
height:dp(100)
obj_pos:[dp(40), self.pos[-1] + dp(40)]
canvas:
Color:
rgba:app.theme_cls.primaryContainerColor
Rectangle:
size:[self.size[0], dp(5)]
pos:self.pos[0], self.pos[-1] + dp(50)
Color:
rgba:app.theme_cls.primaryColor
Rectangle:
size:[dp(30)] * 2
pos:root.obj_pos
MDLabel:
adaptive_height:True
text:root.transition
padding:[dp(10), 0]
halign:"center"
MDGridLayout:
orientation:"lr-tb"
cols:1
md_bg_color:app.theme_cls.backgroundColor
spacing:dp(10)
"""


class MotionApp(MDApp):

def build(self):
return Builder.load_string(UI)

def on_start(self):
for transition in [
"easing_linear",
"easing_accelerated",
"easing_decelerated",
"easing_standard",
"in_out_cubic"
]: # Add more here for comparison
print(transition)
widget = AnimBox()
widget.transition = transition
self.root.add_widget(widget)
Clock.schedule_once(self.run_animation, 1)

_inverse = True

def run_animation(self, dt):
x = (self.root.children[0].width - dp(30)) if self._inverse else 0
for widget in self.root.children:
Animation(
obj_pos=[x, widget.obj_pos[-1]], t=widget.transition, d=3
).start(widget)
self._inverse = not self._inverse
Clock.schedule_once(self.run_animation, 3.1)


MotionApp().run()
1 change: 1 addition & 0 deletions kivymd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@

import kivymd.factory_registers # NOQA
import kivymd.font_definitions # NOQA
import kivymd.animation # NOQA
from kivymd.tools.packaging.pyinstaller import hooks_path # NOQA
219 changes: 219 additions & 0 deletions kivymd/animation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
"""
Animation
=========
.. versionadded:: 2.0.0
Adds new transitions to the :class:`~kivy.animation.AnimationTransition` class:
- "easing_standard"
- "easing_decelerated"
- "easing_accelerated"
- "easing_linear"
.. code-block:: python
from kivy.lang import Builder
from kivy.animation import Animation
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
from kivy.metrics import dp
from kivy.properties import ListProperty
from kivymd.app import MDApp
class AnimBox(BoxLayout):
obj_pos = ListProperty([0, 0])
UI = '''
<AnimBox>:
transition:"in_out_bounce"
size_hint_y:None
height:dp(100)
obj_pos:[dp(40), self.pos[-1] + dp(40)]
canvas:
Color:
rgba:app.theme_cls.primaryContainerColor
Rectangle:
size:[self.size[0], dp(5)]
pos:self.pos[0], self.pos[-1] + dp(50)
Color:
rgba:app.theme_cls.primaryColor
Rectangle:
size:[dp(30)] * 2
pos:root.obj_pos
MDLabel:
adaptive_height:True
text:root.transition
padding:[dp(10), 0]
halign:"center"
MDGridLayout:
orientation:"lr-tb"
cols:1
md_bg_color:app.theme_cls.backgroundColor
spacing:dp(10)
'''
class MotionApp(MDApp):
def build(self):
return Builder.load_string(UI)
def on_start(self):
for transition in [
"easing_linear",
"easing_accelerated",
"easing_decelerated",
"easing_standard",
"in_out_cubic"
]: # Add more here for comparison
print(transition)
widget = AnimBox()
widget.transition = transition
self.root.add_widget(widget)
Clock.schedule_once(self.run_animation, 1)
_inverse = True
def run_animation(self, dt):
x = (self.root.children[0].width - dp(30)) if self._inverse else 0
for widget in self.root.children:
Animation(
obj_pos=[x, widget.obj_pos[-1]], t=widget.transition, d=3
).start(widget)
self._inverse = not self._inverse
Clock.schedule_once(self.run_animation, 3.1)
MotionApp().run()
.. image:: https://github.com/kivymd/KivyMD/assets/68729523/21c847b0-284a-4796-b704-e4a2531fbb1b
:align: center
"""

import math
import kivy.animation

float_epsilon = 8.3446500e-7


class CubicBezier:
"""Ported from Android source code"""

p0 = 0
p1 = 0
p2 = 0
p3 = 0

def __init__(self, *args):
self.p0, self.p1, self.p2, self.p3 = args

def evaluate_cubic(self, p1, p2, t):
a = 1.0 / 3.0 + (p1 - p2)
b = p2 - 2.0 * p1
c = p1
return 3.0 * ((a * t + b) * t + c) * t

def clamp_range(self, r):
if r < 0.0:
if -float_epsilon <= r < 0.0:
return 0.0
else:
return math.nan
elif r > 1.0:
if 1.0 <= r <= 1.0 + float_epsilon:
return 1.0
else:
return math.nan
else:
return r

def close_to(self, x, y):
return abs(x - y) < float_epsilon

def find_first_cubic_root(self, p0, p1, p2, p3):
a = 3.0 * (p0 - 2.0 * p1 + p2)
b = 3.0 * (p1 - p0)
c = p0
d = -p0 + 3.0 * (p1 - p2) + p3
if self.close_to(d, 0.0):
if self.close_to(a, 0.0):
if self.close_to(b, 0.0):
return math.nan
return self.clamp_range(-c / b)
else:
q = math.sqrt(b * b - 4.0 * a * c)
a2 = 2.0 * a
root = self.clamp_range((q - b) / a2)
if not math.isnan(root):
return root
return self.clamp_range((-b - q) / a2)
a /= d
b /= d
c /= d
o3 = (3.0 * b - a * a) / 9.0
q2 = (2.0 * a * a * a - 9.0 * a * b + 27.0 * c) / 54.0
discriminant = q2 * q2 + o3 * o3 * o3
a3 = a / 3.0

if discriminant < 0.0:
mp33 = -(o3 * o3 * o3)
r = math.sqrt(mp33)
t = -q2 / r
cos_phi = max(-1.0, min(t, 1.0))
phi = math.acos(cos_phi)
t1 = 2.0 * math.cbrt(r)
root = self.clamp_range(t1 * math.cos(phi / 3.0) - a3)
if not math.isnan(root):
return root
root = self.clamp_range(
t1 * math.cos((phi + 2.0 * math.pi) / 3.0) - a3
)
if not math.isnan(root):
return root
return self.clamp_range(
t1 * math.cos((phi + 4.0 * math.pi) / 3.0) - a3
)

elif self.close_to(discriminant, 0.0):
u1 = -math.cbrt(q2)
root = self.clamp_range(2.0 * u1 - a3)
if not math.isnan(root):
return root
return self.clamp_range(-u1 - a3)

sd = math.sqrt(discriminant)
u1 = math.cbrt(-q2 + sd)
v1 = math.cbrt(q2 + sd)
return self.clamp_range(u1 - v1 - a3)

def t(self, value: float):
return self.evaluate_cubic(
self.p1,
self.p3,
self.find_first_cubic_root(
-value,
self.p0 - value,
self.p2 - value,
1.0 - value,
),
)


class MDAnimationTransition(kivy.animation.AnimationTransition):
"""KivyMD's equivalent of kivy's `AnimationTransition`"""

easing_standard = CubicBezier(0.4, 0.0, 0.2, 1.0).t
easing_decelerated = CubicBezier(0.0, 0.0, 0.2, 1.0).t
easing_accelerated = CubicBezier(0.4, 0.0, 1.0, 1.0).t
easing_linear = CubicBezier(0.0, 0.0, 1.0, 1.0).t

# TODO: add `easing_emphasized` here
# it's defination is
# path(M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1)

# Monkey patch kivy's animation module
kivy.animation.AnimationTransition = MDAnimationTransition
27 changes: 25 additions & 2 deletions kivymd/uix/transition/transition.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@

from kivymd.uix.hero import MDHeroFrom, MDHeroTo
from kivymd.uix.screenmanager import MDScreenManager
from kivymd.animation import MDAnimationTransition


class MDTransitionBase(TransitionBase):
Expand Down Expand Up @@ -354,6 +355,22 @@ class MDSharedAxisTransition(MDTransitionBase):
defaults to 0.15 (= 150ms).
"""

switch_animation = OptionProperty(
"easing_decelerated",
options=[
"easing_standard",
"easing_decelerated",
"easing_accelerated",
"easing_linear",
],
)
"""
Custom material design animation transition.
:attr:`switch_animation` is a :class:`~kivy.properties.OptionProperty` and
defaults to `"easing_decelerated"`.
"""

slide_distance = NumericProperty(dp(15))
"""
Distance to which it slides left, right, bottom or up depending on axis.
Expand Down Expand Up @@ -389,6 +406,10 @@ def start(self, manager):
self.ih = hash(self.screen_in)
self.oh = hash(self.screen_out)

# Init pos
self.screen_in.pos = manager.pos
self.screen_out.pos = manager.pos

if self.transition_axis == "z":
if self.ih not in self._s_map.keys():
# Save scale instructions.
Expand Down Expand Up @@ -420,8 +441,8 @@ def start(self, manager):
super().start(manager)

def on_progress(self, progress):
# This code could be simplyfied with setattr, but it's slow.
progress = AnimationTransition.out_cubic(progress)
# This code could be simplyfied with setattr, but it's slow
progress = getattr(MDAnimationTransition, self.switch_animation)(progress)
progress_i = progress - 1
progress_d = progress * 2
# First half.
Expand Down Expand Up @@ -467,6 +488,8 @@ def on_progress(self, progress):
def on_complete(self):
self.screen_in.pos = self.manager.pos
self.screen_out.pos = self.manager.pos
self.screen_out.opacity = 1
self.screen_in.opacity = 1
if self.oh in self._s_map.keys():
self._s_map[self.oh].xyz = (1, 1, 1)
if self.ih in self._s_map.keys():
Expand Down

0 comments on commit 7971b36

Please sign in to comment.