感謝 https://zhuanlan.zhihu.com/p/41731950
這里的原生指Android,當(dāng)然針對iOS的思路是一樣可取的。
開始
如果要想在flutter中使用本地圖片資源,都會將圖片放入工程的某個目錄,然后在pubspec.xml中注冊:
assets:
- images/apk_coin_money.png
- images/apk_coin_moneybig.png
-
-
當(dāng)你把flutter某塊嵌入到已有的項目中,混合開發(fā)自然會有很多圖片資源已經(jīng)存在了,按以上操作的方式容易帶來資源浪費、給安裝包增加額外的體積。
所以,本文要解決的一個問題就是如何將原生的資源重復(fù)利用,換句話說就是如何讀取原生的圖片資源呢?
原生實現(xiàn)
大概的思路是從網(wǎng)絡(luò)圖片說起,我們知道網(wǎng)絡(luò)圖片一般存于服務(wù)器上我們要想使用它需要進(jìn)行異步下載然后再顯示。那么我們可以把原生<Android/iOS>理解成服務(wù)器,一張張原生圖片名就是對應(yīng)這個特殊服務(wù)器上的一個uri。
由于服務(wù)器指的是原生,flutter跟原生交互的方式就是xxxChannel,所以第一步需要先定一個MethodChannel<Android版本>:
public class FlutterNativeImageChannel implements MethodChannel.MethodCallHandler {
private static final String TAG = "FlutterNativeImageChannel";
private static final String CHANNEL_ROUTER = "com.meetyou.flutter/native_image";
private Context mContext;
private FlutterNativeImageChannel(FlutterView flutterView) {
MethodChannel methodChannel = new MethodChannel(flutterView, CHANNEL_ROUTER);
methodChannel.setMethodCallHandler(this);
}
public static FlutterNativeImageChannel create(FlutterView flutterView) {
return new FlutterNativeImageChannel(flutterView);
}
@Override
public void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) {
if (methodCall.method.equals("getNativeImage")) {
// 待實現(xiàn).....
}
}
}
這個Channel負(fù)責(zé)接收從flutter傳來進(jìn)來的地址,這里我們規(guī)定是圖片名,比如說是icon.png,那么當(dāng)原生收到這么一個地址就需要從對應(yīng)的drawable目錄中去拿去相關(guān)的資源返回給flutter:
int drawableId = mContext.getResources().getIdentifier(images[0], "drawable", mContext.getPackageName());
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), drawableId);
很明顯,只要拿的到Bitmap我們就可以做很多事情了,可是純Bitmap又沒法傳給flutter端。
這里關(guān)鍵的一步就是:把當(dāng)前的Bitmap寫入到手機存儲空間中去如sd卡或者data路徑中去:
String successPath = dataCacheFile.getAbsolutePath() + File.separator + imageName;
FileOutputStream fos = new FileOutputStream(successPath);
boolean isSuccess = bitmap.compress(format, 100, fos);
result.success(isSuccess ? successPath : "");// 如果保存成功就將當(dāng)前的路徑返回給flutter端
由于flutter端可以加載本地路徑下的圖片,所以到這一步我們等于說原生這個服務(wù)器已經(jīng)處理完成。
Flutter實現(xiàn)
原生端已經(jīng)可以返回一個地址供flutter加載,那么flutter應(yīng)該怎么跟原生連接呢?我們知道網(wǎng)絡(luò)圖片是通過http的方式,所以flutter端也得有個類似的玩意,就是自定義ImageProvider:
第一步,先定義Chanle然后flutter端可以跟原生連接上:
/// 自定義讀取原生圖片的Channel
const MethodChannel _channel = const MethodChannel('com.meetyou.flutter/native_image');
/// 讀取對應(yīng)的原生圖片
Future<Directory> getNativeImage(String imageName) async {
String path = await _channel.invokeMethod('getNativeImage', imageName);
return new Directory(path);
}
第二步,寫一個ImageProvider用于調(diào)用這個Channel然后將返回的結(jié)果直接轉(zhuǎn)成Image可以顯示的東西:
/// 自定義ImageProvider用于讀取原生資源中的圖片
class NativeImageProvider extends ImageProvider<NativeImageProvider> {
/// 需要從原生中去加載的圖片名格式如: icon_back.png / icon_new.jpg
final String imageName;
/// Scale of the image
final double scale;
const NativeImageProvider(this.imageName, {this.scale: 1.0});
@override
ImageStreamCompleter load(key) {
LogUtil.d("========= Native load === ");
return new MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
scale: key.scale,
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.write('Image key: $key');
});
}
Future<ui.Codec> _loadAsync(NativeImageProvider key) async {
Directory directory = await getNativeImage(imageName);
var file = File(directory.path);
return await _loadAsyncFromFile(key, file);
}
Future<ui.Codec> _loadAsyncFromFile(NativeImageProvider key, File file) async {
assert(key == this);
final Uint8List bytes = await file.readAsBytes();
if (bytes.lengthInBytes == 0) {
throw new Exception("File was empty");
}
return await ui.instantiateImageCodec(bytes);
}
@override
Future<NativeImageProvider> obtainKey(ImageConfiguration configuration) {
return new SynchronousFuture<NativeImageProvider>(this);
}
}
加載的方式模仿Image如何加載網(wǎng)絡(luò)圖片的方式,更多的可以參考相關(guān)的源碼。
到了這一步,我們可以成功加載原生的圖片了,
但是
但是
但是
如果原生也沒有這個圖片資源,也就是說這個圖片服務(wù)器上并沒有自然就會加載失敗,既然原生這個服務(wù)器上沒有對應(yīng)的資源那么肯定是flutter工程下的圖片,那這個時候需要怎么加載呢?
依然查看AssetImage的源碼看它是怎么加載資源,然后我們在返回的地址做一次判斷如果返回的是空的地址,就走AssetImage的加載邏輯:
if (directory.path.isEmpty) {// 如果圖片不存在于原生的話 那么讀取images下的圖片
LogUtil.d("========= 讀不到原生圖片,開始讀取images中 ========= ");
AssetBundle assetBundle = PlatformAssetBundle();
ByteData byteData = await assetBundle.load("images/$imageName");
return await PaintingBinding.instance.instantiateImageCodec(byteData.buffer.asUint8List());
}
Ok,整個讀取原生的圖片資源方式就是這樣子,這里看下使用方式:
new Image(image: new NativeImageProvider("icon.png") );
默認(rèn)都是會先從原生去讀取資源,如果原生沒有再讀取本地自己的圖片資源,如果你已經(jīng)知道這個圖片資源不存在于原生中那還是用原來的那套讀取流程即可。
Thanks
一套流程下來混合開發(fā)再也不用擔(dān)心圖片資源浪費,最終生成的安裝包也不會因為浪費的圖片而增加額外的體積大小。