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——仿同花順自選股列表

相關文章
相關標籤/搜索