From e30188fc68e296bfd029770e35b8617c4e099af9 Mon Sep 17 00:00:00 2001 From: iizzaya Date: Thu, 21 Nov 2019 16:17:52 +0800 Subject: [PATCH] [Widget] Add Material Search Widget --- Runtime/material/search.cs | 284 ++++++++++++++++++++++++++++++++ Runtime/material/search.cs.meta | 11 ++ Runtime/widgets/pages.cs | 2 + Runtime/widgets/routes.cs | 5 +- 4 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 Runtime/material/search.cs create mode 100644 Runtime/material/search.cs.meta diff --git a/Runtime/material/search.cs b/Runtime/material/search.cs new file mode 100644 index 00000000..2644b761 --- /dev/null +++ b/Runtime/material/search.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using RSG; +using Unity.UIWidgets.animation; +using Unity.UIWidgets.foundation; +using Unity.UIWidgets.service; +using Unity.UIWidgets.widgets; +using UnityEngine; +using Color = Unity.UIWidgets.ui.Color; + +namespace Unity.UIWidgets.material { + public class SearchUtils { + public static IPromise showSearch( + BuildContext context, + SearchDelegate del, + string query = "" + ) { + D.assert(del != null); + D.assert(context != null); + + del.query = query ?? del.query; + del._currentBody = _SearchBody.suggestions; + return Navigator.of(context).push(new _SearchPageRoute( + del: del + )); + } + } + + public abstract class SearchDelegate { + public abstract Widget buildSuggestions(BuildContext context); + public abstract Widget buildResults(BuildContext context); + public abstract Widget buildLeading(BuildContext context); + public abstract List buildActions(BuildContext context); + + public virtual ThemeData appBarTheme(BuildContext context) { + D.assert(context != null); + ThemeData theme = Theme.of(context); + D.assert(theme != null); + return theme.copyWith( + primaryColor: Colors.white, + primaryIconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), + primaryColorBrightness: Brightness.light, + primaryTextTheme: theme.textTheme + ); + } + + public virtual string query { + get { return this._queryTextController.text; } + set { + D.assert(this.query != null); + this._queryTextController.text = value; + } + } + + public virtual void showResults(BuildContext context) { + this._focusNode.unfocus(); + this._currentBody = _SearchBody.results; + } + + public virtual void showSuggestions(BuildContext context) { + FocusScope.of(context).requestFocus(this._focusNode); + this._currentBody = _SearchBody.suggestions; + } + + public virtual void close(BuildContext context, object result) { + this._currentBody = null; + this._focusNode.unfocus(); + var state = Navigator.of(context); + state.popUntil((Route route) => route == this._route); + state.pop(result); + } + + + public virtual Animation transitionAnimation { + get { return this._proxyAnimation; } + } + + readonly internal FocusNode _focusNode = new FocusNode(); + + readonly internal TextEditingController _queryTextController = new TextEditingController(); + + readonly internal ProxyAnimation _proxyAnimation = new ProxyAnimation(Animations.kAlwaysDismissedAnimation); + + readonly internal ValueNotifier<_SearchBody?> _currentBodyNotifier = new ValueNotifier<_SearchBody?>(null); + + internal _SearchBody? _currentBody { + get { return this._currentBodyNotifier.value; } + set { this._currentBodyNotifier.value = value; } + } + + internal _SearchPageRoute _route; + } + + enum _SearchBody { + suggestions, + results + } + + class _SearchPageRoute : PageRoute { + public _SearchPageRoute(SearchDelegate del) { + D.assert(del != null); + D.assert(del._route == null, + () => $"The {this.del.GetType()} instance is currently used by another active " + + "search. Please close that search by calling close() on the SearchDelegate " + + "before openening another search with the same delegate instance." + ); + this.del = del; + this.del._route = this; + } + + public readonly SearchDelegate del; + + public override Color barrierColor { + get { return null; } + } + + public override TimeSpan transitionDuration { + get { return new TimeSpan(0, 0, 0, 0, 300); } + } + + public override bool maintainState { + get { return false; } + } + + public override Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child + ) { + return new FadeTransition( + opacity: animation, + child: child + ); + } + + public override Animation createAnimation() { + Animation animation = base.createAnimation(); + this.del._proxyAnimation.parent = animation; + return animation; + } + + public override Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation + ) { + return new _SearchPage( + del: this.del, + animation: animation + ); + } + + protected internal override void didComplete(object result) { + base.didComplete(result); + D.assert(this.del._route == this); + this.del._route = null; + this.del._currentBody = null; + } + } + + class _SearchPage : StatefulWidget { + public _SearchPage( + SearchDelegate del, + Animation animation + ) { + this.del = del; + this.animation = animation; + } + + public readonly SearchDelegate del; + + public readonly Animation animation; + + public override State createState() { + return new _SearchPageState(); + } + } + + class _SearchPageState : State<_SearchPage> { + public override void initState() { + base.initState(); + this.queryTextController.addListener(this._onQueryChanged); + this.widget.animation.addStatusListener(this._onAnimationStatusChanged); + this.widget.del._currentBodyNotifier.addListener(this._onSearchBodyChanged); + this.widget.del._focusNode.addListener(this._onFocusChanged); + } + + public override void dispose() { + base.dispose(); + this.queryTextController.removeListener(this._onQueryChanged); + this.widget.animation.removeStatusListener(this._onAnimationStatusChanged); + this.widget.del._currentBodyNotifier.removeListener(this._onSearchBodyChanged); + this.widget.del._focusNode.removeListener(this._onFocusChanged); + } + + void _onAnimationStatusChanged(AnimationStatus status) { + if (status != AnimationStatus.completed) { + return; + } + + this.widget.animation.removeStatusListener(this._onAnimationStatusChanged); + if (this.widget.del._currentBody == _SearchBody.suggestions) { + FocusScope.of(this.context).requestFocus(this.widget.del._focusNode); + } + } + + void _onFocusChanged() { + if (this.widget.del._focusNode.hasFocus && this.widget.del._currentBody != _SearchBody.suggestions) { + this.widget.del.showSuggestions(this.context); + } + } + + void _onQueryChanged() { + this.setState(() => { }); + } + + void _onSearchBodyChanged() { + this.setState(() => { }); + } + + public override Widget build(BuildContext context) { + MaterialD.debugCheckHasMaterialLocalizations(context); + + ThemeData theme = this.widget.del.appBarTheme(context); + string searchFieldLabel = MaterialLocalizations.of(context).searchFieldLabel; + Widget body = null; + switch (this.widget.del._currentBody) { + case _SearchBody.suggestions: + body = new KeyedSubtree( + key: new ValueKey<_SearchBody>(_SearchBody.suggestions), + child: this.widget.del.buildSuggestions(context) + ); + break; + case _SearchBody.results: + body = new KeyedSubtree( + key: new ValueKey<_SearchBody>(_SearchBody.results), + child: this.widget.del.buildResults(context) + ); + break; + } + + string routeName; + switch (Theme.of(this.context).platform) { + case RuntimePlatform.IPhonePlayer: + routeName = ""; + break; + case RuntimePlatform.Android: + routeName = searchFieldLabel; + break; + } + + return new Scaffold( + appBar: new AppBar( + backgroundColor: theme.primaryColor, + iconTheme: theme.primaryIconTheme, + textTheme: theme.primaryTextTheme, + brightness: theme.primaryColorBrightness, + leading: this.widget.del.buildLeading(context), + title: new TextField( + controller: this.queryTextController, + focusNode: this.widget.del._focusNode, + style: theme.textTheme.title, + textInputAction: TextInputAction.search, + onSubmitted: (string _) => { this.widget.del.showResults(context); }, + decoration: new InputDecoration( + border: InputBorder.none, + hintText: searchFieldLabel + ) + ), + actions: this.widget.del.buildActions(context) + ), + body: new AnimatedSwitcher( + duration: new TimeSpan(0, 0, 0, 0, 300), + child: body + ) + ); + } + + TextEditingController queryTextController { + get { return this.widget.del._queryTextController; } + } + } +} \ No newline at end of file diff --git a/Runtime/material/search.cs.meta b/Runtime/material/search.cs.meta new file mode 100644 index 00000000..d08e6aee --- /dev/null +++ b/Runtime/material/search.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 421b92c9ff7d642af9b2e60e013c297d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/widgets/pages.cs b/Runtime/widgets/pages.cs index faab8093..ee314532 100644 --- a/Runtime/widgets/pages.cs +++ b/Runtime/widgets/pages.cs @@ -7,6 +7,8 @@ namespace Unity.UIWidgets.widgets { public abstract class PageRoute : ModalRoute { public readonly bool fullscreenDialog; + public PageRoute() {} + public PageRoute(RouteSettings settings, bool fullscreenDialog = false) : base(settings) { this.fullscreenDialog = fullscreenDialog; } diff --git a/Runtime/widgets/routes.cs b/Runtime/widgets/routes.cs index 594042d9..035bb042 100644 --- a/Runtime/widgets/routes.cs +++ b/Runtime/widgets/routes.cs @@ -471,8 +471,9 @@ public override Widget build(BuildContext context) { } public abstract class ModalRoute : LocalHistoryRouteTransitionRoute { - protected ModalRoute(RouteSettings settings) : base(settings) { - } + + protected ModalRoute() {} + protected ModalRoute(RouteSettings settings) : base(settings) { } public static Color _kTransparent = new Color(0x00000000);