предисловие
Перейти к предыдущей статьеРеализация боковой панели Flutter и пользовательского интерфейса выбора города, Сегодня я продолжу рассказывать о реализации Flutter, реализации эффекта картинка в картинке. Давайте сначала посмотрим на эффект реализации PIP.
Дополнительные эффекты см. в PIP DEMO. Кодовый адрес:FlutterPIP
Зачем эта статья?
Когда я просматривал круг друзей за один день, я обнаружил, что друг отправил картину (конечно, не подруга, но женщину), похожую на часть эффекта выше. Эффект довольно круто на первый взгляд, как это Достигнут? Вы хотите сделать это сами? Поэтому я начал готовиться к его реализации с Android.
Но я недавно изучил Flutter, и когда я изучил Flutter для настройки View CustomPainter, я обнаружил на Android тот же API, Canvas, Paint, Path и т. д. Проверьте часть рисования Canvas Код drawImage выглядит следующим образом
/// 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 использует код «Canvas_drawImage» нативного Flutter Engine, который передается для рисования Flutter Native.Тогда рисование Canvas может быть столь же эффективным, как и нативное на мобильная сторона (Flutter Принцип рисования Flutter определяет эффективность Flutter). Для эффективности Flutter вы можете просмотретьПринцип высокой производительности флаттера
Этапы реализации
Глядя на эффект от нижнего слоя к верхнему слою, изображение разделено на три части, первая часть представляет собой эффект размытия по Гауссу нижнего слоя, второй слой представляет собой обрезанную часть исходного изображения, а третий слой это маска эффекта.
Реализация эффекта флаттерного размытия по Гауссу
Flutter предоставляет BackdropFilter, об этом говорится в официальной документации BackdropFilter.
A widget that applies a filter to the existing painted content and then paints child.
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.
Проще говоря, это фильтр, который отфильтровывает все виджеты, нарисованные на подконтенте.Официальный демонстрационный пример выглядит следующим образом.
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(' '),
),
),
),
),
],
);
Размер Контейнера такой же, как размер изображения, и Контейнер должен иметь дочерние элементы управления или цвета фона.Дочерние элементы управления и цвета фона могут быть произвольными. Эффект показан на рисунке
Обрезка флаттера
Принцип кадрирования изображения
Пиксель при построении с использованием Android Canvas может соответствовать положению с помощью PorterDuffXfermode. много интересных эффектов.
Flutter также имеет тот же API.Установив свойство blendMode кисти Paint, можно добиться того же эффекта.Для получения подробной информации о режиме наложения вы можете проверить официальную документацию по 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.png) для смешивания с исходным изображением. Изображение кадра выглядит следующим образом.
код реализации
/// 通过 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;
}
в три основных шага
- Первым шагом является загрузка исходного изображения и изображения кадра, а также использование Future.wait для ожидания загрузки обоих изображений.
- Исходное изображение масштабируется, панорамируется, масштабируется до соответствующего размера кадра, а затем панорамируется в центр изображения.
- Установите режим наложения краски, нарисуйте рамку и завершите обрезку.
Эффект после обрезки следующий
Синтез и сохранение изображений Flutter
Композиция обрезанного изображения и изображения эффекта (mask.png)
Посмотрите на картинки длиннописей
Комбинация обрезанного изображения и изображения маски не требует установки режима наложения.Обрезанное изображение находится на нижнем слое, а синтезированное изображение – на верхнем слое.Этого можно добиться, но следует отметить, что обрезанное изображение должно быть отрисовано в области эффекта, поэтому 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, а затем сохраните снимок экрана Код реализации выглядит следующим образом.
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();
},
),
],
);
},
);
}
Идея жестового взаимодействия
Текущий метод реализации: переместите исходное изображение в центр для кадрирования.По умолчанию важной областью отображения изображения считается центр, поэтому будет проблема.Если важная область отображения изображение не в центре или область отображения эффекта «картинка в картинке». Если оно не в центре, будет определенное отклонение.
Следовательно, необходимо добавить взаимодействие жестов.Когда важная область изображения находится не в центре или эффект «картинка в картинке» не находится в центре, вы можете вручную настроить область отображения.
Идея реализации: добавить операцию жеста, получить смещение текущего жеста, заново обрезать исходное изображение и область кадра, после чего можно будет нормально отображать (в настоящее время не реализовано).
конец статьи
приветственная звездаGithub Code
Все ресурсные изображения, используемые в этой статье, предназначены только для использования в учебных целях. Пожалуйста, удалите их в течение 24 часов после изучения. Если есть какие-либо нарушения, пожалуйста, свяжитесь с автором, чтобы удалить его.