此功能只是针对GestureDetector、Event Channel 和 Method Channel 的综合协做进行的研究练习,我的认为是没法用于生产的。而就加载大图来讲,Flutter image自己的的cacheWidth和cacheHeight就能够实现(以及其它一些方案)。android
练习记录,代码可能写的有些随意。
复制代码
咱们的目标是经过GestureDetector、Event Channel 和 Method Channel的协做,经过原生端(Android)的BitmapRegionDecoder对大图进行分区域显示。markdown
这个使咱们要显示的图片。async
尺寸:7680*4320 JPEG 5.86MBide
首先咱们进行基础页面的绘制函数
@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用于通讯。
首先咱们定义一个_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的。
这里介绍一下,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
咱们定义一个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));
}
}
复制代码
完成了这步,咱们再次运行后,就能够对大图进行区域性的截取并显示了。
谢谢你们阅读,有误之处还请指正。