From e2cf2f0fa257dcea97ce4eb425cb19d665d471a6 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Fri, 22 Nov 2019 11:12:09 -0800 Subject: [PATCH] SliverOpacity (#44289) --- packages/flutter/lib/src/rendering/flow.dart | 7 +- .../flutter/lib/src/rendering/proxy_box.dart | 12 +- .../flutter/lib/src/rendering/sliver.dart | 161 +++++++++++++++++- packages/flutter/lib/src/widgets/sliver.dart | 102 +++++++++++ .../flutter/test/widgets/slivers_test.dart | 157 +++++++++++++++-- 5 files changed, 403 insertions(+), 36 deletions(-) diff --git a/packages/flutter/lib/src/rendering/flow.dart b/packages/flutter/lib/src/rendering/flow.dart index 81e72c8149..eec0f30208 100644 --- a/packages/flutter/lib/src/rendering/flow.dart +++ b/packages/flutter/lib/src/rendering/flow.dart @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; +import 'dart:ui' as ui show Color; +import 'package:flutter/foundation.dart'; import 'package:vector_math/vector_math_64.dart'; import 'box.dart'; @@ -134,8 +135,6 @@ abstract class FlowDelegate { String toString() => '$runtimeType'; } -int _getAlphaFromOpacity(double opacity) => (opacity * 255).round(); - /// Parent data for use with [RenderFlow]. /// /// The [offset] property is ignored by [RenderFlow] and is always set to @@ -343,7 +342,7 @@ class RenderFlow extends RenderBox if (opacity == 1.0) { _paintingContext.pushTransform(needsCompositing, _paintingOffset, transform, painter); } else { - _paintingContext.pushOpacity(_paintingOffset, _getAlphaFromOpacity(opacity), (PaintingContext context, Offset offset) { + _paintingContext.pushOpacity(_paintingOffset, ui.Color.getAlphaFromOpacity(opacity), (PaintingContext context, Offset offset) { context.pushTransform(needsCompositing, offset, transform, painter); }); } diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 645db5b084..531986b735 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -4,7 +4,7 @@ import 'dart:async'; -import 'dart:ui' as ui show ImageFilter, Gradient, Image; +import 'dart:ui' as ui show ImageFilter, Gradient, Image, Color; import 'package:flutter/animation.dart'; import 'package:flutter/foundation.dart'; @@ -713,8 +713,6 @@ class RenderIntrinsicHeight extends RenderProxyBox { } -int _getAlphaFromOpacity(double opacity) => (opacity * 255).round(); - /// Makes its child partially transparent. /// /// This class paints its child into an intermediate buffer and then blends the @@ -737,7 +735,7 @@ class RenderOpacity extends RenderProxyBox { assert(alwaysIncludeSemantics != null), _opacity = opacity, _alwaysIncludeSemantics = alwaysIncludeSemantics, - _alpha = _getAlphaFromOpacity(opacity), + _alpha = ui.Color.getAlphaFromOpacity(opacity), super(child); @override @@ -765,11 +763,11 @@ class RenderOpacity extends RenderProxyBox { final bool didNeedCompositing = alwaysNeedsCompositing; final bool wasVisible = _alpha != 0; _opacity = value; - _alpha = _getAlphaFromOpacity(_opacity); + _alpha = ui.Color.getAlphaFromOpacity(_opacity); if (didNeedCompositing != alwaysNeedsCompositing) markNeedsCompositingBitsUpdate(); markNeedsPaint(); - if (wasVisible != (_alpha != 0)) + if (wasVisible != (_alpha != 0) && !alwaysIncludeSemantics) markNeedsSemanticsUpdate(); } @@ -895,7 +893,7 @@ class RenderAnimatedOpacity extends RenderProxyBox { void _updateOpacity() { final int oldAlpha = _alpha; - _alpha = _getAlphaFromOpacity(_opacity.value.clamp(0.0, 1.0)); + _alpha = ui.Color.getAlphaFromOpacity(_opacity.value); if (oldAlpha != _alpha) { final bool didNeedCompositing = _currentlyNeedsCompositing; _currentlyNeedsCompositing = _alpha > 0 && _alpha < 255; diff --git a/packages/flutter/lib/src/rendering/sliver.dart b/packages/flutter/lib/src/rendering/sliver.dart index 769cd22bc2..754ebbea37 100644 --- a/packages/flutter/lib/src/rendering/sliver.dart +++ b/packages/flutter/lib/src/rendering/sliver.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:math' as math; +import 'dart:ui' as ui show Color; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -1818,6 +1819,156 @@ class RenderSliverToBoxAdapter extends RenderSliverSingleBoxAdapter { } } +/// Makes its sliver child partially transparent. +/// +/// This class paints its sliver child into an intermediate buffer and then +/// blends the sliver child back into the scene, partially transparent. +/// +/// For values of opacity other than 0.0 and 1.0, this class is relatively +/// expensive, because it requires painting the sliver child into an intermediate +/// buffer. For the value 0.0, the sliver child is simply not painted at all. +/// For the value 1.0, the sliver child is painted immediately without an +/// intermediate buffer. +class RenderSliverOpacity extends RenderSliver with RenderObjectWithChildMixin { + /// Creates a partially transparent render object. + /// + /// The [opacity] argument must be between 0.0 and 1.0, inclusive. + RenderSliverOpacity({ + double opacity = 1.0, + bool alwaysIncludeSemantics = false, + RenderSliver sliver, + }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0), + assert(alwaysIncludeSemantics != null), + _opacity = opacity, + _alwaysIncludeSemantics = alwaysIncludeSemantics, + _alpha = ui.Color.getAlphaFromOpacity(opacity) { + child = sliver; + } + + @override + bool get alwaysNeedsCompositing => child != null && (_alpha != 0 && _alpha != 255); + + int _alpha; + + /// The fraction to scale the child's alpha value. + /// + /// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent + /// (i.e. invisible). + /// + /// The opacity must not be null. + /// + /// Values 1.0 and 0.0 are painted with a fast path. Other values + /// require painting the child into an intermediate buffer, which is + /// expensive. + double get opacity => _opacity; + double _opacity; + set opacity(double value) { + assert(value != null); + assert(value >= 0.0 && value <= 1.0); + if (_opacity == value) + return; + final bool didNeedCompositing = alwaysNeedsCompositing; + final bool wasVisible = _alpha != 0; + _opacity = value; + _alpha = ui.Color.getAlphaFromOpacity(_opacity); + if (didNeedCompositing != alwaysNeedsCompositing) + markNeedsCompositingBitsUpdate(); + markNeedsPaint(); + if (wasVisible != (_alpha != 0) && !alwaysIncludeSemantics) + markNeedsSemanticsUpdate(); + } + + /// Whether child semantics are included regardless of the opacity. + /// + /// If false, semantics are excluded when [opacity] is 0.0. + /// + /// Defaults to false. + bool get alwaysIncludeSemantics => _alwaysIncludeSemantics; + bool _alwaysIncludeSemantics; + set alwaysIncludeSemantics(bool value) { + if (value == _alwaysIncludeSemantics) + return; + _alwaysIncludeSemantics = value; + markNeedsSemanticsUpdate(); + } + + @override + void setupParentData(RenderObject child) { + if (child.parentData is! SliverPhysicalParentData) + child.parentData = SliverPhysicalParentData(); + } + + @override + void performLayout() { + assert(child != null); + child.layout(constraints, parentUsesSize: true); + geometry = child.geometry; + } + + @override + bool hitTestChildren(SliverHitTestResult result, {double mainAxisPosition, double crossAxisPosition}) { + return child != null + && child.geometry.hitTestExtent > 0 + && child.hitTest( + result, + mainAxisPosition: mainAxisPosition, + crossAxisPosition: crossAxisPosition, + ); + } + + @override + double childMainAxisPosition(RenderSliver child) { + assert(child != null); + assert(child == this.child); + return 0.0; + } + + @override + void paint(PaintingContext context, Offset offset) { + void _paintWithOpacity(PaintingContext context, Offset offset) => context.paintChild(child, offset); + if (child != null && child.geometry.visible) { + if (_alpha == 0) { + // No need to keep the layer. We'll create a new one if necessary. + layer = null; + return; + } + if (_alpha == 255) { + // No need to keep the layer. We'll create a new one if necessary. + layer = null; + context.paintChild(child, offset); + return; + } + assert(needsCompositing); + layer = context.pushOpacity( + offset, + _alpha, + _paintWithOpacity, + oldLayer: layer, + ); + } + } + + @override + void applyPaintTransform(RenderObject child, Matrix4 transform) { + assert(child != null); + final SliverPhysicalParentData childParentData = child.parentData; + childParentData.applyPaintTransform(transform); + } + + @override + void visitChildrenForSemantics(RenderObjectVisitor visitor) { + if (child != null && (_alpha != 0 || alwaysIncludeSemantics)) + visitor(child); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('opacity', opacity)); + properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics',)); + } +} + /// A render object that is invisible during hit testing. /// /// When [ignoring] is true, this render object (and its subtree) is invisible @@ -1928,14 +2079,6 @@ class RenderSliverIgnorePointer extends RenderSliver with RenderObjectWithChildM void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('ignoring', ignoring)); - properties.add( - DiagnosticsProperty( - 'ignoringSemantics', - _effectiveIgnoringSemantics, - description: ignoringSemantics == null ? - 'implicitly $_effectiveIgnoringSemantics' : - null, - ), - ); + properties.add(DiagnosticsProperty('ignoringSemantics', _effectiveIgnoringSemantics, description: ignoringSemantics == null ? 'implicitly $_effectiveIgnoringSemantics' : null,),); } } diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index feb729d2d1..0cab703e0b 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -1625,6 +1625,108 @@ class SliverFillRemaining extends SingleChildRenderObjectWidget { } } +/// A sliver widget that makes its sliver child partially transparent. +/// +/// This class paints its sliver child into an intermediate buffer and then +/// blends the sliver back into the scene partially transparent. +/// +/// For values of opacity other than 0.0 and 1.0, this class is relatively +/// expensive because it requires painting the sliver child into an intermediate +/// buffer. For the value 0.0, the sliver child is simply not painted at all. +/// For the value 1.0, the sliver child is painted immediately without an +/// intermediate buffer. +/// +/// {@tool sample} +/// +/// This example shows a [SliverList] when the `_visible` member field is true, +/// and hides it when it is false: +/// +/// ```dart +/// bool _visible = true; +/// List listItems = [ +/// Text('Now you see me,'), +/// Text('Now you don\'t!'), +/// ]; +/// +/// SliverOpacity( +/// opacity: _visible ? 1.0 : 0.0, +/// sliver: SliverList( +/// delegate: SliverChildListDelegate(listItems), +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// This is more efficient than adding and removing the sliver child widget +/// from the tree on demand. +/// +/// See also: +/// +/// * [Opacity], which can apply a uniform alpha effect to its child using the +/// RenderBox layout protocol. +/// * [AnimatedOpacity], which uses an animation internally to efficiently +/// animate [Opacity]. +class SliverOpacity extends SingleChildRenderObjectWidget { + /// Creates a sliver that makes its sliver child partially transparent. + /// + /// The [opacity] argument must not be null and must be between 0.0 and 1.0 + /// (inclusive). + const SliverOpacity({ + Key key, + @required this.opacity, + this.alwaysIncludeSemantics = false, + Widget sliver, + }) + : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0), + assert(alwaysIncludeSemantics != null), + super(key: key, child: sliver); + + /// The fraction to scale the sliver child's alpha value. + /// + /// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent + /// (i.e. invisible). + /// + /// The opacity must not be null. + /// + /// Values 1.0 and 0.0 are painted with a fast path. Other values + /// require painting the sliver child into an intermediate buffer, which is + /// expensive. + final double opacity; + + /// Whether the semantic information of the sliver child is always included. + /// + /// Defaults to false. + /// + /// When true, regardless of the opacity settings, the sliver child semantic + /// information is exposed as if the widget were fully visible. This is + /// useful in cases where labels may be hidden during animations that + /// would otherwise contribute relevant semantics. + final bool alwaysIncludeSemantics; + + @override + RenderSliverOpacity createRenderObject(BuildContext context) { + return RenderSliverOpacity( + opacity: opacity, + alwaysIncludeSemantics: alwaysIncludeSemantics, + ); + } + + @override + void updateRenderObject(BuildContext context, + RenderSliverOpacity renderObject) { + renderObject + ..opacity = opacity + ..alwaysIncludeSemantics = alwaysIncludeSemantics; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('opacity', opacity)); + properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics',)); + } +} + /// A sliver widget that is invisible during hit testing. /// /// When [ignoring] is true, this widget (and its subtree) is invisible diff --git a/packages/flutter/test/widgets/slivers_test.dart b/packages/flutter/test/widgets/slivers_test.dart index d095e83fcd..eccb1af741 100644 --- a/packages/flutter/test/widgets/slivers_test.dart +++ b/packages/flutter/test/widgets/slivers_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/rendering.dart'; +import '../rendering/mock_canvas.dart'; import 'semantics_tester.dart'; Future test(WidgetTester tester, double offset, { double anchor = 0.0 }) { @@ -422,24 +423,148 @@ void main() { expect(controller.offset, 800.0); }); - group('SliverIgnorePointer - ', () { - Widget _boilerPlate(Widget sliver) { - return Localizations( - locale: const Locale('en', 'us'), - delegates: const >[ - DefaultWidgetsLocalizations.delegate, - DefaultMaterialLocalizations.delegate, - ], - child: Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: CustomScrollView(slivers: [sliver]) - ) + Widget _boilerPlate(Widget sliver) { + return Localizations( + locale: const Locale('en', 'us'), + delegates: const >[ + DefaultWidgetsLocalizations.delegate, + DefaultMaterialLocalizations.delegate, + ], + child: Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: CustomScrollView(slivers: [sliver]) ) - ); - } + ) + ); + } + group('SliverOpacity - ', () { + testWidgets('painting & semantics', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + + // Opacity 1.0: Semantics and painting + await tester.pumpWidget(_boilerPlate( + const SliverOpacity( + sliver: SliverToBoxAdapter( + child: Text( + 'a', + textDirection: TextDirection.rtl, + ) + ), + opacity: 1.0, + ), + )); + + expect(semantics.nodesWith(label: 'a'), hasLength(1)); + expect(find.byType(SliverOpacity), paints..paragraph()); + + // Opacity 0.0: Nothing + await tester.pumpWidget(_boilerPlate( + const SliverOpacity( + sliver: SliverToBoxAdapter( + child: Text( + 'a', + textDirection: TextDirection.rtl, + ) + ), + opacity: 0.0, + ) + )); + + expect(semantics.nodesWith(label: 'a'), hasLength(0)); + expect(find.byType(SliverOpacity), paintsNothing); + + // Opacity 0.0 with semantics: Just semantics + await tester.pumpWidget(_boilerPlate( + const SliverOpacity( + sliver: SliverToBoxAdapter( + child: Text( + 'a', + textDirection: TextDirection.rtl, + ) + ), + opacity: 0.0, + alwaysIncludeSemantics: true, + ), + )); + + expect(semantics.nodesWith(label: 'a'), hasLength(1)); + expect(find.byType(SliverOpacity), paintsNothing); + + // Opacity 0.0 without semantics: Nothing + await tester.pumpWidget(_boilerPlate( + const SliverOpacity( + sliver: SliverToBoxAdapter( + child: Text( + 'a', + textDirection: TextDirection.rtl, + ) + ), + opacity: 0.0, + alwaysIncludeSemantics: false, + ), + )); + + expect(semantics.nodesWith(label: 'a'), hasLength(0)); + expect(find.byType(SliverOpacity), paintsNothing); + + // Opacity 0.1: Semantics and painting + await tester.pumpWidget(_boilerPlate( + const SliverOpacity( + sliver: SliverToBoxAdapter( + child: Text( + 'a', + textDirection: TextDirection.rtl, + ) + ), + opacity: 0.1, + ), + )); + + expect(semantics.nodesWith(label: 'a'), hasLength(1)); + expect(find.byType(SliverOpacity), paints..paragraph()); + + // Opacity 0.1 without semantics: Still has semantics and painting + await tester.pumpWidget(_boilerPlate( + const SliverOpacity( + sliver: SliverToBoxAdapter( + child: Text( + 'a', + textDirection: TextDirection.rtl, + ) + ), + opacity: 0.1, + alwaysIncludeSemantics: false, + ), + )); + + expect(semantics.nodesWith(label: 'a'), hasLength(1)); + expect(find.byType(SliverOpacity), paints..paragraph()); + + // Opacity 0.1 with semantics: Semantics and painting + await tester.pumpWidget(_boilerPlate( + const SliverOpacity( + sliver: SliverToBoxAdapter( + child: Text( + 'a', + textDirection: TextDirection.rtl, + ) + ), + opacity: 0.1, + alwaysIncludeSemantics: true, + ), + )); + + expect(semantics.nodesWith(label: 'a'), hasLength(1)); + expect(find.byType(SliverOpacity), paints..paragraph()); + + semantics.dispose(); + }); + }); + + group('SliverIgnorePointer - ', () { testWidgets('ignores pointer events', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final List events = [];