Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add custom animation transition #1694

Merged
merged 2 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading