Flutter学习指南 系列文章
UI布局和控件
熟悉Dart语言
编写第一个应用
开发环境搭建html
在这一篇文章中,咱们首先介绍手势事件的处理和页面跳转的基础知识,而后经过实现一个 echo 客户端的前端页面来增强学习;最后咱们再学习内置的动画 Widget 以及如何自定义动画效果。前端
为了获取按钮的点击事件,只须要设置 onPressed 参数就能够了:git
class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('click'),
onPressed: () => debugPrint('clicked'),
);
}
}
复制代码
跟 button 不一样,大多数的控件没有手势事件监听函数能够设置,为了监听这些控件上的手势事件,咱们须要使用另外一个控件——GestureDetector(没错,它也是一个控件):github
class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Text('text'),
onTap: () => debugPrint('clicked'),
);
}
}
复制代码
除了上面代码使用到的 onTap,GestureDetector 还支持许多其余事件:web
若是同时设置了 onVerticalXXX 和 onHorizontalXXX,在一个手势里,只有一个会触发(若是用户首先在水平方向移动,则整个过程只触发 onHorizontalUpdate;竖直方向的相似)shell
这里要说明的是,onVerticalXXX/onHorizontalXXX 和 onPanXXX 不能同时设置。若是同时须要水平、竖直方向的移动,使用 onPanXXX。编程
若是读者但愿在用户点击的时候可以有个水波纹效果,可使用 InkWell,它的用法跟 GestureDetector 相似,只是少了拖动相关的手势(毕竟,这个水波纹效果只有在点击的时候才有意义)。bash
GestureDetector 在绝大部分时候都可以知足咱们的需求,若是真的知足不了,咱们还可使用最原始的 Listener 控件。app
class TestWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Listener(
child: Text('text'),
onPointerDown: (event) => print('onPointerDown'),
onPointerUp: (event) => print('onPointerUp'),
onPointerMove: (event) => print('onPointerMove'),
onPointerCancel: (event) => print('onPointerCancel'),
);
}
}
复制代码
Flutter 里全部的东西都是 widget,因此,一个页面,也是 widget。为了调整到新的页面,咱们能够 push 一个 route 到 Navigator 管理的栈中。less
Navigator.push(
context,
MaterialPageRoute(builder: (_) => SecondScreen())
);
复制代码
须要返回的话,pop 掉就能够了:
Navigator.pop(context);
复制代码
下面是完整的例子:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter navigation',
home: FirstScreen(),
);
}
}
class FirstScreen extends StatefulWidget {
@override
State createState() {
return _FirstScreenState();
}
}
class _FirstScreenState extends State<FirstScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Navigation deme'),),
body: Center(
child: RaisedButton(
child: Text('First screen'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => SecondScreen())
);
}
),
),
);
}
}
class SecondScreen extends StatefulWidget {
@override
State createState() {
return _SecondScreenState();
}
}
class _SecondScreenState extends State<SecondScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Navigation deme'),),
body: Center(
child: RaisedButton(
child: Text('Second screen'),
onPressed: () {
Navigator.pop(context);
}
),
),
);
}
}
复制代码
除了打开一个页面,Flutter 也支持从页面返回数据:
Navigator.pop(context, 'message from second screen');
复制代码
因为打开页面是异步的,页面的结果经过一个 Future 来返回:
onPressed: () async {
// Navigator.push 会返回一个 Future<T>,若是你对这里使用的 await不太熟悉,能够参考
// https://www.dartlang.org/guides/language/language-tour#asynchrony-support
var msg = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => SecondScreen())
);
debugPrint('msg = $msg');
}
复制代码
咱们还能够在 MaterialApp 里设置好每一个 route 对应的页面,而后使用 Navigator.pushNamed(context, routeName) 来打开它们:
MaterialApp(
// 从名字叫作 '/' 的 route 开始(也就是 home)
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/about': (context) => AboutScreen(),
},
);
复制代码
接下来,咱们经过实现一个 echo 客户端的前端页面来综合运用前面所学的知识(逻辑部分咱们留到下一篇文章再补充)。
这一节咱们来实现一个用户输入的页面。UI 很简单,就是一个文本框和一个按钮。
class MessageForm extends StatefulWidget {
@override
State createState() {
return _MessageFormState();
}
}
class _MessageFormState extends State<MessageForm> {
final editController = TextEditingController();
// 对象被从 widget 树里永久移除的时候调用 dispose 方法(能够理解为对象要销毁了)
// 这里咱们须要主动再调用 editController.dispose() 以释放资源
@override
void dispose() {
super.dispose();
editController.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16.0),
child: Row(
children: <Widget>[
// 咱们让输入框占满一行里除按钮外的全部空间
Expanded(
child: Container(
margin: EdgeInsets.only(right: 8.0),
child: TextField(
decoration: InputDecoration(
hintText: 'Input message',
contentPadding: EdgeInsets.all(0.0),
),
style: TextStyle(
fontSize: 22.0,
color: Colors.black54
),
controller: editController,
// 自动获取焦点。这样在页面打开时就会自动弹出输入法
autofocus: true,
),
),
),
InkWell(
onTap: () => debugPrint('send: ${editController.text}'),
onDoubleTap: () => debugPrint('double tapped'),
onLongPress: () => debugPrint('long pressed'),
child: Container(
padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
decoration: BoxDecoration(
color: Colors.black12,
borderRadius: BorderRadius.circular(5.0)
),
child: Text('Send'),
),
)
],
),
);
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter UX demo',
home: AddMessageScreen(),
);
}
}
class AddMessageScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Add message'),
),
body: MessageForm(),
);
}
}
复制代码
这里的按钮本应该使用 RaisedButton 或 FlatButton。为了演示如何监听手势事件,咱们这里故意本身用 Container 作了一个按钮,而后经过 InkWell 监听手势事件。InkWell 除了上面展现的几个事件外,还带有一个水波纹效果。若是不须要这个水波纹效果,读者也可使用 GestureDetector。
咱们的 echo 客户端共有两个页面,一个用于展现全部的消息,另外一个页面用户输入消息,后者在上一小节咱们已经写好了。下面,咱们来实现用于展现消息的页面。
咱们的页面包含一个列表和一个按钮,列表用于展现信息,按钮则用来打开上一节咱们所实现的 AddMessageScreen。这里咱们先添加一个按钮并实现页面间的跳转。
// 这是咱们的消息展现页面
class MessageListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Echo client'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// push 一个新的 route 到 Navigator 管理的栈中,以此来打开一个页面
Navigator.push(
context,
MaterialPageRoute(builder: (_) => AddMessageScreen())
);
},
tooltip: 'Add message',
child: Icon(Icons.add),
)
);
}
}
复制代码
在消息的输入页面,咱们点击 Send 按钮后就返回:
onTap: () {
debugPrint('send: ${editController.text}');
Navigator.pop(context);
}
复制代码
最后,咱们加入一些骨架代码,实现一个完整的应用:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter UX demo',
home: MessageListScreen(),
);
}
}
复制代码
可是,上面代码所提供的功能还不够,咱们须要从 AddMessageScreen 中返回一个消息。
首先咱们对数据建模:
class Message {
final String msg;
final int timestamp;
Message(this.msg, this.timestamp);
@override
String toString() {
return 'Message{msg: $msg, timestamp: $timestamp}';
}
}
复制代码
下面是返回数据和接收数据的代码:
onTap: () {
debugPrint('send: ${editController.text}');
final msg = Message(
editController.text,
DateTime.now().millisecondsSinceEpoch
);
Navigator.pop(context, msg);
},
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => AddMessageScreen())
);
debugPrint('result = $result');
},
// ...
)
复制代码
class MessageList extends StatefulWidget {
// 先忽略这里的参数 key,后面咱们就会看到他的做用了
MessageList({Key key}): super(key: key);
@override
State createState() {
return _MessageListState();
}
}
class _MessageListState extends State<MessageList> {
final List<Message> messages = [];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
final msg = messages[index];
final subtitle = DateTime.fromMillisecondsSinceEpoch(msg.timestamp)
.toLocal().toIso8601String();
return ListTile(
title: Text(msg.msg),
subtitle: Text(subtitle),
);
}
);
}
void addMessage(Message msg) {
setState(() {
messages.add(msg);
});
}
}
复制代码
这段代码里惟一的新知识就是给 MessageList 的 key 参数,咱们下面先看看如何使用他,而后再说明它的做用:
class MessageListScreen extends StatelessWidget {
final messageListKey = GlobalKey<_MessageListState>(debugLabel: 'messageListKey');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Echo client'),
),
body: MessageList(key: messageListKey),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => AddMessageScreen())
);
debugPrint('result = $result');
if (result is Message) {
messageListKey.currentState.addMessage(result);
}
},
tooltip: 'Add message',
child: Icon(Icons.add),
)
);
}
}
复制代码
引入一个 GlobalKey 的缘由在于,MessageListScreen 须要把从 AddMessageScreen 返回的数据放到 _MessageListState 中,而咱们没法从 MessageList 拿到这个 state。
GlobalKey 的是应用全局惟一的 key,把这个 key 设置给 MessageList 后,咱们就可以经过这个 key 拿到对应的 statefulWidget 的 state。
如今,总体的效果是这个样子的:
若是你遇到了麻烦,在 Github 上找到全部的代码:
git clone https://github.com/Jekton/flutter_demo.git
cd flutter_demo
git checkout ux-basic
复制代码
Flutter 动画的核心是 Animation,Animation 接受一个时钟信号(vsync),转换为 T 值输出。它控制着动画的进度和状态,但不参与图像的绘制。最基本的 Animation 是 AnimationController,它输出 [0, 1] 之间的值。
为了使用动画,咱们能够用 Flutter 提供的 AnimatedContainer、FadeTransition、ScaleTransition 和 RotationTransition 等 Widget 来完成。
下面咱们就来演示如何使用 ScaleTransition:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'animation',
home: Scaffold(
appBar: AppBar(title: Text('animation'),),
body: AnimWidget(),
),
);
}
}
// 动画是有状态的
class AnimWidget extends StatefulWidget {
@override
State createState() {
return _AnimWidgetState();
}
}
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
var controller;
@override
void initState() {
super.initState();
controller = AnimationController(
// 动画的时长
duration: Duration(milliseconds: 5000),
// 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin
vsync: this,
);
// 调用 forward 方法开始动画
controller.forward();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
child: FlutterLogo(size: 200.0),
scale: controller,
);
}
}
复制代码
AnimationController 的输出是线性的。非线性的效果可使用 CurveAnimation 来实现:
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
CurvedAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(
// 动画的时长
duration: Duration(milliseconds: 5000),
// 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin
vsync: this,
);
curve = CurvedAnimation(
parent: controller,
// 更多的效果,参考 https://docs.flutter.io/flutter/animation/Curves-class.html
curve: Curves.easeInOut,
);
// 调用 forward 方法开始动画
controller.forward();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
child: FlutterLogo(size: 200.0),
// 注意,这里咱们把原先的 controller 改成了 curve
scale: curve,
);
}
}
复制代码
固然,咱们还能够组合不一样的动画:
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
// ...
@override
Widget build(BuildContext context) {
var scaled = ScaleTransition(
child: FlutterLogo(size: 200.0),
scale: curve,
);
return FadeTransition(
child: scaled,
opacity: curve,
);
}
}
复制代码
更多的动画控件,读者能够参考 flutter.io/widgets/ani…。
上一节咱们使用 Flutter 内置的 Widget 来实现动画。他们虽然可以完成平常开发的大部分需求,但总有一些时候不太适用。这时咱们就得本身实现动画效果了。
前面咱们说,AnimationController 的输出在 [0, 1] 之间,这每每对咱们须要实现的动画效果不太方便。为了将数值从 [0, 1] 映射到目标空间,可使用 Tween:
animationValue = Tween(begin: 0.0, end: 200.0).animate(controller)
// 每一帧都会触发 listener 回调
..addListener(() {
// animationValue.value 随着动画的进行不断地变化。咱们利用这个值来实现
// 动画效果
print('value = ${animationValue.value}');
});
复制代码
下面咱们来画一个小圆点,让它往复不断地在正弦曲线上运动。
先来实现小圆点沿着曲线运动的效果:
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
class AnimationDemoView extends StatefulWidget {
@override
State createState() {
return _AnimationState();
}
}
class _AnimationState extends State<AnimationDemoView>
with SingleTickerProviderStateMixin {
static const padding = 16.0;
AnimationController controller;
Animation<double> left;
@override
void initState() {
super.initState();
// 只有在 initState 执行完,咱们才能经过 MediaQuery.of(context) 获取
// mediaQueryData。这里经过建立一个 Future 从而在 Dart 事件队列里插入
// 一个事件,以达到延后执行的目的(相似于在 Android 里 post 一个 Runnable)
// 关于 Dart 的事件队列,读者能够参考 https://webdev.dartlang.org/articles/performance/event-loop
Future(_initState);
}
void _initState() {
controller = AnimationController(
duration: const Duration(milliseconds: 2000),
// 注意类定义的 with SingleTickerProviderStateMixin,提供 vsync 最简单的方法
// 就是继承一个 SingleTickerProviderStateMixin。这里的 vsync 跟 Android 里
// 的 vsync 相似,用来提供时针滴答,触发动画的更新。
vsync: this);
// 咱们经过 MediaQuery 获取屏幕宽度
final mediaQueryData = MediaQuery.of(context);
final displayWidth = mediaQueryData.size.width;
debugPrint('width = $displayWidth');
left = Tween(begin: padding, end: displayWidth - padding).animate(controller)
..addListener(() {
// 调用 setState 触发他从新 build 一个 Widget。在 build 方法里,咱们根据
// Animatable<T> 的当前值来建立 Widget,达到动画的效果(相似 Android 的属性动画)。
setState(() {
// have nothing to do
});
})
// 监听动画状态变化
..addStatusListener((status) {
// 这里咱们让动画往复不断执行
// 一次动画完成
if (status == AnimationStatus.completed) {
// 咱们让动画反正执行一遍
controller.reverse();
// 反着执行的动画结束
} else if (status == AnimationStatus.dismissed) {
// 正着从新开始
controller.forward();
}
});
controller.forward();
}
@override
Widget build(BuildContext context) {
// 假定一个单位是 24
final unit = 24.0;
final marginLeft = left == null ? padding : left.value;
// 把 marginLeft 单位化
final unitizedLeft = (marginLeft - padding) / unit;
final unitizedTop = math.sin(unitizedLeft);
// unitizedTop + 1 是了把 [-1, 1] 之间的值映射到 [0, 2]
// (unitizedTop+1) * unit 后把单位化的值转回来
final marginTop = (unitizedTop + 1) * unit + padding;
return Container(
// 咱们根据动画的进度设置圆点的位置
margin: EdgeInsets.only(left: marginLeft, top: marginTop),
// 画一个小红点
child: Container(
decoration: BoxDecoration(
color: Colors.red, borderRadius: BorderRadius.circular(7.5)),
width: 15.0,
height: 15.0,
),
);
}
@override
void dispose() {
super.dispose();
controller.dispose();
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter animation demo',
home: Scaffold(
appBar: AppBar(title: Text('Animation demo')),
body: AnimationDemoView(),
),
);
}
}
复制代码
上面的动画中,咱们只是对位置作出了改变,下面咱们将在位置变化的同时,也让小圆点从红到蓝进行颜色的变化。
class _AnimationState extends State<AnimationDemoView>
with SingleTickerProviderStateMixin {
// ...
Animation<Color> color;
void _initState() {
// ...
color = ColorTween(begin: Colors.red, end: Colors.blue).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) {
// ...
final color = this.color == null ? Colors.red : this.color.value;
return Container(
// 咱们根据动画的进度设置圆点的位置
margin: EdgeInsets.only(left: marginLeft, top: marginTop),
// 画一个小圆点
child: Container(
decoration: BoxDecoration(
color: color, borderRadius: BorderRadius.circular(7.5)),
width: 15.0,
height: 15.0,
),
);
}
}
复制代码
在 GitHub 上,能够找到全部的代码:
git clone https://github.com/Jekton/flutter_demo.git
cd flutter_demo
git checkout sin-curve
复制代码
在这个例子中,咱们还能够加多一些效果,比方说让小圆点在运动的过程当中大小也不断变化、使用 CurveAnimation 改变它运动的速度,这些就留给读者做为练习吧。