diff --git a/lib/ui/compositing.dart b/lib/ui/compositing.dart index 232514c0f180581c550348a4a12a8b2460ef5944..3eb13535e44cb23ef906097a2ab78841dafcf582 100644 --- a/lib/ui/compositing.dart +++ b/lib/ui/compositing.dart @@ -410,11 +410,11 @@ class SceneBuilder extends NativeFieldWrapperClass2 { /// See [pop] for details about the operation stack. BackdropFilterEngineLayer pushBackdropFilter(ImageFilter filter, { BackdropFilterEngineLayer oldLayer }) { assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushBackdropFilter')); - final BackdropFilterEngineLayer layer = BackdropFilterEngineLayer._(_pushBackdropFilter(filter)); + final BackdropFilterEngineLayer layer = BackdropFilterEngineLayer._(_pushBackdropFilter(filter._toNativeImageFilter())); assert(_debugPushLayer(layer)); return layer; } - EngineLayer _pushBackdropFilter(ImageFilter filter) native 'SceneBuilder_pushBackdropFilter'; + EngineLayer _pushBackdropFilter(_ImageFilter filter) native 'SceneBuilder_pushBackdropFilter'; /// Pushes a shader mask operation onto the operation stack. /// diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index b3861970b55d61ffbfda4d9b79cd8f8de8dfdd7c..b30f3835902c58e9aaf4e7aaebe1555e7383d3bf 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -1387,16 +1387,24 @@ class Paint { /// /// * [MaskFilter], which is used for drawing geometry. ImageFilter get imageFilter { - if (_objects == null) + if (_objects == null || _objects[_kImageFilterIndex] == null) return null; - return _objects[_kImageFilterIndex]; + return _objects[_kImageFilterIndex].creator; } + set imageFilter(ImageFilter value) { - _objects ??= List(_kObjectCount); - _objects[_kImageFilterIndex] = value; + if (value == null) { + if (_objects != null) { + _objects[_kImageFilterIndex] = null; + } + } else { + _objects ??= List(_kObjectCount); + if (_objects[_kImageFilterIndex]?.creator != value) { + _objects[_kImageFilterIndex] = value._toNativeImageFilter(); + } + } } - /// Whether the colors of the image are inverted when drawn. /// /// Inverting the colors of an image applies a new color filter that will @@ -2659,7 +2667,7 @@ class ColorFilter { /// This is a private class, rather than being the implementation of the public /// ColorFilter, because we want ColorFilter to be const constructible and /// efficiently comparable, so that widgets can check for ColorFilter equality to -// avoid repainting. +/// avoid repainting. class _ColorFilter extends NativeFieldWrapperClass2 { _ColorFilter.mode(this.creator) : assert(creator != null), @@ -2706,13 +2714,107 @@ class _ColorFilter extends NativeFieldWrapperClass2 { /// * [BackdropFilter], a widget that applies [ImageFilter] to its rendering. /// * [SceneBuilder.pushBackdropFilter], which is the low-level API for using /// this class. -class ImageFilter extends NativeFieldWrapperClass2 { +class ImageFilter { + /// Creates an image filter that applies a Gaussian blur. + ImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0 }) + : _data = _makeList(sigmaX, sigmaY), + _filterQuality = null, + _type = _kTypeBlur; + + /// Creates an image filter that applies a matrix transformation. + /// + /// For example, applying a positive scale matrix (see [Matrix4.diagonal3]) + /// when used with [BackdropFilter] would magnify the background image. + ImageFilter.matrix(Float64List matrix4, + { FilterQuality filterQuality = FilterQuality.low }) + : _data = Float64List.fromList(matrix4), + _filterQuality = filterQuality, + _type = _kTypeMatrix { + if (matrix4.length != 16) + throw ArgumentError('"matrix4" must have 16 entries.'); + } + + static Float64List _makeList(double a, double b) { + final Float64List list = Float64List(2); + if (a != null) + list[0] = a; + if (b != null) + list[1] = b; + return list; + } + + final Float64List _data; + final FilterQuality _filterQuality; + final int _type; + _ImageFilter _nativeFilter; + + // The type of SkImageFilter class to create for Skia. + static const int _kTypeBlur = 0; // MakeBlurFilter + static const int _kTypeMatrix = 1; // MakeMatrixFilterRowMajor255 + + @override + bool operator ==(dynamic other) { + if (other is! ImageFilter) { + return false; + } + final ImageFilter typedOther = other; + + if (_type != typedOther._type) { + return false; + } + if (!_listEquals(_data, typedOther._data)) { + return false; + } + + return _filterQuality == typedOther._filterQuality; + } + + _ImageFilter _toNativeImageFilter() => _nativeFilter ??= _makeNativeImageFilter(); + + _ImageFilter _makeNativeImageFilter() { + if (_data == null) { + return null; + } + switch (_type) { + case _kTypeBlur: + return _ImageFilter.blur(this); + case _kTypeMatrix: + return _ImageFilter.matrix(this); + default: + throw StateError('Unknown mode $_type for ImageFilter.'); + } + } + + @override + int get hashCode => hashValues(_filterQuality, hashList(_data), _type); + + @override + String toString() { + switch (_type) { + case _kTypeBlur: + return 'ImageFilter.blur(${_data[0]}, ${_data[1]})'; + case _kTypeMatrix: + return 'ImageFilter.matrix($_data, $_filterQuality)'; + default: + return 'Unknown ImageFilter type. This is an error. If you\'re seeing this, please file an issue at https://github.com/flutter/flutter/issues/new.'; + } + } +} + +/// An [ImageFilter] that is backed by a native SkImageFilter. +/// +/// This is a private class, rather than being the implementation of the public +/// ImageFilter, because we want ImageFilter to be efficiently comparable, so that +/// widgets can check for ImageFilter equality to avoid repainting. +class _ImageFilter extends NativeFieldWrapperClass2 { void _constructor() native 'ImageFilter_constructor'; /// Creates an image filter that applies a Gaussian blur. - ImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0 }) { + _ImageFilter.blur(this.creator) + : assert(creator != null), + assert(creator._type == ImageFilter._kTypeBlur) { _constructor(); - _initBlur(sigmaX, sigmaY); + _initBlur(creator._data[0], creator._data[1]); } void _initBlur(double sigmaX, double sigmaY) native 'ImageFilter_initBlur'; @@ -2720,14 +2822,19 @@ class ImageFilter extends NativeFieldWrapperClass2 { /// /// For example, applying a positive scale matrix (see [Matrix4.diagonal3]) /// when used with [BackdropFilter] would magnify the background image. - ImageFilter.matrix(Float64List matrix4, - { FilterQuality filterQuality = FilterQuality.low }) { - if (matrix4.length != 16) + _ImageFilter.matrix(this.creator) + : assert(creator != null), + assert(creator._type == ImageFilter._kTypeMatrix) { + if (creator._data.length != 16) throw ArgumentError('"matrix4" must have 16 entries.'); _constructor(); - _initMatrix(matrix4, filterQuality.index); + _initMatrix(creator._data, creator._filterQuality.index); } void _initMatrix(Float64List matrix4, int filterQuality) native 'ImageFilter_initMatrix'; + + /// The original Dart object that created the native wrapper, which retains + /// the values used for the filter. + final ImageFilter creator; } /// Base class for objects such as [Gradient] and [ImageShader] which diff --git a/lib/web_ui/lib/src/engine/compositor/image_filter.dart b/lib/web_ui/lib/src/engine/compositor/image_filter.dart index b638ffc13a068e3ff9393bd1f10ad76e3afe88b9..0cd8840ca057e197cf85cdbdebc3d34d42154aab 100644 --- a/lib/web_ui/lib/src/engine/compositor/image_filter.dart +++ b/lib/web_ui/lib/src/engine/compositor/image_filter.dart @@ -10,7 +10,9 @@ part of engine; class SkImageFilter implements ui.ImageFilter { js.JsObject skImageFilter; - SkImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0}) { + SkImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0}) + : _sigmaX = sigmaX, + _sigmaY = sigmaY { skImageFilter = canvasKit['SkImageFilter'].callMethod( 'MakeBlur', [ @@ -21,4 +23,24 @@ class SkImageFilter implements ui.ImageFilter { ], ); } + + final double _sigmaX; + final double _sigmaY; + + @override + bool operator ==(dynamic other) { + if (other is! SkImageFilter) { + return false; + } + final SkImageFilter typedOther = other; + return _sigmaX == typedOther._sigmaX && _sigmaY == typedOther._sigmaY; + } + + @override + int get hashCode => ui.hashValues(_sigmaX, _sigmaY); + + @override + String toString() { + return 'ImageFilter.blur($_sigmaX, $_sigmaY)'; + } } diff --git a/lib/web_ui/lib/src/engine/shader.dart b/lib/web_ui/lib/src/engine/shader.dart index 52799bf5e299588d0755109d018a3b8e6b11d386..f1a5856be51ead5ef7f8b216b25d18bc9ad43812 100644 --- a/lib/web_ui/lib/src/engine/shader.dart +++ b/lib/web_ui/lib/src/engine/shader.dart @@ -249,4 +249,21 @@ class EngineImageFilter implements ui.ImageFilter { final double sigmaX; final double sigmaY; + + @override + bool operator ==(dynamic other) { + if (other is! EngineImageFilter) { + return false; + } + final EngineImageFilter typedOther = other; + return sigmaX == typedOther.sigmaX && sigmaY == typedOther.sigmaY; + } + + @override + int get hashCode => ui.hashValues(sigmaX, sigmaY); + + @override + String toString() { + return 'ImageFilter.blur($sigmaX, $sigmaY)'; + } } diff --git a/testing/dart/image_filter_test.dart b/testing/dart/image_filter_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..3d6d201018b774a315fd4b5e2405a5f9ba9e1d71 --- /dev/null +++ b/testing/dart/image_filter_test.dart @@ -0,0 +1,126 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:test/test.dart'; + +const Color green = Color(0xFF00AA00); + +const int greenCenterBlurred = 0x1C001300; +const int greenSideBlurred = 0x15000E00; +const int greenCornerBlurred = 0x10000A00; + +const int greenCenterScaled = 0xFF00AA00; +const int greenSideScaled = 0x80005500; +const int greenCornerScaled = 0x40002B00; + +void main() { + Future getBytesForPaint(Paint paint, {int width = 3, int height = 3}) async { + final PictureRecorder recorder = PictureRecorder(); + final Canvas recorderCanvas = Canvas(recorder); + recorderCanvas.drawRect(const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), paint); + final Picture picture = recorder.endRecording(); + final Image image = await picture.toImage(width, height); + final ByteData bytes = await image.toByteData(); + + expect(bytes.lengthInBytes, equals(width * height * 4)); + return bytes.buffer.asUint32List(); + } + + ImageFilter makeBlur(double sigmaX, double sigmaY) => + ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY); + + ImageFilter makeScale(double scX, double scY, + [double trX = 0.0, double trY = 0.0, + FilterQuality quality = FilterQuality.low]) { + trX *= 1.0 - scX; + trY *= 1.0 - scY; + return ImageFilter.matrix(Float64List.fromList([ + scX, 0.0, 0.0, 0.0, + 0.0, scY, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + trX, trY, 0.0, 1.0, + ]), filterQuality: quality); + } + + List makeList() { + return [ + makeBlur(10.0, 10.0), + makeBlur(10.0, 20.0), + makeBlur(20.0, 20.0), + makeScale(10.0, 10.0), + makeScale(10.0, 20.0), + makeScale(20.0, 10.0), + makeScale(10.0, 10.0, 1.0, 1.0), + makeScale(10.0, 10.0, 0.0, 0.0, FilterQuality.medium), + makeScale(10.0, 10.0, 0.0, 0.0, FilterQuality.high), + makeScale(10.0, 10.0, 0.0, 0.0, FilterQuality.none), + ]; + } + + void checkEquality(List a, List b) { + for (int i = 0; i < a.length; i++) { + for(int j = 0; j < a.length; j++) { + if (i == j) { + expect(a[i], equals(b[j])); + expect(a[i].hashCode, equals(b[j].hashCode)); + expect(a[i].toString(), equals(b[j].toString())); + } else { + expect(a[i], isNot(b[j])); + // No expectations on hashCode if objects are not equal + expect(a[i].toString(), isNot(b[j].toString())); + } + } + } + } + + test('ImageFilter - equals', () async { + final List A = makeList(); + final List B = makeList(); + checkEquality(A, A); + checkEquality(A, B); + checkEquality(B, B); + }); + + test('ImageFilter - nulls', () async { + final Paint paint = Paint()..imageFilter = ImageFilter.blur(sigmaX: null, sigmaY: null); + expect(paint.imageFilter, equals(ImageFilter.blur())); + + expect(() => ImageFilter.matrix(null), throwsNoSuchMethodError); + }); + + void checkBytes(Uint32List bytes, int center, int side, int corner) { + expect(bytes[0], equals(corner)); + expect(bytes[1], equals(side)); + expect(bytes[2], equals(corner)); + + expect(bytes[3], equals(side)); + expect(bytes[4], equals(center)); + expect(bytes[5], equals(side)); + + expect(bytes[6], equals(corner)); + expect(bytes[7], equals(side)); + expect(bytes[8], equals(corner)); + } + + test('ImageFilter - blur', () async { + final Paint paint = Paint() + ..color = green + ..imageFilter = makeBlur(1.0, 1.0); + + final Uint32List bytes = await getBytesForPaint(paint); + checkBytes(bytes, greenCenterBlurred, greenSideBlurred, greenCornerBlurred); + }); + + test('ImageFilter - matrix', () async { + final Paint paint = Paint() + ..color = green + ..imageFilter = makeScale(2.0, 2.0, 1.5, 1.5); + + final Uint32List bytes = await getBytesForPaint(paint); + checkBytes(bytes, greenCenterScaled, greenSideScaled, greenCornerScaled); + }); +}