Flutter实战一Flutter聊天应用(十)

119 篇文章 9 订阅
82 篇文章 1416 订阅

首先,我们要修复一下之前几篇文章中存在的缺陷。在发送超过两行的消息时,屏幕上显示的消息不会自动换行,会超出最大宽度。我们可以通过将Text包装在Container控件中,再添加一个width属性,使其获得一个不超出屏幕大小的宽度。

class ChatMessage extends StatelessWidget {
  //...
  @override
  Widget build(BuildContext context) {
    return new SizeTransition(
        //...
                new Container(
                  margin: const EdgeInsets.only(top: 5.0),
                  child: snapshot.value['imageUrl'] != null ?
                  new Image.network(
                    snapshot.value['imageUrl'],
                    width: 250.0,
                  ):
                  new Container(
                    width: MediaQuery.of(context).size.width * 0.8,
                    child: new Text(snapshot.value['text']),
                  ),
        //...
    );
  }
}

使用MediaQuery可以获取了解当前媒体的大小,我们可以从MediaQuery.of返回的MediaQueryData中读取MediaQueryData.size属性。比如上面代码中的MediaQuery.of(context).size.width可以获得当前屏幕的宽度。

现在应用程序不会因为消息太长而超出屏幕宽度,但这是通过硬编码的方式解决的。我们有更好的解决方案,将显示发送人姓名和消息的Column包装在Flexible控件中,使其填充Row主轴中的可用空间。

class ChatMessage extends StatelessWidget {
  //...
  @override
  Widget build(BuildContext context) {
    return new SizeTransition(
      //...
      child: new Container(
        margin: const EdgeInsets.symmetric(vertical: 10.0),
        child: new Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            new Container(
              //...
            ),
            new Flexible(
              child: new Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  new Text(
                    snapshot.value['senderName'],
                    style: Theme.of(context).textTheme.subhead
                  ),
                  new Container(
                    margin: const EdgeInsets.only(top: 5.0),
                    child: snapshot.value['imageUrl'] != null ?
                    new Image.network(
                      snapshot.value['imageUrl'],
                      width: 250.0,
                    ):
                    new Text(snapshot.value['text']),
                  )
        //...
    );
  }
}

Text控件的父级控件必须有一个相对或固定宽度才能自动换行,如果其父级没有设置宽度,则在父级的上一级中寻找宽度。

这里写图片描述

回到正题,如果用户想要点击查看原图,这是一个很常见的用户操作,因此我们将实现这个功能。首先,我们在项目的lib目录下创建一个image_zoomable.dart文件,并添加下面的代码。

import 'package:flutter/material.dart';

class ImageZoomable extends StatefulWidget {
  ImageZoomable({Key key}) : super(key: key);

  @override
  _ImageZoomableState createState() => new _ImageZoomableState();
}

class _ImageZoomableState extends State<ImageZoomable> {
  @override
  Widget build(BuildContext context) {
    return new Text('图片查看屏幕');
  }
}

现在回到main.dart文件中来,首先我们需要导入image_zoomable.dart文件。

import 'image_zoomable.dart';

为了使用户能够点击应用程序中的图片,我们在ChatMessage类的build()方法中使用新的GestureDetector控件替换Image.network函数,以及添加一个导航器(Navigator)。

class ChatMessage extends StatelessWidget {
  //...
  @override
  Widget build(BuildContext context) {
    return new SizeTransition(
        //...
                  new Container(
                    margin: const EdgeInsets.only(top: 5.0),
                    child: snapshot.value['imageUrl'] != null ?
                    new GestureDetector(
                      onTap: (){
                        Navigator.of(context).push( new MaterialPageRoute<Null>(
                          builder: (BuildContext context) {
                            return new ImageZoomable();
                          }
                        ));
                      },
                      child: new Image.network(
                        snapshot.value['imageUrl'],
                        width: 250.0,
                      ),
                    ):
                    new Text(snapshot.value['text']),
                  )
        //...
    );
  }
}

GestureDetector控件是检测手势的控件,可以识别用户的各种操作手势。比如上面代码中的onTap属性可以识别用户的点击操作,并调用回调处理事件。Navigator是导航器,使用户能从当前屏幕平滑过渡到另一个屏幕,具体内容可以查看《Flutter进阶—路由和导航》。在用户点击图片时,我们使用导航器将用户从聊天屏幕过渡到图片查看屏幕。

如果我们只是想显示图片,不需要放大、缩小和移动图片,可以使用Image控件显示图像。具体实现可以看《Flutter基础—常用控件之图片》。在我们的项目中,想要实现放大、缩小和移动图片的功能,需要使用paintImage函数将图像绘制到画布中的给定矩形内。使用参数canvas设置将画出图像的画布,参数rect设置画布中的矩形,参数image设置要画在画布上的图像。如下面的代码,在image_zoomable.dart文件中添加_ImageZoomablePainter类。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:ui' as ui;

//...

class _ImageZoomablePainter extends CustomPainter {
  const _ImageZoomablePainter({this.image, this.offset, this.zoom});

  final ui.Image image;
  final Offset offset;
  final double zoom;

  @override
  void paint(Canvas canvas, Size size) {
    paintImage(canvas: canvas, rect: offset & (size * zoom), image: image);
  }

  @override
  bool shouldRepaint(_ImageZoomablePainter old) {
    return old.image != image || old.offset != offset || old.zoom != zoom;
  }
}

_ImageZoomablePainter类的构造函数有三个参数,分别是ui.Image类型的image变量,存储用于原始解码的图像数据(像素),Offset类型的offset变量和double类型的_zoom变量,用于计算矩形的大小。关于动画的相关内容,可以查看《Flutter进阶—实现动画效果(一)》

ImageZoomable类定义中,添加一个成员变量来存储图像文件。

class ImageZoomable extends StatefulWidget {
  ImageZoomable(this.image, {Key key}) : super(key: key);

  final ImageProvider image;

  @override
  _ImageZoomableState createState() => new _ImageZoomableState();
}

现在将图像文件传到新的_ImageZoomableState实例,由于paintImage函数的image参数需要的是原始解码图像数据(像素),即ui.Image对象,所以我们需要将ImageProvider转成ui.Image对象。ImageStream表示ui.Image对象及其缩放(由ImageInfo对象表示)。

class _ImageZoomableState extends State<ImageZoomable> {
  ImageStream _imageStream;
  //...
  void _resolveImage() {
    _imageStream = widget.image.resolve(createLocalImageConfiguration(context));
    _imageStream.addListener(_handleImageLoaded);
  }
  //...
}

我们通过抽像类widget找到ImageZoomable类的成员变量imagecreateLocalImageConfiguration函数基于给定的上下文创建一个ImageConfiguration类实例。ImageProviderresolve方法使用给定的ImageConfiguration处理图像来源,返回ImageStream实例。ImageStream类的addListener方法添加一个监听器回调,当具体的ImageInfo对象可用时调用。

接下来,我们在_ImageZoomableState类中添加_handleImageLoaded方法,作为ImageInfo对象可用时的回调。

class _ImageZoomableState extends State<ImageZoomable> {
  ImageStream _imageStream;
  ui.Image _image;
  //...
  void _handleImageLoaded(ImageInfo info, bool synchronousCall) {
    setState(() {
      _image = info.image;
    });
  }
  //...
}

ImageInfo表示一个ui.Image对象与其对应的缩放比例,其image属性返回原始图像像素。

我们现在需要调用_resolveImage方法来加载图像,InheritedWidget会在当前State对象的依赖关系发生变化时调用,例如,如果之前对build的调用引用了随后更改的InheritedWidget(控件的基类可以有效地将信息传播到树枝),则框架将调用此方法来通知此对象有关更改。

class _ImageZoomableState extends State<ImageZoomable> {
  //...
  @override
  void didChangeDependencies() {
    _resolveImage();
    super.didChangeDependencies();
  }
  //...
}

为了防止图像缓存被刷新,我们需要使用reassemble方法。reassemble方法会在调试期间重组应用程序时调用,该方法应重新运行依赖于全局状态的任何初始化逻辑,例如,本地资源的图像加载,因为本地资源可能已经更改。

class _ImageZoomableState extends State<ImageZoomable> {
  //...
  @override
  void reassemble() {
    _resolveImage();
    super.reassemble();
  }
  //...
}

我们需要dispose方法在当前对象永久从树中删除时调用ImageStreamremoveListener方法,以停止监听新的具体的ImageInfo对象。

class _ImageZoomableState extends State<ImageZoomable> {
  //...
  @override
  void dispose() {
    _imageStream.removeListener(_handleImageLoaded);
    super.dispose();
  }
  //...
}

现在我们修改一下_ImageZoomableState类的build方法,使图像绘制在屏幕上。

class _ImageZoomableState extends State<ImageZoomable> {
  //...
  @override
  Widget build(BuildContext context) {
    return new Transform(
        transform: new Matrix4.diagonal3Values(1.0, 1.0, 1.0),
        child: new CustomPaint(
            painter: new _ImageZoomablePainter(
              image: _image,
              offset: Offset.zero,
              zoom: 1.0,
            )
        )
    );
  }
  //...
}

Transform是在绘制子控件之前应用转换的控件,其transform属性在绘制期间转换子控件的矩阵,Matrix4的构建函数Matrix4.diagonal3Values()会创建一个比例矩阵。CustomPaint控件提供了一个在绘制阶段绘制的画布,CustomPaintpainter属性设置在子控件中绘制的画家,也就是项目中的_ImageZoomablePainter

这里写图片描述

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

何小有

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值