Skip to content

Commit 5fb6654

Browse files
committed
Merge remote-tracking branch 'origin/master'
2 parents b5f2a7b + 8de9393 commit 5fb6654

File tree

7 files changed

+359
-25
lines changed

7 files changed

+359
-25
lines changed

examples/md_axis_transition.py

Lines changed: 2 additions & 3 deletions
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

examples/md_transitions.py

Lines changed: 77 additions & 0 deletions
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()

kivymd/__init__.py

Lines changed: 1 addition & 0 deletions
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

kivymd/animation.py

Lines changed: 219 additions & 0 deletions
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

kivymd/uix/segmentedbutton/segmentedbutton.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -666,20 +666,21 @@ def get_items(self) -> list:
666666
def adjust_segment_radius(self, *args) -> None:
667667
"""Rounds off the first and last elements."""
668668

669-
if self.ids.container.children[0].radius == [0, 0, 0, 0]:
670-
self.ids.container.children[0].radius = (
671-
0,
672-
self.height / 2,
673-
self.height / 2,
674-
0,
675-
)
676-
if self.ids.container.children[-1].radius == [0, 0, 0, 0]:
677-
self.ids.container.children[-1].radius = (
678-
self.height / 2,
679-
0,
680-
0,
681-
self.height / 2,
682-
)
669+
_rad = self.height / 2
670+
671+
_last_radius = [0, _rad, _rad, 0]
672+
_first_radius = [_rad, 0, 0, _rad]
673+
_optimal_radius = [0, 0, 0, 0]
674+
675+
_child_count = len(self.ids.container.children)
676+
677+
for count, child in enumerate(self.ids.container.children):
678+
if count == 0:
679+
child.radius = _last_radius
680+
elif count == _child_count - 1:
681+
child.radius = _first_radius
682+
else:
683+
child.radius = _optimal_radius
683684

684685
def mark_item(self, segment_item: MDSegmentedButtonItem) -> None:
685686
"""Fired when a segment element is clicked (`on_release` event)."""
@@ -700,14 +701,28 @@ def add_widget(self, widget, *args, **kwargs):
700701
widget._segmented_button = self
701702
widget.bind(on_release=self.mark_item)
702703
self.ids.container.add_widget(widget)
703-
Clock.schedule_once(self.adjust_segment_radius)
704+
self.adjust_segment_radius()
704705
elif isinstance(widget, MDSegmentedButtonContainer):
705706
return super().add_widget(widget)
706707

708+
def remove_widget(self, widget, *args, **kwargs):
709+
if isinstance(widget, MDSegmentedButtonItem):
710+
for child in widget.children[0].children:
711+
if isinstance(child, MDSegmentButtonLabel) or isinstance(
712+
child, MDSegmentButtonIcon
713+
):
714+
self._set_size_hint_min_x(child, sign=-1)
715+
self.ids.container.remove_widget(widget)
716+
self.adjust_segment_radius()
717+
elif isinstance(widget, MDSegmentedButtonContainer):
718+
return super().remove_widget(widget)
719+
707720
def _set_size_hint_min_x(
708-
self, widget: MDSegmentButtonLabel | MDSegmentButtonIcon
721+
self, widget: MDSegmentButtonLabel | MDSegmentButtonIcon, sign: int = 1
709722
):
710-
self.ids.container.size_hint_min_x += widget.texture_size[0] + dp(36)
723+
self.ids.container.size_hint_min_x += sign * (
724+
widget.texture_size[0] + dp(36)
725+
)
711726

712727

713728
class MDSegmentedButtonContainer(BoxLayout):

0 commit comments

Comments
 (0)