Flutter:交互、手势和动画

转载自:https://mp.weixin.qq.com/s/653zL1YvuMwysKu907EQ-g

手势处理

按钮点击

为了获取按钮的点击事件,只需要设置 onPressed 参数就可以了:

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      child: Text('click'),
      onPressed: () => debugPrint('clicked'),
    );
  }
}

任意控件的手势事件

跟 button 不同,大多数的控件没有手势事件监听函数可以设置,为了监听这些控件上的手势事件,我们需要使用另一个控件——GestureDetector(没错,它也是一个控件):

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: Text('text'),
      onTap: () => debugPrint('clicked'),
    );
  }
}

除了上面代码使用到的 onTap,GestureDetector 还支持许多其他事件:

  • onTapDown:按下

  • onTap:点击动作

  • onTapUp:抬起

  • onTapCancel:前面触发了 onTapDown,但并没有完成一个 onTap 动作

  • onDoubleTap:双击

  • onLongPress:长按

  • onScaleStart, onScaleUpdate, onScaleEnd:缩放

  • onVerticalDragDown, onVerticalDragStart, onVerticalDragUpdate, onVerticalDragEnd, onVerticalDragCancel, onVerticalDragUpdate:在竖直方向上移动

  • onHorizontalDragDown, onHorizontalDragStart, onHorizontalDragUpdate, onHorizontalDragEnd, onHorizontalDragCancel, onHorizontalDragUpdate:在水平方向上移动

  • onPanDown, onPanStart, onPanUpdate, onPanEnd, onPanCancel:拖曳(水平、竖直方向上移动)

如果同时设置了 onVerticalXXX 和 onHorizontalXXX,在一个手势里,只有一个会触发(如果用户首先在水平方向移动,则整个过程只触发 onHorizontalUpdate;竖直方向的类似)

这里要说明的是,onVerticalXXX/onHorizontalXXX 和 onPanXXX 不能同时设置。如果同时需要水平、竖直方向的移动,使用 onPanXXX。

如果读者希望在用户点击的时候能够有个水波纹效果,可以使用 InkWell,它的用法跟 GestureDetector 类似,只是少了拖动相关的手势(毕竟,这个水波纹效果只有在点击的时候才有意义)。

原始手势事件监听

GestureDetector 在绝大部分时候都能够满足我们的需求,如果真的满足不了,我们还可以使用最原始的 Listener 控件。

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 管理的栈中。

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');
  },
  // ...
)

把数据展示到 ListView

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。

猜你喜欢

转载自blog.csdn.net/parsebobo/article/details/85264743