Add colorFilterLayer/Widget (#35468)
diff --git a/bin/internal/goldens.version b/bin/internal/goldens.version index d264de3..3299376 100644 --- a/bin/internal/goldens.version +++ b/bin/internal/goldens.version
@@ -1 +1,2 @@ -b2cef0060add1b33bf65c97aaf80146b54cf7b86 +09a19be7d0cd25eb7602b6ead198e8cf234a9ed2 +
diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index e41b6c0..22ef5d0 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart
@@ -1196,6 +1196,45 @@ } } +/// A composite layer that applies a [ColorFilter] to its children. +class ColorFilterLayer extends ContainerLayer { + /// Creates a layer that applies a [ColorFilter] to its children. + /// + /// The [ColorFilter] property must be non-null before the compositing phase + /// of the pipeline. + ColorFilterLayer({ + @required ColorFilter colorFilter, + }) : _colorFilter = colorFilter, + assert(colorFilter != null); + + /// The color filter to apply to children. + /// + /// The scene must be explicitly recomposited after this property is changed + /// (as described at [Layer]). + ColorFilter get colorFilter => _colorFilter; + ColorFilter _colorFilter; + set colorFilter(ColorFilter value) { + if (value != _colorFilter) { + _colorFilter = value; + markNeedsAddToScene(); + } + } + + @override + ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + builder.pushColorFilter(colorFilter); + addChildrenToScene(builder, layerOffset); + builder.pop(); + return null; // this does not return an engine layer yet. + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty<ColorFilter>('colorFilter', colorFilter)); + } +} + /// A composited layer that applies a given transformation matrix to its /// children. ///
diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 3aeb6d8..8d9424f 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart
@@ -444,6 +444,24 @@ } } + /// Blend further painting with a color filter. + /// + /// * `offset` is the offset from the origin of the canvas' coordinate system + /// to the origin of the caller's coordinate system. + /// * `colorFilter` is the [ColorFilter] value to use when blending the + /// painting done by `painter`. + /// * `painter` is a callback that will paint with the `colorFilter` applied. + /// This function calls the `painter` synchronously. + /// + /// A [RenderObject] that uses this function is very likely to require its + /// [RenderObject.alwaysNeedsCompositing] property to return true. That informs + /// ancestor render objects that this render object will include a composited + /// layer, which, for example, causes them to use composited clips. + void pushColorFilter(Offset offset, ColorFilter colorFilter, PaintingContextCallback painter) { + assert(colorFilter != null); + pushLayer(ColorFilterLayer(colorFilter: colorFilter), painter, offset); + } + /// Transform further painting using a matrix. /// /// * `needsCompositing` is whether the child needs compositing. Typically
diff --git a/packages/flutter/lib/src/widgets/color_filter.dart b/packages/flutter/lib/src/widgets/color_filter.dart new file mode 100644 index 0000000..923784d --- /dev/null +++ b/packages/flutter/lib/src/widgets/color_filter.dart
@@ -0,0 +1,60 @@ +// 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:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'framework.dart'; + +/// Applies a [ColorFilter] to its child. +@immutable +class ColorFiltered extends SingleChildRenderObjectWidget { + /// Creates a widget that applies a [ColorFilter] to its child. + /// + /// The [colorFilter] must not be null. + const ColorFiltered({@required this.colorFilter, Widget child, Key key}) + : assert(colorFilter != null), + super(key: key, child: child); + + /// The color filter to apply to the child of this widvget. + final ColorFilter colorFilter; + + @override + RenderObject createRenderObject(BuildContext context) => _ColorFilterRenderObject(colorFilter); + + @override + void updateRenderObject(BuildContext context, _ColorFilterRenderObject renderObject) { + renderObject..colorFilter = colorFilter; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty<ColorFilter>('colorFilter', colorFilter)); + } +} + +class _ColorFilterRenderObject extends RenderProxyBox { + _ColorFilterRenderObject(this._colorFilter); + + ColorFilter get colorFilter => _colorFilter; + ColorFilter _colorFilter; + set colorFilter(ColorFilter value) { + assert(value != null); + if (value != _colorFilter) { + _colorFilter = value; + markNeedsPaint(); + } + } + + @override + bool get alwaysNeedsCompositing => child != null; + + @override + void paint(PaintingContext context, Offset offset) { + context.pushColorFilter(offset, colorFilter, super.paint); + } +}
diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index d0871cf..5c8e006 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart
@@ -27,6 +27,7 @@ export 'src/widgets/basic.dart'; export 'src/widgets/binding.dart'; export 'src/widgets/bottom_navigation_bar_item.dart'; +export 'src/widgets/color_filter.dart'; export 'src/widgets/container.dart'; export 'src/widgets/debug.dart'; export 'src/widgets/dismissible.dart';
diff --git a/packages/flutter/test/rendering/layers_test.dart b/packages/flutter/test/rendering/layers_test.dart index 8abcf84..0137209 100644 --- a/packages/flutter/test/rendering/layers_test.dart +++ b/packages/flutter/test/rendering/layers_test.dart
@@ -250,6 +250,15 @@ }); }); + test('mutating ColorFilterLayer fields triggers needsAddToScene', () { + final ColorFilterLayer layer = ColorFilterLayer( + colorFilter: const ColorFilter.mode(Color(0xFFFF0000), BlendMode.color), + ); + checkNeedsAddToScene(layer, () { + layer.colorFilter = const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color); + }); + }); + test('mutating ShaderMaskLayer fields triggers needsAddToScene', () { const Gradient gradient = RadialGradient(colors: <Color>[Color(0x00000000), Color(0x00000001)]); final Shader shader = gradient.createShader(Rect.zero);
diff --git a/packages/flutter/test/widgets/color_filter_test.dart b/packages/flutter/test/widgets/color_filter_test.dart new file mode 100644 index 0000000..2f09cd1 --- /dev/null +++ b/packages/flutter/test/widgets/color_filter_test.dart
@@ -0,0 +1,69 @@ +// 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 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; + +void main() { + testWidgets('Color filter - red', (WidgetTester tester) async { + await tester.pumpWidget( + const RepaintBoundary( + child: ColorFiltered( + colorFilter: ColorFilter.mode(Colors.red, BlendMode.color), + child: Placeholder(), + ), + ), + ); + await expectLater( + find.byType(ColorFiltered), + matchesGoldenFile( + 'color_filter_red.png', + version: 1, + ), + skip: !isLinux + ); + }); + + testWidgets('Color filter - sepia', (WidgetTester tester) async { + // TODO(dnfield): This should be const. https://github.com/dart-lang/sdk/issues/37503 + final ColorFilter sepia = ColorFilter.matrix(<double>[ + 0.39, 0.769, 0.189, 0, 0, // + 0.349, 0.686, 0.168, 0, 0, // + 0.272, 0.534, 0.131, 0, 0, // + 0, 0, 0, 1, 0, // + ]); + await tester.pumpWidget( + RepaintBoundary( + child: ColorFiltered( + colorFilter: sepia, + child: MaterialApp( + title: 'Flutter Demo', + theme: ThemeData(primarySwatch: Colors.blue), + home: Scaffold( + appBar: AppBar( + title: const Text('Sepia ColorFilter Test'), + ), + body: const Center( + child:Text('Hooray!'), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { }, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ), + ), + ), + ) + ); + await expectLater( + find.byType(ColorFiltered), + matchesGoldenFile( + 'color_filter_sepia.png', + version: 1, + ), + skip: !isLinux + ); + }); +} \ No newline at end of file