Skip to content

Commit 1385aa8

Browse files
committed
add MDAnimationTransition
1 parent cfe5aac commit 1385aa8

File tree

6 files changed

+323
-4
lines changed

6 files changed

+323
-4
lines changed

Diff for: examples/material_motion.py

Whitespace-only changes.

Diff for: examples/md_axis_transition.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@
4141
text:root.subtext
4242
font_style:"Label"
4343
role:"large"
44-
theme_text_color:"Custom"
45-
text_color:app.theme_cls.surfaceContainerLowestColor[:-1] + [0.5]
44+
text_color:app.theme_cls.onSurfaceVariantColor
4645
4746
<SettingsScreen@MDScreen>:
4847
name:"main"
@@ -96,7 +95,7 @@
9695
font_style:"Body"
9796
role:"large"
9897
theme_text_color:"Custom"
99-
text_color:app.theme_cls.surfaceContainerLowestColor[:-1] + [0.5]
98+
text_color:app.theme_cls.onSurfaceVariantColor
10099
Image:
101100
size_hint_y:1
102101
source:app.image_path

Diff for: examples/md_transitions.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from kivy.lang import Builder
2+
from kivy.animation import Animation
3+
from kivy.uix.boxlayout import BoxLayout
4+
from kivy.clock import Clock
5+
from kivy.metrics import dp
6+
from kivy.properties import ListProperty
7+
8+
from kivymd.app import MDApp
9+
10+
11+
class AnimBox(BoxLayout):
12+
obj_pos = ListProperty([0, 0])
13+
14+
15+
UI = """
16+
<AnimBox>:
17+
transition:"in_out_bounce"
18+
size_hint_y:None
19+
height:dp(100)
20+
obj_pos:[dp(40), self.pos[-1] + dp(40)]
21+
canvas:
22+
Color:
23+
rgba:app.theme_cls.primaryContainerColor
24+
Rectangle:
25+
size:[self.size[0], dp(5)]
26+
pos:self.pos[0], self.pos[-1] + dp(50)
27+
Color:
28+
rgba:app.theme_cls.primaryColor
29+
Rectangle:
30+
size:[dp(30)] * 2
31+
pos:root.obj_pos
32+
MDLabel:
33+
adaptive_height:True
34+
text:root.transition
35+
padding:[dp(10), 0]
36+
halign:"center"
37+
38+
MDGridLayout:
39+
orientation:"lr-tb"
40+
cols:1
41+
md_bg_color:app.theme_cls.backgroundColor
42+
spacing:dp(10)
43+
"""
44+
45+
46+
class MotionApp(MDApp):
47+
48+
def build(self):
49+
return Builder.load_string(UI)
50+
51+
def on_start(self):
52+
for transition in [
53+
"easing_linear",
54+
"easing_accelerated",
55+
"easing_decelerated",
56+
"easing_standard",
57+
"in_out_cubic"
58+
]: # Add more here for comparison
59+
print(transition)
60+
widget = AnimBox()
61+
widget.transition = transition
62+
self.root.add_widget(widget)
63+
Clock.schedule_once(self.run_animation, 1)
64+
65+
_inverse = True
66+
67+
def run_animation(self, dt):
68+
x = (self.root.children[0].width - dp(30)) if self._inverse else 0
69+
for widget in self.root.children:
70+
Animation(
71+
obj_pos=[x, widget.obj_pos[-1]], t=widget.transition, d=3
72+
).start(widget)
73+
self._inverse = not self._inverse
74+
Clock.schedule_once(self.run_animation, 3.1)
75+
76+
77+
MotionApp().run()

Diff for: kivymd/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,5 @@
6060

6161
import kivymd.factory_registers # NOQA
6262
import kivymd.font_definitions # NOQA
63+
import kivymd.animation # NOQA
6364
from kivymd.tools.packaging.pyinstaller import hooks_path # NOQA

Diff for: kivymd/animation.py

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""
2+
Animation
3+
=========
4+
5+
.. versionadded:: 2.0.0
6+
7+
Adds new transitions to the :class:`~kivy.animation.AnimationTransition` class:
8+
- "easing_standard"
9+
- "easing_decelerated"
10+
- "easing_accelerated"
11+
- "easing_linear"
12+
13+
14+
.. code-block:: python
15+
16+
17+
from kivy.lang import Builder
18+
from kivy.animation import Animation
19+
from kivy.uix.boxlayout import BoxLayout
20+
from kivy.clock import Clock
21+
from kivy.metrics import dp
22+
from kivy.properties import ListProperty
23+
24+
from kivymd.app import MDApp
25+
26+
27+
class AnimBox(BoxLayout):
28+
obj_pos = ListProperty([0, 0])
29+
30+
31+
UI = '''
32+
<AnimBox>:
33+
transition:"in_out_bounce"
34+
size_hint_y:None
35+
height:dp(100)
36+
obj_pos:[dp(40), self.pos[-1] + dp(40)]
37+
canvas:
38+
Color:
39+
rgba:app.theme_cls.primaryContainerColor
40+
Rectangle:
41+
size:[self.size[0], dp(5)]
42+
pos:self.pos[0], self.pos[-1] + dp(50)
43+
Color:
44+
rgba:app.theme_cls.primaryColor
45+
Rectangle:
46+
size:[dp(30)] * 2
47+
pos:root.obj_pos
48+
MDLabel:
49+
adaptive_height:True
50+
text:root.transition
51+
padding:[dp(10), 0]
52+
halign:"center"
53+
54+
MDGridLayout:
55+
orientation:"lr-tb"
56+
cols:1
57+
md_bg_color:app.theme_cls.backgroundColor
58+
spacing:dp(10)
59+
'''
60+
61+
62+
class MotionApp(MDApp):
63+
64+
def build(self):
65+
return Builder.load_string(UI)
66+
67+
def on_start(self):
68+
for transition in [
69+
"easing_linear",
70+
"easing_accelerated",
71+
"easing_decelerated",
72+
"easing_standard",
73+
"in_out_cubic"
74+
]: # Add more here for comparison
75+
print(transition)
76+
widget = AnimBox()
77+
widget.transition = transition
78+
self.root.add_widget(widget)
79+
Clock.schedule_once(self.run_animation, 1)
80+
81+
_inverse = True
82+
def run_animation(self, dt):
83+
x = (self.root.children[0].width - dp(30)) if self._inverse else 0
84+
for widget in self.root.children:
85+
Animation(
86+
obj_pos=[x, widget.obj_pos[-1]], t=widget.transition, d=3
87+
).start(widget)
88+
self._inverse = not self._inverse
89+
Clock.schedule_once(self.run_animation, 3.1)
90+
91+
MotionApp().run()
92+
93+
.. image:: https://github.com/kivymd/KivyMD/assets/68729523/21c847b0-284a-4796-b704-e4a2531fbb1b
94+
:align: center
95+
"""
96+
97+
import math
98+
import kivy.animation
99+
100+
float_epsilon = 8.3446500e-7
101+
102+
103+
class CubicBezier:
104+
"""Ported from Android source code"""
105+
106+
p0 = 0
107+
p1 = 0
108+
p2 = 0
109+
p3 = 0
110+
111+
def __init__(self, *args):
112+
self.p0, self.p1, self.p2, self.p3 = args
113+
114+
def evaluate_cubic(self, p1, p2, t):
115+
a = 1.0 / 3.0 + (p1 - p2)
116+
b = p2 - 2.0 * p1
117+
c = p1
118+
return 3.0 * ((a * t + b) * t + c) * t
119+
120+
def clamp_range(self, r):
121+
if r < 0.0:
122+
if -float_epsilon <= r < 0.0:
123+
return 0.0
124+
else:
125+
return math.nan
126+
elif r > 1.0:
127+
if 1.0 <= r <= 1.0 + float_epsilon:
128+
return 1.0
129+
else:
130+
return math.nan
131+
else:
132+
return r
133+
134+
def close_to(self, x, y):
135+
return abs(x - y) < float_epsilon
136+
137+
def find_first_cubic_root(self, p0, p1, p2, p3):
138+
a = 3.0 * (p0 - 2.0 * p1 + p2)
139+
b = 3.0 * (p1 - p0)
140+
c = p0
141+
d = -p0 + 3.0 * (p1 - p2) + p3
142+
if self.close_to(d, 0.0):
143+
if self.close_to(a, 0.0):
144+
if self.close_to(b, 0.0):
145+
return math.nan
146+
return self.clamp_range(-c / b)
147+
else:
148+
q = math.sqrt(b * b - 4.0 * a * c)
149+
a2 = 2.0 * a
150+
root = self.clamp_range((q - b) / a2)
151+
if not math.isnan(root):
152+
return root
153+
return self.clamp_range((-b - q) / a2)
154+
a /= d
155+
b /= d
156+
c /= d
157+
o3 = (3.0 * b - a * a) / 9.0
158+
q2 = (2.0 * a * a * a - 9.0 * a * b + 27.0 * c) / 54.0
159+
discriminant = q2 * q2 + o3 * o3 * o3
160+
a3 = a / 3.0
161+
162+
if discriminant < 0.0:
163+
mp33 = -(o3 * o3 * o3)
164+
r = math.sqrt(mp33)
165+
t = -q2 / r
166+
cos_phi = max(-1.0, min(t, 1.0))
167+
phi = math.acos(cos_phi)
168+
t1 = 2.0 * math.cbrt(r)
169+
root = self.clamp_range(t1 * math.cos(phi / 3.0) - a3)
170+
if not math.isnan(root):
171+
return root
172+
root = self.clamp_range(
173+
t1 * math.cos((phi + 2.0 * math.pi) / 3.0) - a3
174+
)
175+
if not math.isnan(root):
176+
return root
177+
return self.clamp_range(
178+
t1 * math.cos((phi + 4.0 * math.pi) / 3.0) - a3
179+
)
180+
181+
elif self.close_to(discriminant, 0.0):
182+
u1 = -math.cbrt(q2)
183+
root = self.clamp_range(2.0 * u1 - a3)
184+
if not math.isnan(root):
185+
return root
186+
return self.clamp_range(-u1 - a3)
187+
188+
sd = math.sqrt(discriminant)
189+
u1 = math.cbrt(-q2 + sd)
190+
v1 = math.cbrt(q2 + sd)
191+
return self.clamp_range(u1 - v1 - a3)
192+
193+
def t(self, value: float):
194+
return self.evaluate_cubic(
195+
self.p1,
196+
self.p3,
197+
self.find_first_cubic_root(
198+
-value,
199+
self.p0 - value,
200+
self.p2 - value,
201+
1.0 - value,
202+
),
203+
)
204+
205+
206+
class MDAnimationTransition(kivy.animation.AnimationTransition):
207+
"""KivyMD's equivalent of kivy's `AnimationTransition`"""
208+
209+
easing_standard = CubicBezier(0.4, 0.0, 0.2, 1.0).t
210+
easing_decelerated = CubicBezier(0.0, 0.0, 0.2, 1.0).t
211+
easing_accelerated = CubicBezier(0.4, 0.0, 1.0, 1.0).t
212+
easing_linear = CubicBezier(0.0, 0.0, 1.0, 1.0).t
213+
214+
# TODO: add `easing_emphasized` here
215+
# it's defination is
216+
# 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)
217+
218+
# Monkey patch kivy's animation module
219+
kivy.animation.AnimationTransition = MDAnimationTransition

Diff for: kivymd/uix/transition/transition.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656

5757
from kivymd.uix.hero import MDHeroFrom, MDHeroTo
5858
from kivymd.uix.screenmanager import MDScreenManager
59+
from kivymd.animation import MDAnimationTransition
5960

6061

6162
class MDTransitionBase(TransitionBase):
@@ -351,6 +352,22 @@ class MDSharedAxisTransition(MDTransitionBase):
351352
defaults to 0.15 (= 150ms).
352353
"""
353354

355+
switch_animation = OptionProperty(
356+
"easing_decelerated",
357+
options=[
358+
"easing_standard",
359+
"easing_decelerated",
360+
"easing_accelerated",
361+
"easing_linear",
362+
],
363+
)
364+
"""
365+
Custom material design animation transition.
366+
367+
:attr:`switch_animation` is a :class:`~kivy.properties.OptionProperty` and
368+
defaults to `"easing_decelerated"`.
369+
"""
370+
354371
slide_distance = NumericProperty(dp(15))
355372
"""
356373
Distance to which it slides left, right, bottom or up depending on axis.
@@ -386,6 +403,10 @@ def start(self, manager):
386403
self.ih = hash(self.screen_in)
387404
self.oh = hash(self.screen_out)
388405

406+
# Init pos
407+
self.screen_in.pos = manager.pos
408+
self.screen_out.pos = manager.pos
409+
389410
if self.transition_axis == "z":
390411
if self.ih not in self._s_map.keys():
391412
# Save scale instructions
@@ -418,7 +439,7 @@ def start(self, manager):
418439

419440
def on_progress(self, progress):
420441
# This code could be simplyfied with setattr, but it's slow
421-
progress = AnimationTransition.out_cubic(progress)
442+
progress = getattr(MDAnimationTransition, self.switch_animation)(progress)
422443
progress_i = progress - 1
423444
progress_d = progress * 2
424445
# first half
@@ -464,6 +485,8 @@ def on_progress(self, progress):
464485
def on_complete(self):
465486
self.screen_in.pos = self.manager.pos
466487
self.screen_out.pos = self.manager.pos
488+
self.screen_out.opacity = 1
489+
self.screen_in.opacity = 1
467490
if self.oh in self._s_map.keys():
468491
self._s_map[self.oh].xyz = (1, 1, 1)
469492
if self.ih in self._s_map.keys():

0 commit comments

Comments
 (0)