diff --git a/examples/md_axis_transition.py b/examples/md_axis_transition.py new file mode 100644 index 000000000..33ecda2ee --- /dev/null +++ b/examples/md_axis_transition.py @@ -0,0 +1,204 @@ +from kivy.lang import Builder +import os +import kivymd +from kivymd.app import MDApp +from kivy.uix.screenmanager import ScreenManager +from kivymd.uix.screen import MDScreen +from kivymd.uix.transition import MDSharedAxisTransition +from examples.common_app import CommonApp + +KV = """ +: + group: 'group' + size_hint: None, 1 + width:self.height + +: + icon:"wifi" + text:"Network & Internet" + subtext:"Network settings" + size_hint_y:None + height:dp(70) + padding:dp(10) + spacing:dp(10) + on_release: + app.root.get_screen("battery").ids.main_icon.icon = self.icon + app.root.get_screen("battery").ids.main_text.text = self.text + app.root.transition.opposite = False + app.root.current = "battery" + MDIconButton: + style: "tonal" + size_hint:None, 1 + width:self.height + icon:root.icon + BoxLayout: + orientation:"vertical" + MDLabel: + text:root.text + font_style:"Title" + role:"medium" + MDLabel: + text:root.subtext + font_style:"Label" + role:"large" + theme_text_color:"Custom" + text_color:app.theme_cls.surfaceContainerLowestColor[:-1] + [0.5] + +: + name:"main" + md_bg_color:app.theme_cls.surfaceContainerLowColor + MDBoxLayout: + padding:[dp(10), 0] + orientation:"vertical" + BoxLayout: + size_hint_y:None + height:dp(70) + padding:[0, dp(10)] + MDIconButton: + size_hint:None, None + size:[dp(50)] * 2 + on_release: app.open_menu(self) + icon: "menu" + Widget: + MDIconButton: + size_hint:None, None + size:[dp(50)] * 2 + on_release: app.open_menu(self) + icon: "magnify" + MDIconButton: + size_hint:None, None + size:[dp(50)] * 2 + on_release: app.open_menu(self) + icon: "dots-vertical" + MDLabel: + text:"Settings" + halign:"center" + theme_font_size:"Custom" + font_size:"30sp" + style:"bold" + size_hint_y:None + height:dp(70) + MDBoxLayout: + md_bg_color:app.theme_cls.surfaceContainerHighColor + padding:[dp(10), 0] + radius:[self.height / 2]*4 + size_hint_y:None + height:dp(60) + padding:[dp(10), dp(10)] + spacing:dp(10) + MDIconButton: + size_hint_y:1 + icon:"magnify" + size_hint_x:None + width:self.height + MDLabel: + text:"Search in settings" + font_style:"Body" + role:"large" + theme_text_color:"Custom" + text_color:app.theme_cls.surfaceContainerLowestColor[:-1] + [0.5] + Image: + size_hint_y:1 + source:app.image_path + size_hint_x:None + width:self.height + BoxLayout: + size_hint_y:None + height:dp(20) + MDBoxLayout: + md_bg_color:app.theme_cls.surfaceContainerHighColor + radius:[dp(25)] * 4 + size_hint_y:None + height:self.minimum_height + orientation:"vertical" + SettingsItem: + icon:"wifi" + SettingsItem: + icon:"battery-90" + text:"Battery & Power" + subtext:"42% - About 14hr left" + SettingsItem: + icon:"palette-outline" + text:"Wallpaper & Style" + subtext:"Colors, theme style" + SettingsItem: + icon:"android" + text:"System Info" + subtext:"About system" + BoxLayout: + size_hint_y:None + height:dp(70) + padding:[(self.width - dp(50)*6), dp(25)] + spacing:dp(10) + Check: + active:True + on_active: + setattr(app.transition, "transition_axis", "x") if self.active else app + MDLabel: + size_hint_x:None + width:dp(50) + text:"X" + Check: + on_active: + setattr(app.transition, "transition_axis", "y") if self.active else app + MDLabel: + size_hint_x:None + width:dp(50) + text:"Y" + Check: + on_active: + setattr(app.transition, "transition_axis", "z") if self.active else app + MDLabel: + size_hint_x:None + width:dp(50) + text:"Z" + Widget: + +: + name:"battery" + md_bg_color:app.theme_cls.surfaceContainerLowColor + MDLabel: + id:main_text + text:"Battery" + halign:"center" + theme_font_size:"Custom" + font_size:"30sp" + style:"bold" + size_hint_y:None + height:dp(100) + pos_hint:{"center_y":0.8} + MDIconButton: + id:main_icon + icon:"wifi" + style:"tonal" + pos_hint:{"center_y":0.7, "center_x":0.5} + MDButton: + pos_hint:{"center_x":0.5, "center_y":0.5} + style: "filled" + on_release: + app.root.transition.opposite = True + app.root.current = "main" + MDButtonText: + text: "Go Back" + +ScreenManager: + transition:app.transition + canvas: + Color: + # this is important, else you will see black + rgba:app.theme_cls.surfaceContainerLowColor + Rectangle: + pos:self.pos + size:self.size + SettingsScreen: + BatteryScreen: +""" + + +class ExampleApp(MDApp, CommonApp): + image_path = os.path.join(kivymd.__path__[0], "images", "logo", "kivymd-icon-256.png") + def build(self): + self.transition = MDSharedAxisTransition() + return Builder.load_string(KV) + +ExampleApp().run() diff --git a/kivymd/uix/transition/__init__.py b/kivymd/uix/transition/__init__.py index f2c0a582f..6302f350b 100644 --- a/kivymd/uix/transition/__init__.py +++ b/kivymd/uix/transition/__init__.py @@ -2,4 +2,5 @@ MDFadeSlideTransition, MDSlideTransition, MDSwapTransition, + MDSharedAxisTransition, ) diff --git a/kivymd/uix/transition/transition.py b/kivymd/uix/transition/transition.py index e905ea67c..c429f8edb 100644 --- a/kivymd/uix/transition/transition.py +++ b/kivymd/uix/transition/transition.py @@ -33,17 +33,26 @@ "MDSlideTransition", "MDSwapTransition", "MDTransitionBase", + "MDSharedAxisTransition", ) from kivy import Logger from kivy.animation import Animation, AnimationTransition -from kivy.properties import DictProperty +from kivy.properties import ( + DictProperty, + OptionProperty, + NumericProperty, + BooleanProperty, +) from kivy.uix.screenmanager import ( ScreenManagerException, SlideTransition, SwapTransition, TransitionBase, ) +from kivy.graphics import PopMatrix, PushMatrix, Scale +from kivy.animation import Animation, AnimationTransition +from kivy.metrics import dp from kivymd.uix.hero import MDHeroFrom, MDHeroTo from kivymd.uix.screenmanager import MDScreenManager @@ -84,9 +93,7 @@ class documentation. def start(self, instance_screen_manager: MDScreenManager) -> None: super().start(instance_screen_manager) - {"in": self.animated_hero_in, "out": self.animated_hero_out}[ - self._direction - ]() + {"in": self.animated_hero_in, "out": self.animated_hero_out}[self._direction]() def animated_hero_in(self) -> None: """Animates the flight of heroes from screen **A** to screen **B**.""" @@ -99,9 +106,9 @@ def animated_hero_in(self) -> None: # Get child widget of the 'MDHeroFrom' container. hero_widget = hero_from_widget.children[0] - self._hero_from_widget_children[ - hero_from_widget.tag - ] = hero_widget + self._hero_from_widget_children[hero_from_widget.tag] = ( + hero_widget + ) # Removing the child widget from the 'MDHeroFrom' # container. @@ -145,36 +152,24 @@ def animated_hero_out(self) -> None: for heroes_tag in self.manager.current_heroes: for hero_to_widget in self.screen_out.heroes_to: if hero_to_widget.tag == heroes_tag: - hero_from_children = self._hero_from_widget_children[ - heroes_tag - ] + hero_from_children = self._hero_from_widget_children[heroes_tag] hero_to_widget.remove_widget(hero_from_children) - self.manager.get_root_window().add_widget( - hero_from_children - ) + self.manager.get_root_window().add_widget(hero_from_children) - for ( - hero_from_widget - ) in self.manager.get_hero_from_widget(): + for hero_from_widget in self.manager.get_hero_from_widget(): hero_from_widget.dispatch( "on_transform_out", - self._hero_from_widget_children[ - hero_from_widget.tag - ], + self._hero_from_widget_children[hero_from_widget.tag], self.duration, ) Animation( pos=self.screen_in.to_widget( - *hero_from_widget.to_window( - *hero_from_widget.pos - ) + *hero_from_widget.to_window(*hero_from_widget.pos) ), size=hero_from_widget.size, d=self.duration, ).start( - self._hero_from_widget_children[ - hero_from_widget.tag - ] + self._hero_from_widget_children[hero_from_widget.tag] ) def on_complete(self) -> None: @@ -189,21 +184,15 @@ def on_complete(self) -> None: for hero_from_widget in self.manager.get_hero_from_widget(): for heroes_tag in self.manager.current_heroes: if heroes_tag == hero_from_widget.tag: - hero_from_children = self._hero_from_widget_children[ - heroes_tag - ] - self.manager.get_root_window().remove_widget( - hero_from_children - ) + hero_from_children = self._hero_from_widget_children[heroes_tag] + self.manager.get_root_window().remove_widget(hero_from_children) # Adding a child widget from the 'MDHeraFrom' container # to the 'MDHeroTo' container. if self._direction == "in": for hero_to_widget in self.screen_in.heroes_to: if hero_to_widget.tag == heroes_tag: - hero_to_widget.add_widget( - hero_from_children - ) + hero_to_widget.add_widget(hero_from_children) # Restores the child widget for the 'MDHeraFrom' # container. elif self._direction == "out": @@ -225,9 +214,7 @@ def _check_widget_properties(self, hero_from_widget: MDHeroFrom): # The 'MDHeroFrom' widget allows you to place only one widget in # itself. if len(hero_from_widget.children) > 1: - raise Exception( - f"{hero_from_widget.__class__} accept only one widget" - ) + raise Exception(f"{hero_from_widget.__class__} accept only one widget") # For new API support. def _check_hero_to_widget_tag( @@ -258,9 +245,7 @@ def start(self, instance_screen_manager: MDScreenManager) -> None: self.manager = instance_screen_manager self._anim = Animation(d=self.duration, s=0) - self._anim.bind( - on_progress=self._on_progress, on_complete=self._on_complete - ) + self._anim.bind(on_progress=self._on_progress, on_complete=self._on_complete) if self._direction == "in": self.add_screen(self.screen_in) @@ -294,7 +279,96 @@ def on_progress(self, progression: float) -> None: ) - self.screen_in.height self.screen_in.opacity = progression if self._direction == "out": - self.screen_out.y = ( - self.manager.y - self.manager.height * progression - ) + self.screen_out.y = self.manager.y - self.manager.height * progression self.screen_out.opacity = 1 - progression + + +class MDSharedAxisTransition(TransitionBase): + transition_axis = OptionProperty("x", options=["x", "y", "z"]) + + duration = OptionProperty( + 0.15, + options=[0.075, 0.15, 0.2, 0.25, 0.3, 0.35], + ) + slide_distance = NumericProperty(dp(30)) + opposite = BooleanProperty(False) + + _s_map = {} # scale instruction map + _const = 0 # for x and y + _width_diff = 0 # for z + _height_diff = 0 # for z + + def start(self, manager): + self.screen_in_hash = hash(self.screen_in) + self.screen_out_hash = hash(self.screen_out) + if self.transition_axis == "z": + if self.screen_in_hash not in self._s_map.keys(): + self._s_map[self.screen_in_hash] = [] + self._s_map[self.screen_out_hash] = [] + with self.screen_in.canvas.before: + self._s_map[self.screen_in_hash].append(PushMatrix()) + self._s_map[self.screen_in_hash].append(Scale()) + with self.screen_in.canvas.after: + self._s_map[self.screen_in_hash].append(PopMatrix()) + with self.screen_out.canvas.before: + self._s_map[self.screen_out_hash].append(PushMatrix()) + self._s_map[self.screen_out_hash].append(Scale()) + with self.screen_out.canvas.after: + self._s_map[self.screen_out_hash].append(PopMatrix()) + # Assuming screen sizes are same + self._s_map[self.screen_out_hash][1].origin = [ + self.screen_out.width / 2, + self.screen_out.height / 2, + ] + self._s_map[self.screen_in_hash][1].origin = self._s_map[ + self.screen_out_hash + ][1].origin + # set constants + self._width_diff = 1 - (self.screen_out.width - self.slide_distance) / ( + self.screen_out.width + ) + self._height_diff = 1 - (self.screen_out.height - self.slide_distance) / ( + self.screen_out.height + ) + elif self.transition_axis in ["x", "y"]: + self._const = (1 if self.opposite else -1) * self.slide_distance * 2 + super().start(manager) + + def on_progress(self, progress): + progress = AnimationTransition.out_cubic(progress) + if progress <= 0.5: + # Screen out animation + if self.transition_axis == "z": + self._s_map[self.screen_out_hash][1].x = ( + 1 - self._width_diff * 2 * progress + ) + self._s_map[self.screen_out_hash][1].y = ( + 1 - self._height_diff * 2 * progress + ) + self.screen_out.pos = [0,0] + elif self.transition_axis == "x": + self.screen_out.x = self._const * progress + self.screen_out.y = 0 + else: + self.screen_out.y = self._const * progress + self.screen_out.x = 0 + self.screen_out.opacity = 1 - (2 * progress) + self.screen_in.opacity = 0 + else: + # Screen in animation + if self.transition_axis == "z": + self._s_map[self.screen_in_hash][1].x = ( + -self._width_diff + 1 + (self._width_diff * progress) + ) + self._s_map[self.screen_in_hash][1].y = ( + -self._height_diff + 1 + (self._height_diff * progress) + ) + self.screen_in.pos = [0,0] + elif self.transition_axis == "x": + self.screen_in.x = -self._const * (1 - progress) + self.screen_in.y = 0 + else: + self.screen_in.y = -self._const * (1 - progress) + self.screen_in.x = 0 + self.screen_in.opacity = (2 * progress) - 1 + self.screen_out.opacity = 0