前言
此功能只是针对GestureDetector、Event Channel 和 Method Channel 的综合协作进行的研究练习,个人认为是无法用于生产的。而就加载大图来说,Flutter image本身的的cacheWidth和cacheHeight就可以实现(以及其它一些方案)。
练习记录,代码可能写的有些随意。
介绍
我们的目标是通过GestureDetector、Event Channel 和 Method Channel的协作,通过原生端(Android)的BitmapRegionDecoder对大图进行分区域显示。
实现图
这个使我们要显示的图片。
尺寸:7680*4320 JPEG 5.86MB
实现
Flutter & GestureDetector
首先我们进行基础页面的绘制
@override
Widget build(BuildContext context) {
return Container(
width: size.width,height: size.height,
color: Colors.white,
child: image(),
);
}
Widget image(){
//GestureDetector 对缩放手势进行监听
return GestureDetector(
onScaleUpdate: scaleUpdate,
onScaleStart: scaleStart,
onScaleEnd: scaleEnd,
child: Stack(
alignment: Alignment.center,
children: [
Container(
color: Colors.grey,
//显示窗口是 400*400
width: 400,height: 400,
//没有数据时,我们加载一个空widget,有图片数据时我们进行图片显示
child:imageData == null ? emptyWidget() : Image.memory(imageData,fit: BoxFit.fill,),
)
],
),
);
}
Widget emptyWidget(){
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,height: 100,color: Colors.red,
),
],
);
}
这个画出来如下图(也就是初次启动,没有任何图像数据时):
下面我们看一下手势回调
缩放手势的回调
手势有三个回调,分别是:(这里要注意,并不是只有两个手指才会触发下面的回调,单指滑动依然会触发)
scaleStart // 触碰屏幕会调用一次
scaleUpdate // 手指滑动时会一直调用这个方法
scaleEnd // 手指离屏后会调用一次
接下来我们声明一些回调中用到的变量。
Offset _lastOffset; //用于记录手指上次的位置
double _x = 0; //手指上次水平的偏移量(即 left)
double _y = 0; //手指上次垂直的偏移量(即 top)
final SplayTreeMap _treeMap = SplayTreeMap(); //用于传递值到 android
在scaleStart 我们记录一下手指的位置
void scaleStart(ScaleStartDetails details){
_lastOffset = details.focalPoint;
}
scaleUpdate中我们记录需要的值,并传递到android端
void scaleUpdate(ScaleUpdateDetails details){
///计算手指每次滑动的值
_x = (details.focalPoint.dx - _lastOffset.dx) ;
_y = (details.focalPoint.dy - _lastOffset.dy);
_treeMap['scale'] = details.scale; //缩放值
_treeMap['left'] = _x;
_treeMap['top'] = _y;
_lastOffset = details.focalPoint;
//将值传递到 android端,这个后面会讲
nativeProxy.onSizeChange(args: _treeMap);
}
void scaleEnd(ScaleEndDetails details){
//咱们要实现的功能里,这个回调啥都不用干
}
至此,我们的flutter侧的手势处理就完成了,下面我们定义 event和method channel用于通信。
flutter侧Event & Method channel
首先我们定义一个_NativeProxy 算是通道总成了,代码很简单:
///定义一个全局变量,用于使用
_NativeProxy nativeProxy = new _NativeProxy();
// channel 和方法 名字要与原生段保持一致
class _NativeProxy{
//event channel 的名字
static const String EVENT_CHANNEL = "lijiaqi.event";
//method channel 的名字
static const String PLUGIN_NAME = "com.lijiaqi.flutter_big_image";
//method channel的具体方法名字
static const String ORDER_DECODE = 'order_decode';
//创建两个channel
final EventChannel eventChannel = EventChannel(EVENT_CHANNEL);
final MethodChannel methodChannel = MethodChannel(PLUGIN_NAME);
// 调用order_decode方法 ,此方法就在上面的 scaleUpdate中调用的
void onSizeChange({Map args})async{
debugPrint('invoke');
return await methodChannel.invokeMethod(ORDER_DECODE,args);
}
}
然后我们在页面的initState方法中,监听一下event channel :
//图像数据 ,对应android的 byte[]
Uint8List imageData;
nativeProxy.eventChannel.receiveBroadcastStream()
.listen((event) {
//原生端 发送来的图片数据
setState(() {
imageData = event;
});
});
齐活,这样我们就完成了flutter端的开发,下面我们开始android的。
Android & ImageEventChannel
这里介绍一下,Event channel可以由android端对flutter传递数据,flutter则以 ‘监听流’ 形式来接收数据。 Method channel 则多用于flutter调用原生端的方法(也可以相互传递数据)。
代码如下:
public class ImageEventChannel implements EventChannel.StreamHandler {
//要确保和flutter一样
private static final String EVENT_CHANNEL = "lijiaqi.event";
//随手写个单例,避免浪费内存
private static volatile ImageEventChannel singleton;
public static ImageEventChannel getSingleton(FlutterPlugin.FlutterPluginBinding binding){
if(singleton == null){
synchronized (ImageEventChannel.class){
if(singleton == null){
singleton = new ImageEventChannel(binding);
}
}
}
return singleton;
}
//通过sink就可以向flutter发送数据了,和stream一样
private EventChannel.EventSink eventSink;
//传送数据的方法,对外开放
public void sinkData(byte[] datas){
if(eventSink == null){
Log.d("event channel","data is empty");
}else{
eventSink.success(datas);
}
}
//初始化并绑定 event channel
private ImageEventChannel(FlutterPlugin.FlutterPluginBinding binding){
EventChannel eventChannel = new EventChannel(binding.getBinaryMessenger(),EVENT_CHANNEL);
eventChannel.setStreamHandler(this);
}
//初始化 event sink
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
this.eventSink = events;
}
//取消后,置空
@Override
public void onCancel(Object arguments) {
eventSink = null;
}
}
下面我们看一下ImageDecoderPlugin
Android & ImageDecoderPlugin
我们定义一个ImageDecoderPlugin插件。
为了阅读时对功能函数的归属有一个概览,我将代码一次性贴在下面,并将说明写在注释里:
public class ImageDecoderPlugin implements FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler {
//字符串要一一对应不然会无效
///这个是 我们的method channel
private static final String PLUGIN_NAME = "com.lijiaqi.flutter_big_image";
//这个是我们method channel的 方法 名字
private static final String ORDER_DECODE = "order_decode";
//event channel
private ImageEventChannel imageEventChannel;
private MethodChannel methodChannel;
private WeakReference<Activity> mActivity;
//读取文件的输入流
private InputStream is;
///构造函数
public ImageDecoderPlugin(Activity mActivity) {
this.mActivity = new WeakReference<>(mActivity);
//raw 文件夹下有咱们的图片
is = mActivity.getResources().openRawResource(R.raw.big5m);
//初始化一些对象,然后对图片进行一个尺寸解析
initDecoder();
}
///图像解码相关的对象
private BitmapFactory.Options options;
private BitmapRegionDecoder regionDecoder;
//用于保存解码后的图片
private Bitmap bitmap;
//原图尺寸
private int imageW,imageH;
private void initDecoder(){
//下面这几行 只解析一下图片的尺寸
options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is,null,options);
imageW = options.outWidth;
imageH = options.outHeight;
//图片编码使用 565 去掉了透明层,可以更节省一些内存,
options.inPreferredConfig = Bitmap.Config.RGB_565;
//将 ‘只解析尺寸’ 关闭
options.inJustDecodeBounds = false;
try {
//初始化我们的 区域解码器
regionDecoder = BitmapRegionDecoder.newInstance(is,false);
} catch (IOException e) {
e.printStackTrace();
}
}
private void logger(String info){
Log.d("android " , info);
}
//当我们通过method channel调用 原生方法时,就会走这个回调
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method){
//我们定义的 order_decode
case ORDER_DECODE:
//先确定一下解码的区域 rect
onSizeChanged(call);
//当我确定了rect后,开始进行解码
final byte[] datas = decodeBitmap();
if(datas == null) return;
//解码后
//我们就通过 event channel将图片返回了
imageEventChannel.sinkData(datas);
break;
default:
break;
}
}
//通过regionDecoder 对 原图 截取rect大小的图片,并返回数据
private byte[] decodeBitmap(){
bitmap = regionDecoder.decodeRegion(rect,options);
if(bitmap == null) return null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG,100,baos);
return baos.toByteArray();
}
// 解码的区域
private final Rect rect = new Rect();
//手势缩放的值,从flutter传递过来的
private double scale;
//图像显示区域 这个就与我们flutter端的灰色窗口对应
private int rectW = 400,rectH = 400;
//最小解码尺寸,用于限定 解码区域
private final int decodeDimenMin = 300;
//最大解码尺寸,用于限定 解码区域
private final int decodeDimenMax = 800;
///扩大缩小系数,不用scale 因为其变化速度过快
private final double expandR = 1.1;
private final double reduce = 0.9;
//第一个调用的方法
private void onSizeChanged(MethodCall call){;
scale = call.argument("scale");
//用于确定 rect左上角的位置
//根据传过来的 两个值,这个矩形的左上角会移动(也就是整个rect会移动)
rect.left -= (int)((double) call.argument("left"));
rect.top -= (int)((double)call.argument("top"));
//对宽高进行相应的缩放
if(scale > 1.0 ){
///放大
rectW = (int)Math.max((rectW/expandR),300);
rectH = (int)Math.max((rectH/expandR),300);
}else if(scale < 1.0 ){
///缩小
rectW = (int)Math.min((rectW/reduce), 800);
rectH = (int)Math.min((rectH/reduce), 800);
}
// 宽度或高度 + left或top 就得出 rect的范围了
rect.right = rect.left + rectW;
rect.bottom = rect.top + rectH;
//为了确保rect不溢出图像区域,我们要进行校准
adjustRect();
}
private void adjustRect(){
//确保 左上角 不会向左上溢出
rect.top = Math.max(rect.top, 0);
rect.left = Math.max(rect.left, 0);
//确保 左上角的尺寸加上 宽高,不会向右下溢出
rect.top = Math.min(rect.top, imageH-rectH);
rect.left = Math.min(rect.left, imageW-rectW);
//与上面同理,我们要确保这个 rect 不会溢出图片的范围
rect.right = Math.min(rect.right, imageW);
rect.bottom = Math.min(rect.bottom, imageH);
rect.right = Math.max(rect.right, rectW);
rect.bottom = Math.max(rect.bottom, rectH);
}
//引擎初始化成功时,会调用此方法
//在此处,我们初始化我们的channel
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
imageEventChannel = ImageEventChannel.getSingleton(binding);
methodChannel = new MethodChannel(binding.getBinaryMessenger(),PLUGIN_NAME);
methodChannel.setMethodCallHandler(this);
}
//解除绑定
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
methodChannel.setMethodCallHandler(null);
methodChannel = null;
}
...
}
至此,插件功能就完成了,我们对这个插件进行一下注册。
注册插件
在我们的MainActivity中, configureFlutterEngine,注册我们刚才的插件:
public class MainActivity extends FlutterActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
flutterEngine.getPlugins().add(new ImageDecoderPlugin(this));
}
}
完成了这步,我们再次运行后,就可以对大图进行区域性的截取并显示了。
谢谢大家阅读,有误之处还请指正。