(5) AppPage()
基础页面
AppPage
是整个App的入口,在这里实现Tabbar、SearchBar等基础控件。
在分析AppPage
页面之前先说一下Scaffold
这个widget,这里我们可以把它理解为页面,类似OC里面的UIViewController:
this.appBar, //横向水平布局,通常显示在顶部(*)
this.body, // 内容(*)
this.floatingActionButton, //悬浮按钮,就是上图右下角按钮(*)
this.floatingActionButtonLocation, //悬浮按钮位置
//悬浮按钮在[floatingActionButtonLocation]出现/消失动画
this.floatingActionButtonAnimator,
//在底部呈现一组button,显示于[bottomNavigationBar]之上,[body]之下
this.persistentFooterButtons,
//一个垂直面板,显示于左侧,初始处于隐藏状态(*)
this.drawer,
this.endDrawer,
//出现于底部的一系列水平按钮(*)
this.bottomNavigationBar,
//底部持久化提示框
this.bottomSheet,
//内容背景颜色
this.backgroundColor,
//弃用,使用[resizeToAvoidBottomInset]
this.resizeToAvoidBottomPadding,
//重新计算布局空间大小
this.resizeToAvoidBottomInset,
//是否显示到底部,默认为true将显示到顶部状态栏
this.primary = true,
//
this.drawerDragStartBehavior = DragStartBehavior.down,
AppPage:
Widget build(BuildContext context) {
var db = Provider.db;
return new Scaffold(
appBar: new AppBar(title: buildSearchInput(context)),
body: new TabBarView(controller: controller, children: <Widget>[
new FirstPage(),
new WidgetPage(db),
new CollectionPage(),
FourthPage()
]),
bottomNavigationBar: Material(
color: const Color(0xFFF0EEEF), //底部导航栏主题颜色
child: SafeArea(
child: Container(
height: 65.0,
decoration: BoxDecoration(
color: const Color(0xFFF0F0F0),
boxShadow: <BoxShadow>[
BoxShadow(
color: const Color(0xFFd0d0d0),
blurRadius: 3.0,
spreadRadius: 2.0,
offset: Offset(-1.0, -1.0),
),
],
),
child: TabBar(
controller: controller,
//tab标签的下划线颜色
indicatorColor: Theme.of(context).primaryColor,
// labelColor: const Color(0xFF000000),
indicatorWeight: 3.0,
//labelcolor 选中的
labelColor: Theme.of(context).primaryColor,
//labelColor: Colors.green,
unselectedLabelColor: const Color(0xFF8E8E8E),
tabs: myTabs),
),
),
),
);
主要是通过AppBar-->buildSearchInput()
(搜索框)、body-->TabBarView()
(页面)、bottomNavigationBar-->TabBar()
(tabbar)三部分组成,下面我们来依次拆解。
-
buildSearchInput
buildSearchInput
函数里是创建的SearchInput
对象:
构造函数:
final getResults;//获取搜索内容函数
final ValueChanged<String> onSubmitted;//没有用到
final VoidCallback onSubmitPressed;//没有用到
SearchInput(this.getResults, this.onSubmitted, this.onSubmitPressed);//构造函数
build
函数除了MaterialSearchInput
之外都是一些基础wiget布局,其他不做阐述,我们来看一下MaterialSearchInput
:
build
函数:
Widget build(BuildContext context) {
final TextStyle valueStyle = Theme.of(context).textTheme.subhead;
return new InkWell(
onTap: () => _showMaterialSearch(context),
child: new FormField<T>(
key: _formFieldKey,
validator: widget.validator,
onSaved: widget.onSaved,
autovalidate: autovalidate,
builder: (FormFieldState<T> field) {
return new InputDecorator(
isEmpty: _isEmpty(field),
decoration: new InputDecoration(
labelText: widget.placeholder,
border: InputBorder.none,
errorText: field.errorText,
),
child: _isEmpty(field)
? null
: new Text(
widget.formatter != null
? widget.formatter(field.value)
: field.value.toString(),
style: valueStyle),
);
},
),
);
}
可以看到这搜索框是有一个FormField
来实现的,这里实例化对象的时候只用到了getResults
和placeholder
我暂时只对这两个做说明,其他属性如果有感兴趣的同学可以自行去了解。
接下来我们主要研究_showMaterialSearch
,也就是点击之后跳转的搜索页面。
_showMaterialSearch(BuildContext context) {
Navigator.of(context)
.push(_buildMaterialSearchPage(context))
.then((dynamic value) {
if (value != null) {
_formFieldKey.currentState.didChange(value);
widget.onSelect(value);
}
});
}
_showMaterialSearch
-->_MaterialSearchPageRoute
-->MaterialSearch
build函数:
Widget build(BuildContext context) {
var results =
(widget.results ?? _results).where((MaterialSearchResult result) {
if (widget.filter != null) {
return widget.filter(result.value, _criteria);
}
//only apply default filter if used the `results` option
//because getResults may already have applied some filter if `filter` option was omited.
else if (widget.results != null) {
return _filter(result.value, _criteria);
}
return true;
}).toList();
if (widget.sort != null) {
results.sort((a, b) => widget.sort(a.value, b.value, _criteria));
}
results = results.take(widget.limit).toList();
IconThemeData iconTheme =
Theme.of(context).iconTheme.copyWith(color: widget.iconColor);
return new Scaffold(
appBar: new AppBar(
leading: widget.leading,
backgroundColor: widget.barBackgroundColor,
iconTheme: iconTheme,
title: new TextField(
controller: _controller,
autofocus: true,
decoration:
new InputDecoration.collapsed(hintText: widget.placeholder),
style: Theme.of(context).textTheme.title,
onSubmitted: (String value) {
if (widget.onSubmit != null) {
widget.onSubmit(value);
}
},
),
actions: _criteria.length == 0
? []
: [
new IconButton(
icon: new Icon(Icons.clear),
onPressed: () {
setState(() {
_controller.text = _criteria = '';
});
}),
],
),
body: buildBody(results),
);
}
组成部分有两部分
- 1)
AppBar
(主要是textfield)
用一个TextEditingController
来配合监听输入框的文字变化
_controller.addListener(() {
setState(() {
_criteria = _controller.value.text;
if (widget.getResults != null) {
_getResultsDebounced();
}
});
});
通过构造函数传进来的getResults
来去数据库获取搜索结果。
Timer _resultsTimer;
Future _getResultsDebounced() async {
if (_results.length == 0) {
setState(() {
_loading = true;
});
}
if (_resultsTimer != null && _resultsTimer.isActive) {
_resultsTimer.cancel();
}
//延迟400毫秒再执行
_resultsTimer = new Timer(new Duration(milliseconds: 400), () async {
if (!mounted) {
return;
}
setState(() {
_loading = true;
});
var results = await widget.getResults(_criteria);
if (!mounted) {
return;
}
if (results != null) {
setState(() {
_loading = false;
_results = results;
});
}
});
}
- 2)
body
(buildBody)
最外层代码不讲解了,我都注释好了:
Widget buildBody(List results) {
if (_criteria.isEmpty) {//如果没有搜索关键字则显示历史记录
return History();
} else if (_loading) {//正在搜索显示加载框
return new Center(
child: new Padding(
padding: const EdgeInsets.only(top: 50.0),
child: new CircularProgressIndicator()
)
);
}
if (results.isNotEmpty) {//如果有搜索结果就显示搜索列表
var content = new SingleChildScrollView(
child: new Column(
children: results
)
);
return content;
}
return Center(child: Text("暂无数据"));//这个是有搜索关键字而没有搜索结果的时候显示暂无数据
}
看一下搜索历史记录页面:
-
History()
build
函数:
Widget build(BuildContext context) {
//获取历史记录的widget list
List<Widget> childList = buildChips(context);
if (childList.length == 0) {//如果没有历史记录
return Center(
child: Text("当前历史面板为空"),
);
}
return Column(//有历史记录
children: <Widget>[
Container(//头部 历史搜索文字
alignment: Alignment.centerLeft,
padding: EdgeInsets.fromLTRB(12.0, 12, 12, 0),
child: InkWell(
onLongPress: () {//长按情况搜索历史记录
searchHistoryList.clear();
},
child: Text('历史搜索'),
),
),
Container(//搜索历史列表
padding: EdgeInsets.only(left: 10),
alignment: Alignment.topLeft,
child: Wrap(
spacing: 6.0, // gap between adjacent chips
runSpacing: 0.0, // gap between lines
children: childList
),
)
],
);
}
获取历史记录的widget list 方法buildChips
:
buildChips(BuildContext context) {
List<Widget> list = [];//存储搜索列表widget
List<SearchHistory> historyList = searchHistoryList.getList();//获取搜索记录数据(SearchHistory)
print("historyList> $historyList");
Color bgColor = Theme.of(context).primaryColor;
historyList.forEach((SearchHistory value) {//遍历历史记录数据 转成widget装入list
Widget icon = CircleAvatar(
backgroundColor: bgColor,
child: Text(
value.name.substring(0, 1),
style: TextStyle(color: Colors.white),
),
);
if (WidgetName2Icon.icons[value.name] != null) {
icon = Icon(WidgetName2Icon.icons[value.name], size: 25);
}
list.add(
InkWell(
onTap: () {//跳转
Application.router.navigateTo(context, "${value.targetRouter}", transition: TransitionType.inFromRight);
},
child: Chip(
avatar: icon,
label: Text("${value.name}"),
),
)
);
});
return list;
}
代码都已做了详细注释,不做过多解释了。
- 搜索结果列表
首先我们看results
数据:
Widget buildSearchInput(BuildContext context) {
return new SearchInput((value) async {
if (value != '') {
List<WidgetPoint> list = await widgetControl.search(value);
return list
.map((item) => new MaterialSearchResult<String>(
value: item.name,
icon: WidgetName2Icon.icons[item.name] ?? null,
text: 'widget',
onTap: () {
onWidgetTap(item, context);
},
))
.toList();
} else {
return null;
}
}, (value) {}, () {});
}
不难看出results
里面装的是MaterialSearchResult
的实例对象,MaterialSearchResult
build 方法:
Widget build(BuildContext context) {
return new InkWell(
onTap: this.onTap,
child: new Container(
height: 64.0,
padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 10.0),
child: new Row(
children: <Widget>[
new Container(width: 30.0, margin: EdgeInsets.only(right: 10), child: new Icon(icon)) ?? null,
new Expanded(child: new Text(value, style: Theme.of(context).textTheme.subhead)),
new Text(text, style: Theme.of(context).textTheme.subhead)
],
),
),
);
}
跳转代码:
oid onWidgetTap(WidgetPoint widgetPoint, BuildContext context) {
List widgetDemosList = new WidgetDemoList().getDemos();//获取所有注册过的demo页面
String targetName = widgetPoint.name;
String targetRouter = '/category/error/404';
widgetDemosList.forEach((item) {
if (item.name == targetName) {
targetRouter = item.routerName;
}
});
//添加历史记录到SharedPreferences
searchHistoryList
.add(SearchHistory(name: targetName, targetRouter: targetRouter));
print("searchHistoryList ${searchHistoryList.toString()}");
Application.router.navigateTo(context, "$targetRouter");
}
这也搜索结果列表的逻辑也就出来了。
-
TabBarView
这个在稍后我们进行详细的分开拆解。 bottomNavigationBar
bottomNavigationBar: Material(
color: const Color(0xFFF0EEEF), //底部导航栏主题颜色
child: SafeArea(//safeArea
child: Container(
height: 65.0,
decoration: BoxDecoration(//阴影
color: const Color(0xFFF0F0F0),
boxShadow: <BoxShadow>[
BoxShadow(
color: const Color(0xFFd0d0d0),
blurRadius: 3.0,
spreadRadius: 2.0,
offset: Offset(-1.0, -1.0),
),
],
),
child: TabBar(//下面的tabbar
controller: controller,
//tab标签的下划线颜色
indicatorColor: Theme.of(context).primaryColor,
// labelColor: const Color(0xFF000000),
indicatorWeight: 3.0,
//labelcolor 选中的
labelColor: Theme.of(context).primaryColor,
//labelColor: Colors.green,
unselectedLabelColor: const Color(0xFF8E8E8E),
tabs: myTabs),
),
),
),
这里通过controller
将TabBarView
和TabBar
关联起来进行联动。