簡單專案實戰flutter(功能篇)

朱子宥發表於2019-04-15

上篇傳送門:簡單專案實戰flutter(佈局篇)

通過State改變介面

不同於在原生Android中手動改變控制元件的屬性,Flutter使用State來管理介面狀態,使用setState方法改變state將觸發頁面重繪,達到變化的目的,因此只需要維護一組可以改變的值來控制widget究竟如何顯示,而無需去關注widget們本身,以下是一些簡單的例子:

int langType = 0; // 0 中文 1 英文

Text(
  today.weekday == 5
    ? langType == 1 ? "YES!" : "是"
    : langType == 1 ? "NO" : "不是",
  style:
    TextStyle(fontSize: 90, color: textColor, fontFamily: fontName),
),

String fontName = "kaiTi"; // 字型名
style: TextStyle(color: textColor, fontFamily: fontName),

Color bgColor; // 背景顏色
Color bubbleColor; // 氣泡顏色
Color textColor; // 文字顏色

decoration: BoxDecoration(
  color: bubbleColor, // 設定氣泡(文字的背景)顏色
  borderRadius: BorderRadius.circular(30.0),
),
int screenType = 0; // 全屏/ 正方形

bool showControlPanel = true; // 是否顯示控制皮膚
showControlPanel ? _buildControlPanel() : Container(),
複製程式碼

在需要設定控制元件的可見性時,查了一些實現方式看看有什麼對應於Android裡面的setVisibility,有設定透明度為0和使用Offstage控制元件兩種方式,但是在查閱Offstage的原始碼說明時發現,官方建議Offstage適用於測量一些不能顯示的控制元件大小,如果單純只是顯示和隱藏永健,只需要在整個widget樹中移除這個widget即可。恍然大悟,從這方面說flutter對於修改整個頁面結構可以說非常隨心所欲,和原生的思維很不一樣,不能從原生直接“翻譯”過來。

顏色值使用

可以使用以下辦法來使用確定的顏色值:

Color c = const Color(0xFF42A5F5);
Color c = const Color.fromARGB(0xFF, 0x42, 0xA5, 0xF5);
Color c = const Color.fromARGB(255, 66, 165, 245);
Color c = const Color.fromRGBO(66, 165, 245, 1.0);
Color c1 = const Color(0xFFFFFF); // fully transparent white (invisible)
Color c2 = const Color(0xFFFFFFFF); // fully opaque white (visible)
複製程式碼

由最後兩行可知,傳入六位顏色值是完全看不見的,必須要在前面加上FF構成八位。 同時flutter內建了一些常用的顏色,用Colors.xxx即可,同時還可以在後面跟數值來描述顏色的深淺:Colors.xxx[100]

使用自定義字型

flutter使用字型也很簡單,將字型檔案準備好,在packages.yaml檔案中配置之後就可以在Textstyle中使用了。 配置檔案的格式如下:

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中<string>xxx</string>就能表達的東西在arb檔案中需要新增更多的資訊,用於給專業的翻譯人員參考可能會比較有用,個人感覺還是略繁瑣了,如果不是用sublime批量操作我可能要專門寫個指令碼把xml轉成arb?

arb格式的字串資訊:

"appName": "今天是週五嗎?",
"@appName": {
  "description": "Title for the Friday application",
  "type": "text",
  "placeholders": {}
},
複製程式碼

具體步驟《Flutter實戰》中都有,就不大段複製了,說一下這個過程中我遇到的坑:

  1. 一開始我直接跳到第三節使用intl包,沒注意設定LocalesDelegates這一段,導致一直不成功,仔細看了好幾遍示例程式碼才發現問題在哪裡:
import 'package:flutter_localizations/flutter_localizations.dart';

new MaterialApp(
 localizationsDelegates: [
   // 本地化的代理類
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: [
    const Locale('en', 'US'), // 美國英語
    const Locale('zh', 'CN'), // 中文簡體
    //其它Locales
  ],
  // ...
)
複製程式碼
  1. 使用佔位符的字串在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(); // 圖片轉成位元組
複製程式碼

這裡有幾個需要注意的地方:

  • 此處用的Imagedart: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<File> _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 jikeUrl = "jike://xxxx.xx/topic/565ac9dd4b715411006b5ecd";
static const downJikeLink =
    "http://a.app.qq.com/o/simple.jsp?pkgname=com.ruguoapp.jike&ckey=CK1411402428437";

_toJike() async {
  if (await canLaunch(jikeUrl)) {
    await launch(jikeUrl);
  } else {
    await launch(downJikeLink);
  }
}
複製程式碼

SharedPreferences儲存

使用了shared_preferences: ^0.5.1+2這個包,然後簡單封裝了幾個方法:

import 'package:shared_preferences/shared_preferences.dart';

void saveString(key, value) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  prefs.setString(key, value);
}

void saveInt(key, value) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  prefs.setInt(key, value);
}

Future<String> getString(key) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  return prefs.getString(key);
}

Future<int> getInt(key) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  return prefs.getInt(key);
}

// 使用時
getString(CN_FONT_NAME).then((name) {
  setState(() {
    fontName = null == name ? 'kaiTi' : name;
  });
});
複製程式碼

以上幾個功能用的包都是封裝了和原生的互動,其實自己實現也未嘗不可,但是ios還不會寫,所以不如用現成的輪子來的方便,不過設定桌布這個功能還沒有找到符合需要的包,只能自己先寫了。

相關文章