首先,我们要修复一下之前几篇文章中存在的缺陷。在发送超过两行的消息时,屏幕上显示的消息不会自动换行,会超出最大宽度。我们可以通过将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
类的成员变量image
。createLocalImageConfiguration
函数基于给定的上下文创建一个ImageConfiguration
类实例。ImageProvider
的resolve
方法使用给定的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方法在当前对象永久从树中删除时调用ImageStream
的removeListener
方法,以停止监听新的具体的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
控件提供了一个在绘制阶段绘制的画布,CustomPaint
的painter
属性设置在子控件中绘制的画家,也就是项目中的_ImageZoomablePainter
。