使用自定义字体

flutter使用字体也很简单,将字体文件准备好,在packages.yaml文件中配置之后就可以在Text的style中使用了。 配置文件的格式如下:

fonts:
  - family: MyCustomFont
    fonts:
      - asset: fonts/MyCustomFont.ttf
      - style: italic

要注意缩进

这里踩了一个坑,我起初以为所有的资源文件都是放在assets文件夹下,或者在assets文件夹里面再新建子文件夹fonts之类,结果字体始终读取不到,才知道fonts文件夹应该在根目录之下。

使用自定义字体的语法:

Text(
  'This is a custom font text',
  style: new TextStyle(fontFamily: 'MyCustomFont'),
);

支持多语言

我是按照《Flutter实战》这本书中的国际化部分完成的,感觉相比于原生只需要配置几个xml文件,还是复杂了很多,android中xxx就能表达的东西在arb文件中需要添加更多的信息,用于给专业的翻译人员参考可能会比较有用,个人感觉还是略繁琐了,如果不是用sublime批量操作我可能要专门写个脚本把xml转成arb?

arb格式的字符串信息:

"appName": "今天是周五吗?",
"@appName": {
  "description": "Title for the Friday application",
  "type": "text",
  "placeholders": {}
},

具体步骤《Flutter实战》中都有,就不大段复制了,说一下这个过程中我遇到的坑:

一开始我直接跳到第三节使用intl包,没注意设置Locales和Delegates这一段,导致一直不成功,仔细看了好几遍示例代码才发现问题在哪里:

import 'package:flutter_localizations/flutter_localizations.dart';

new MaterialApp(
 localizationsDelegates: [
   // 本地化的代理类
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: [
    const Locale('en', 'US'), // 美国英语
    const Locale('zh', 'CN'), // 中文简体
    //其它Locales
  ],
  // ...
)

使用占位符的字符串在flutter中是用${}来表示空位处的字符串,与普通字符串的对比:

// 添加字符串时:
String get titleText {
return Intl.message(
    'Text',
    name: 'titleText',
    desc: '',
  );
}

String titleMoreColor(title) => Intl.message(
  'More $title Color',
  name: 'titleMoreColor',
  desc: '',
  args: [title],
);

生成的原始arb文件:

"titleText": "Text",
"@titleText": {
  "description": "",
  "type": "text",
  "placeholders": {}
},
"titleMoreColor": "More {title} Color",
"@titleMoreColor": {
  "description": "",
  "type": "text",
  "placeholders": {
    "title": {}
  }
},

在手动翻译成中文时,我不小心把%1$s复制到了字符串中,而不是{}这样的格式,于是在插入文字的时候就失败了,不过这种应该是小概率问题。

截图&保存图片

截图

在Flutter中对widget进行截图需要在widget外部套一层RepaintBoundary,同时给它指定一个key,在截图时通过这个key拿到该RepaintBoundary进行截图:

RepaintBoundary(
  key: screenKey,
  child: ...
),

截图时:

import 'package:flutter/rendering.dart';
import 'dart:ui' as ui;
import 'dart:typed_data';
import 'dart:io';

RenderRepaintBoundary boundary =
        screenKey.currentContext.findRenderObject(); // 获取要截图的部分
ui.Image image = await boundary.toImage(pixelRatio: 3.0); // 用toImage转为图片
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List(); // 图片转成字节

这里有几个需要注意的地方:

  • 此处用的Image是dart:ui库中的Image,与widget库中的Image重名,所以需要加以区分,先导入ui库:import 'dart:ui' as ui;,然后用ui.Image去引用,同样处理字节需要import 'dart:typed_data';处理文件需要import 'dart:io';
  • 转换为图片时的pixelRatio属性默认是1.0,但是实测1.0很糊,调到3.0才是高清原图的样子。

保存图片

拿到字节之后,就是创建一个文件然后把字节写进去就可以了,这里为了获取系统目录,使用了一个path_provider的库:path_provider: ^0.5.0

import 'package:path_provider/path_provider.dart';

Future _getLocalFile() async {
  // 获取应用目录
  Directory dir =
      new Directory((await getExternalStorageDirectory()).path + "/Friday");
  if (!await dir.exists()) {
    dir.createSync();
  }
  return new File('${dir.absolute.path}/screenshot_${DateTime.now()}.png');
}

这里getExternalStorageDirectory()获取的是sd卡根目录,我在后面加了一个代表app根目录的路径,如果没有就创建一下,接着拼接出想要的文件名就可以了。 除了getExternalStorageDirectory(),还可以用getTemporaryDirectory()获取临时文件夹目录。 我的需求有保存图片也有临时保存用于设置桌面壁纸和分享,所以用不同的type去获取不同的文件夹下的文件,然后用writeAsBytes方法写入字节。

File file = await (type == 0 ? _getLocalFile() : _getCacheFile());
await file.writeAsBytes(pngBytes);

当然,要保存文件不要忘了在原生代码清单文件中添加权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />

保存资源图片

如何把assets里面的图片保存到手机中:

// 获取本地保存的文件位置
File file = await getLocalFile(donationImgName.substring(7));
// 使用DefaultAssetBundle加载图片文件
DefaultAssetBundle.of(context)
    .load(donationImgName)
    .then((data) {
    // 加载的字节数据(ByteData)
  Uint8List pngBytes = data.buffer.asUint8List();
  // 转成Uint8List然后保存到文件
  file.writeAsBytes(pngBytes);
});

和前面一样也是保存过程基本一样,所以关键是用DefaultAssetBundle获取到图片资源文件的数据。

同字体文件一样,图片也是在根目录新建images文件夹,而不是什么assets文件夹。

调用原生方法

在用上一节的方法保存好文件之后,我有一个设置图片为壁纸的需求,这个在Flutter里面是没办法实现的,就需要和原生交互了。 首先需要定义一个channel,和原生代码对上暗号:

import 'package:flutter/services.dart';
static const _channel = const MethodChannel('wallpaper');
await _channel.invokeMethod('setWallpaper', file.path); // 调用setWallpaper方法,文件路径作为参数传递

在原生代码中:

val channel = "wallpaper"
MethodChannel(flutterView, channel).setMethodCallHandler { methodCall, result ->
    // 判断方法名
	if (methodCall.method == "setWallpaper") {
        // 设置壁纸的方法封装在setWallpaper中,methodCall.arguments as String拿到路径参数
        val setWallpaperResult = setWallpaper(methodCall.arguments as String)

        if (setWallpaperResult == 0) {
	        // 成功的回调
            result.success(setWallpaperResult)
        } else {
	        // 失败的回调
            result.error("UNAVAILABLE", "", null)
        }
    }
}

具体怎么设置壁纸就不说了可以看代码。

分享到外部

分享也用了一个包,其实搜了一下用于分享的包有好几个,看示例代码选了一个比较符合需求的esys_flutter_share: ^1.0.0:

import 'package:esys_flutter_share/esys_flutter_share.dart';
await Share.file('Friday', 'friday.png', pngBytes, 'image/png');

其他的使用可以参考这个包的example,具体原理还是调用原生代码,esys_flutter_share.dart这个源码非常简单。

打开其他app

这个功能用到了url_launcher: ^5.0.2,在这里做了一个判断,如果手机上有安装某黄色app,就用scheme直接打开,如果没有就跳转腾讯应用宝下载(我真贴心):

import 'package:url_launcher/url_launcher.dart';

static const downUrl = "mouapp://xxxx.xx/topic/abcxxxxxx";
static const downMouappLink =
    "http://a.app.qq.com/o/simple.jsp?pkgname=com.abc.mouapp&ckey=CK1411402428437";

_toMouapp() async {
  if (await canLaunch(downUrl)) {
    await launch(downUrl);
  } else {
    await launch(downMouappLink);
  }
}

图片全屏

new ConstrainedBox(
  constraints: BoxConstraints.expand(),
  child: new Image.asset(
  "assets/images/ad.png",
    fit: BoxFit.fill,
  ),
),