您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
324 行
11 KiB
324 行
11 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using uiwidgets;
|
|
using Unity.UIWidgets.animation;
|
|
using Unity.UIWidgets.async2;
|
|
using Unity.UIWidgets.foundation;
|
|
using Unity.UIWidgets.service;
|
|
using Unity.UIWidgets.ui;
|
|
using Unity.UIWidgets.widgets;
|
|
using UnityEngine;
|
|
using Color = Unity.UIWidgets.ui.Color;
|
|
|
|
namespace Unity.UIWidgets.material {
|
|
public class SearchUtils {
|
|
public static Future<T> showSearch<T>(
|
|
BuildContext context,
|
|
SearchDelegate<T> 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<T>(new _SearchPageRoute<T>(
|
|
del: del
|
|
));
|
|
}
|
|
}
|
|
|
|
public abstract class SearchDelegate<T> {
|
|
public SearchDelegate(
|
|
string searchFieldLabel = null,
|
|
TextInputType keyboardType = null,
|
|
TextInputAction textInputAction = TextInputAction.search
|
|
) {
|
|
this.searchFieldLabel = searchFieldLabel;
|
|
this.keyboardType = keyboardType;
|
|
this.textInputAction = textInputAction;
|
|
}
|
|
public abstract Widget buildSuggestions(BuildContext context);
|
|
public abstract Widget buildResults(BuildContext context);
|
|
public abstract Widget buildLeading(BuildContext context);
|
|
public abstract List<Widget> 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 _queryTextController.text; }
|
|
set {
|
|
D.assert(query != null);
|
|
_queryTextController.text = value;
|
|
}
|
|
}
|
|
|
|
public virtual void showResults(BuildContext context) {
|
|
_focusNode?.unfocus();
|
|
_currentBody = _SearchBody.results;
|
|
}
|
|
|
|
public virtual void showSuggestions(BuildContext context) {
|
|
D.assert(_focusNode != null, () => "_focusNode must be set by route before showSuggestions is called.");
|
|
_focusNode.requestFocus();
|
|
_currentBody = _SearchBody.suggestions;
|
|
}
|
|
|
|
public virtual void close(BuildContext context, object result) {
|
|
_currentBody = null;
|
|
_focusNode?.unfocus();
|
|
var state = Navigator.of(context);
|
|
state.popUntil((Route route) => route == _route);
|
|
state.pop(result);
|
|
}
|
|
|
|
public readonly string searchFieldLabel;
|
|
|
|
public readonly TextInputType keyboardType;
|
|
|
|
public readonly TextInputAction textInputAction;
|
|
|
|
public virtual Animation<float> transitionAnimation {
|
|
get { return _proxyAnimation; }
|
|
}
|
|
|
|
internal FocusNode _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 _currentBodyNotifier.value; }
|
|
set { _currentBodyNotifier.value = value; }
|
|
}
|
|
|
|
internal _SearchPageRoute<T> _route;
|
|
}
|
|
|
|
enum _SearchBody {
|
|
suggestions,
|
|
results
|
|
}
|
|
|
|
class _SearchPageRoute<T> : PageRoute {
|
|
public _SearchPageRoute(SearchDelegate<T> 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<T> del;
|
|
|
|
public override Color barrierColor {
|
|
get { return null; }
|
|
}
|
|
|
|
public override string barrierLabel => 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<float> animation,
|
|
Animation<float> secondaryAnimation,
|
|
Widget child
|
|
) {
|
|
return new FadeTransition(
|
|
opacity: animation,
|
|
child: child
|
|
);
|
|
}
|
|
|
|
public override Animation<float> createAnimation() {
|
|
Animation<float> animation = base.createAnimation();
|
|
del._proxyAnimation.parent = animation;
|
|
return animation;
|
|
}
|
|
|
|
public override Widget buildPage(
|
|
BuildContext context,
|
|
Animation<float> animation,
|
|
Animation<float> secondaryAnimation
|
|
) {
|
|
return new _SearchPage<T>(
|
|
del: del,
|
|
animation: animation
|
|
);
|
|
}
|
|
|
|
protected internal override void didComplete(object result) {
|
|
base.didComplete(result);
|
|
D.assert(del._route == this);
|
|
del._route = null;
|
|
del._currentBody = null;
|
|
}
|
|
}
|
|
|
|
class _SearchPage<T> : StatefulWidget {
|
|
public _SearchPage(
|
|
SearchDelegate<T> del,
|
|
Animation<float> animation
|
|
) {
|
|
this.del = del;
|
|
this.animation = animation;
|
|
}
|
|
|
|
public readonly SearchDelegate<T> del;
|
|
|
|
public readonly Animation<float> animation;
|
|
|
|
public override State createState() {
|
|
return new _SearchPageState<T>();
|
|
}
|
|
}
|
|
|
|
class _SearchPageState<T> : State<_SearchPage<T>> {
|
|
|
|
FocusNode focusNode = new FocusNode();
|
|
public override void initState() {
|
|
base.initState();
|
|
widget.del._queryTextController.addListener(_onQueryChanged);
|
|
widget.animation.addStatusListener(_onAnimationStatusChanged);
|
|
widget.del._currentBodyNotifier.addListener(_onSearchBodyChanged);
|
|
focusNode.addListener(_onFocusChanged);
|
|
widget.del._focusNode = focusNode;
|
|
}
|
|
|
|
public override void dispose() {
|
|
base.dispose();
|
|
widget.del._queryTextController.removeListener(_onQueryChanged);
|
|
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
|
widget.del._currentBodyNotifier.removeListener(_onSearchBodyChanged);
|
|
widget.del._focusNode = null;
|
|
focusNode.dispose();
|
|
}
|
|
|
|
void _onAnimationStatusChanged(AnimationStatus status) {
|
|
if (status != AnimationStatus.completed) {
|
|
return;
|
|
}
|
|
|
|
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
|
if (widget.del._currentBody == _SearchBody.suggestions) {
|
|
focusNode.requestFocus();
|
|
}
|
|
}
|
|
|
|
public override void didUpdateWidget(StatefulWidget oldWidget) {
|
|
var _oldWidget = (_SearchPage<T>) oldWidget;
|
|
base.didUpdateWidget(oldWidget);
|
|
if (widget.del != _oldWidget.del) {
|
|
_oldWidget.del._queryTextController.removeListener(_onQueryChanged);
|
|
widget.del._queryTextController.addListener(_onQueryChanged);
|
|
_oldWidget.del._currentBodyNotifier.removeListener(_onSearchBodyChanged);
|
|
widget.del._currentBodyNotifier.addListener(_onSearchBodyChanged);
|
|
_oldWidget.del._focusNode = null;
|
|
widget.del._focusNode = focusNode;
|
|
}
|
|
}
|
|
|
|
void _onFocusChanged() {
|
|
if (focusNode.hasFocus && widget.del._currentBody != _SearchBody.suggestions) {
|
|
widget.del.showSuggestions(context);
|
|
}
|
|
}
|
|
|
|
void _onQueryChanged() {
|
|
setState(() => { });
|
|
}
|
|
|
|
void _onSearchBodyChanged() {
|
|
setState(() => { });
|
|
}
|
|
|
|
public override Widget build(BuildContext context) {
|
|
material_.debugCheckHasMaterialLocalizations(context);
|
|
|
|
ThemeData theme = widget.del.appBarTheme(context);
|
|
string searchFieldLabel = widget.del.searchFieldLabel ?? MaterialLocalizations.of(context).searchFieldLabel;
|
|
Widget body = null;
|
|
switch (widget.del._currentBody) {
|
|
case _SearchBody.suggestions:
|
|
body = new KeyedSubtree(
|
|
key: new ValueKey<_SearchBody>(_SearchBody.suggestions),
|
|
child: widget.del.buildSuggestions(context)
|
|
);
|
|
break;
|
|
case _SearchBody.results:
|
|
body = new KeyedSubtree(
|
|
key: new ValueKey<_SearchBody>(_SearchBody.results),
|
|
child: widget.del.buildResults(context)
|
|
);
|
|
break;
|
|
}
|
|
|
|
string routeName;
|
|
switch (Theme.of(this.context).platform) {
|
|
case RuntimePlatform.IPhonePlayer:
|
|
case RuntimePlatform.OSXEditor:
|
|
case RuntimePlatform.OSXPlayer:
|
|
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: widget.del.buildLeading(context),
|
|
title: new TextField(
|
|
controller: widget.del._queryTextController,
|
|
focusNode: focusNode,
|
|
style: theme.textTheme.headline6,
|
|
textInputAction: widget.del.textInputAction,
|
|
keyboardType: widget.del.keyboardType,
|
|
onSubmitted: (string _) => { widget.del.showResults(context); },
|
|
decoration: new InputDecoration(
|
|
border: InputBorder.none,
|
|
hintText: searchFieldLabel,
|
|
hintStyle: theme.inputDecorationTheme.hintStyle
|
|
)
|
|
),
|
|
actions: widget.del.buildActions(context)
|
|
),
|
|
body: new AnimatedSwitcher(
|
|
duration: new TimeSpan(0, 0, 0, 0, 300),
|
|
child: body
|
|
)
|
|
);
|
|
}
|
|
|
|
TextEditingController queryTextController {
|
|
get { return widget.del._queryTextController; }
|
|
}
|
|
}
|
|
}
|