Skip to content

Commit 3f835ae

Browse files
committed
Update Readme
1 parent e63fcb7 commit 3f835ae

File tree

1 file changed

+367
-10
lines changed

1 file changed

+367
-10
lines changed

README.md

Lines changed: 367 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,373 @@
1-
# pip_image
1+
# 前言
22

3-
A new Flutter application.
3+
继续上一篇 [Flutter侧滑栏及城市选择UI的实现](https://juejin.im/post/5d316609f265da1b6836f593),今天继续讲Flutter的实现篇,画中画效果的实现。先看一下PIP的实现效果.
44

5-
## Getting Started
5+
![](https://github.com/DingProg/FlutterPIP/blob/master/screen/pip_cd.png)
6+
![](https://github.com/DingProg/FlutterPIP/blob/master/screen/pip_pao.png)
7+
![](https://github.com/DingProg/FlutterPIP/blob/master/screen/pip_movie.png)
8+
![](https://github.com/DingProg/FlutterPIP/blob/master/screen/pip_gloass.png)
9+
![](https://github.com/DingProg/FlutterPIP/blob/master/screen/pip_photo.png)
10+
![](https://github.com/DingProg/FlutterPIP/blob/master/screen/pip_draw.png)
11+
12+
更多效果请查看PIP DEMO
13+
代码地址:[FlutterPIP](https://github.com/DingProg/FlutterPIP)
14+
15+
# 为什么会有此文?
16+
一天在浏览朋友圈时,发现了一个朋友发了一张图(当然不是女朋友,但是个女的),类似上面效果部分. 一看效果挺牛啊,这是怎么实现的呢?心想要不自己实现一下吧?于是开始准备用Android实现一下.
17+
18+
但最近正好学了一下Flutter,并在学习Flutter 自定义View CustomPainter时,发现了和Android上有相同的API,Canvas,Paint,Path等. 查看Canvas的绘图部分drawImage代码如下
19+
```dart
20+
/// Draws the given [Image] into the canvas with its top-left corner at the
21+
/// given [Offset]. The image is composited into the canvas using the given [Paint].
22+
void drawImage(Image image, Offset p, Paint paint) {
23+
assert(image != null); // image is checked on the engine side
24+
assert(_offsetIsValid(p));
25+
assert(paint != null);
26+
_drawImage(image, p.dx, p.dy, paint._objects, paint._data);
27+
}
28+
void _drawImage(Image image,
29+
double x,
30+
double y,
31+
List<dynamic> paintObjects,
32+
ByteData paintData) native 'Canvas_drawImage';
33+
```
34+
可以看出drawImage 调用了内部的_drawImage,而内部的_drawImage使用的是native Flutter Engine的代码 'Canvas_drawImage',交给了Flutter Native去绘制.那Canvas的绘图就可以和移动端的Native一样高效 (Flutter的绘制原理,决定了Flutter的高效性).
35+
36+
# 实现步骤
37+
看效果从底层往上层,图片被分为3个部分,第一部分是底层的高斯模糊效果,第二层是原图被裁剪的部分,第三层是一个效果遮罩。
38+
39+
## Flutter 高斯模糊效果的实现
40+
Flutter提供了BackdropFilter,关于BackdropFilter的官方文档是这么说的
41+
> A widget that applies a filter to the existing painted content and then paints child.
42+
>
43+
> 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.
44+
45+
简单来说,他就是一个筛选器,筛选所有绘制到子内容的小控件,官方demo例子如下
46+
47+
```dart
48+
Stack(
49+
fit: StackFit.expand,
50+
children: <Widget>[
51+
Text('0' * 10000),
52+
Center(
53+
child: ClipRect( // <-- clips to the 200x200 [Container] below
54+
child: BackdropFilter(
55+
filter: ui.ImageFilter.blur(
56+
sigmaX: 5.0,
57+
sigmaY: 5.0,
58+
),
59+
child: Container(
60+
alignment: Alignment.center,
61+
width: 200.0,
62+
height: 200.0,
63+
child: Text('Hello World'),
64+
),
65+
),
66+
),
67+
),
68+
],
69+
)
70+
```
71+
效果就是对中间200*200大小的地方实现了模糊效果.
72+
本文对底部图片高斯模糊效果的实现如下
73+
74+
```dart
75+
Stack(
76+
fit: StackFit.expand,
77+
children: <Widget>[
78+
Container(
79+
alignment: Alignment.topLeft,
80+
child: CustomPaint(
81+
painter: DrawPainter(widget._originImage),
82+
size: Size(_width, _width))),
83+
Center(
84+
child: ClipRect(
85+
child: BackdropFilter(
86+
filter: flutterUi.ImageFilter.blur(
87+
sigmaX: 5.0,
88+
sigmaY: 5.0,
89+
),
90+
child: Container(
91+
alignment: Alignment.topLeft,
92+
color: Colors.white.withOpacity(0.1),
93+
width: _width,
94+
height: _width,
95+
// child: Text(' '),
96+
),
97+
),
98+
),
99+
),
100+
],
101+
);
102+
```
103+
其中Container的大小和图片大小一致,并且Container需要有子控件,或者背景色. 其中子控件和背景色可以任意.
104+
实现效果如图
105+
![](https://github.com/DingProg/FlutterPIP/blob/master/screen/backgroud.png)
106+
107+
108+
## Flutter 图片裁剪
109+
110+
### 图片裁剪原理
111+
在用Android中的Canvas进行绘图时,可以通过使用PorterDuffXfermode将所绘制的图形的像素与Canvas中对应位置的像素按照一定规则进行混合,形成新的像素值,从而更新Canvas中最终的像素颜色值,这样会创建很多有趣的效果.
112+
113+
Flutter 中也有相同的API,通过设置画笔Paint的blendMode属性,可以达到相同的效果.混合模式具体可以Flutter查看官方文档,有示例.
114+
115+
此处用到的混合模式是BlendMode.dstIn,文档注释如下
116+
117+
> /// Show the destination image, but only where the two images overlap. The
118+
/// source image is not rendered, it is treated merely as a mask. The color
119+
/// channels of the source are ignored, only the opacity has an effect.
120+
/// To show the source image instead, consider [srcIn].
121+
// To reverse the semantic of the mask (only showing the source where the
122+
/// destination is present, rather than where it is absent), consider [dstOut].
123+
/// This corresponds to the "Destination in Source" Porter-Duff operator.
124+
125+
![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstIn.png)
126+
大概说的意思就是,只在源图像和目标图像相交的地方绘制【目标图像】,绘制效果受到源图像对应地方透明度影响. 用Android里面的一个公式表示为
127+
```
128+
\(\alpha_{out} = \alpha_{src}\)
129+
130+
\(C_{out} = \alpha_{src} * C_{dst} + (1 - \alpha_{dst}) * C_{src}\)
131+
```
132+
133+
### 实际裁剪
134+
135+
我们要用到一个Frame图片(frame.png),用来和原图进行混合,Frame图片如下
136+
137+
![](https://github.com/DingProg/FlutterPIP/blob/master/screen/frame.png)
138+
139+
140+
实现代码
141+
```dart
142+
/// 通过 frameImage 和 原图,绘制出 被裁剪的图形
143+
static Future<flutterUi.Image> drawFrameImage(
144+
String originImageUrl, String frameImageUrl) {
145+
Completer<flutterUi.Image> completer = new Completer<flutterUi.Image>();
146+
//加载图片
147+
Future.wait([
148+
OriginImage.getInstance().loadImage(originImageUrl),
149+
ImageLoader.load(frameImageUrl)
150+
]).then((result) {
151+
Paint paint = new Paint();
152+
PictureRecorder recorder = PictureRecorder();
153+
Canvas canvas = Canvas(recorder);
154+
155+
int width = result[1].width;
156+
int height = result[1].height;
157+
158+
//图片缩放至frame大小,并移动到中央
159+
double originWidth = 0.0;
160+
double originHeight = 0.0;
161+
if (width > height) {
162+
double scale = height / width.toDouble();
163+
originWidth = result[0].width.toDouble();
164+
originHeight = result[0].height.toDouble() * scale;
165+
} else {
166+
double scale = width / height.toDouble();
167+
originWidth = result[0].width.toDouble() * scale;
168+
originHeight = result[0].height.toDouble();
169+
}
170+
canvas.drawImageRect(
171+
result[0],
172+
Rect.fromLTWH(
173+
(result[0].width - originWidth) / 2.0,
174+
(result[0].height - originHeight) / 2.0,
175+
originWidth,
176+
originHeight),
177+
Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
178+
paint);
179+
180+
//裁剪图片
181+
paint.blendMode = BlendMode.dstIn;
182+
canvas.drawImage(result[1], Offset(0, 0), paint);
183+
recorder.endRecording().toImage(width, height).then((image) {
184+
completer.complete(image);
185+
});
186+
}).catchError((e) {
187+
print("加载error:" + e);
188+
});
189+
return completer.future;
190+
}
191+
```
192+
193+
分为三个主要步骤
194+
- 第一个步骤,加载原图和Frame图片,使用Future.wait 等待两张图片都加载完成
195+
- 原图进行缩放,平移处理,缩放至frame合适大小,在将图片平移至图片中央
196+
- 设置paint的混合模式,绘制Frame图片,完成裁剪
197+
198+
裁剪后的效果图如下
199+
![](https://github.com/DingProg/FlutterPIP/blob/master/screen/crop.png)
200+
201+
202+
## Flutter 图片合成及保存
203+
### 裁剪完的图片和效果图片(mask.png)的合成
204+
先看一下mask图片长啥样
205+
![](https://github.com/DingProg/FlutterPIP/blob/master/screen/mask.png)
206+
裁剪完的图片和mask图片的合成,不需要设置混合模式,裁剪图片在底层,合成完的图片在上层.既可实现,但需要注意的是,裁剪的图片需要画到效果区域,所以x,y需要有偏移量,实现代码如下:
207+
```dart
208+
209+
/// mask 图形 和被裁剪的图形 合并
210+
static Future<flutterUi.Image> drawMaskImage(String originImageUrl,
211+
String frameImageUrl, String maskImage, Offset offset) {
212+
Completer<flutterUi.Image> completer = new Completer<flutterUi.Image>();
213+
Future.wait([
214+
ImageLoader.load(maskImage),
215+
//获取裁剪图片
216+
drawFrameImage(originImageUrl, frameImageUrl)
217+
]).then((result) {
218+
Paint paint = new Paint();
219+
PictureRecorder recorder = PictureRecorder();
220+
Canvas canvas = Canvas(recorder);
221+
222+
int width = result[0].width;
223+
int height = result[0].height;
224+
225+
//合成
226+
canvas.drawImage(result[1], offset, paint);
227+
canvas.drawImageRect(
228+
result[0],
229+
Rect.fromLTWH(
230+
0, 0, result[0].width.toDouble(), result[0].height.toDouble()),
231+
Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
232+
paint);
233+
234+
//生成图片
235+
recorder.endRecording().toImage(width, height).then((image) {
236+
completer.complete(image);
237+
});
238+
}).catchError((e) {
239+
print("加载error:" + e);
240+
});
241+
return completer.future;
242+
}
243+
```
244+
245+
### 效果实现
246+
本文开始介绍了,图片分为三层,所以此处使用了Stack组件来包装PIP图片
247+
```dart
248+
new Container(
249+
width: _width,
250+
height: _width,
251+
child: new Stack(
252+
children: <Widget>[
253+
getBackgroundImage(),//底部高斯模糊图片
254+
//合成后的效果图片,使用CustomPaint 绘制出来
255+
CustomPaint(
256+
painter: DrawPainter(widget._image),
257+
size: Size(_width, _width)),
258+
],
259+
)
260+
)
261+
```
262+
263+
```dart
264+
class DrawPainter extends CustomPainter {
265+
DrawPainter(this._image);
266+
267+
flutterUi.Image _image;
268+
Paint _paint = new Paint();
269+
270+
@override
271+
void paint(Canvas canvas, Size size) {
272+
if (_image != null) {
273+
print("draw this Image");
274+
print("width =" + size.width.toString());
275+
print("height =" + size.height.toString());
276+
277+
canvas.drawImageRect(
278+
_image,
279+
Rect.fromLTWH(
280+
0, 0, _image.width.toDouble(), _image.height.toDouble()),
281+
Rect.fromLTWH(0, 0, size.width, size.height),
282+
_paint);
283+
}
284+
}
285+
286+
@override
287+
bool shouldRepaint(CustomPainter oldDelegate) {
288+
return true;
289+
}
290+
}
291+
```
292+
293+
### 图片保存
294+
Flutter 是一个跨平台的高性能UI框架,使用到Native Service的部分,需要各自实现,此处需要把图片保存到本地,使用了一个库,用于获取各自平台的可以保存文件的文件路径.
295+
```
296+
path_provider: ^0.4.1
297+
```
298+
299+
实现步骤,先将上面的PIP用一个RepaintBoundary 组件包裹,然后通过给RepaintBoundary设置key,再去截图保存,实现代码如下
300+
```dart
301+
Widget getPIPImageWidget() {
302+
return RepaintBoundary(
303+
key: pipCaptureKey,
304+
child: new Center(child: new DrawPIPWidget(_originImage, _image)),
305+
);
306+
}
307+
308+
```
309+
310+
截屏保存
311+
```dart
312+
Future<void> _captureImage() async {
313+
RenderRepaintBoundary boundary =
314+
pipCaptureKey.currentContext.findRenderObject();
315+
var image = await boundary.toImage();
316+
ByteData byteData = await image.toByteData(format: ImageByteFormat.png);
317+
Uint8List pngBytes = byteData.buffer.asUint8List();
318+
getApplicationDocumentsDirectory().then((dir) {
319+
String path = dir.path + "/pip.png";
320+
new File(path).writeAsBytesSync(pngBytes);
321+
_showPathDialog(path);
322+
});
323+
}
324+
```
325+
326+
显示图片的保存路径
327+
```dart
328+
Future<void> _showPathDialog(String path) async {
329+
return showDialog<void>(
330+
context: context,
331+
barrierDismissible: false,
332+
builder: (BuildContext context) {
333+
return AlertDialog(
334+
title: Text('PIP Path'),
335+
content: SingleChildScrollView(
336+
child: ListBody(
337+
children: <Widget>[
338+
Text('Image is save in $path'),
339+
],
340+
),
341+
),
342+
actions: <Widget>[
343+
FlatButton(
344+
child: Text('退出'),
345+
onPressed: () {
346+
Navigator.of(context).pop();
347+
},
348+
),
349+
],
350+
);
351+
},
352+
);
353+
}
354+
```
355+
356+
# 手势交互实现思路
357+
358+
目前的实现方式是:把原图移动到中央进行裁剪,默认认为图片的重要显示区域在中央,这样就会存在一个问题,如果图片的重要显示区域没有在中央,或者画中画效果的显示区域不在中央,会存在一定的偏差.
359+
360+
所以需要添加手势交互,当图片重要区域不在中央,或者画中画效果不在中央,可以手动调整显示区域。
361+
362+
363+
实现思路:添加手势操作,获取当前手势的offset,重新拿原图和frame区域进行裁剪,就可以正常显示.(目前暂未去实现)
364+
365+
366+
# 文末
367+
欢迎star [Github Code](https://github.com/DingProg/FlutterPIP)
368+
369+
文中所有使用的资源图片,仅供学习使用,请在学习后,24小时内删除,如若有侵权,请联系作者删除。
6370

7-
This project is a starting point for a Flutter application.
8371

9-
A few resources to get you started if this is your first Flutter project:
10372

11-
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
12-
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
13373

14-
For help getting started with Flutter, view our
15-
[online documentation](https://flutter.dev/docs), which offers tutorials,
16-
samples, guidance on mobile development, and a full API reference.

0 commit comments

Comments
 (0)