Flutter入门练习——Evenet&Method Channel协做加载大图

前言

此功能只是针对GestureDetector、Event Channel 和 Method Channel 的综合协做进行的研究练习,我的认为是没法用于生产的。而就加载大图来讲,Flutter image自己的的cacheWidth和cacheHeight就能够实现(以及其它一些方案)。android

练习记录,代码可能写的有些随意。
复制代码

介绍

咱们的目标是经过GestureDetector、Event Channel 和 Method Channel的协做,经过原生端(Android)的BitmapRegionDecoder对大图进行分区域显示。markdown

实现图

这个使咱们要显示的图片。async

尺寸:7680*4320 JPEG 5.86MBide

实现

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,
        ),
      ],
    );
  }
  
复制代码

这个画出来以下图(也就是初次启动,没有任何图像数据时):post

下面咱们看一下手势回调ui

缩放手势的回调

手势有三个回调,分别是:(这里要注意,并非只有两个手指才会触发下面的回调,单指滑动依然会触发)this

scaleStart // 触碰屏幕会调用一次
scaleUpdate // 手指滑动时会一直调用这个方法
scaleEnd // 手指离屏后会调用一次
复制代码

接下来咱们声明一些回调中用到的变量。编码

Offset _lastOffset;  //用于记录手指上次的位置
  double _x = 0;  //手指上次水平的偏移量(即 left)
  double _y = 0;  //手指上次垂直的偏移量(即 top)
  
  final SplayTreeMap _treeMap = SplayTreeMap(); //用于传递值到 android
  
复制代码

在scaleStart 咱们记录一下手指的位置spa

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));
    }
}

复制代码

完成了这步,咱们再次运行后,就能够对大图进行区域性的截取并显示了。

谢谢你们阅读,有误之处还请指正。

系列文章

Flutter——仿网易云音乐App(基础版)

实现网易云音乐的滑动冲突处理效果

Flutter自定义View——仿高德三级联动Drawer

Flutter 自定义View——仿同花顺自选股列表

相关文章
相关标签/搜索