作者:左手木亽
链接:
https://blog.youkuaiyun.com/Neacy_Zz/article/details/88542817
本文由作者授权发布。
前言
相信大家近来有在关注 996 事件,奔驰案,火烧巴黎圣母院,某职工晕倒等等,有些事就发生在我们身边,每个人的想法可能都不一样,怎么去看待它们?「小新」依旧处于忙碌的学习中,做好当下的自己。
本篇是「小新」的前辈投稿的文章。flutter 的先驱者,接下来一同学习前辈的思想。
开始
如果要想在 flutter 中使用本地图片资源,都会将图片放入工程的某个目录,然后在 pubspec.xml 中注册:
assets:
- images/apk_coin_money.png
- images/apk_coin_moneybig.png
当你把 flutter 某块嵌入到已有的项目中,混合开发自然会有很多图片资源已经存在了,按以上操作的方式容易带来资源浪费、给安装包增加额外的体积。
所以,本文要解决的一个问题就是如何将原生的资源重复利用,换句话说就是如何读取原生的图片资源呢?
原生实现
大概的思路是从网络图片说起,我们知道网络图片一般存于服务器上我们要想使用它需要进行异步下载然后再显示。那么我们可以把原生 <Android/iOS>理解成服务器,一张张原生图片名就是对应这个特殊服务器上的一个 uri。
由于服务器指的是原生,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")) {
// 待实现.....
}
}
}
这个 Channel 负责接收从 flutter 传来进来的地址,这里我们规定是图片名,比如说是 icon.png,那么当原生收到这么一个地址就需要从对应的drawable 目录中去拿去相关的资源返回给 flutter:
int drawableId = mContext.getResources().getIdentifier(images[0], "drawable", mContext.getPackageName());
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), drawableId);
很明显,只要拿的到 Bitmap 我们就可以做很多事情了,可是纯 Bitmap 又没法传给 flutter 端。
这里关键的一步就是:把当前的 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 : "");// 如果保存成功就将当前的路径返回给flutter端
由于 flutter 端可以加载本地路径下的图片,所以到这一步我们等于说原生这个服务器已经处理完成。
Flutter实现
原生端已经可以返回一个地址供 flutter 加载,那么 flutter 应该怎么跟原生连接呢?我们知道网络图片是通过 http 的方式,所以 flutter 端也得有个类似的玩意,就是自定义 ImageProvider:
第一步,先定义 Chanle 然后 flutter 端可以跟原生连接上:
/// 自定义读取原生图片的Channel
const MethodChannel _channel = const MethodChannel('com.meetyou.flutter/native_image');
/// 读取对应的原生图片
Future<Directory> getNativeImage(String imageName) async {
String path = await _channel.invokeMethod('getNativeImage', imageName);
return new Directory(path);
}
第二步,写一个 ImageProvider 用于调用这个 Channel 然后将返回的结果直接转成 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 如何加载网络图片的方式,更多的可以参考相关的源码。
到了这一步,我们可以成功加载原生的图片了,但是,但是,但是。
如果原生也没有这个图片资源,也就是说这个图片服务器上并没有自然就会加载失败,既然原生这个服务器上没有对应的资源那么肯定是 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") );
默认都是会先从原生去读取资源,如果原生没有再读取本地自己的图片资源,如果你已经知道这个图片资源不存在于原生中那还是用原来的那套读取流程即可。
Thanks
一套流程下来混合开发再也不用担心图片资源浪费,最终生成的安装包也不会因为浪费的图片而增加额外的体积大小。
fluro框架的接入
1、导包
在 pubspec.yaml 文件的 dependencies 节点下添加如下代码:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
fluro: ^1.4.0
2、app_route.dart
新建 app_route.dart ,添加以下代码:
import 'package:fluro/fluro.dart';
Router router = new Router();
3、login.dart
新建 login.dart ,添加以下代码:
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'app_route.dart';
void main() {
router.define('home/:data', handler: new Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return new Home(params['data'][0]);
}));
runApp(new Login());
}
class Login extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return new LoginState();
}
}
class LoginState extends State<Login> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new MaterialApp(
title: 'Fluro demo',
home: new Scaffold(
appBar: new AppBar(
title: new Text('登录'),
),
body: new Builder(builder: (BuildContext context) {
return new Center(
child: new Container(
height: 30.0,
color: Colors.pink,
child: new FlatButton(
onPressed: () {
var bodyJson = '{"user":1254,"pass":1515}';
router.navigateTo(context, '/home/$bodyJson');
},
child: const Text('传递账号密码')),
),
);
}),
),
);
}
}
class Home extends StatefulWidget {
final String _result;
Home(this._result);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return new HomeState();
}
}
class HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Center(
child: new Scaffold(
appBar: new AppBar(
title: new Text('个人主页'),
),
body: new Center(
child: new Text(widget._result),
),
),
);
}
}
4、效果图
推荐阅读:
扫一扫 关注我的公众号
想了解更多flutter最新资讯吗~