diff --git a/examples/material_motion.py b/examples/material_motion.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/md_axis_transition.py b/examples/md_axis_transition.py index 47249083f..7548c60e0 100644 --- a/examples/md_axis_transition.py +++ b/examples/md_axis_transition.py @@ -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 : name:"main" @@ -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 diff --git a/kivymd/__init__.py b/kivymd/__init__.py index 52f969ccc..c773d74d2 100644 --- a/kivymd/__init__.py +++ b/kivymd/__init__.py @@ -60,4 +60,5 @@ import kivymd.factory_registers # NOQA import kivymd.font_definitions # NOQA +import kivymd.motion # NOQA from kivymd.tools.packaging.pyinstaller import hooks_path # NOQA diff --git a/kivymd/motion.py b/kivymd/motion.py index ed7db14ef..d78a158d4 100644 --- a/kivymd/motion.py +++ b/kivymd/motion.py @@ -3,18 +3,22 @@ """ 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 @@ -37,13 +41,12 @@ def clamp_range(self, 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): @@ -52,13 +55,10 @@ def find_first_cubic_root(self, p0, p1, p2, p3): 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 @@ -66,7 +66,7 @@ def find_first_cubic_root(self, p0, p1, p2, p3): 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) @@ -77,12 +77,16 @@ def find_first_cubic_root(self, p0, p1, p2, p3): 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) + 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) + return self.clamp_range( + t1 * math.cos((phi + 4.0 * math.pi) / 3.0) - a3 + ) - elif self.close_to(discriminant, 0.0): + elif self.close_to(discriminant, 0.0): u1 = -math.cbrt(q2) root = self.clamp_range(2.0 * u1 - a3) if not math.isnan(root): @@ -94,24 +98,30 @@ def find_first_cubic_root(self, p0, p1, p2, p3): v1 = math.cbrt(q2 + sd) return self.clamp_range(u1 - v1 - a3) - def transform(self, value: float): + def t(self, value: float): return self.evaluate_cubic( - self.p1, self.p3, self.find_first_cubic_root( + self.p1, + self.p3, + self.find_first_cubic_root( -value, self.p0 - value, self.p2 - value, 1.0 - value, - ) + ), ) -class MaterialMotion: +class MaterialMotion(kivy.animation.AnimationTransition): + """KivyMD's equivalent of kivy's `AnimationTransition`""" - easing_standard = CubicBezier(0.4, 0.0, 0.2, 1.0) - easing_decelerated = CubicBezier(0.0, 0.0, 0.2, 1.0) - easing_accelerated = CubicBezier(0.4, 0.0, 1.0, 1.0) - easing_linear = CubicBezier(0.0, 0.0, 1.0, 1.0) - - # 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) + 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 = MaterialMotion diff --git a/kivymd/uix/transition/transition.py b/kivymd/uix/transition/transition.py index e350c19a1..31d656e03 100644 --- a/kivymd/uix/transition/transition.py +++ b/kivymd/uix/transition/transition.py @@ -402,7 +402,7 @@ def start(self, manager): # Save hash of the objects 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 @@ -439,9 +439,7 @@ def start(self, manager): def on_progress(self, progress): # This code could be simplyfied with setattr, but it's slow - progress = getattr(MaterialMotion, self.switch_animation).transform( - progress - ) + progress = getattr(MaterialMotion, self.switch_animation)(progress) progress_i = progress - 1 progress_d = progress * 2 # first half diff --git a/main.py b/main.py new file mode 100644 index 000000000..ab55d72d4 --- /dev/null +++ b/main.py @@ -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 = """ +: + 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()