preface

This function is just a research exercise for the comprehensive cooperation of GestureDetector, Event Channel and Method Channel. I personally think it cannot be used in production. For loading large images, the cacheWidth and cacheHeight of the Flutter image itself can be implemented (among other schemes).

Practice notes, code may be a little arbitrary.Copy the code

introduce

Our goal is to regionalize the large image through the native (Android) BitmapRegionDecoder through the collaboration of GestureDetector, Event Channel and Method Channel.

Realize the figure

This is the image we want to display.

Size: 7680*4320 JPEG 5.86MB

implementation

Flutter & GestureDetector

First we draw the base page

@override Widget build(BuildContext context) { return Container( width: size.width,height: size.height, color: Colors.white, child: image(), ); Return GestureDetector(onScaleUpdate: scaleUpdate, onScaleStart: scaleStart, onScaleEnd: scaleEnd, child: Stack( alignment: Alignment.center, children: [ Container( color: Color.grey, // display window is 400*400 width: 400,height: 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, ), ], ); }Copy the code

This is drawn as follows (i.e., the first boot with no image data) :

Now let’s look at gesture callbacks

A callback to the zoom gesture

There are three callbacks for gestures :(it is important to note that not only two fingers trigger the following callbacks, but one-finger swiping still does)

ScaleStart // is called once when the screen is touched scaleUpdate // is called all the time when the finger is swiped scaleEnd // is called once when the finger is off the screenCopy the code

Next we declare some of the variables used in the callback.

Offset _lastOffset; Double _x = 0; double _x = 0; Double _y = 0; double _y = 0; // Final SplayTreeMap _treeMap = SplayTreeMap(); // Used to pass values to AndroidCopy the code

At scaleStart we record the position of our fingers

  void scaleStart(ScaleStartDetails details){
    _lastOffset = details.focalPoint;
  }
Copy the code

In scaleUpdate we record the required values and pass them to the Android side

Void scaleUpdate(ScaleUpdateDetails details){/// Calculate the value of each finger slide _x = (details.focalpoint.dx - _lastoff.dx); _y = (details.focalPoint.dy - _lastOffset.dy); _treeMap['scale'] = details.scale; _treeMap['left'] = _x; _treeMap['top'] = _y; _lastOffset = details.focalPoint; Nativeproxy.onsizechange (args: _treeMap); }Copy the code
Void scaleEnd(ScaleEndDetails details){// This callback needs to do nothing}Copy the code

Now that gesture processing on the flutter side is complete, we define event and Method channels for communication.

Event & Method channel on flutter side

First, we define a _NativeProxy as the channel assembly. The code is very simple:

// define a global variable to use _NativeProxy nativeProxy = new _NativeProxy(); Class _NativeProxy{// Event channel name static const String EVENT_CHANNEL = "lijiaqi.event"; // Method channel name static const String PLUGIN_NAME = "com.lijiaqi.flutter_big_image"; Static const String ORDER_DECODE = 'ORDER_DECODE '; Final EventChannel EventChannel = EventChannel(EVENT_CHANNEL); final MethodChannel methodChannel = MethodChannel(PLUGIN_NAME); // Call the order_decode method, which is called void onSizeChange({Map args})async{debugPrint('invoke') in scaleUpdate above; return await methodChannel.invokeMethod(ORDER_DECODE,args); }}Copy the code

We then listen for the Event Channel in the initState method of the page:

// imageData corresponding to android byte[] Uint8List imageData; NativeProxy. EventChannel. ReceiveBroadcastStream (). Listen ((event) {/ / native client sends to the image data of setState (() {imageData = event; }); });Copy the code

Now that we’ve finished developing the Flutter side, let’s start android.

Android & ImageEventChannel

An Event Channel can transmit data from the Android side to a flutter, and a flutter receives data as a ‘listening stream’. Method channels are often used to invoke methods on the native end of a flutter (and can also pass data to each other).

The code is as follows:

Public class ImageEventChannel implements the EventChannel. StreamHandler {/ / make sure and flutter as private static final String EVENT_CHANNEL = "lijiaqi.event"; Private static Volatile ImageEventChannel Singleton; 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; } private eventChannel. EventSink EventSink; Public void sinkData(byte[] datas){if(eventSink == null){log. d("event channel","data is empty"); }else{ eventSink.success(datas); }} / / initialization and bind the event channel private ImageEventChannel (FlutterPlugin. FlutterPluginBinding binding) {EventChannel eventChannel = new EventChannel(binding.getBinaryMessenger(),EVENT_CHANNEL); eventChannel.setStreamHandler(this); } // Initialize event sink@override public void onListen EventChannel.EventSink events) { this.eventSink = events; } @override public void onCancel(Object arguments) {eventSink = null; }}Copy the code

Let’s take a look at the ImageDecoderPlugin

Android & ImageDecoderPlugin

We define an ImageDecoderPlugin.

To get an overview of where functions belong while reading, I post the code below once and for all, with instructions in comments:

public class ImageDecoderPlugin implements FlutterPlugin, ActivityAware, MethodChannel. MethodCallHandler {/ / String to one to one correspondence or it will invalid / / / this is our method of channel private static final String PLUGIN_NAME = "com.lijiaqi.flutter_big_image"; 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); // constructor public ImageDecoderPlugin(Activity mActivity) {this.mActivity = new WeakReference<>(mActivity); Is = mactivity.getResources ().openRawResource(r.raw.big5m); // Initialize some objects and do a size resolution to the image initDecoder(); } /// Image decoding related objects private bitmapFactory. Options Options; private BitmapRegionDecoder regionDecoder; Private Bitmap Bitmap; Private int imageW,imageH; Options = new bitmapFactory.options (); private void initDecoder(){// New bitmapFactory.options (); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(is,null,options); imageW = options.outWidth; imageH = options.outHeight; / / image coding using 565 removed the transparent layer, it can save some memory, more options. InPreferredConfig = Bitmap. Config. RGB_565; / / will shut 'size of parsing options. InJustDecodeBounds = false; Try {/ / initialization. Our regional decoder regionDecoder = BitmapRegionDecoder newInstance (is, false); } catch (IOException e) { e.printStackTrace(); } } private void logger(String info){ Log.d("android " , info); } // When we call the native method through the method channel, @override public void onMethodCall(@nonnull MethodCall call, @nonnull methodChannel. Result Result){switch (call.method){// We define order_decode case order_decode: Rect onSizeChanged(call); Final byte[] datas = decodeBitmap(); if(datas == null) return; After / / decoding / / we will through the event channel will return to the image imageEventChannel. SinkData (datas); break; default: break; }} // regionDecoder (regionDecoder) And return the private data byte [] decodeBitmap () {bitmap = regionDecoder. DecodeRegion (the 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(); // Gesture zoom value, private double scale from flutter; Private int rectW = 400,rectH = 400; Private final int decodeDimenMin = 300; Private final int decodeDimenMax = 800; Private final double expandR = 1.1; private final double expandR = 1.1; Private final double reduce = 0.9; // The first method called private void onSizeChanged(MethodCall call){; scale = call.argument("scale"); 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); Rect. Right = rect. Left + rectW; rect.bottom = rect.top + rectH; AdjustRect () = adjustRect(); } Private void adjustRect(){// Make sure that the upper left corner doesn't overflow to the left. rect.left = Math.max(rect.left, 0); Rect. top = math. min(rect.top, imageh-recth); rect.left = Math.min(rect.left, imageW-rectW); 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); } // This method is called when the engine is successfully initialized. We initialize our 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; }... }Copy the code

At this point, the plug-in function is complete, let’s register the plug-in.

To register the plugin

In our MainActivity, configureFlutterEngine, register the plugin we just created:

public class MainActivity extends FlutterActivity { @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { super.configureFlutterEngine(flutterEngine); flutterEngine.getPlugins().add(new ImageDecoderPlugin(this)); }}Copy the code

Once this is done, we run it again, and we are ready to cull and display the larger image.

Thank you for reading and correct any mistakes.

series

Flutter — Imitation of netease Cloud Music App(Basic Version)

Realize netease cloud music sliding conflict processing effect

Flutter custom View — modelled on the Autonavi tertiary linkage Drawer

Flutter custom View – a list of self-selected stocks that mimic a flush