diff --git a/examples/md_axis_transition.py b/examples/md_axis_transition.py new file mode 100644 index 000000000..47249083f --- /dev/null +++ b/examples/md_axis_transition.py @@ -0,0 +1,221 @@ +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" + BoxLayout: + size_hint_y:None + height:dp(100) + orientation:"vertical" + MDLabel: + id:duration + text:"Duration: 0.2" + adaptive_height:True + halign:"center" + MDSlider: + size_hint_y:None + height:dp(50) + step: 10 + value: 10 + on_value: + duration.text = "Duration: " + str(self.value / 50) + app.transition.duration = self.value/50 + MDSliderHandle: + 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" + +MDScreenManager: + id:s_m + md_bg_color:app.theme_cls.surfaceContainerLowColor + transition:app.transition + 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..78e062aaa 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 @@ -298,3 +307,165 @@ def on_progress(self, progression: float) -> None: self.manager.y - self.manager.height * progression ) self.screen_out.opacity = 1 - progression + + +class MDSharedAxisTransition(MDTransitionBase): + """Android default screen transition""" + + transition_axis = OptionProperty("x", options=["x", "y", "z"]) + """ + Axis of the transition. Available values "x", "y", and "z". + + .. image:: https://github.com/kivymd/KivyMD/assets/68729523/063e478c-9e23-40d4-a8ce-4663b428b575 + :height: 350px + :align: left + + :attr:`transition_axis` is an :class:`~kivy.properties.OptionProperty` + and defaults to `"x"`. + """ + + duration = NumericProperty(0.15) + """ + Duration in seconds of the transition. Android recommends these intervals: + + .. list-table:: Android transition values (in seconds) + :align: left + :header-rows: 1 + + * - Name + - value + * - small_1 + - 0.075 + * - small_2 + - 0.15 + * - medium_1 + - 0.2 + * - medium_2 + - 0.25 + * - large_1 + - 0.3 + * - large_2 + - 0.35 + + :attr:`duration` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0.15 (= 150ms). + """ + + slide_distance = NumericProperty(dp(15)) + """ + Distance to which it slides left, right, bottom or up depending on axis. + + :attr:`slide_distance` is a :class:`~kivy.properties.NumericProperty` and + defaults to `dp(15)`. + """ + + opposite = BooleanProperty(False) + """ + Decides Transition direction. + + :attr:`opposite` is a :class:`~kivy.properties.BooleanProperty` and + defaults to `False`. + """ + + _s_map = {} # scale instruction map + _slide_diff = 0 + + def start(self, manager): + # Transition internal working (for developer only): + # x: + # First half: screen_out opacity 1 -> 0, pos_x: 0 -> - slide distance + # Second half: screen_in opacity 0 -> 1, pos_x: slide distance -> 0 + # y: + # First half: screen_out opacity 1 -> 0, pos_y: 0 -> - slide distance + # Second half: screen_in opacity 0 -> 1, pos_y: slide distance -> 0 + # z: + # First half: screen_out opacity 1 -> 0, scale: 1 -> relative subtracted area + # Second half: screen_in opacity 0 -> 1, scale: relative subtracted area -> 1 + + # Save hash of the objects + self.ih = hash(self.screen_in) + self.oh = hash(self.screen_out) + + if self.transition_axis == "z": + if self.ih not in self._s_map.keys(): + # Save scale instructions + with self.screen_in.canvas.before: + PushMatrix() + self._s_map[self.ih] = Scale() + with self.screen_in.canvas.after: + PopMatrix() + with self.screen_out.canvas.before: + PushMatrix() + self._s_map[self.oh] = Scale() + with self.screen_out.canvas.after: + PopMatrix() + + self._s_map[self.oh].origin = [ + (manager.pos[0] + manager.width) / 2, + (manager.pos[1] + manager.height) / 2, + ] + self._s_map[self.ih].origin = self._s_map[self.oh].origin + # relative subtracted area + self._slide_diff = (manager.width - self.slide_distance) * ( + manager.height - self.slide_distance + ) / (manager.width * manager.height) - 1 + elif self.transition_axis in ["x", "y"]: + # slide distance with opposite logic + self._slide_diff = ( + (1 if self.opposite else -1) * self.slide_distance * 2 + ) + super().start(manager) + + def on_progress(self, progress): + # This code could be simplyfied with setattr, but it's slow + progress = AnimationTransition.out_cubic(progress) + progress_i = progress - 1 + progress_d = progress * 2 + # first half + if progress <= 0.5: + # Screen out animation + if self.transition_axis == "z": + self._s_map[self.oh].xyz = ( + *[1 + self._slide_diff * progress_d] * 2, + 1, + ) + elif self.transition_axis == "x": + self.screen_out.pos = [ + self.manager.pos[0] + self._slide_diff * progress, + self.manager.pos[1], + ] + else: + self.screen_out.pos = [ + self.manager.pos[0], + self.manager.pos[1] - self._slide_diff * progress, + ] + self.screen_out.opacity = 1 - progress_d + self.screen_in.opacity = 0 + # second half + else: + if self.transition_axis == "z": + self._s_map[self.ih].xyz = ( + *[1 - self._slide_diff * progress_i * 2] * 2, + 1, + ) + elif self.transition_axis == "x": + self.screen_in.pos = [ + self.manager.pos[0] + self._slide_diff * progress_i, + self.manager.pos[1], + ] + else: + self.screen_in.pos = [ + self.manager.pos[0], + self.manager.pos[1] - self._slide_diff * progress_i, + ] + self.screen_in.opacity = progress_d - 1 + self.screen_out.opacity = 0 + + def on_complete(self): + self.screen_in.pos = self.manager.pos + self.screen_out.pos = self.manager.pos + 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(): + self._s_map[self.ih].xyz = (1, 1, 1) + super().on_complete()