Unverified Commit 21f22ed3 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Visibility widget (#20365)

* Visibility widget

This attempts to address the confusion around how to hide a widget subtree.

* Apply review comments

* More clarifications
parent 6146c0f1
......@@ -717,16 +717,23 @@ class RenderOpacity extends RenderProxyBox {
/// Creates a partially transparent render object.
///
/// The [opacity] argument must be between 0.0 and 1.0, inclusive.
RenderOpacity({ double opacity = 1.0, RenderBox child })
: assert(opacity != null),
RenderOpacity({
double opacity = 1.0,
bool alwaysIncludeSemantics = false,
RenderBox child,
}) : assert(opacity != null),
assert(opacity >= 0.0 && opacity <= 1.0),
assert(alwaysIncludeSemantics != null),
_opacity = opacity,
_alwaysIncludeSemantics = alwaysIncludeSemantics,
_alpha = _getAlphaFromOpacity(opacity),
super(child);
@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
......@@ -755,13 +762,26 @@ class RenderOpacity extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
int _alpha;
/// 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 paint(PaintingContext context, Offset offset) {
if (child != null) {
if (_alpha == 0)
if (_alpha == 0) {
return;
}
if (_alpha == 255) {
context.paintChild(child, offset);
return;
......@@ -773,7 +793,7 @@ class RenderOpacity extends RenderProxyBox {
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && _alpha != 0)
if (child != null && (_alpha != 0 || alwaysIncludeSemantics))
visitor(child);
}
......@@ -781,6 +801,7 @@ class RenderOpacity extends RenderProxyBox {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DoubleProperty('opacity', opacity));
properties.add(new FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics'));
}
}
......@@ -832,6 +853,8 @@ class RenderAnimatedOpacity extends RenderProxyBox {
/// 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;
......@@ -893,6 +916,7 @@ class RenderAnimatedOpacity extends RenderProxyBox {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<Animation<double>>('opacity', opacity));
properties.add(new FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics'));
}
}
......
......@@ -145,15 +145,15 @@ class Directionality extends InheritedWidget {
///
/// ## Opacity Animation
///
/// [Opacity] animations should be built using [AnimatedOpacity] rather than
/// manually rebuilding the [Opacity] widget.
///
/// Animating an [Opacity] widget directly causes the widget (and possibly its
/// subtree) to rebuild each frame, which is not very efficient. Consider using
/// an [AnimatedOpacity] instead.
///
/// See also:
///
/// * [Visibility], which can hide a child more efficiently (albeit less
/// subtly, because it is either visible or hidden, rather than allowing
/// fractional opacity values).
/// * [ShaderMask], which can apply more elaborate effects to its child.
/// * [Transform], which applies an arbitrary transform to its child widget at
/// paint time.
......@@ -169,8 +169,10 @@ class Opacity extends SingleChildRenderObjectWidget {
const Opacity({
Key key,
@required this.opacity,
this.alwaysIncludeSemantics = false,
Widget child,
}) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
assert(alwaysIncludeSemantics != null),
super(key: key, child: child);
/// The fraction to scale the child's alpha value.
......@@ -185,18 +187,36 @@ class Opacity extends SingleChildRenderObjectWidget {
/// expensive.
final double opacity;
/// Whether the semantic information of the children is always included.
///
/// Defaults to false.
///
/// When true, regardless of the opacity settings the 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
RenderOpacity createRenderObject(BuildContext context) => new RenderOpacity(opacity: opacity);
RenderOpacity createRenderObject(BuildContext context) {
return new RenderOpacity(
opacity: opacity,
alwaysIncludeSemantics: alwaysIncludeSemantics,
);
}
@override
void updateRenderObject(BuildContext context, RenderOpacity renderObject) {
renderObject.opacity = opacity;
renderObject
..opacity = opacity
..alwaysIncludeSemantics = alwaysIncludeSemantics;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DoubleProperty('opacity', opacity));
properties.add(new FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics'));
}
}
......@@ -1710,6 +1730,12 @@ class SizedBox extends SingleChildRenderObjectWidget {
height = double.infinity,
super(key: key, child: child);
/// Creates a box that will become as small as its parent allows.
const SizedBox.shrink({ Key key, Widget child })
: width = 0.0,
height = 0.0,
super(key: key, child: child);
/// Creates a box with the specified size.
SizedBox.fromSize({ Key key, Widget child, Size size })
: width = size?.width,
......@@ -1740,17 +1766,27 @@ class SizedBox extends SingleChildRenderObjectWidget {
@override
String toStringShort() {
final String type = (width == double.infinity && height == double.infinity) ?
'$runtimeType.expand' : '$runtimeType';
String type;
if (width == double.infinity && height == double.infinity) {
type = '$runtimeType.expand';
} else if (width == 0.0 && height == 0.0) {
type = '$runtimeType.shrink';
} else {
type = '$runtimeType';
}
return key == null ? '$type' : '$type-$key';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
final DiagnosticLevel level = (width == double.infinity && height == double.infinity)
? DiagnosticLevel.hidden
: DiagnosticLevel.info;
DiagnosticLevel level;
if ((width == double.infinity && height == double.infinity) ||
(width == 0.0 && height == 0.0)) {
level = DiagnosticLevel.hidden;
} else {
level = DiagnosticLevel.info;
}
properties.add(new DoubleProperty('width', width, defaultValue: null, level: level));
properties.add(new DoubleProperty('height', height, defaultValue: null, level: level));
}
......@@ -2237,9 +2273,9 @@ class SizedOverflowBox extends SingleChildRenderObjectWidget {
}
}
/// A widget that lays the child out as if it was in the tree, but without painting anything,
/// without making the child available for hit testing, and without taking any
/// room in the parent.
/// A widget that lays the child out as if it was in the tree, but without
/// painting anything, without making the child available for hit testing, and
/// without taking any room in the parent.
///
/// Animations continue to run in offstage children, and therefore use battery
/// and CPU time, regardless of whether the animations end up being visible.
......@@ -2251,8 +2287,10 @@ class SizedOverflowBox extends SingleChildRenderObjectWidget {
///
/// See also:
///
/// * The [catalog of layout widgets](https://flutter.io/widgets/layout/).
/// * [Visibility], which can hide a child more efficiently (albeit less
/// subtly).
/// * [TickerMode], which can be used to disable animations in a subtree.
/// * The [catalog of layout widgets](https://flutter.io/widgets/layout/).
class Offstage extends SingleChildRenderObjectWidget {
/// Creates a widget that visually hides its child.
const Offstage({ Key key, this.offstage = true, Widget child })
......
......@@ -341,8 +341,8 @@ class FadeTransition extends SingleChildRenderObjectWidget {
const FadeTransition({
Key key,
@required this.opacity,
Widget child,
this.alwaysIncludeSemantics = false,
Widget child,
}) : super(key: key, child: child);
/// The animation that controls the opacity of the child.
......@@ -382,6 +382,7 @@ class FadeTransition extends SingleChildRenderObjectWidget {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<Animation<double>>('opacity', opacity));
properties.add(new FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics'));
}
}
......
This diff is collapsed.
......@@ -99,5 +99,6 @@ export 'src/widgets/title.dart';
export 'src/widgets/transitions.dart';
export 'src/widgets/unique_widget.dart';
export 'src/widgets/viewport.dart';
export 'src/widgets/visibility.dart';
export 'src/widgets/widget_inspector.dart';
export 'src/widgets/will_pop_scope.dart';
......@@ -126,6 +126,13 @@ class TestRecordingPaintingContext extends ClipContext implements PaintingContex
canvas.restore();
}
@override
void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) {
canvas.saveLayer(null, null); // TODO(ianh): Expose the alpha somewhere.
painter(this, offset);
canvas.restore();
}
@override
void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset, {Rect childPaintBounds}) {
painter(this, offset);
......
// Copyright 2018 The Chromium 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import 'semantics_tester.dart';
void main() {
testWidgets('Opacity', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
// Opacity 1.0: Semantics and painting
await tester.pumpWidget(
const Opacity(
child: Text('a', textDirection: TextDirection.rtl),
opacity: 1.0,
),
);
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
label: 'a',
textDirection: TextDirection.rtl,
)
],
),
));
expect(find.byType(Opacity), paints..paragraph());
// Opacity 0.0: Nothing
await tester.pumpWidget(
const Opacity(
child: Text('a', textDirection: TextDirection.rtl),
opacity: 0.0,
),
);
expect(semantics, hasSemantics(
new TestSemantics.root(),
));
expect(find.byType(Opacity), paintsNothing);
// Opacity 0.0 with semantics: Just semantics
await tester.pumpWidget(
const Opacity(
child: Text('a', textDirection: TextDirection.rtl),
opacity: 0.0,
alwaysIncludeSemantics: true,
),
);
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
label: 'a',
textDirection: TextDirection.rtl,
)
],
),
));
expect(find.byType(Opacity), paintsNothing);
// Opacity 0.0 without semantics: Nothing
await tester.pumpWidget(
const Opacity(
child: Text('a', textDirection: TextDirection.rtl),
opacity: 0.0,
alwaysIncludeSemantics: false,
),
);
expect(semantics, hasSemantics(
new TestSemantics.root(),
));
expect(find.byType(Opacity), paintsNothing);
// Opacity 0.1: Semantics and painting
await tester.pumpWidget(
const Opacity(
child: Text('a', textDirection: TextDirection.rtl),
opacity: 0.1,
),
);
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
label: 'a',
textDirection: TextDirection.rtl,
)
],
),
));
expect(find.byType(Opacity), paints..paragraph());
// Opacity 0.1 without semantics: Still has semantics and painting
await tester.pumpWidget(
const Opacity(
child: Text('a', textDirection: TextDirection.rtl),
opacity: 0.1,
alwaysIncludeSemantics: false,
),
);
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
label: 'a',
textDirection: TextDirection.rtl,
)
],
),
));
expect(find.byType(Opacity), paints..paragraph());
// Opacity 0.1 with semantics: Semantics and painting
await tester.pumpWidget(
const Opacity(
child: Text('a', textDirection: TextDirection.rtl),
opacity: 0.1,
alwaysIncludeSemantics: true,
),
);
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
label: 'a',
textDirection: TextDirection.rtl,
)
],
),
));
expect(find.byType(Opacity), paints..paragraph());
semantics.dispose();
});
}
......@@ -225,8 +225,6 @@ class TestSemantics {
DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.inverseHitTest,
}
) {
final SemanticsData nodeData = node.getSemanticsData();
bool fail(String message) {
matchState[TestSemantics] = '$message';
return false;
......@@ -237,6 +235,8 @@ class TestSemantics {
if (!ignoreId && id != node.id)
return fail('expected node id $id but found id ${node.id}.');
final SemanticsData nodeData = node.getSemanticsData();
final int flagsBitmask = flags is int
? flags
: flags.fold<int>(0, (int bitmask, SemanticsFlag flag) => bitmask | flag.index);
......@@ -372,7 +372,7 @@ class SemanticsTester {
}
@override
String toString() => 'SemanticsTester for ${tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode}';
String toString() => 'SemanticsTester for ${tester.binding.pipelineOwner.semanticsOwner?.rootSemanticsNode}';
/// Returns all semantics nodes in the current semantics tree whose properties
/// match the non-null arguments.
......@@ -487,7 +487,7 @@ class SemanticsTester {
/// over-test. Prefer breaking your widgets into smaller widgets and test them
/// individually.
String generateTestSemanticsExpressionForCurrentSemanticsTree(DebugSemanticsDumpOrder childOrder) {
final SemanticsNode node = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
final SemanticsNode node = tester.binding.pipelineOwner.semanticsOwner?.rootSemanticsNode;
return _generateSemanticsTestForNode(node, 0, childOrder);
}
......@@ -520,6 +520,8 @@ class SemanticsTester {
/// Recursively generates [TestSemantics] code for [node] and its children,
/// indenting the expression by `indentAmount`.
static String _generateSemanticsTestForNode(SemanticsNode node, int indentAmount, DebugSemanticsDumpOrder childOrder) {
if (node == null)
return 'null';
final String indent = ' ' * indentAmount;
final StringBuffer buf = new StringBuffer();
final SemanticsData nodeData = node.getSemanticsData();
......@@ -590,7 +592,7 @@ class _HasSemantics extends Matcher {
@override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
final bool doesMatch = _semantics._matches(
item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode,
item.tester.binding.pipelineOwner.semanticsOwner?.rootSemanticsNode,
matchState,
ignoreTransform: ignoreTransform,
ignoreRect: ignoreRect,
......@@ -600,6 +602,9 @@ class _HasSemantics extends Matcher {
if (!doesMatch) {
matchState['would-match'] = item.generateTestSemanticsExpressionForCurrentSemanticsTree(childOrder);
}
if (item.tester.binding.pipelineOwner.semanticsOwner == null) {
matchState['additional-notes'] = '(Check that the SemanticsTester has not been disposed early.)';
}
return doesMatch;
}
......@@ -608,14 +613,25 @@ class _HasSemantics extends Matcher {
return description.add('semantics node matching:\n$_semantics');
}
String _indent(String text) {
return text.toString().trimRight().split('\n').map((String line) => ' $line').join('\n');
}
@override
Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
return mismatchDescription
Description result = mismatchDescription
.add('${matchState[TestSemantics]}\n')
.add('Current SemanticsNode tree:\n')
.add(RendererBinding.instance?.renderView?.debugSemantics?.toStringDeep(childOrder: childOrder))
.add(_indent(RendererBinding.instance?.renderView?.debugSemantics?.toStringDeep(childOrder: childOrder)))
.add('\n')
.add('The semantics tree would have matched the following configuration:\n')
.add(matchState['would-match']);
.add(_indent(matchState['would-match']));
if (matchState.containsKey('additional-notes')) {
result = result
.add('\n')
.add(matchState['additional-notes']);
}
return result;
}
}
......
......@@ -30,6 +30,10 @@ void main() {
const SizedBox f = SizedBox.expand();
expect(f.width, double.infinity);
expect(f.height, double.infinity);
const SizedBox g = SizedBox.shrink();
expect(g.width, 0.0);
expect(g.height, 0.0);
});
testWidgets('SizedBox - no child', (WidgetTester tester) async {
......@@ -95,6 +99,15 @@ void main() {
)
);
expect(patient.currentContext.size, equals(const Size(800.0, 600.0)));
await tester.pumpWidget(
new Center(
child: new SizedBox.shrink(
key: patient,
)
)
);
expect(patient.currentContext.size, equals(const Size(0.0, 0.0)));
});
testWidgets('SizedBox - container child', (WidgetTester tester) async {
......@@ -166,5 +179,15 @@ void main() {
)
);
expect(patient.currentContext.size, equals(const Size(800.0, 600.0)));
await tester.pumpWidget(
new Center(
child: new SizedBox.shrink(
key: patient,
child: new Container(),
)
)
);
expect(patient.currentContext.size, equals(const Size(0.0, 0.0)));
});
}
This diff is collapsed.
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