diff --git a/examples/search.py b/examples/search.py index 8c2049d4d..72653a55c 100644 --- a/examples/search.py +++ b/examples/search.py @@ -9,18 +9,38 @@ MDScreen: md_bg_color:app.theme_cls.backgroundColor BoxLayout: - padding:[dp(10), dp(10)] + padding:[dp(10), dp(50)] orientation:"vertical" MDSearchBar: - id:bar + id: search_bar supporting_text: "Search in text" - # if you want it to be of fixed size - # adaptive_width:True - MDSearchTrailingIcon: - icon:"microphone" - MDSearchTrailingAvatar: - source:f"{images_path}/logo/kivymd-icon-128.png" + view_root: root + + # Search Bar + MDSearchBarLeadingContainer: + MDSearchLeadingIcon: + icon:"magnify" + + MDSearchBarTrailingContainer: + MDSearchTrailingIcon: + icon:"microphone" + MDSearchTrailingAvatar: + source:f"{images_path}/logo/kivymd-icon-128.png" + + # Search View + MDSearchViewLeadingContainer: + MDSearchLeadingIcon: + icon:"arrow-left" + on_release: search_bar.close_view() + + MDSearchViewTrailingContainer: + MDSearchTrailingIcon: + icon:"window-close" + + MDSearchViewContainer: + MDLabel: + text:"Hello World!" Widget: MDSwitch: diff --git a/kivymd/factory_registers.py b/kivymd/factory_registers.py index 5f709659b..3c473b47c 100644 --- a/kivymd/factory_registers.py +++ b/kivymd/factory_registers.py @@ -133,6 +133,11 @@ register("MDHeroFrom", module="kivymd.uix.hero") register("MDHeroTo", module="kivymd.uix.hero") register("MDSearchBar", module="kivymd.uix.search") -register("MDSearchView", module="kivymd.uix.search") register("MDSearchTrailingAvatar", module="kivymd.uix.search") register("MDSearchTrailingIcon", module="kivymd.uix.search") +register("MDSearchLeadingIcon", module="kivymd.uix.search") +register("MDSearchViewContainer", module="kivymd.uix.search") +register("MDSearchBarLeadingContainer", module="kivymd.uix.search") +register("MDSearchBarTrailingContainer", module="kivymd.uix.search") +register("MDSearchViewLeadingContainer", module="kivymd.uix.search") +register("MDSearchViewTrailingContainer", module="kivymd.uix.search") diff --git a/kivymd/uix/search/__init__.py b/kivymd/uix/search/__init__.py index b3c85ab2b..114d7eee5 100644 --- a/kivymd/uix/search/__init__.py +++ b/kivymd/uix/search/__init__.py @@ -1,6 +1 @@ -from .search import ( - MDSearchBar, - MDSearchTrailingAvatar, - MDSearchTrailingIcon, - MDSearchView, -) +from .search import * diff --git a/kivymd/uix/search/search.kv b/kivymd/uix/search/search.kv index 5864afe50..29ab0eb62 100644 --- a/kivymd/uix/search/search.kv +++ b/kivymd/uix/search/search.kv @@ -1,5 +1,3 @@ -#: import images_path kivymd.images_path - : size_hint_x: None width: dp(30) @@ -7,70 +5,69 @@ : size_hint: [None, 1] width: dp(24) - theme_icon_color: "Custom" icon_color: app.theme_cls.onSurfaceColor -: - overlay_color: [0] * 4 - background: f"{images_path}/transparent.png" - boder: [0] * 4 - _anim_duration: 0 - FloatLayout - MDBoxLayout: - id: root_container - orientation: 'vertical' - md_bg_color: app.theme_cls.surfaceContainerHighColor - adaptive_height: True - size_hint_x: None - canvas: - Color: - rgba: app.theme_cls.outlineColor - Line: - points: - [[self.x,self.y-root._header_height+self.height], - [self.x+self.width, self.y-root._header_height+self.height]] - BoxLayout: - size_hint_y: None - height: root._header_height - spacing: dp(16) - padding: [dp(16), 0] - MDIconButton: - icon: "arrow-left" - size_hint: [None, 1] - width: dp(24) - on_release: root.close_view() - theme_icon_color: "Custom" - icon_color: app.theme_cls.onSurfaceColor +: + size_hint: [None, 1] + width: dp(24) + icon_color: app.theme_cls.onSurfaceColor - BoxLayout: - size_hint: [None, None] - size:[ dp(360), dp(240)] +: + size_hint_x: None + width: self.minimum_width + spacing: dp(16) + +: + size_hint_x: None + width: self.minimum_width + spacing: dp(16) +: + size_hint_x: None + width: self.minimum_width + spacing: dp(16) + +: + size_hint_x: None + width: self.minimum_width + spacing: dp(16) + +: + size_hint_y:None + height:dp(55) + canvas: + Color: + rgba: app.theme_cls.outlineColor + Line: + points: + [[self.x,self.y+self.height], + [self.x+self.width, self.y+self.height]] + +: + MDBoxLayout: + id: root_container + orientation: 'vertical' + md_bg_color: app.theme_cls.surfaceContainerHighColor + size_hint: [None, None] + orientation: 'vertical' + # header + BoxLayout: + id: header + padding: [dp(16), 0] + spacing: dp(16) + size_hint_y: None + height: dp(56) + TextInput: + id: text_input + background_color:[0,0,0,0] + foreground_color: app.theme_cls.onSurfaceColor + cursor_color:app.theme_cls.outlineColor + hint_text_color: app.theme_cls.onSurfaceVariantColor + padding: [0, (self.parent.height - self.font_size - dp(3)) / 2] + multiline: False + font_size: root._font_style["font-size"] + on_focus: if args[-1]: root.switch_state("open") + Widget: : size_hint_y: None height: dp(56) - md_bg_color: app.theme_cls.surfaceContainerHighColor - radius: dp(28) - spacing: dp(16) - padding: [dp(16), 0] - adaptive_width: False - on_release: self._search_view.open_view() - MDIconButton: - icon: root.leading_icon - size_hint: [None, 1] - width: dp(24) - on_release: root.on_leading_icon_release() - on_press: root.on_leading_icon_press() - theme_icon_color: "Custom" - icon_color: app.theme_cls.onSurfaceColor - MDLabel: - text: root.supporting_text - theme_text_color: "Custom" - text_color: app.theme_cls.onSurfaceVariantColor - theme_font_size: "Custom" - font_style: "Title" - role: "medium" - size_hint_x: None - padding: root._supporting_text_padding if root.adaptive_width else 0 - adaptive_width:root.adaptive_width - size_hint_x: 1 if not root.adaptive_width else None diff --git a/kivymd/uix/search/search.py b/kivymd/uix/search/search.py index 41a44175e..00f032c73 100644 --- a/kivymd/uix/search/search.py +++ b/kivymd/uix/search/search.py @@ -1,25 +1,42 @@ from __future__ import annotations __all__ = ( - "MDSearchView", "MDSearchBar", - "MDSearchTrailingIcon", "MDSearchTrailingAvatar", + "MDSearchTrailingIcon", + "MDSearchLeadingIcon", + "MDSearchViewContainer", + "MDSearchBarLeadingContainer", + "MDSearchBarTrailingContainer", + "MDSearchViewLeadingContainer", + "MDSearchViewTrailingContainer", ) import os +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window from kivy.lang import Builder from kivy.metrics import dp -from kivy.properties import StringProperty, ListProperty +from kivy.properties import ( + ColorProperty, + ListProperty, + NumericProperty, + ObjectProperty, + StringProperty, +) from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout from kivy.uix.image import Image -from kivy.uix.modalview import ModalView +from kivy.uix.relativelayout import RelativeLayout +from kivy.uix.widget import Widget from kivymd import uix_path -from kivymd.uix.behaviors import RectangularRippleBehavior +from kivymd.font_definitions import theme_font_styles +from kivymd.uix.behaviors import StencilBehavior from kivymd.uix.boxlayout import MDBoxLayout -from kivymd.uix.button import MDIconButton +from kivymd.uix.label import MDIcon with open( os.path.join(uix_path, "search", "search.kv"), encoding="utf-8" @@ -31,48 +48,195 @@ class MDSearchTrailingAvatar(ButtonBehavior, Image): pass -class MDSearchTrailingIcon(MDIconButton): +class MDSearchLeadingIcon(ButtonBehavior, MDIcon): pass -class MDSearchView(ModalView): - _header_height = dp(70) - def __init__(self, search_bar, *args, **kwargs): - super().__init__(*args, **kwargs) - self.search_bar = search_bar - - def open_view(self): - self.ids.root_container.width = self.search_bar.width - self.ids.root_container.pos = [ - self.search_bar.pos[0], - self.search_bar.pos[1] - self.ids.root_container.height + self.search_bar.height - ] - # TODO: make it like animating from that view - self.open() +class MDSearchTrailingIcon(ButtonBehavior, MDIcon): + pass - def close_view(self): - self.dismiss() + +class MDSearchBarTrailingContainer(BoxLayout): + pass + + +class MDSearchBarLeadingContainer(BoxLayout): + pass + + +class MDSearchViewTrailingContainer(BoxLayout): + pass + + +class MDSearchViewLeadingContainer(BoxLayout): + pass + + +class MDSearchViewContainer(StencilBehavior, BoxLayout): + pass -class MDSearchBar(ButtonBehavior, MDBoxLayout): +class MDSearchWidget(RelativeLayout): + + _font_style = theme_font_styles["Title"]["medium"] + _duration = 0.4 + _transition = "in_out_circ" + state = "closed" + + def __init__(self, root, *args, **kwargs): + super().__init__(*args, **kwargs) + self.root = root + + def update_pos(self, *args): + self.ids.root_container.pos = self.root.pos + + def update_size(self, *args): + self.ids.root_container.size = self.root.size + self.ids.root_container.radius = dp(28) + + def _delayed_run(self, delay, func, *args): + Clock.schedule_once(lambda dt: func(*args), delay) + + def _open(self, d, t, opacity_down, opacity_up, docked): + self.root._view_container.size_hint_y = 1 + self.root._view_container.opacity = 0 + self.ids.root_container.add_widget(self.root._view_container, index=1) + self._delayed_run(d / 4, opacity_up.start, self.root._view_container) + Animation(size=self.size, pos=self.pos, radius=[0] * 4, t=t, d=d).start( + self.ids.root_container + ) + Animation(height=dp(70), t=t, d=d).start(self.ids.header) + opacity_down.start(self.root._bar_trailing_container) + opacity_down.start(self.root._bar_leading_container) + self.root._view_leading_container.opacity = 0 + self.root._view_trailing_container.opacity = 0 + self._delayed_run(d / 2, self.update_state_opened) + self._delayed_run( + d / 2, opacity_up.start, self.root._view_trailing_container + ) + self._delayed_run( + d / 2, opacity_up.start, self.root._view_leading_container + ) + + def _close(self, d, t, opacity_down, opacity_up, docked): + self.root._view_container.size_hint_y = None + self.root._view_container.opacity = 1 + opacity_down.start(self.root._view_container) + if self.root._view_container in self.ids.root_container.children: + self._delayed_run( + d / 2, + self.ids.root_container.remove_widget, + self.root._view_container, + ) + self._delayed_run( + d, setattr, self.root._view_container, "height", dp(55) + ) + Animation( + size=[self.root.width, dp(56)], + pos=self.root.pos, + radius=[dp(28)] * 4, + t=t, + d=d, + ).start(self.ids.root_container) + Animation(height=dp(56), t=t, d=d).start(self.ids.header) + opacity_down.start(self.root._view_trailing_container) + opacity_down.start(self.root._view_leading_container) + self.root._bar_leading_container.opacity = 0 + self.root._bar_trailing_container.opacity = 0 + self._delayed_run(d / 2, self.update_state_closed) + self._delayed_run( + d / 2, opacity_up.start, self.root._bar_trailing_container + ) + self._delayed_run( + d / 2, opacity_up.start, self.root._bar_leading_container + ) + + switching_state = False + + def switch_state(self, new_state): + if self.switching_state or new_state == self.state: + return + self.switching_state = True + docked = False + opacity_down = Animation( + opacity=0, t=self._transition, d=self._duration / 2 + ) + opacity_up = Animation( + opacity=1, t=self._transition, d=self._duration / 2 + ) + + getattr(self, "_" + new_state)( + self._duration, self._transition, opacity_down, opacity_up, docked + ) + Clock.schedule_once( + lambda dt: setattr(self, "switching_state", False), self._duration + ) + + def init_state(self): + self.clean_header() + + def clean_header(self): + for child in self.ids.header.children: + if child.__class__.__name__ != "TextInput": + self.ids.header.remove_widget(child) + + def init_state(self): + self.ids.root_container.size = [self.root.width, dp(56)] + self.update_state_closed() + + def update_state_opened(self, *args): + self.clean_header() + self.ids.header.add_widget(self.root._view_leading_container, index=2) + self.ids.header.add_widget(self.root._view_trailing_container, index=0) + + def update_state_closed(self, *args): + self.clean_header() + self.ids.header.add_widget(self.root._bar_leading_container, index=2) + self.ids.header.add_widget(self.root._bar_trailing_container, index=0) + + +class MDSearchBar(Widget): leading_icon = StringProperty("magnify") supporting_text = StringProperty("Hinted search text") + view_root = ObjectProperty(None) + + # internal props + _search_widget = None + _bar_leading_container = None + _bar_trailing_container = None + _view_leading_container = None + _view_trailing_container = None + _view_container = None + _view_map = { + "MDSearchBarLeadingContainer": "_bar_leading_container", + "MDSearchBarTrailingContainer": "_bar_trailing_container", + "MDSearchViewLeadingContainer": "_view_leading_container", + "MDSearchViewTrailingContainer": "_view_trailing_container", + "MDSearchViewContainer": "_view_container", + } - # internal - _supporting_text_padding = ListProperty([0, 0, dp(30), 0]) - _search_view = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.register_event_type("on_leading_icon_release") - self.register_event_type("on_leading_icon_press") - self._search_view = MDSearchView(self) + self._search_widget = MDSearchWidget(self) + self.bind(pos=self._search_widget.update_pos) + self.bind(size=self._search_widget.update_size) + + def on_supporting_text(self, instance, text): + self._search_widget.ids.text_input.hint_text = text - def on_leading_icon_release(self): - pass + def on_view_root(self, *args): + if self._search_widget.parent: + self._search_widget.parent.remove_widget(self._search_widget) + self.view_root.add_widget(self._search_widget) + self._search_widget.init_state() - def on_leading_icon_press(self): - pass + def add_widget(self, widget): + if widget.__class__.__name__ in self._view_map.keys(): + setattr(self, self._view_map[widget.__class__.__name__], widget) - def on_release(self): - pass + def close_view(self): + self._search_widget.switch_state("close") + + def open_view(self): + self._search_widget.switch_state("open")