此功能只是針對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));
}
}
複製代碼
完成了這步,咱們再次運行後,就能夠對大圖進行區域性的截取並顯示了。
謝謝你們閱讀,有誤之處還請指正。