Flutter混合开发练习——Evenet&Method Channel协作加载大图

49 篇文章 0 订阅
47 篇文章 0 订阅

前言

此功能只是针对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));
    }
}

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

谢谢大家阅读,有误之处还请指正。

系列文章

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

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值