繼續上一篇 Flutter側滑欄及城市選擇UI的實現,今天繼續講Flutter的實現篇,畫中畫效果的實現。先看一下PIP的實現效果.git
更多效果請查看PIP DEMO 代碼地址:FlutterPIPgithub
一天在瀏覽朋友圈時,發現了一個朋友發了一張圖(固然不是女友,可是個女的),相似上面效果部分. 一看效果挺牛啊,這是怎麼實現的呢?心想要不本身實現一下吧?因而開始準備用Android實現一下.canvas
但最近正好學了一下Flutter,並在學習Flutter 自定義View CustomPainter時,發現了和Android上有相同的API,Canvas,Paint,Path等. 查看Canvas的繪圖部分drawImage代碼以下bash
/// Draws the given [Image] into the canvas with its top-left corner at the
/// given [Offset]. The image is composited into the canvas using the given [Paint].
void drawImage(Image image, Offset p, Paint paint) {
assert(image != null); // image is checked on the engine side
assert(_offsetIsValid(p));
assert(paint != null);
_drawImage(image, p.dx, p.dy, paint._objects, paint._data);
}
void _drawImage(Image image,
double x,
double y,
List<dynamic> paintObjects,
ByteData paintData) native 'Canvas_drawImage';
複製代碼
能夠看出drawImage 調用了內部的_drawImage,而內部的_drawImage使用的是native Flutter Engine的代碼 'Canvas_drawImage',交給了Flutter Native去繪製.那Canvas的繪圖就能夠和移動端的Native同樣高效 (Flutter的繪製原理,決定了Flutter的高效性).關於Flutter的高效能夠查看 Flutter 高性能原理markdown
看效果從底層往上層,圖片被分爲3個部分,第一部分是底層的高斯模糊效果,第二層是原圖被裁剪的部分,第三層是一個效果遮罩。app
Flutter提供了BackdropFilter,關於BackdropFilter的官方文檔是這麼說的框架
A widget that applies a filter to the existing painted content and then paints child.async
The filter will be applied to all the area within its parent or ancestor widget's clip. If there's no clip, the filter will be applied to the full screen.ide
簡單來講,他就是一個篩選器,篩選全部繪製到子內容的小控件,官方demo例子以下post
Stack(
fit: StackFit.expand,
children: <Widget>[
Text('0' * 10000),
Center(
child: ClipRect( // <-- clips to the 200x200 [Container] below
child: BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: 5.0,
sigmaY: 5.0,
),
child: Container(
alignment: Alignment.center,
width: 200.0,
height: 200.0,
child: Text('Hello World'),
),
),
),
),
],
)
複製代碼
效果就是對中間200*200大小的地方實現了模糊效果. 本文對底部圖片高斯模糊效果的實現以下
Stack(
fit: StackFit.expand,
children: <Widget>[
Container(
alignment: Alignment.topLeft,
child: CustomPaint(
painter: DrawPainter(widget._originImage),
size: Size(_width, _width))),
Center(
child: ClipRect(
child: BackdropFilter(
filter: flutterUi.ImageFilter.blur(
sigmaX: 5.0,
sigmaY: 5.0,
),
child: Container(
alignment: Alignment.topLeft,
color: Colors.white.withOpacity(0.1),
width: _width,
height: _width,
// child: Text(' '),
),
),
),
),
],
);
複製代碼
其中Container的大小和圖片大小一致,而且Container須要有子控件,或者背景色. 其中子控件和背景色能夠任意. 實現效果如圖
在用Android中的Canvas進行繪圖時,能夠經過使用PorterDuffXfermode將所繪製的圖形的像素與Canvas中對應位置的像素按照必定規則進行混合,造成新的像素值,從而更新Canvas中最終的像素顏色值,這樣會建立不少有趣的效果.
Flutter 中也有相同的API,經過設置畫筆Paint的blendMode屬性,能夠達到相同的效果.混合模式具體能夠Flutter查看官方文檔,有示例.
此處用到的混合模式是BlendMode.dstIn,文檔註釋以下
/// Show the destination image, but only where the two images overlap. The /// source image is not rendered, it is treated merely as a mask. The color /// channels of the source are ignored, only the opacity has an effect. /// To show the source image instead, consider [srcIn]. // To reverse the semantic of the mask (only showing the source where the /// destination is present, rather than where it is absent), consider [dstOut]. /// This corresponds to the "Destination in Source" Porter-Duff operator.
大概說的意思就是,只在源圖像和目標圖像相交的地方繪製【目標圖像】,繪製效果受到源圖像對應地方透明度影響. 用Android裏面的一個公式表示爲
\(\alpha_{out} = \alpha_{src}\)
\(C_{out} = \alpha_{src} * C_{dst} + (1 - \alpha_{dst}) * C_{src}\)
複製代碼
咱們要用到一個Frame圖片(frame.png),用來和原圖進行混合,Frame圖片以下
實現代碼
/// 經過 frameImage 和 原圖,繪製出 被裁剪的圖形
static Future<flutterUi.Image> drawFrameImage(
String originImageUrl, String frameImageUrl) {
Completer<flutterUi.Image> completer = new Completer<flutterUi.Image>();
//加載圖片
Future.wait([
OriginImage.getInstance().loadImage(originImageUrl),
ImageLoader.load(frameImageUrl)
]).then((result) {
Paint paint = new Paint();
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
int width = result[1].width;
int height = result[1].height;
//圖片縮放至frame大小,並移動到中央
double originWidth = 0.0;
double originHeight = 0.0;
if (width > height) {
double scale = height / width.toDouble();
originWidth = result[0].width.toDouble();
originHeight = result[0].height.toDouble() * scale;
} else {
double scale = width / height.toDouble();
originWidth = result[0].width.toDouble() * scale;
originHeight = result[0].height.toDouble();
}
canvas.drawImageRect(
result[0],
Rect.fromLTWH(
(result[0].width - originWidth) / 2.0,
(result[0].height - originHeight) / 2.0,
originWidth,
originHeight),
Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
paint);
//裁剪圖片
paint.blendMode = BlendMode.dstIn;
canvas.drawImage(result[1], Offset(0, 0), paint);
recorder.endRecording().toImage(width, height).then((image) {
completer.complete(image);
});
}).catchError((e) {
print("加載error:" + e);
});
return completer.future;
}
複製代碼
分爲三個主要步驟
裁剪後的效果圖以下
先看一下mask圖片長啥樣
裁剪完的圖片和mask圖片的合成,不須要設置混合模式,裁剪圖片在底層,合成完的圖片在上層.既可實現,但須要注意的是,裁剪的圖片須要畫到效果區域,因此x,y須要有偏移量,實現代碼以下:/// mask 圖形 和被裁剪的圖形 合併
static Future<flutterUi.Image> drawMaskImage(String originImageUrl,
String frameImageUrl, String maskImage, Offset offset) {
Completer<flutterUi.Image> completer = new Completer<flutterUi.Image>();
Future.wait([
ImageLoader.load(maskImage),
//獲取裁剪圖片
drawFrameImage(originImageUrl, frameImageUrl)
]).then((result) {
Paint paint = new Paint();
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
int width = result[0].width;
int height = result[0].height;
//合成
canvas.drawImage(result[1], offset, paint);
canvas.drawImageRect(
result[0],
Rect.fromLTWH(
0, 0, result[0].width.toDouble(), result[0].height.toDouble()),
Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
paint);
//生成圖片
recorder.endRecording().toImage(width, height).then((image) {
completer.complete(image);
});
}).catchError((e) {
print("加載error:" + e);
});
return completer.future;
}
複製代碼
本文開始介紹了,圖片分爲三層,因此此處使用了Stack組件來包裝PIP圖片
new Container(
width: _width,
height: _width,
child: new Stack(
children: <Widget>[
getBackgroundImage(),//底部高斯模糊圖片
//合成後的效果圖片,使用CustomPaint 繪製出來
CustomPaint(
painter: DrawPainter(widget._image),
size: Size(_width, _width)),
],
)
)
複製代碼
class DrawPainter extends CustomPainter {
DrawPainter(this._image);
flutterUi.Image _image;
Paint _paint = new Paint();
@override
void paint(Canvas canvas, Size size) {
if (_image != null) {
print("draw this Image");
print("width =" + size.width.toString());
print("height =" + size.height.toString());
canvas.drawImageRect(
_image,
Rect.fromLTWH(
0, 0, _image.width.toDouble(), _image.height.toDouble()),
Rect.fromLTWH(0, 0, size.width, size.height),
_paint);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
複製代碼
Flutter 是一個跨平臺的高性能UI框架,使用到Native Service的部分,須要各自實現,此處須要把圖片保存到本地,使用了一個庫,用於獲取各自平臺的能夠保存文件的文件路徑.
path_provider: ^0.4.1
複製代碼
實現步驟,先將上面的PIP用一個RepaintBoundary 組件包裹,而後經過給RepaintBoundary設置key,再去截圖保存,實現代碼以下
Widget getPIPImageWidget() {
return RepaintBoundary(
key: pipCaptureKey,
child: new Center(child: new DrawPIPWidget(_originImage, _image)),
);
}
複製代碼
截屏保存
Future<void> _captureImage() async {
RenderRepaintBoundary boundary =
pipCaptureKey.currentContext.findRenderObject();
var image = await boundary.toImage();
ByteData byteData = await image.toByteData(format: ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
getApplicationDocumentsDirectory().then((dir) {
String path = dir.path + "/pip.png";
new File(path).writeAsBytesSync(pngBytes);
_showPathDialog(path);
});
}
複製代碼
顯示圖片的保存路徑
Future<void> _showPathDialog(String path) async {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('PIP Path'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('Image is save in $path'),
],
),
),
actions: <Widget>[
FlatButton(
child: Text('退出'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
複製代碼
目前的實現方式是:把原圖移動到中央進行裁剪,默認認爲圖片的重要顯示區域在中央,這樣就會存在一個問題,若是圖片的重要顯示區域沒有在中央,或者畫中畫效果的顯示區域不在中央,會存在必定的誤差.
因此須要添加手勢交互,當圖片重要區域不在中央,或者畫中畫效果不在中央,能夠手動調整顯示區域。
實現思路:添加手勢操做,獲取當前手勢的offset,從新拿原圖和frame區域進行裁剪,就能夠正常顯示.(目前暫未去實現)
歡迎star Github Code
文中全部使用的資源圖片,僅供學習使用,請在學習後,24小時內刪除,如如有侵權,請聯繫做者刪除。