Unverified Commit e2cf2f0f authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

SliverOpacity (#44289)

parent 94270759
......@@ -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);
});
}
......
......@@ -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;
......
......@@ -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<RenderSliver> {
/// 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<bool>('ignoring', ignoring));
properties.add(
DiagnosticsProperty<bool>(
'ignoringSemantics',
_effectiveIgnoringSemantics,
description: ignoringSemantics == null ?
'implicitly $_effectiveIgnoringSemantics' :
null,
),
);
properties.add(DiagnosticsProperty<bool>('ignoringSemantics', _effectiveIgnoringSemantics, description: ignoringSemantics == null ? 'implicitly $_effectiveIgnoringSemantics' : null,),);
}
}
......@@ -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<Widget> listItems = <Widget>[
/// 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<double>('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
......
......@@ -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<void> 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 <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CustomScrollView(slivers: <Widget>[sliver])
)
Widget _boilerPlate(Widget sliver) {
return Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CustomScrollView(slivers: <Widget>[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<String> events = <String>[];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment