Flutter第一部分(UI)第六篇:一文搞懂Flutter中的资源引用机制
前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件、数据库、网络)再到Flutter进阶(平台特定代码编写、测试、插件开发等),欢迎感兴趣的读者持续关注(可以扫描左边栏二维码或者微信搜索”IT工匠“关注微信公众号哦,会同步推送)。
Flutter应用程序可以包括代码(
code)和资产(
asset),有时也将资产称为资源(
resource),由于大多数人比较习惯资源的概念,本文如果不加说明,都将
asset称为资源。 资源中包含的文件会与你的应用程序绑定在一起最后部署到宿主机上,可在软件运行期间访问这部分文件。 常见的资源类型包括静态数据(例如
JSON文件),配置文件,图标和图像(
JPEG,
WebP,
GIF,
动画WebP / GIF,
PNG,
BMP和
WBMP)等。
资源的注册(指定)
Flutter使用位于项目根目录下的
pubspec.yaml文件来标识
app所需要的资源,如下所示:
flutter: assets: - assets/my_icon.png - assets/background.png
上面的代码将
assets目录下的
my_icon.png和
background.png文件进行了注册(指定),这样我们就可以直接在代码中访问到这两个文件,有时我们可能有很多个资源文件需要在
pubspec.yaml文件中指定,这时如果像上面代码那样一个文件一个文件注册就显得有点麻烦,我们可以使用以
/字符结尾的格式将整个文件夹下的文件同时指定进来,就像下面这样:
flutter: assets: - assets/
注意,上面的代码会将
assets目录下的所有直接子文件都指定进来,但是
assets下的子文件夹下的文件(即间接子文件)是不会被指定进来的,需要对这些间接子文件或者子文件夹进行再次指定。
其实说白了,就是将代码中需要访问的资源文件在pubspec.yaml
文件中进行一个注册,这样就能通过pubspec.yaml
文件的注册信息定位到最终需要的文件进而实现访问,比如,假设现在文件的目录结构如下:
pubspec.yaml文件的声明如下:
flutter: assets: - assets/
这样我们只能访问到
assets的直接子文件
flutter_blue.png,不能访问到
assets的子目录
child中的文件
flutter_red.png,如果想要访问,可以将
pubspec.yaml文件更新为:
flutter: assets: - assets/- assets/child/ #或者:- assets/child/flutter_blue.png
这样既可以访问到文件
flutter_blue.png了。
注意:在
flutter项目的pubspec.yaml文件中指定资源时,assets节点下的目录名是没有限制的,这个实例中用的是assets,你也可以根据自己的需要使用其他目录,只要保证使用的目录位置再项目的根目录下即可,比如:flutter: assets: - images/将资源文件夹命名为
images,这也是完全可以的。
资源的打包
pubspec.yaml文件中
flutter节下的
assets子节中指定的文件会包含在最终的
app内(即一起被打包),每一个资源文件的位置都会以一个确定的路径(相对于
pubspec.yaml的相对路径)来进行标识。
在
app的构建(
build)期间,
Flutter会将资源(
asset)打包放入名为资源包(
asset bundle)的特殊存档中,应用程序可以在运行时读取这些存档。
资源的自动版本管理
app的构建进程可以自动对资源进行版本管理:可以在不同的上下文中使用资源的不同版本。 在
pubspec.yaml的
assets部分中指定资源的路径时,构建进程会查找相邻子目录中具有相同名称的所有文件,这些文件会与指定的资源一起打包进资源包中。
举个例子,假设你的项目的文件目录结构是这样的:
.../pubspec.yaml .../assets/flutter_icon.png .../assets/red_icon/flutter_icon. .../assets/icon.png ...etc.
而你的
pubspec.yaml文件中是这么声明的:
flutter: assets: - assets/flutter_icon.png
这样的话
assets/flutter_icon.png和
assets/red/flutter_icon.png文件都会被打包进你的资源包中,前者被认为是主版本,后者被认为是主版本的变体。
如果
pubspec.yaml文件使用如下的声明方法:
flutter: assets: - assets/
这样
assets/icon.png、
assets/flutter_icon.png和
assets/red/flutter_icon.png都会被包含进去。
Flutter会根据不同的屏幕分辨率选择合适的图像(不同的资源版本)进行使用,这一点在下面我会再进行介绍。将来,这种机制可能还会扩展到根据宿主机所处的不同地区使用不同版本的资源、根据屏幕的方向(横屏还是竖屏)选择不同版本的资源等其他场景。
加载资源
我们可以在
app中使用
AssetBundle类的对象来访问资源中的文件,具体的,根据加载资源的类型,可以将资源的加载分为两类:
- 加载资源中的
string/text
文件(通过AssetBundle.loadString(String key)
方法) - 加载资源中的图像/二进制文件(通过
AssetBundle.load(String key)
方法)
这两种方法的使用前提是需要提供的
key值,这里的
key值指的是
pubspec.yaml文件中注册(指定)的资源的路径,比如在
pubspec.yaml文件中声明了:
flutter: assets: - assets/flutter_icon.png
那么如果想加载
flutter_icon.png,
key值就是
'assets/flutter_icon.png'。
下面我们来分别介绍一下如何具体地加载文本资源文件和图像/二进制文件。
加载文本资源
我们刚才提到,加载资源文件需要借助
AssetBundle类的对象,每一个
Flutter app的
import 'package:flutter/services.dart'包下都提供有一个实例化好的
AssetBundle rootBundle对象,我们可以通过该对象访问资源包中的主资源,比如:
class _MyAppState extends State<MyApp> { Image image; _loadImage() async { ByteData byteData = await rootBundle.load('assets/flutter_icon.png'); setState(() { image = Image.memory(byteData.buffer.asUint8List()); }); } @override void initState() { _loadImage(); super.initState(); } @override Widget build(BuildContext context) { return MaterialApp( title: 'Assets demo', debugShowCheckedModeBanner: false, home: Scaffold( appBar: AppBar( title: Text('Assets demo'), ), body: Center( child: image, ), ), ); } }
这样就可以加载到我们需要的资源,但是请注意,通过
Flutter app给我们实例化好的
rootBundle对象加载的是默认的主资源,也就是说它是无法根据不同的上下文环境(比如不同的屏幕分辨率)加载不同版本的资源、它永远加载的都是默认的主资源中的资源,那么如何解决这个问题呢?
Flutter官方推荐的做法是通过
DefaultAssetBundle类结合当前的
BuildContext来实例化与当前上下文环境相关联的
AssetBundle对象,这样不同的上下文环境会实例化到不同的
AssetBundle对象,就能保证程序在加载资源的时候根据不同的上下文环境动态决定加载最适合版本对应的资源,具体的做法分为以下两步:
- 实例化
AssetBundle
类的对象 - 加载资源
Talk is cheap ,show you my code:
class _MyAppState extends State<MyApp> { Image image; _loadImage(AssetBundle assetBundle) async { ByteData byteData = await assetBundle.load('assets/flutter_icon.png'); image = Image.memory(byteData.buffer.asUint8List()); } @override Widget build(BuildContext context) { //这里通过DefaultAssetBundle和当前的上下文环境context实例化AssetBundle AssetBundle assetBundle = DefaultAssetBundle.of(context); _loadImage(assetBundle); return MaterialApp( title: 'Assets demo', debugShowCheckedModeBanner: false, home: Scaffold( appBar: AppBar( title: Text('Assets demo'), ), body: Center( child: image, ), ), ); } }
以上介绍了两种加载资源文件的方法:
- 第一种:直接使用
Flutter app
为我们实例化好的rootBundle
对象 - 第二种:使用
DefaultAssetBundle
和context
实例化自己的assetBundle
对象
那么我们在实际的生产中什么时候应该使用第一种、什么时候应该使用第二种呢?答案是一般情况下我们应该尽可能地使用第二种方法,但是如果我们需要在
Widget上下文之外加载资源,我们就无法获取当前环境的上下文
context,这个时候就只能使用第一种方法了。
加载图像
Flutter可以根据当前设备的分辨率加载合适的图像。
声明分辨率感知(resolution-aware)图像资源
我们可以通过
AssetImage类将图像的加载请求自动映射到最接近当前设备像素比例的资源文件,当然这种做法的前提是使用特定的目录结构来保存图像资源,就像这样:
.../image.png .../Mx/image.png .../Nx/image.png ...etc.
其中
M和
N是数字标识符,代表对应文件夹下图像的分辨率等级,也就是说,它们指定了不同设备像素下应该加载的不同图片,主资源默认对应
1.0倍的分辨率图片。比如,考虑如下名为
my_icon.png的文件:
.../my_icon.png .../2.0x/my_icon.png .../3.0x/my_icon.png
在设备像素比例为
1.8的设备上,将会使用
.../2.0x/my_icon.png这个资源;
在像素比例为
2.7的设备上,将会使用
.../3.0x/my_icon.png这个资源,以此类推。
如果在使用
Image Widget的时候没有指定渲染图像的宽度和高度,
Flutter默认会使用标准分辨率来缩放图片资源,使其与主资源占用相同的屏幕空间。 也就是说,如果
.../my_icon.png是
72px乘
72px,那么
.../3.0x/my_icon.png应该是
216px乘
216px, 但如果未指定宽度和高度,它们都将渲染为
72px乘
72px(
px以逻辑像素为单位)。
pubspec.yaml中
asset部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到高的顺序去选择最接近自己标准的资源,比如如果需要
1x,但是没找到,那么会去
2x中找,
2x中还没有的话就在
3x中找,以此类推。
加载图片
要加载图片,我们应该在
Widget的
build()方法中使用
AssetImage类。
例如:
Widget build(BuildContext context) { // ... return new DecoratedBox( decoration: new BoxDecoration( image: new DecorationImage( image: new AssetImage('assets/icon.png'), // ... ), // ... ), ); // ... }
使用默认的资源包加载资源时,内部会自动处理分辨率,这些处理对开发者来说是不可见的、自动的, 如果你使用一些更靠近底层的类,比如
ImageStream或
ImageCache, 你就需要自己配置一些与缩放相关的参数。
依赖包(dependencies)中的资源图片
要加载依赖包(
dependencies)中的图片,必须给
AssetImage提供
package参数。
比如,假设你的应用程序依赖于一个名为
“my_icons”的包,它具有如下目录结构:
.../pubspec.yaml .../icons/heart.png .../icons/1.5x/heart.png .../icons/2.0x/heart.png ...etc.
要加载该依赖包下的图像,你应该使用类似如下的代码:
new AssetImage('icons/heart.png', package: 'my_icons')
打包 package assets
如果在
pubspec.yaml文件中声明了期望的资源,它将会打包到相应的
package中。特别是,包本身使用的资源必须在
pubspec.yaml中指定。
包也可以选择在其
lib/文件夹中包含未在其
pubspec.yaml文件中声明的资源。在这种情况下,对于要打包的图片,应用程序必须在
pubspec.yaml中指定包含哪些图像。 例如,一个名为
“fancy_backgrounds”的包,可能包含以下文件:
.../lib/backgrounds/background1.png .../lib/backgrounds/background2.png .../lib/backgrounds/background3.png
要包含第一张图像,必须在
pubspec.yaml的
assets部分中声明它:
flutter: assets: - packages/fancy_backgrounds/backgrounds/background1.png
lib/是隐含的,所以它不应该包含在资源路径中。
与特定平台共享资源
通过
Android上的
AssetManager和
iOS上的
NSBundle可以在编写特定平台代码的时候获取到
Flutter中的资源。
Android
在
Android中,可以通过
AssetManager获得
Flutter中的资源。具体的做法是使用
AssetManager.getopenFd(String key);来获取对应资源,那么这个
key应该是什么?答案是应该根据开发类型的不同来决定:
- 如果开发的是
Flutter
插件,则应该使用PluginRegistry.Registrar.lookupKeyForAsset(assetPath)
获取 - 如果开发的是普通的
app
,则应该使用FlutterView.getLookupKeyForAsset()
获取
比如,假设你的
pubspec.yaml文件中声明了如下代码:
flutter: assets: - icons/heart.png
而你
app的包结构是这样的:
.../pubspec.yaml .../icons/heart.png ...etc.
那么你可以使用类似如下
Java代码访问到
icons/heart.png:
AssetManager assetManager = registrar.context().getAssets(); String key = registrar.lookupKeyForAsset("icons/heart.png"); AssetFileDescriptor fd = assetManager.openFd(key);
ios
在
iOS中,可以通过
mainBundle获取资源。 用于
pathForResource:ofType:的
key可以有两种过获取方式:
- 通过
FlutterPluginRegistrar
中的lookupKeyForAsset
或lookupKeyForAsset:fromPackage:
获取 - 通过
FlutterViewController
中的lookupKeyForAsset:
或lookupKeyForAsset:fromPackage:
获取
同样应该根据开发类型的不同选择不同的资源获取方式:
- 开发插件时可以使用
FlutterPluginRegistrar
- 开发普通
Flutter app
时使用FlutterViewController
比如你可以使用类似如下
Object-C代码来访问
flutter项目中的
icons/heart.png:
NSString* key = [registrar lookupKeyForAsset:@"icons/heart.png"]; NSString* path = [[NSBundle mainBundle] pathForResource:key ofType:nil];
平台资源
还有时候可以直接在平台项目中使用资源,以下是在加载和运行
Flutter框架之前使用资源的两种常见情况。
更新app 图标
更新
Flutter应用程序的启动图标与在原生
Android或
iOS应用程序中更新启动图标的方式相同。
Android
在
Flutter项目的根目录中,导航到
.../android/app/src/main/res。各个位图资源文件夹(如
mipmap-hdpi)已包含名为
ic_launcher.png的占位符图像。 只需遵守
Android官方文档中指示的不同屏幕像素密度推荐的图标大小原则,使用我们自己的图标替换掉
ic_launcher.png即可:
iOS
在你的
Flutter项目的根目录中,导航到
.../ios/Runner,在目录
Assets.xcassets/AppIcon.appiconset下已包含了占位符图像, 只需将它们替换为适当大小的图片即可:
更新启动页
当
Flutter框架进行加载时,
Flutter会使用本机平台机制将
app启动页面绘制到屏幕上,这个启动页面会一直展示,直到
Flutter呈现应用程序的第一帧。
注意: 这意味着如果你不在应用程序的
main()方法中调用
runApp()函数(底层原理其实是调用
window.render()去响应
window.onDrawFrame())的话, 你的
app启动后会永远启只显示启动页面。
Android
要在
Flutter应用程序中添加启动画面,请在
.../android/app/src/main。 在
res/drawable/launch_background.xml文件中自定义你的启动背景,这样就可以达到更改启动页面的目的。
iOS
要将图片添加到启动屏幕(
splash screen)的中心,请导航至
.../ios/Runner。在
Assets.xcassets/LaunchImage.imageset, 拖入图片,并命名为
images LaunchImage.png、
LaunchImage@2x.png、
LaunchImage@3x.png。 如果你使用了其他的文件名,那你还必须更新同一目录中的
Contents.json文件。
您也可以通过打开
Xcode完全自定义
storyboard。在
Project Navigator中导航到
Runner/Runner然后通过打开
Assets.xcassets拖入图片,或者通过在
LaunchScreen.storyboard中使用
Interface Builder进行自定义。
- ModernUI教程:主题资源引用
- 一文彻底搞懂Java回调机制
- Android UI详解之颜色资源的使用
- Http Status 304响应状态的资源更新机制
- 【分享】20个很不错的UI图标集资源
- Android引用XML中的arrays 资源
- 使用 ng build 构建后资源地址引用错误的问题
- Android Studio如何查看资源或者函数在哪些类中被引用
- 一文搞懂隐马尔可夫模型(HMM)
- Android UI开发第六篇——仿QQ的滑动Tab
- 类型或命名空间名称“UI”在类或命名空间“System.Web”中不存在(是否缺少程序集引用?)的解决方法
- 资源访问机制之资源定义与解析流程
- Spark资源调度机制流程
- UI开发第六篇——仿QQ的滑动Tab
- Ui 设计(引用)
- 垃圾回收机制与引用类型
- Http Status 304响应状态的资源更新机制
- Android UI 资源大合集(相当的多)
- Android xml资源文件中@、@android:type、@*、?、@+引用写法含义以及区别
- android面试题目大全<第一部分>,android基本的UI控件和布局文件知识