blob: bf51370fa9a35349f8a3cd5a097897d4024e878f [file] [log] [blame]
// Copyright 2015 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 'dart:async';
import 'dart:ui' as ui show ImageFilter, Gradient, Image;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/semantics.dart';
import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
import 'layer.dart';
import 'object.dart';
export 'package:flutter/gestures.dart' show
PointerEvent,
PointerDownEvent,
PointerMoveEvent,
PointerUpEvent,
PointerCancelEvent;
/// A base class for render objects that resemble their children.
///
/// A proxy box has a single child and simply mimics all the properties of that
/// child by calling through to the child for each function in the render box
/// protocol. For example, a proxy box determines its size by asking its child
/// to layout with the same constraints and then matching the size.
///
/// A proxy box isn't useful on its own because you might as well just replace
/// the proxy box with its child. However, RenderProxyBox is a useful base class
/// for render objects that wish to mimic most, but not all, of the properties
/// of their child.
class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin<RenderBox> {
/// Creates a proxy render box.
///
/// Proxy render boxes are rarely created directly because they simply proxy
/// the render box protocol to [child]. Instead, consider using one of the
/// subclasses.
// TODO(a14n): Remove ignore once https://github.com/dart-lang/sdk/issues/30328 is fixed
RenderProxyBox([RenderBox child = null]) { //ignore: avoid_init_to_null
this.child = child;
}
}
/// Implementation of [RenderProxyBox].
///
/// This class can be used as a mixin for situations where the proxying behavior
/// of [RenderProxyBox] is desired but inheriting from [RenderProxyBox] is
/// impractical (e.g. because you want to mix in other classes as well).
// TODO(ianh): Remove this class once https://github.com/dart-lang/sdk/issues/15101 is fixed
@optionalTypeArgs
abstract class RenderProxyBoxMixin<T extends RenderBox> extends RenderBox with RenderObjectWithChildMixin<T> {
// This class is intended to be used as a mixin, and should not be
// extended directly.
factory RenderProxyBoxMixin._() => null;
@override
void setupParentData(RenderObject child) {
// We don't actually use the offset argument in BoxParentData, so let's
// avoid allocating it at all.
if (child.parentData is! ParentData)
child.parentData = new ParentData();
}
@override
double computeMinIntrinsicWidth(double height) {
if (child != null)
return child.getMinIntrinsicWidth(height);
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child != null)
return child.getMaxIntrinsicWidth(height);
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child != null)
return child.getMinIntrinsicHeight(width);
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child != null)
return child.getMaxIntrinsicHeight(width);
return 0.0;
}
@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
if (child != null)
return child.getDistanceToActualBaseline(baseline);
return super.computeDistanceToActualBaseline(baseline);
}
@override
void performLayout() {
if (child != null) {
child.layout(constraints, parentUsesSize: true);
size = child.size;
} else {
performResize();
}
}
@override
bool hitTestChildren(HitTestResult result, { Offset position }) {
return child?.hitTest(result, position: position) ?? false;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) { }
@override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child, offset);
}
}
/// How to behave during hit tests.
enum HitTestBehavior {
/// Targets that defer to their children receive events within their bounds
/// only if one of their children is hit by the hit test.
deferToChild,
/// Opaque targets can be hit by hit tests, causing them to both receive
/// events within their bounds and prevent targets visually behind them from
/// also receiving events.
opaque,
/// Translucent targets both receive events within their bounds and permit
/// targets visually behind them to also receive events.
translucent,
}
/// A RenderProxyBox subclass that allows you to customize the
/// hit-testing behavior.
abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox {
/// Initializes member variables for subclasses.
///
/// By default, the [behavior] is [HitTestBehavior.deferToChild].
RenderProxyBoxWithHitTestBehavior({
this.behavior: HitTestBehavior.deferToChild,
RenderBox child
}) : super(child);
/// How to behave during hit testing.
HitTestBehavior behavior;
@override
bool hitTest(HitTestResult result, { Offset position }) {
bool hitTarget = false;
if (size.contains(position)) {
hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
if (hitTarget || behavior == HitTestBehavior.translucent)
result.add(new BoxHitTestEntry(this, position));
}
return hitTarget;
}
@override
bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new EnumProperty<HitTestBehavior>('behavior', behavior, defaultValue: null));
}
}
/// Imposes additional constraints on its child.
///
/// A render constrained box proxies most functions in the render box protocol
/// to its child, except that when laying out its child, it tightens the
/// constraints provided by its parent by enforcing the [additionalConstraints]
/// as well.
///
/// For example, if you wanted [child] to have a minimum height of 50.0 logical
/// pixels, you could use `const BoxConstraints(minHeight: 50.0)` as the
/// [additionalConstraints].
class RenderConstrainedBox extends RenderProxyBox {
/// Creates a render box that constrains its child.
///
/// The [additionalConstraints] argument must not be null and must be valid.
RenderConstrainedBox({
RenderBox child,
@required BoxConstraints additionalConstraints,
}) : assert(additionalConstraints != null),
assert(additionalConstraints.debugAssertIsValid()),
_additionalConstraints = additionalConstraints,
super(child);
/// Additional constraints to apply to [child] during layout
BoxConstraints get additionalConstraints => _additionalConstraints;
BoxConstraints _additionalConstraints;
set additionalConstraints(BoxConstraints value) {
assert(value != null);
assert(value.debugAssertIsValid());
if (_additionalConstraints == value)
return;
_additionalConstraints = value;
markNeedsLayout();
}
@override
double computeMinIntrinsicWidth(double height) {
if (_additionalConstraints.hasBoundedWidth && _additionalConstraints.hasTightWidth)
return _additionalConstraints.minWidth;
final double width = super.computeMinIntrinsicWidth(height);
if (_additionalConstraints.hasBoundedWidth)
return _additionalConstraints.constrainWidth(width);
return width;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (_additionalConstraints.hasBoundedWidth && _additionalConstraints.hasTightWidth)
return _additionalConstraints.minWidth;
final double width = super.computeMaxIntrinsicWidth(height);
if (_additionalConstraints.hasBoundedWidth)
return _additionalConstraints.constrainWidth(width);
return width;
}
@override
double computeMinIntrinsicHeight(double width) {
if (_additionalConstraints.hasBoundedHeight && _additionalConstraints.hasTightHeight)
return _additionalConstraints.minHeight;
final double height = super.computeMinIntrinsicHeight(width);
if (_additionalConstraints.hasBoundedHeight)
return _additionalConstraints.constrainHeight(height);
return height;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (_additionalConstraints.hasBoundedHeight && _additionalConstraints.hasTightHeight)
return _additionalConstraints.minHeight;
final double height = super.computeMaxIntrinsicHeight(width);
if (_additionalConstraints.hasBoundedHeight)
return _additionalConstraints.constrainHeight(height);
return height;
}
@override
void performLayout() {
if (child != null) {
child.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
super.debugPaintSize(context, offset);
assert(() {
Paint paint;
if (child == null || child.size.isEmpty) {
paint = new Paint()
..color = const Color(0x90909090);
context.canvas.drawRect(offset & size, paint);
}
return true;
}());
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<BoxConstraints>('additionalConstraints', additionalConstraints));
}
}
/// Constrains the child's [BoxConstraints.maxWidth] and
/// [BoxConstraints.maxHeight] if they're otherwise unconstrained.
///
/// This has the effect of giving the child a natural dimension in unbounded
/// environments. For example, by providing a [maxHeight] to a widget that
/// normally tries to be as big as possible, the widget will normally size
/// itself to fit its parent, but when placed in a vertical list, it will take
/// on the given height.
///
/// This is useful when composing widgets that normally try to match their
/// parents' size, so that they behave reasonably in lists (which are
/// unbounded).
class RenderLimitedBox extends RenderProxyBox {
/// Creates a render box that imposes a maximum width or maximum height on its
/// child if the child is otherwise unconstrained.
///
/// The [maxWidth] and [maxHeight] arguments not be null and must be
/// non-negative.
RenderLimitedBox({
RenderBox child,
double maxWidth: double.infinity,
double maxHeight: double.infinity
}) : assert(maxWidth != null && maxWidth >= 0.0),
assert(maxHeight != null && maxHeight >= 0.0),
_maxWidth = maxWidth,
_maxHeight = maxHeight,
super(child);
/// The value to use for maxWidth if the incoming maxWidth constraint is infinite.
double get maxWidth => _maxWidth;
double _maxWidth;
set maxWidth(double value) {
assert(value != null && value >= 0.0);
if (_maxWidth == value)
return;
_maxWidth = value;
markNeedsLayout();
}
/// The value to use for maxHeight if the incoming maxHeight constraint is infinite.
double get maxHeight => _maxHeight;
double _maxHeight;
set maxHeight(double value) {
assert(value != null && value >= 0.0);
if (_maxHeight == value)
return;
_maxHeight = value;
markNeedsLayout();
}
BoxConstraints _limitConstraints(BoxConstraints constraints) {
return new BoxConstraints(
minWidth: constraints.minWidth,
maxWidth: constraints.hasBoundedWidth ? constraints.maxWidth : constraints.constrainWidth(maxWidth),
minHeight: constraints.minHeight,
maxHeight: constraints.hasBoundedHeight ? constraints.maxHeight : constraints.constrainHeight(maxHeight)
);
}
@override
void performLayout() {
if (child != null) {
child.layout(_limitConstraints(constraints), parentUsesSize: true);
size = constraints.constrain(child.size);
} else {
size = _limitConstraints(constraints).constrain(Size.zero);
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DoubleProperty('maxWidth', maxWidth, defaultValue: double.infinity));
properties.add(new DoubleProperty('maxHeight', maxHeight, defaultValue: double.infinity));
}
}
/// Attempts to size the child to a specific aspect ratio.
///
/// The render object first tries the largest width permitted by the layout
/// constraints. The height of the render object is determined by applying the
/// given aspect ratio to the width, expressed as a ratio of width to height.
///
/// For example, a 16:9 width:height aspect ratio would have a value of
/// 16.0/9.0. If the maximum width is infinite, the initial width is determined
/// by applying the aspect ratio to the maximum height.
///
/// Now consider a second example, this time with an aspect ratio of 2.0 and
/// layout constraints that require the width to be between 0.0 and 100.0 and
/// the height to be between 0.0 and 100.0. We'll select a width of 100.0 (the
/// biggest allowed) and a height of 50.0 (to match the aspect ratio).
///
/// In that same situation, if the aspect ratio is 0.5, we'll also select a
/// width of 100.0 (still the biggest allowed) and we'll attempt to use a height
/// of 200.0. Unfortunately, that violates the constraints because the child can
/// be at most 100.0 pixels tall. The render object will then take that value
/// and apply the aspect ratio again to obtain a width of 50.0. That width is
/// permitted by the constraints and the child receives a width of 50.0 and a
/// height of 100.0. If the width were not permitted, the render object would
/// continue iterating through the constraints. If the render object does not
/// find a feasible size after consulting each constraint, the render object
/// will eventually select a size for the child that meets the layout
/// constraints but fails to meet the aspect ratio constraints.
class RenderAspectRatio extends RenderProxyBox {
/// Creates as render object with a specific aspect ratio.
///
/// The [aspectRatio] argument must be a finite, positive value.
RenderAspectRatio({
RenderBox child,
@required double aspectRatio,
}) : assert(aspectRatio != null),
assert(aspectRatio > 0.0),
assert(aspectRatio.isFinite),
_aspectRatio = aspectRatio,
super(child);
/// The aspect ratio to attempt to use.
///
/// The aspect ratio is expressed as a ratio of width to height. For example,
/// a 16:9 width:height aspect ratio would have a value of 16.0/9.0.
double get aspectRatio => _aspectRatio;
double _aspectRatio;
set aspectRatio(double value) {
assert(value != null);
assert(value > 0.0);
assert(value.isFinite);
if (_aspectRatio == value)
return;
_aspectRatio = value;
markNeedsLayout();
}
@override
double computeMinIntrinsicWidth(double height) {
if (height.isFinite)
return height * _aspectRatio;
if (child != null)
return child.getMinIntrinsicWidth(height);
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (height.isFinite)
return height * _aspectRatio;
if (child != null)
return child.getMaxIntrinsicWidth(height);
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (width.isFinite)
return width / _aspectRatio;
if (child != null)
return child.getMinIntrinsicHeight(width);
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (width.isFinite)
return width / _aspectRatio;
if (child != null)
return child.getMaxIntrinsicHeight(width);
return 0.0;
}
Size _applyAspectRatio(BoxConstraints constraints) {
assert(constraints.debugAssertIsValid());
assert(() {
if (!constraints.hasBoundedWidth && !constraints.hasBoundedHeight) {
throw new FlutterError(
'$runtimeType has unbounded constraints.\n'
'This $runtimeType was given an aspect ratio of $aspectRatio but was given '
'both unbounded width and unbounded height constraints. Because both '
'constraints were unbounded, this render object doesn\'t know how much '
'size to consume.'
);
}
return true;
}());
if (constraints.isTight)
return constraints.smallest;
double width = constraints.maxWidth;
double height;
// We default to picking the height based on the width, but if the width
// would be infinite, that's not sensible so we try to infer the height
// from the width.
if (width.isFinite) {
height = width / _aspectRatio;
} else {
height = constraints.maxHeight;
width = height * _aspectRatio;
}
// Similar to RenderImage, we iteratively attempt to fit within the given
// constraints while maintaining the given aspect ratio. The order of
// applying the constraints is also biased towards inferring the height
// from the width.
if (width > constraints.maxWidth) {
width = constraints.maxWidth;
height = width / _aspectRatio;
}
if (height > constraints.maxHeight) {
height = constraints.maxHeight;
width = height * _aspectRatio;
}
if (width < constraints.minWidth) {
width = constraints.minWidth;
height = width / _aspectRatio;
}
if (height < constraints.minHeight) {
height = constraints.minHeight;
width = height * _aspectRatio;
}
return constraints.constrain(new Size(width, height));
}
@override
void performLayout() {
size = _applyAspectRatio(constraints);
if (child != null)
child.layout(new BoxConstraints.tight(size));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DoubleProperty('aspectRatio', aspectRatio));
}
}
/// Sizes its child to the child's intrinsic width.
///
/// Sizes its child's width to the child's maximum intrinsic width. If
/// [stepWidth] is non-null, the child's width will be snapped to a multiple of
/// the [stepWidth]. Similarly, if [stepHeight] is non-null, the child's height
/// will be snapped to a multiple of the [stepHeight].
///
/// This class is useful, for example, when unlimited width is available and
/// you would like a child that would otherwise attempt to expand infinitely to
/// instead size itself to a more reasonable width.
///
/// This class is relatively expensive, because it adds a speculative layout
/// pass before the final layout phase. Avoid using it where possible. In the
/// worst case, this render object can result in a layout that is O(N²) in the
/// depth of the tree.
class RenderIntrinsicWidth extends RenderProxyBox {
/// Creates a render object that sizes itself to its child's intrinsic width.
RenderIntrinsicWidth({
double stepWidth,
double stepHeight,
RenderBox child
}) : _stepWidth = stepWidth, _stepHeight = stepHeight, super(child);
/// If non-null, force the child's width to be a multiple of this value.
double get stepWidth => _stepWidth;
double _stepWidth;
set stepWidth(double value) {
if (value == _stepWidth)
return;
_stepWidth = value;
markNeedsLayout();
}
/// If non-null, force the child's height to be a multiple of this value.
double get stepHeight => _stepHeight;
double _stepHeight;
set stepHeight(double value) {
if (value == _stepHeight)
return;
_stepHeight = value;
markNeedsLayout();
}
static double _applyStep(double input, double step) {
assert(input.isFinite);
if (step == null)
return input;
return (input / step).ceil() * step;
}
@override
double computeMinIntrinsicWidth(double height) {
return computeMaxIntrinsicWidth(height);
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child == null)
return 0.0;
final double width = child.getMaxIntrinsicWidth(height);
return _applyStep(width, _stepWidth);
}
@override
double computeMinIntrinsicHeight(double width) {
if (child == null)
return 0.0;
if (!width.isFinite)
width = computeMaxIntrinsicWidth(double.infinity);
assert(width.isFinite);
final double height = child.getMinIntrinsicHeight(width);
return _applyStep(height, _stepHeight);
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child == null)
return 0.0;
if (!width.isFinite)
width = computeMaxIntrinsicWidth(double.infinity);
assert(width.isFinite);
final double height = child.getMaxIntrinsicHeight(width);
return _applyStep(height, _stepHeight);
}
@override
void performLayout() {
if (child != null) {
BoxConstraints childConstraints = constraints;
if (!childConstraints.hasTightWidth) {
final double width = child.getMaxIntrinsicWidth(childConstraints.maxHeight);
assert(width.isFinite);
childConstraints = childConstraints.tighten(width: _applyStep(width, _stepWidth));
}
if (_stepHeight != null) {
final double height = child.getMaxIntrinsicHeight(childConstraints.maxWidth);
assert(height.isFinite);
childConstraints = childConstraints.tighten(height: _applyStep(height, _stepHeight));
}
child.layout(childConstraints, parentUsesSize: true);
size = child.size;
} else {
performResize();
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DoubleProperty('stepWidth', stepWidth));
properties.add(new DoubleProperty('stepHeight', stepHeight));
}
}
/// Sizes its child to the child's intrinsic height.
///
/// This class is useful, for example, when unlimited height is available and
/// you would like a child that would otherwise attempt to expand infinitely to
/// instead size itself to a more reasonable height.
///
/// This class is relatively expensive, because it adds a speculative layout
/// pass before the final layout phase. Avoid using it where possible. In the
/// worst case, this render object can result in a layout that is O(N²) in the
/// depth of the tree.
class RenderIntrinsicHeight extends RenderProxyBox {
/// Creates a render object that sizes itself to its child's intrinsic height.
RenderIntrinsicHeight({
RenderBox child
}) : super(child);
@override
double computeMinIntrinsicWidth(double height) {
if (child == null)
return 0.0;
if (!height.isFinite)
height = child.getMaxIntrinsicHeight(double.infinity);
assert(height.isFinite);
return child.getMinIntrinsicWidth(height);
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child == null)
return 0.0;
if (!height.isFinite)
height = child.getMaxIntrinsicHeight(double.infinity);
assert(height.isFinite);
return child.getMaxIntrinsicWidth(height);
}
@override
double computeMinIntrinsicHeight(double width) {
return computeMaxIntrinsicHeight(width);
}
@override
void performLayout() {
if (child != null) {
BoxConstraints childConstraints = constraints;
if (!childConstraints.hasTightHeight) {
final double height = child.getMaxIntrinsicHeight(childConstraints.maxWidth);
assert(height.isFinite);
childConstraints = childConstraints.tighten(height: height);
}
child.layout(childConstraints, parentUsesSize: true);
size = child.size;
} else {
performResize();
}
}
}
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
/// 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 child into an intermediate
/// buffer. For the value 0.0, the child is simply not painted at all. For the
/// value 1.0, the child is painted immediately without an intermediate buffer.
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),
assert(opacity >= 0.0 && opacity <= 1.0),
_opacity = opacity,
_alpha = _getAlphaFromOpacity(opacity),
super(child);
@override
bool get alwaysNeedsCompositing => child != null && (_alpha != 0 && _alpha != 255);
/// 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 = _getAlphaFromOpacity(_opacity);
if (didNeedCompositing != alwaysNeedsCompositing)
markNeedsCompositingBitsUpdate();
markNeedsPaint();
if (wasVisible != (_alpha != 0))
markNeedsSemanticsUpdate();
}
int _alpha;
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (_alpha == 0)
return;
if (_alpha == 255) {
context.paintChild(child, offset);
return;
}
assert(needsCompositing);
context.pushOpacity(offset, _alpha, super.paint);
}
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && _alpha != 0)
visitor(child);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DoubleProperty('opacity', opacity));
}
}
/// Makes its child partially transparent, driven from an [Animation].
///
/// This is a variant of [RenderOpacity] that uses an [Animation<double>] rather
/// than a [double] to control the opacity.
class RenderAnimatedOpacity extends RenderProxyBox {
/// Creates a partially transparent render object.
///
/// The [opacity] argument must not be null.
RenderAnimatedOpacity({ @required Animation<double> opacity, RenderBox child }) : super(child) {
this.opacity = opacity;
}
int _alpha;
@override
bool get alwaysNeedsCompositing => child != null && _currentlyNeedsCompositing;
bool _currentlyNeedsCompositing;
/// The animation that drives this render object's opacity.
///
/// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent
/// (i.e., invisible).
///
/// To change the opacity of a child in a static manner, not animated,
/// consider [RenderOpacity] instead.
Animation<double> get opacity => _opacity;
Animation<double> _opacity;
set opacity(Animation<double> value) {
assert(value != null);
if (_opacity == value)
return;
if (attached && _opacity != null)
_opacity.removeListener(_updateOpacity);
_opacity = value;
if (attached)
_opacity.addListener(_updateOpacity);
_updateOpacity();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_opacity.addListener(_updateOpacity);
_updateOpacity(); // in case it changed while we weren't listening
}
@override
void detach() {
_opacity.removeListener(_updateOpacity);
super.detach();
}
void _updateOpacity() {
final int oldAlpha = _alpha;
_alpha = _getAlphaFromOpacity(_opacity.value.clamp(0.0, 1.0));
if (oldAlpha != _alpha) {
final bool didNeedCompositing = _currentlyNeedsCompositing;
_currentlyNeedsCompositing = _alpha > 0 || _alpha < 255;
if (child != null && didNeedCompositing != _currentlyNeedsCompositing)
markNeedsCompositingBitsUpdate();
markNeedsPaint();
if (oldAlpha == 0 || _alpha == 0)
markNeedsSemanticsUpdate();
}
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (_alpha == 0)
return;
if (_alpha == 255) {
context.paintChild(child, offset);
return;
}
assert(needsCompositing);
context.pushOpacity(offset, _alpha, super.paint);
}
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && _alpha != 0)
visitor(child);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<Animation<double>>('opacity', opacity));
}
}
/// Signature for a function that creates a [Shader] for a given [Rect].
///
/// Used by [RenderShaderMask] and the [ShaderMask] widget.
typedef Shader ShaderCallback(Rect bounds);
/// Applies a mask generated by a [Shader] to its child.
///
/// For example, [RenderShaderMask] can be used to gradually fade out the edge
/// of a child by using a [new ui.Gradient.linear] mask.
class RenderShaderMask extends RenderProxyBox {
/// Creates a render object that applies a mask generated by a [Shader] to its child.
///
/// The [shaderCallback] and [blendMode] arguments must not be null.
RenderShaderMask({
RenderBox child,
@required ShaderCallback shaderCallback,
BlendMode blendMode: BlendMode.modulate,
}) : assert(shaderCallback != null),
assert(blendMode != null),
_shaderCallback = shaderCallback,
_blendMode = blendMode,
super(child);
/// Called to creates the [Shader] that generates the mask.
///
/// The shader callback is called with the current size of the child so that
/// it can customize the shader to the size and location of the child.
// TODO(abarth): Use the delegate pattern here to avoid generating spurious
// repaints when the ShaderCallback changes identity.
ShaderCallback get shaderCallback => _shaderCallback;
ShaderCallback _shaderCallback;
set shaderCallback(ShaderCallback value) {
assert(value != null);
if (_shaderCallback == value)
return;
_shaderCallback = value;
markNeedsPaint();
}
/// The [BlendMode] to use when applying the shader to the child.
///
/// The default, [BlendMode.modulate], is useful for applying an alpha blend
/// to the child. Other blend modes can be used to create other effects.
BlendMode get blendMode => _blendMode;
BlendMode _blendMode;
set blendMode(BlendMode value) {
assert(value != null);
if (_blendMode == value)
return;
_blendMode = value;
markNeedsPaint();
}
@override
bool get alwaysNeedsCompositing => child != null;
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
assert(needsCompositing);
context.pushLayer(
new ShaderMaskLayer(
shader: _shaderCallback(offset & size),
maskRect: offset & size,
blendMode: _blendMode,
),
super.paint,
offset,
);
}
}
}
/// Applies a filter to the existing painted content and then paints [child].
///
/// This effect is relatively expensive, especially if the filter is non-local,
/// such as a blur.
class RenderBackdropFilter extends RenderProxyBox {
/// Creates a backdrop filter.
///
/// The [filter] argument must not be null.
RenderBackdropFilter({ RenderBox child, @required ui.ImageFilter filter })
: assert(filter != null),
_filter = filter,
super(child);
/// The image filter to apply to the existing painted content before painting
/// the child.
///
/// For example, consider using [new ui.ImageFilter.blur] to create a backdrop
/// blur effect.
ui.ImageFilter get filter => _filter;
ui.ImageFilter _filter;
set filter(ui.ImageFilter value) {
assert(value != null);
if (_filter == value)
return;
_filter = value;
markNeedsPaint();
}
@override
bool get alwaysNeedsCompositing => child != null;
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
assert(needsCompositing);
context.pushLayer(new BackdropFilterLayer(filter: _filter), super.paint, offset);
}
}
}
/// An interface for providing custom clips.
///
/// This class is used by a number of clip widgets (e.g., [ClipRect] and
/// [ClipPath]).
///
/// The [getClip] method is called whenever the custom clip needs to be updated.
///
/// The [shouldReclip] method is called when a new instance of the class
/// is provided, to check if the new instance actually represents different
/// information.
///
/// The most efficient way to update the clip provided by this class is to
/// supply a reclip argument to the constructor of the [CustomClipper]. The
/// custom object will listen to this animation and update the clip whenever the
/// animation ticks, avoiding both the build and layout phases of the pipeline.
///
/// See also:
///
/// * [ClipRect], which can be customized with a [CustomClipper].
/// * [ClipRRect], which can be customized with a [CustomClipper].
/// * [ClipOval], which can be customized with a [CustomClipper].
/// * [ClipPath], which can be customized with a [CustomClipper].
abstract class CustomClipper<T> {
/// Creates a custom clipper.
///
/// The clipper will update its clip whenever [reclip] notifies its listeners.
const CustomClipper({ Listenable reclip }) : _reclip = reclip;
final Listenable _reclip;
/// Returns a description of the clip given that the render object being
/// clipped is of the given size.
T getClip(Size size);
/// Returns an approximation of the clip returned by [getClip], as
/// an axis-aligned Rect. This is used by the semantics layer to
/// determine whether widgets should be excluded.
///
/// By default, this returns a rectangle that is the same size as
/// the RenderObject. If getClip returns a shape that is roughly the
/// same size as the RenderObject (e.g. it's a rounded rectangle
/// with very small arcs in the corners), then this may be adequate.
Rect getApproximateClipRect(Size size) => Offset.zero & size;
/// Called whenever a new instance of the custom clipper delegate class is
/// provided to the clip object, or any time that a new clip object is created
/// with a new instance of the custom painter delegate class (which amounts to
/// the same thing, because the latter is implemented in terms of the former).
///
/// If the new instance represents different information than the old
/// instance, then the method should return true, otherwise it should return
/// false.
///
/// If the method returns false, then the [getClip] call might be optimized
/// away.
///
/// It's possible that the [getClip] method will get called even if
/// [shouldReclip] returns false or if the [shouldReclip] method is never
/// called at all (e.g. if the box changes size).
bool shouldReclip(covariant CustomClipper<T> oldClipper);
@override
String toString() => '$runtimeType';
}
/// A [CustomClipper] that clips to the outer path of a [ShapeBorder].
class ShapeBorderClipper extends CustomClipper<Path> {
/// Creates a [ShapeBorder] clipper.
///
/// The [shape] argument must not be null.
///
/// The [textDirection] argument must be provided non-null if [shape]
/// has a text direction dependency (for example if it is expressed in terms
/// of "start" and "end" instead of "left" and "right"). It may be null if
/// the border will not need the text direction to paint itself.
const ShapeBorderClipper({
@required this.shape,
this.textDirection,
}) : assert(shape != null);
/// The shape border whose outer path this clipper clips to.
final ShapeBorder shape;
/// The text direction to use for getting the outer path for [shape].
///
/// [ShapeBorder]s can depend on the text direction (e.g having a "dent"
/// towards the start of the shape).
final TextDirection textDirection;
/// Returns the outer path of [shape] as the clip.
@override
Path getClip(Size size) {
return shape.getOuterPath(Offset.zero & size, textDirection: textDirection);
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) {
if (oldClipper.runtimeType != ShapeBorderClipper)
return true;
final ShapeBorderClipper typedOldClipper = oldClipper;
return typedOldClipper.shape != shape;
}
}
abstract class _RenderCustomClip<T> extends RenderProxyBox {
_RenderCustomClip({
RenderBox child,
CustomClipper<T> clipper
}) : _clipper = clipper, super(child);
/// If non-null, determines which clip to use on the child.
CustomClipper<T> get clipper => _clipper;
CustomClipper<T> _clipper;
set clipper(CustomClipper<T> newClipper) {
if (_clipper == newClipper)
return;
final CustomClipper<T> oldClipper = _clipper;
_clipper = newClipper;
assert(newClipper != null || oldClipper != null);
if (newClipper == null || oldClipper == null ||
oldClipper.runtimeType != oldClipper.runtimeType ||
newClipper.shouldReclip(oldClipper)) {
_markNeedsClip();
}
if (attached) {
oldClipper?._reclip?.removeListener(_markNeedsClip);
newClipper?._reclip?.addListener(_markNeedsClip);
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_clipper?._reclip?.addListener(_markNeedsClip);
}
@override
void detach() {
_clipper?._reclip?.removeListener(_markNeedsClip);
super.detach();
}
void _markNeedsClip() {
_clip = null;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
T get _defaultClip;
T _clip;
@override
void performLayout() {
final Size oldSize = hasSize ? size : null;
super.performLayout();
if (oldSize != size)
_clip = null;
}
void _updateClip() {
_clip ??= _clipper?.getClip(size) ?? _defaultClip;
}
@override
Rect describeApproximatePaintClip(RenderObject child) {
return _clipper?.getApproximateClipRect(size) ?? Offset.zero & size;
}
Paint _debugPaint;
TextPainter _debugText;
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
_debugPaint ??= new Paint()
..shader = new ui.Gradient.linear(
const Offset(0.0, 0.0),
const Offset(10.0, 10.0),
<Color>[const Color(0x00000000), const Color(0xFFFF00FF), const Color(0xFFFF00FF), const Color(0x00000000)],
<double>[0.25, 0.25, 0.75, 0.75],
TileMode.repeated,
)
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
_debugText ??= new TextPainter(
text: const TextSpan(
text: '✂',
style: const TextStyle(
color: const Color(0xFFFF00FF),
fontSize: 14.0,
),
),
textDirection: TextDirection.rtl, // doesn't matter, it's one character
)
..layout();
return true;
}());
}
}
/// Clips its child using a rectangle.
///
/// By default, [RenderClipRect] prevents its child from painting outside its
/// bounds, but the size and location of the clip rect can be customized using a
/// custom [clipper].
class RenderClipRect extends _RenderCustomClip<Rect> {
/// Creates a rectangular clip.
///
/// If [clipper] is null, the clip will match the layout size and position of
/// the child.
RenderClipRect({
RenderBox child,
CustomClipper<Rect> clipper
}) : super(child: child, clipper: clipper);
@override
Rect get _defaultClip => Offset.zero & size;
@override
bool hitTest(HitTestResult result, { Offset position }) {
if (_clipper != null) {
_updateClip();
assert(_clip != null);
if (!_clip.contains(position))
return false;
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
_updateClip();
context.pushClipRect(needsCompositing, offset, _clip, super.paint);
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
if (child != null) {
super.debugPaintSize(context, offset);
context.canvas.drawRect(_clip.shift(offset), _debugPaint);
_debugText.paint(context.canvas, offset + new Offset(_clip.width / 8.0, -_debugText.text.style.fontSize * 1.1));
}
return true;
}());
}
}
/// Clips its child using a rounded rectangle.
///
/// By default, [RenderClipRRect] uses its own bounds as the base rectangle for
/// the clip, but the size and location of the clip can be customized using a
/// custom [clipper].
class RenderClipRRect extends _RenderCustomClip<RRect> {
/// Creates a rounded-rectangular clip.
///
/// The [borderRadius] defaults to [BorderRadius.zero], i.e. a rectangle with
/// right-angled corners.
///
/// If [clipper] is non-null, then [borderRadius] is ignored.
RenderClipRRect({
RenderBox child,
BorderRadius borderRadius: BorderRadius.zero,
CustomClipper<RRect> clipper,
}) : _borderRadius = borderRadius, super(child: child, clipper: clipper) {
assert(_borderRadius != null || clipper != null);
}
/// The border radius of the rounded corners.
///
/// Values are clamped so that horizontal and vertical radii sums do not
/// exceed width/height.
///
/// This value is ignored if [clipper] is non-null.
BorderRadius get borderRadius => _borderRadius;
BorderRadius _borderRadius;
set borderRadius(BorderRadius value) {
assert(value != null);
if (_borderRadius == value)
return;
_borderRadius = value;
_markNeedsClip();
}
@override
RRect get _defaultClip => _borderRadius.toRRect(Offset.zero & size);
@override
bool hitTest(HitTestResult result, { Offset position }) {
if (_clipper != null) {
_updateClip();
assert(_clip != null);
if (!_clip.contains(position))
return false;
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
_updateClip();
context.pushClipRRect(needsCompositing, offset, _clip.outerRect, _clip, super.paint);
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
if (child != null) {
super.debugPaintSize(context, offset);
context.canvas.drawRRect(_clip.shift(offset), _debugPaint);
_debugText.paint(context.canvas, offset + new Offset(_clip.tlRadiusX, -_debugText.text.style.fontSize * 1.1));
}
return true;
}());
}
}
/// Clips its child using an oval.
///
/// By default, inscribes an axis-aligned oval into its layout dimensions and
/// prevents its child from painting outside that oval, but the size and
/// location of the clip oval can be customized using a custom [clipper].
class RenderClipOval extends _RenderCustomClip<Rect> {
/// Creates an oval-shaped clip.
///
/// If [clipper] is null, the oval will be inscribed into the layout size and
/// position of the child.
RenderClipOval({
RenderBox child,
CustomClipper<Rect> clipper
}) : super(child: child, clipper: clipper);
Rect _cachedRect;
Path _cachedPath;
Path _getClipPath(Rect rect) {
if (rect != _cachedRect) {
_cachedRect = rect;
_cachedPath = new Path()..addOval(_cachedRect);
}
return _cachedPath;
}
@override
Rect get _defaultClip => Offset.zero & size;
@override
bool hitTest(HitTestResult result, { Offset position }) {
_updateClip();
assert(_clip != null);
final Offset center = _clip.center;
// convert the position to an offset from the center of the unit circle
final Offset offset = new Offset((position.dx - center.dx) / _clip.width,
(position.dy - center.dy) / _clip.height);
// check if the point is outside the unit circle
if (offset.distanceSquared > 0.25) // x^2 + y^2 > r^2
return false;
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
_updateClip();
context.pushClipPath(needsCompositing, offset, _clip, _getClipPath(_clip), super.paint);
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
if (child != null) {
super.debugPaintSize(context, offset);
context.canvas.drawPath(_getClipPath(_clip).shift(offset), _debugPaint);
_debugText.paint(context.canvas, offset + new Offset((_clip.width - _debugText.width) / 2.0, -_debugText.text.style.fontSize * 1.1));
}
return true;
}());
}
}
/// Clips its child using a path.
///
/// Takes a delegate whose primary method returns a path that should
/// be used to prevent the child from painting outside the path.
///
/// Clipping to a path is expensive. Certain shapes have more
/// optimized render objects:
///
/// * To clip to a rectangle, consider [RenderClipRect].
/// * To clip to an oval or circle, consider [RenderClipOval].
/// * To clip to a rounded rectangle, consider [RenderClipRRect].
class RenderClipPath extends _RenderCustomClip<Path> {
/// Creates a path clip.
///
/// If [clipper] is null, the clip will be a rectangle that matches the layout
/// size and location of the child. However, rather than use this default,
/// consider using a [RenderClipRect], which can achieve the same effect more
/// efficiently.
RenderClipPath({
RenderBox child,
CustomClipper<Path> clipper
}) : super(child: child, clipper: clipper);
@override
Path get _defaultClip => new Path()..addRect(Offset.zero & size);
@override
bool hitTest(HitTestResult result, { Offset position }) {
if (_clipper != null) {
_updateClip();
assert(_clip != null);
if (!_clip.contains(position))
return false;
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
_updateClip();
context.pushClipPath(needsCompositing, offset, Offset.zero & size, _clip, super.paint);
}
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
if (child != null) {
super.debugPaintSize(context, offset);
context.canvas.drawPath(_clip.shift(offset), _debugPaint);
_debugText.paint(context.canvas, offset);
}
return true;
}());
}
}
/// A physical model layer casts a shadow based on its [elevation].
///
/// The concrete implementations [RenderPhysicalModel] and [RenderPhysicalShape]
/// determine the actual shape of the physical model.
abstract class _RenderPhysicalModelBase<T> extends _RenderCustomClip<T> {
/// The [shape], [elevation], [color], and [shadowColor] must not be null.
_RenderPhysicalModelBase({
@required RenderBox child,
@required double elevation,
@required Color color,
@required Color shadowColor,
CustomClipper<T> clipper,
}) : assert(elevation != null),
assert(color != null),
assert(shadowColor != null),
_elevation = elevation,
_color = color,
_shadowColor = shadowColor,
super(child: child, clipper: clipper);
/// The z-coordinate at which to place this material.
double get elevation => _elevation;
double _elevation;
set elevation(double value) {
assert(value != null);
if (elevation == value)
return;
final bool didNeedCompositing = alwaysNeedsCompositing;
_elevation = value;
if (didNeedCompositing != alwaysNeedsCompositing)
markNeedsCompositingBitsUpdate();
markNeedsPaint();
}
/// The shadow color.
Color get shadowColor => _shadowColor;
Color _shadowColor;
set shadowColor(Color value) {
assert(value != null);
if (shadowColor == value)
return;
_shadowColor = value;
markNeedsPaint();
}
/// The background color.
Color get color => _color;
Color _color;
set color(Color value) {
assert(value != null);
if (color == value)
return;
_color = value;
markNeedsPaint();
}
static final Paint _defaultPaint = new Paint();
static final Paint _transparentPaint = new Paint()..color = const Color(0x00000000);
// On Fuchsia, the system compositor is responsible for drawing shadows
// for physical model layers with non-zero elevation.
@override
bool get alwaysNeedsCompositing => _elevation != 0.0 && defaultTargetPlatform == TargetPlatform.fuchsia;
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DoubleProperty('elevation', elevation));
description.add(new DiagnosticsProperty<Color>('color', color));
description.add(new DiagnosticsProperty<Color>('shadowColor', color));
}
}
/// Creates a physical model layer that clips its child to a rounded
/// rectangle.
///
/// A physical model layer casts a shadow based on its [elevation].
class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> {
/// Creates a rounded-rectangular clip.
///
/// The [color] is required.
///
/// The [shape], [elevation], [color], and [shadowColor] must not be null.
RenderPhysicalModel({
RenderBox child,
BoxShape shape: BoxShape.rectangle,
BorderRadius borderRadius,
double elevation: 0.0,
@required Color color,
Color shadowColor: const Color(0xFF000000),
}) : assert(shape != null),
assert(elevation != null),
assert(color != null),
assert(shadowColor != null),
_shape = shape,
_borderRadius = borderRadius,
super(
child: child,
elevation: elevation,
color: color,
shadowColor: shadowColor
);
/// The shape of the layer.
///
/// Defaults to [BoxShape.rectangle]. The [borderRadius] affects the corners
/// of the rectangle.
BoxShape get shape => _shape;
BoxShape _shape;
set shape(BoxShape value) {
assert(value != null);
if (shape == value)
return;
_shape = value;
_markNeedsClip();
}
/// The border radius of the rounded corners.
///
/// Values are clamped so that horizontal and vertical radii sums do not
/// exceed width/height.
///
/// This property is ignored if the [shape] is not [BoxShape.rectangle].
///
/// The value null is treated like [BorderRadius.zero].
BorderRadius get borderRadius => _borderRadius;
BorderRadius _borderRadius;
set borderRadius(BorderRadius value) {
if (borderRadius == value)
return;
_borderRadius = value;
_markNeedsClip();
}
@override
RRect get _defaultClip {
assert(hasSize);
assert(_shape != null);
switch (_shape) {
case BoxShape.rectangle:
return (borderRadius ?? BorderRadius.zero).toRRect(Offset.zero & size);
case BoxShape.circle:
final Rect rect = Offset.zero & size;
return new RRect.fromRectXY(rect, rect.width / 2, rect.height / 2);
}
return null;
}
@override
bool hitTest(HitTestResult result, { Offset position }) {
if (_clipper != null) {
_updateClip();
assert(_clip != null);
if (!_clip.contains(position))
return false;
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
_updateClip();
final RRect offsetClipRRect = _clip.shift(offset);
final Rect offsetBounds = offsetClipRRect.outerRect;
final Path offsetClipPath = new Path()..addRRect(offsetClipRRect);
if (needsCompositing) {
final PhysicalModelLayer physicalModel = new PhysicalModelLayer(
clipPath: offsetClipPath,
elevation: elevation,
color: color,
shadowColor: shadowColor,
);
context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds);
} else {
final Canvas canvas = context.canvas;
if (elevation != 0.0) {
// The drawShadow call doesn't add the region of the shadow to the
// picture's bounds, so we draw a hardcoded amount of extra space to
// account for the maximum potential area of the shadow.
// TODO(jsimmons): remove this when Skia does it for us.
canvas.drawRect(
offsetBounds.inflate(20.0),
_RenderPhysicalModelBase._transparentPaint,
);
canvas.drawShadow(
offsetClipPath,
shadowColor,
elevation,
color.alpha != 0xFF,
);
}
canvas.drawRRect(offsetClipRRect, new Paint()..color = color);
canvas.save();
canvas.clipRRect(offsetClipRRect);
// We only use a new layer for non-rectangular clips, on the basis that
// rectangular clips won't need antialiasing. This is not really
// correct, because if we're e.g. rotated, rectangles will also be
// aliased. Unfortunately, it's too much of a performance win to err on
// the side of correctness here.
// TODO(ianh): Find a better solution.
if (!offsetClipRRect.isRect)
canvas.saveLayer(offsetBounds, _RenderPhysicalModelBase._defaultPaint);
super.paint(context, offset);
if (!offsetClipRRect.isRect)
canvas.restore();
canvas.restore();
assert(context.canvas == canvas, 'canvas changed even though needsCompositing was false');
}
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<BoxShape>('shape', shape));
description.add(new DiagnosticsProperty<BorderRadius>('borderRadius', borderRadius));
}
}
/// Creates a physical shape layer that clips its child to a [Path].
///
/// A physical shape layer casts a shadow based on its [elevation].
///
/// See also:
///
/// * [RenderPhysicalModel], which is optimized for rounded rectangles and
/// circles.
class RenderPhysicalShape extends _RenderPhysicalModelBase<Path> {
/// Creates an arbitrary shape clip.
///
/// The [color] and [shape] parameters are required.
///
/// The [clipper], [elevation], [color] and [shadowColor] must
/// not be null.
RenderPhysicalShape({
RenderBox child,
@required CustomClipper<Path> clipper,
double elevation: 0.0,
@required Color color,
Color shadowColor: const Color(0xFF000000),
}) : assert(clipper != null),
assert(elevation != null),
assert(color != null),
assert(shadowColor != null),
super(
child: child,
elevation: elevation,
color: color,
shadowColor: shadowColor,
clipper: clipper,
);
@override
Path get _defaultClip => new Path()..addRect(Offset.zero & size);
@override
bool hitTest(HitTestResult result, { Offset position }) {
if (_clipper != null) {
_updateClip();
assert(_clip != null);
if (!_clip.contains(position))
return false;
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
_updateClip();
final Rect offsetBounds = offset & size;
final Path offsetPath = _clip.shift(offset);
if (needsCompositing) {
final PhysicalModelLayer physicalModel = new PhysicalModelLayer(
clipPath: offsetPath,
elevation: elevation,
color: color,
shadowColor: shadowColor,
);
context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds);
} else {
final Canvas canvas = context.canvas;
if (elevation != 0.0) {
// The drawShadow call doesn't add the region of the shadow to the
// picture's bounds, so we draw a hardcoded amount of extra space to
// account for the maximum potential area of the shadow.
// TODO(jsimmons): remove this when Skia does it for us.
canvas.drawRect(
offsetBounds.inflate(20.0),
_RenderPhysicalModelBase._transparentPaint,
);
canvas.drawShadow(
offsetPath,
shadowColor,
elevation,
color.alpha != 0xFF,
);
}
canvas.drawPath(offsetPath, new Paint()..color = color..style = PaintingStyle.fill);
canvas.saveLayer(offsetBounds, _RenderPhysicalModelBase._defaultPaint);
canvas.clipPath(offsetPath);
super.paint(context, offset);
canvas.restore();
assert(context.canvas == canvas, 'canvas changed even though needsCompositing was false');
}
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<CustomClipper<Path>>('clipper', clipper));
}
}
/// Where to paint a box decoration.
enum DecorationPosition {
/// Paint the box decoration behind the children.
background,
/// Paint the box decoration in front of the children.
foreground,
}
/// Paints a [Decoration] either before or after its child paints.
class RenderDecoratedBox extends RenderProxyBox {
/// Creates a decorated box.
///
/// The [decoration], [position], and [configuration] arguments must not be
/// null. By default the decoration paints behind the child.
///
/// The [ImageConfiguration] will be passed to the decoration (with the size
/// filled in) to let it resolve images.
RenderDecoratedBox({
@required Decoration decoration,
DecorationPosition position: DecorationPosition.background,
ImageConfiguration configuration: ImageConfiguration.empty,
RenderBox child,
}) : assert(decoration != null),
assert(position != null),
assert(configuration != null),
_decoration = decoration,
_position = position,
_configuration = configuration,
super(child);
BoxPainter _painter;
/// What decoration to paint.
///
/// Commonly a [BoxDecoration].
Decoration get decoration => _decoration;
Decoration _decoration;
set decoration(Decoration value) {
assert(value != null);
if (value == _decoration)
return;
_painter?.dispose();
_painter = null;
_decoration = value;
markNeedsPaint();
}
/// Whether to paint the box decoration behind or in front of the child.
DecorationPosition get position => _position;
DecorationPosition _position;
set position(DecorationPosition value) {
assert(value != null);
if (value == _position)
return;
_position = value;
markNeedsPaint();
}
/// The settings to pass to the decoration when painting, so that it can
/// resolve images appropriately. See [ImageProvider.resolve] and
/// [BoxPainter.paint].
///
/// The [ImageConfiguration.textDirection] field is also used by
/// direction-sensitive [Decoration]s for painting and hit-testing.
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration(ImageConfiguration value) {
assert(value != null);
if (value == _configuration)
return;
_configuration = value;
markNeedsPaint();
}
@override
void detach() {
_painter?.dispose();
_painter = null;
super.detach();
// Since we're disposing of our painter, we won't receive change
// notifications. We mark ourselves as needing paint so that we will
// resubscribe to change notifications. If we didn't do this, then, for
// example, animated GIFs would stop animating when a DecoratedBox gets
// moved around the tree due to GlobalKey reparenting.
markNeedsPaint();
}
@override
bool hitTestSelf(Offset position) {
return _decoration.hitTest(size, position, textDirection: configuration.textDirection);
}
@override
void paint(PaintingContext context, Offset offset) {
assert(size.width != null);
assert(size.height != null);
_painter ??= _decoration.createBoxPainter(markNeedsPaint);
final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
if (position == DecorationPosition.background) {
int debugSaveCount;
assert(() {
debugSaveCount = context.canvas.getSaveCount();
return true;
}());
_painter.paint(context.canvas, offset, filledConfiguration);
assert(() {
if (debugSaveCount != context.canvas.getSaveCount()) {
throw new FlutterError(
'${_decoration.runtimeType} painter had mismatching save and restore calls.\n'
'Before painting the decoration, the canvas save count was $debugSaveCount. '
'After painting it, the canvas save count was ${context.canvas.getSaveCount()}. '
'Every call to save() or saveLayer() must be matched by a call to restore().\n'
'The decoration was:\n'
' $decoration\n'
'The painter was:\n'
' $_painter'
);
}
return true;
}());
if (decoration.isComplex)
context.setIsComplexHint();
}
super.paint(context, offset);
if (position == DecorationPosition.foreground) {
_painter.paint(context.canvas, offset, filledConfiguration);
if (decoration.isComplex)
context.setIsComplexHint();
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(_decoration.toDiagnosticsNode(name: 'decoration'));
properties.add(new DiagnosticsProperty<ImageConfiguration>('configuration', configuration));
}
}
/// Applies a transformation before painting its child.
class RenderTransform extends RenderProxyBox {
/// Creates a render object that transforms its child.
///
/// The [transform] argument must not be null.
RenderTransform({
@required Matrix4 transform,
Offset origin,
AlignmentGeometry alignment,
TextDirection textDirection,
this.transformHitTests: true,
RenderBox child
}) : assert(transform != null),
super(child) {
this.transform = transform;
this.alignment = alignment;
this.textDirection = textDirection;
this.origin = origin;
}
/// The origin of the coordinate system (relative to the upper left corner of
/// this render object) in which to apply the matrix.
///
/// Setting an origin is equivalent to conjugating the transform matrix by a
/// translation. This property is provided just for convenience.
Offset get origin => _origin;
Offset _origin;
set origin(Offset value) {
if (_origin == value)
return;
_origin = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// The alignment of the origin, relative to the size of the box.
///
/// This is equivalent to setting an origin based on the size of the box.
/// If it is specified at the same time as an offset, both are applied.
///
/// An [AlignmentDirectional.start] value is the same as an [Alignment]
/// whose [Alignment.x] value is `-1.0` if [textDirection] is
/// [TextDirection.ltr], and `1.0` if [textDirection] is [TextDirection.rtl].
/// Similarly [AlignmentDirectional.end] is the same as an [Alignment]
/// whose [Alignment.x] value is `1.0` if [textDirection] is
/// [TextDirection.ltr], and `-1.0` if [textDirection] is [TextDirection.rtl].
AlignmentGeometry get alignment => _alignment;
AlignmentGeometry _alignment;
set alignment(AlignmentGeometry value) {
if (_alignment == value)
return;
_alignment = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// The text direction with which to resolve [alignment].
///
/// This may be changed to null, but only after [alignment] has been changed
/// to a value that does not depend on the direction.
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (_textDirection == value)
return;
_textDirection = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// When set to true, hit tests are performed based on the position of the
/// child as it is painted. When set to false, hit tests are performed
/// ignoring the transformation.
///
/// [applyPaintTransform], and therefore [localToGlobal] and [globalToLocal],
/// always honor the transformation, regardless of the value of this property.
bool transformHitTests;
// Note the lack of a getter for transform because Matrix4 is not immutable
Matrix4 _transform;
/// The matrix to transform the child by during painting.
set transform(Matrix4 value) {
assert(value != null);
if (_transform == value)
return;
_transform = new Matrix4.copy(value);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Sets the transform to the identity matrix.
void setIdentity() {
_transform.setIdentity();
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Concatenates a rotation about the x axis into the transform.
void rotateX(double radians) {
_transform.rotateX(radians);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Concatenates a rotation about the y axis into the transform.
void rotateY(double radians) {
_transform.rotateY(radians);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Concatenates a rotation about the z axis into the transform.
void rotateZ(double radians) {
_transform.rotateZ(radians);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Concatenates a translation by (x, y, z) into the transform.
void translate(double x, [double y = 0.0, double z = 0.0]) {
_transform.translate(x, y, z);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// Concatenates a scale into the transform.
void scale(double x, [double y, double z]) {
_transform.scale(x, y, z);
markNeedsPaint();
markNeedsSemanticsUpdate();
}
Matrix4 get _effectiveTransform {
final Alignment resolvedAlignment = alignment?.resolve(textDirection);
if (_origin == null && resolvedAlignment == null)
return _transform;
final Matrix4 result = new Matrix4.identity();
if (_origin != null)
result.translate(_origin.dx, _origin.dy);
Offset translation;
if (resolvedAlignment != null) {
translation = resolvedAlignment.alongSize(size);
result.translate(translation.dx, translation.dy);
}
result.multiply(_transform);
if (resolvedAlignment != null)
result.translate(-translation.dx, -translation.dy);
if (_origin != null)
result.translate(-_origin.dx, -_origin.dy);
return result;
}
@override
bool hitTest(HitTestResult result, { Offset position }) {
if (transformHitTests) {
final Matrix4 inverse = Matrix4.tryInvert(_effectiveTransform);
if (inverse == null) {
// We cannot invert the effective transform. That means the child
// doesn't appear on screen and cannot be hit.
return false;
}
position = MatrixUtils.transformPoint(inverse, position);
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final Matrix4 transform = _effectiveTransform;
final Offset childOffset = MatrixUtils.getAsTranslation(transform);
if (childOffset == null)
context.pushTransform(needsCompositing, offset, transform, super.paint);
else
super.paint(context, offset + childOffset);
}
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.multiply(_effectiveTransform);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new TransformProperty('transform matrix', _transform));
properties.add(new DiagnosticsProperty<Offset>('origin', origin));
properties.add(new DiagnosticsProperty<Alignment>('alignment', alignment));
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(new DiagnosticsProperty<bool>('transformHitTests', transformHitTests));
}
}
/// Scales and positions its child within itself according to [fit].
class RenderFittedBox extends RenderProxyBox {
/// Scales and positions its child within itself.
///
/// The [fit] and [alignment] arguments must not be null.
RenderFittedBox({
BoxFit fit: BoxFit.contain,
AlignmentGeometry alignment: Alignment.center,
TextDirection textDirection,
RenderBox child,
}) : assert(fit != null),
assert(alignment != null),
_fit = fit,
_alignment = alignment,
_textDirection = textDirection,
super(child);
Alignment _resolvedAlignment;
void _resolve() {
if (_resolvedAlignment != null)
return;
_resolvedAlignment = alignment.resolve(textDirection);
}
void _markNeedResolution() {
_resolvedAlignment = null;
markNeedsPaint();
}
/// How to inscribe the child into the space allocated during layout.
BoxFit get fit => _fit;
BoxFit _fit;
set fit(BoxFit value) {
assert(value != null);
if (_fit == value)
return;
_fit = value;
_clearPaintData();
markNeedsPaint();
}
/// How to align the child within its parent's bounds.
///
/// An alignment of (0.0, 0.0) aligns the child to the top-left corner of its
/// parent's bounds. An alignment of (1.0, 0.5) aligns the child to the middle
/// of the right edge of its parent's bounds.
///
/// If this is set to an [AlignmentDirectional] object, then
/// [textDirection] must not be null.
AlignmentGeometry get alignment => _alignment;
AlignmentGeometry _alignment;
set alignment(AlignmentGeometry value) {
assert(value != null);
if (_alignment == value)
return;
_alignment = value;
_clearPaintData();
_markNeedResolution();
}
/// The text direction with which to resolve [alignment].
///
/// This may be changed to null, but only after [alignment] has been changed
/// to a value that does not depend on the direction.
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (_textDirection == value)
return;
_textDirection = value;
_clearPaintData();
_markNeedResolution();
}
// TODO(ianh): The intrinsic dimensions of this box are wrong.
@override
void performLayout() {
if (child != null) {
child.layout(const BoxConstraints(), parentUsesSize: true);
size = constraints.constrainSizeAndAttemptToPreserveAspectRatio(child.size);
_clearPaintData();
} else {
size = constraints.smallest;
}
}
bool _hasVisualOverflow;
Matrix4 _transform;
void _clearPaintData() {
_hasVisualOverflow = null;
_transform = null;
}
void _updatePaintData() {
if (_transform != null)
return;
if (child == null) {
_hasVisualOverflow = false;
_transform = new Matrix4.identity();
} else {
_resolve();
final Size childSize = child.size;
final FittedSizes sizes = applyBoxFit(_fit, childSize, size);
final double scaleX = sizes.destination.width / sizes.source.width;
final double scaleY = sizes.destination.height / sizes.source.height;
final Rect sourceRect = _resolvedAlignment.inscribe(sizes.source, Offset.zero & childSize);
final Rect destinationRect = _resolvedAlignment.inscribe(sizes.destination, Offset.zero & size);
_hasVisualOverflow = sourceRect.width < childSize.width || sourceRect.height < childSize.height;
_transform = new Matrix4.translationValues(destinationRect.left, destinationRect.top, 0.0)
..scale(scaleX, scaleY, 1.0)
..translate(-sourceRect.left, -sourceRect.top);
}
}
void _paintChildWithTransform(PaintingContext context, Offset offset) {
final Offset childOffset = MatrixUtils.getAsTranslation(_transform);
if (childOffset == null)
context.pushTransform(needsCompositing, offset, _transform, super.paint);
else
super.paint(context, offset + childOffset);
}
@override
void paint(PaintingContext context, Offset offset) {
if (size.isEmpty)
return;
_updatePaintData();
if (child != null) {
if (_hasVisualOverflow)
context.pushClipRect(needsCompositing, offset, Offset.zero & size, _paintChildWithTransform);
else
_paintChildWithTransform(context, offset);
}
}
@override
bool hitTest(HitTestResult result, { Offset position }) {
if (size.isEmpty)
return false;
_updatePaintData();
final Matrix4 inverse = Matrix4.tryInvert(_transform);
if (inverse == null) {
// We cannot invert the effective transform. That means the child
// doesn't appear on screen and cannot be hit.
return false;
}
position = MatrixUtils.transformPoint(inverse, position);
return super.hitTest(result, position: position);
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
if (size.isEmpty) {
transform.setZero();
} else {
_updatePaintData();
transform.multiply(_transform);
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new EnumProperty<BoxFit>('fit', fit));
properties.add(new DiagnosticsProperty<Alignment>('alignment', alignment));
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
}
}
/// Applies a translation transformation before painting its child.
///
/// The translation is expressed as an [Offset] scaled to the child's size. For
/// example, an [Offset] with a `dx` of 0.25 will result in a horizontal
/// translation of one quarter the width of the child.
///
/// Hit tests will only be detected inside the bounds of the
/// [RenderFractionalTranslation], even if the contents are offset such that
/// they overflow.
class RenderFractionalTranslation extends RenderProxyBox {
/// Creates a render object that translates its child's painting.
///
/// The [translation] argument must not be null.
RenderFractionalTranslation({
@required Offset translation,
this.transformHitTests: true,
RenderBox child
}) : assert(translation != null),
_translation = translation,
super(child);
/// The translation to apply to the child, scaled to the child's size.
///
/// For example, an [Offset] with a `dx` of 0.25 will result in a horizontal
/// translation of one quarter the width of the child.
Offset get translation => _translation;
Offset _translation;
set translation(Offset value) {
assert(value != null);
if (_translation == value)
return;
_translation = value;
markNeedsPaint();
}
/// When set to true, hit tests are performed based on the position of the
/// child as it is painted. When set to false, hit tests are performed
/// ignoring the transformation.
///
/// applyPaintTransform(), and therefore localToGlobal() and globalToLocal(),
/// always honor the transformation, regardless of the value of this property.
bool transformHitTests;
@override
bool hitTest(HitTestResult result, { Offset position }) {
assert(!debugNeedsLayout);
if (transformHitTests) {
position = new Offset(
position.dx - translation.dx * size.width,
position.dy - translation.dy * size.height,
);
}
return super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
assert(!debugNeedsLayout);
if (child != null) {
super.paint(context, new Offset(
offset.dx + translation.dx * size.width,
offset.dy + translation.dy * size.height,
));
}
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.translate(
translation.dx * size.width,
translation.dy * size.height,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<Offset>('translation', translation));
properties.add(new DiagnosticsProperty<bool>('transformHitTests', transformHitTests));
}
}
/// Signature for listening to [PointerDownEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef void PointerDownEventListener(PointerDownEvent event);
/// Signature for listening to [PointerMoveEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef void PointerMoveEventListener(PointerMoveEvent event);
/// Signature for listening to [PointerUpEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef void PointerUpEventListener(PointerUpEvent event);
/// Signature for listening to [PointerCancelEvent] events.
///
/// Used by [Listener] and [RenderPointerListener].
typedef void PointerCancelEventListener(PointerCancelEvent event);
/// Calls callbacks in response to pointer events.
///
/// If it has a child, defers to the child for sizing behavior.
///
/// If it does not have a child, grows to fit the parent-provided constraints.
class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
/// Creates a render object that forwards point events to callbacks.
///
/// The [behavior] argument defaults to [HitTestBehavior.deferToChild].
RenderPointerListener({
this.onPointerDown,
this.onPointerMove,
this.onPointerUp,
this.onPointerCancel,
HitTestBehavior behavior: HitTestBehavior.deferToChild,
RenderBox child
}) : super(behavior: behavior, child: child);
/// Called when a pointer comes into contact with the screen at this object.
PointerDownEventListener onPointerDown;
/// Called when a pointer that triggered an [onPointerDown] changes position.
PointerMoveEventListener onPointerMove;
/// Called when a pointer that triggered an [onPointerDown] is no longer in
/// contact with the screen.
PointerUpEventListener onPointerUp;
/// Called when the input from a pointer that triggered an [onPointerDown] is
/// no longer directed towards this receiver.
PointerCancelEventListener onPointerCancel;
@override
void performResize() {
size = constraints.biggest;
}
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (onPointerDown != null && event is PointerDownEvent)
return onPointerDown(event);
if (onPointerMove != null && event is PointerMoveEvent)
return onPointerMove(event);
if (onPointerUp != null && event is PointerUpEvent)
return onPointerUp(event);
if (onPointerCancel != null && event is PointerCancelEvent)
return onPointerCancel(event);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
final List<String> listeners = <String>[];
if (onPointerDown != null)
listeners.add('down');
if (onPointerMove != null)
listeners.add('move');
if (onPointerUp != null)
listeners.add('up');
if (onPointerCancel != null)
listeners.add('cancel');
if (listeners.isEmpty)
listeners.add('<none>');
properties.add(new IterableProperty<String>('listeners', listeners));
// TODO(jacobr): add raw listeners to the diagnostics data.
}
}
/// Creates a separate display list for its child.
///
/// This render object creates a separate display list for its child, which
/// can improve performance if the subtree repaints at different times than
/// the surrounding parts of the tree. Specifically, when the child does not
/// repaint but its parent does, we can re-use the display list we recorded
/// previously. Similarly, when the child repaints but the surround tree does
/// not, we can re-record its display list without re-recording the display list
/// for the surround tree.
///
/// In some cases, it is necessary to place _two_ (or more) repaint boundaries
/// to get a useful effect. Consider, for example, an e-mail application that
/// shows an unread count and a list of e-mails. Whenever a new e-mail comes in,
/// the list would update, but so would the unread count. If only one of these
/// two parts of the application was behind a repaint boundary, the entire
/// application would repaint each time. On the other hand, if both were behind
/// a repaint boundary, a new e-mail would only change those two parts of the
/// application and the rest of the application would not repaint.
///
/// To tell if a particular RenderRepaintBoundary is useful, run your
/// application in checked mode, interacting with it in typical ways, and then
/// call [debugDumpRenderTree]. Each RenderRepaintBoundary will include the
/// ratio of cases where the repaint boundary was useful vs the cases where it
/// was not. These counts can also be inspected programmatically using
/// [debugAsymmetricPaintCount] and [debugSymmetricPaintCount] respectively.
class RenderRepaintBoundary extends RenderProxyBox {
/// Creates a repaint boundary around [child].
RenderRepaintBoundary({ RenderBox child }) : super(child);
@override
bool get isRepaintBoundary => true;
/// Capture an image of the current state of this render object and its
/// children.
///
/// The returned [ui.Image] has uncompressed raw RGBA bytes in the dimensions
/// of the render object, multiplied by the [pixelRatio].
///
/// To use [toImage], the render object must have gone through the paint phase
/// (i.e. [debugNeedsPaint] must be false).
///
/// The [pixelRatio] describes the scale between the logical pixels and the
/// size of the output image. It is independent of the
/// [window.devicePixelRatio] for the device, so specifying 1.0 (the default)
/// will give you a 1:1 mapping between logical pixels and the output pixels
/// in the image.
///
/// ## Sample code
///
/// The following is an example of how to go from a `GlobalKey` on a
/// `RepaintBoundary` to a PNG:
///
/// ```dart
/// class PngHome extends StatefulWidget {
/// PngHome({Key key}) : super(key: key);
///
/// @override
/// _PngHomeState createState() => new _PngHomeState();
/// }
///
/// class _PngHomeState extends State<PngHome> {
/// GlobalKey globalKey = new GlobalKey();
///
/// Future<void> _capturePng() async {
/// RenderRepaintBoundary boundary = globalKey.currentContext.findRenderObject();
/// ui.Image image = await boundary.toImage();
/// ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
/// Uint8List pngBytes = byteData.buffer.asUint8List();
/// print(pngBytes);
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return RepaintBoundary(
/// key: globalKey,
/// child: Center(
/// child: FlatButton(
/// child: Text('Hello World', textDirection: TextDirection.ltr),
/// onPressed: _capturePng,
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// See also:
///
/// * [OffsetLayer.toImage] for a similar API at the layer level.
/// * [dart:ui.Scene.toImage] for more information about the image returned.
Future<ui.Image> toImage({double pixelRatio: 1.0}) {
assert(!debugNeedsPaint);
return layer.toImage(Offset.zero & size, pixelRatio: pixelRatio);
}
/// The number of times that this render object repainted at the same time as
/// its parent. Repaint boundaries are only useful when the parent and child
/// paint at different times. When both paint at the same time, the repaint
/// boundary is redundant, and may be actually making performance worse.
///
/// Only valid when asserts are enabled. In release builds, always returns
/// zero.
///
/// Can be reset using [debugResetMetrics]. See [debugAsymmetricPaintCount]
/// for the corresponding count of times where only the parent or only the
/// child painted.
int get debugSymmetricPaintCount => _debugSymmetricPaintCount;
int _debugSymmetricPaintCount = 0;
/// The number of times that either this render object repainted without the
/// parent being painted, or the parent repainted without this object being
/// painted. When a repaint boundary is used at a seam in the render tree
/// where the parent tends to repaint at entirely different times than the
/// child, it can improve performance by reducing the number of paint
/// operations that have to be recorded each frame.
///
/// Only valid when asserts are enabled. In release builds, always returns
/// zero.
///
/// Can be reset using [debugResetMetrics]. See [debugSymmetricPaintCount] for
/// the corresponding count of times where both the parent and the child
/// painted together.
int get debugAsymmetricPaintCount => _debugAsymmetricPaintCount;
int _debugAsymmetricPaintCount = 0;
/// Resets the [debugSymmetricPaintCount] and [debugAsymmetricPaintCount]
/// counts to zero.
///
/// Only valid when asserts are enabled. Does nothing in release builds.
void debugResetMetrics() {
assert(() {
_debugSymmetricPaintCount = 0;
_debugAsymmetricPaintCount = 0;
return true;
}());
}
@override
void debugRegisterRepaintBoundaryPaint({ bool includedParent: true, bool includedChild: false }) {
assert(() {
if (includedParent && includedChild)
_debugSymmetricPaintCount += 1;
else
_debugAsymmetricPaintCount += 1;
return true;
}());
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
bool inReleaseMode = true;
assert(() {
inReleaseMode = false;
if (debugSymmetricPaintCount + debugAsymmetricPaintCount == 0) {
properties.add(new MessageProperty('usefulness ratio', 'no metrics collected yet (never painted)'));
} else {
final double fraction = debugAsymmetricPaintCount / (debugSymmetricPaintCount + debugAsymmetricPaintCount);
String diagnosis;
if (debugSymmetricPaintCount + debugAsymmetricPaintCount < 5) {
diagnosis = 'insufficient data to draw conclusion (less than five repaints)';
} else if (fraction > 0.9) {
diagnosis = 'this is an outstandingly useful repaint boundary and should definitely be kept';
} else if (fraction > 0.5) {
diagnosis = 'this is a useful repaint boundary and should be kept';
} else if (fraction > 0.30) {
diagnosis = 'this repaint boundary is probably useful, but maybe it would be more useful in tandem with adding more repaint boundaries elsewhere';
} else if (fraction > 0.1) {
diagnosis = 'this repaint boundary does sometimes show value, though currently not that often';
} else if (debugAsymmetricPaintCount == 0) {
diagnosis = 'this repaint boundary is astoundingly ineffectual and should be removed';
} else {
diagnosis = 'this repaint boundary is not very effective and should probably be removed';
}
properties.add(new PercentProperty('metrics', fraction, unit: 'useful', tooltip: '$debugSymmetricPaintCount bad vs $debugAsymmetricPaintCount good'));
properties.add(new MessageProperty('diagnosis', diagnosis));
}
return true;
}());
if (inReleaseMode)
properties.add(new DiagnosticsNode.message('(run in checked mode to collect repaint boundary statistics)'));
}
}
/// A render object that is invisible during hit testing.
///
/// When [ignoring] is true, this render object (and its subtree) is invisible
/// to hit testing. It still consumes space during layout and paints its child
/// as usual. It just cannot be the target of located events, because its render
/// object returns false from [hitTest].
///
/// When [ignoringSemantics] is true, the subtree will be invisible to
/// the semantics layer (and thus e.g. accessibility tools). If
/// [ignoringSemantics] is null, it uses the value of [ignoring].
///
/// See also:
///
/// * [RenderAbsorbPointer], which takes the pointer events but prevents any
/// nodes in the subtree from seeing them.
class RenderIgnorePointer extends RenderProxyBox {
/// Creates a render object that is invisible to hit testing.
///
/// The [ignoring] argument must not be null. If [ignoringSemantics], this
/// render object will be ignored for semantics if [ignoring] is true.
RenderIgnorePointer({
RenderBox child,
bool ignoring: true,
bool ignoringSemantics
}) : _ignoring = ignoring, _ignoringSemantics = ignoringSemantics, super(child) {
assert(_ignoring != null);
}
/// Whether this render object is ignored during hit testing.
///
/// Regardless of whether this render object is ignored during hit testing, it
/// will still consume space during layout and be visible during painting.
bool get ignoring => _ignoring;
bool _ignoring;
set ignoring(bool value) {
assert(value != null);
if (value == _ignoring)
return;
_ignoring = value;
if (ignoringSemantics == null)
markNeedsSemanticsUpdate();
}
/// Whether the semantics of this render object is ignored when compiling the semantics tree.
///
/// If null, defaults to value of [ignoring].
///
/// See [SemanticsNode] for additional information about the semantics tree.
bool get ignoringSemantics => _ignoringSemantics;
bool _ignoringSemantics;
set ignoringSemantics(bool value) {
if (value == _ignoringSemantics)
return;
final bool oldEffectiveValue = _effectiveIgnoringSemantics;
_ignoringSemantics = value;
if (oldEffectiveValue != _effectiveIgnoringSemantics)
markNeedsSemanticsUpdate();
}
bool get _effectiveIgnoringSemantics => ignoringSemantics == null ? ignoring : ignoringSemantics;
@override
bool hitTest(HitTestResult result, { Offset position }) {
return ignoring ? false : super.hitTest(result, position: position);
}
// TODO(ianh): figure out a way to still include labels and flags in
// descendants, just make them non-interactive, even when
// _effectiveIgnoringSemantics is true
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (child != null && !_effectiveIgnoringSemantics)
visitor(child);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<bool>('ignoring', ignoring));
properties.add(
new DiagnosticsProperty<bool>(
'ignoringSemantics',
_effectiveIgnoringSemantics,
description: ignoringSemantics == null ? 'implicitly $_effectiveIgnoringSemantics' : null,
)
);
}
}
/// 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.
class RenderOffstage extends RenderProxyBox {
/// Creates an offstage render object.
RenderOffstage({
bool offstage: true,
RenderBox child
}) : assert(offstage != null),
_offstage = offstage,
super(child);
/// Whether the child is hidden from the rest of the tree.
///
/// If true, the child is laid 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.
///
/// If false, the child is included in the tree as normal.
bool get offstage => _offstage;
bool _offstage;
set offstage(bool value) {
assert(value != null);
if (value == _offstage)
return;
_offstage = value;
markNeedsLayoutForSizedByParentChange();
}
@override
double computeMinIntrinsicWidth(double height) {
if (offstage)
return 0.0;
return super.computeMinIntrinsicWidth(height);
}
@override
double computeMaxIntrinsicWidth(double height) {
if (offstage)
return 0.0;
return super.computeMaxIntrinsicWidth(height);
}
@override
double computeMinIntrinsicHeight(double width) {
if (offstage)
return 0.0;
return super.computeMinIntrinsicHeight(width);
}
@override
double computeMaxIntrinsicHeight(double width) {
if (offstage)
return 0.0;
return super.computeMaxIntrinsicHeight(width);
}
@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
if (offstage)
return null;
return super.computeDistanceToActualBaseline(baseline);
}
@override
bool get sizedByParent => offstage;
@override
void performResize() {
assert(offstage);
size = constraints.smallest;
}
@override
void performLayout() {
if (offstage) {
child?.layout(constraints);
} else {
super.performLayout();
}
}
@override
bool hitTest(HitTestResult result, { Offset position }) {
return !offstage && super.hitTest(result, position: position);
}
@override
void paint(PaintingContext context, Offset offset) {
if (offstage)
return;
super.paint(context, offset);
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (offstage)
return;
super.visitChildrenForSemantics(visitor);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<bool>('offstage', offstage));
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
if (child == null)
return <DiagnosticsNode>[];
return <DiagnosticsNode>[
child.toDiagnosticsNode(
name: 'child',
style: offstage ? DiagnosticsTreeStyle.offstage : DiagnosticsTreeStyle.sparse,
),
];
}
}
/// A render object that absorbs pointers during hit testing.
///
/// When [absorbing] is true, this render object prevents its subtree from
/// receiving pointer events by terminating hit testing at itself. It still
/// consumes space during layout and paints its child as usual. It just prevents
/// its children from being the target of located events, because its render
/// object returns true from [hitTest].
///
/// See also:
///
/// * [RenderIgnorePointer], which has the opposite effect: removing the
/// subtree from considering entirely for the purposes of hit testing.
class RenderAbsorbPointer extends RenderProxyBox {
/// Creates a render object that absorbs pointers during hit testing.
///
/// The [absorbing] argument must not be null.
RenderAbsorbPointer({
RenderBox child,
this.absorbing: true
}) : assert(absorbing != null),
super(child);
/// Whether this render object absorbs pointers during hit testing.
///
/// Regardless of whether this render object absorbs pointers during hit
/// testing, it will still consume space during layout and be visible during
/// painting.
bool absorbing;
@override
bool hitTest(HitTestResult result, { Offset position }) {
return absorbing
? size.contains(position)
: super.hitTest(result, position: position);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<bool>('absorbing', absorbing));
}
}
/// Holds opaque meta data in the render tree.
///
/// Useful for decorating the render tree with information that will be consumed
/// later. For example, you could store information in the render tree that will
/// be used when the user interacts with the render tree but has no visual
/// impact prior to the interaction.
class RenderMetaData extends RenderProxyBoxWithHitTestBehavior {
/// Creates a render object that hold opaque meta data.
///
/// The [behavior] argument defaults to [HitTestBehavior.deferToChild].
RenderMetaData({
this.metaData,
HitTestBehavior behavior: HitTestBehavior.deferToChild,
RenderBox child
}) : super(behavior: behavior, child: child);
/// Opaque meta data ignored by the render tree
dynamic metaData;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<dynamic>('metaData', metaData));
}
}
/// Listens for the specified gestures from the semantics server (e.g.
/// an accessibility tool).
class RenderSemanticsGestureHandler extends RenderProxyBox {
/// Creates a render object that listens for specific semantic gestures.
///
/// The [scrollFactor] argument must not be null.
RenderSemanticsGestureHandler({
RenderBox child,
GestureTapCallback onTap,
GestureLongPressCallback onLongPress,
GestureDragUpdateCallback onHorizontalDragUpdate,
GestureDragUpdateCallback onVerticalDragUpdate,
this.scrollFactor: 0.8
}) : assert(scrollFactor != null),
_onTap = onTap,
_onLongPress = onLongPress,
_onHorizontalDragUpdate = onHorizontalDragUpdate,
_onVerticalDragUpdate = onVerticalDragUpdate,
super(child);
/// If non-null, the set of actions to allow. Other actions will be omitted,
/// even if their callback is provided.
///
/// For example, if [onTap] is non-null but [validActions] does not contain
/// [SemanticsAction.tap], then the semantic description of this node will
/// not claim to support taps.
///
/// This is normally used to filter the actions made available by
/// [onHorizontalDragUpdate] and [onVerticalDragUpdate]. Normally, these make
/// both the right and left, or up and down, actions available. For example,
/// if [onHorizontalDragUpdate] is set but [validActions] only contains
/// [SemanticsAction.scrollLeft], then the [SemanticsAction.scrollRight]
/// action will be omitted.
Set<SemanticsAction> get validActions => _validActions;
Set<SemanticsAction> _validActions;
set validActions(Set<SemanticsAction> value) {
if (setEquals<SemanticsAction>(value, _validActions))
return;
_validActions = value;
markNeedsSemanticsUpdate();
}
/// Called when the user taps on the render object.
GestureTapCallback get onTap => _onTap;
GestureTapCallback _onTap;
set onTap(GestureTapCallback value) {
if (_onTap == value)
return;
final bool hadHandler = _onTap != null;
_onTap = value;
if ((value != null) != hadHandler)
markNeedsSemanticsUpdate();
}
/// Called when the user presses on the render object for a long period of time.
GestureLongPressCallback get onLongPress => _onLongPress;
GestureLongPressCallback _onLongPress;
set onLongPress(GestureLongPressCallback value) {
if (_onLongPress == value)
return;
final bool hadHandler = _onLongPress != null;
_onLongPress = value;
if ((value != null) != hadHandler)
markNeedsSemanticsUpdate();
}
/// Called when the user scrolls to the left or to the right.
GestureDragUpdateCallback get onHorizontalDragUpdate => _onHorizontalDragUpdate;
GestureDragUpdateCallback _onHorizontalDragUpdate;
set onHorizontalDragUpdate(GestureDragUpdateCallback value) {
if (_onHorizontalDragUpdate == value)
return;
final bool hadHandler = _onHorizontalDragUpdate != null;
_onHorizontalDragUpdate = value;
if ((value != null) != hadHandler)
markNeedsSemanticsUpdate();
}
/// Called when the user scrolls up or down.
GestureDragUpdateCallback get onVerticalDragUpdate => _onVerticalDragUpdate;
GestureDragUpdateCallback _onVerticalDragUpdate;
set onVerticalDragUpdate(GestureDragUpdateCallback value) {
if (_onVerticalDragUpdate == value)
return;
final bool hadHandler = _onVerticalDragUpdate != null;
_onVerticalDragUpdate = value;
if ((value != null) != hadHandler)
markNeedsSemanticsUpdate();
}
/// The fraction of the dimension of this render box to use when
/// scrolling. For example, if this is 0.8 and the box is 200 pixels
/// wide, then when a left-scroll action is received from the
/// accessibility system, it will translate into a 160 pixel
/// leftwards drag.
double scrollFactor;
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
if (onTap != null && _isValidAction(SemanticsAction.tap))
config.onTap = onTap;
if (onLongPress != null && _isValidAction(SemanticsAction.longPress))
config.onLongPress = onLongPress;
if (onHorizontalDragUpdate != null) {
if (_isValidAction(SemanticsAction.scrollRight))
config.onScrollRight = _performSemanticScrollRight;
if (_isValidAction(SemanticsAction.scrollLeft))
config.onScrollLeft = _performSemanticScrollLeft;
}
if (onVerticalDragUpdate != null) {
if (_isValidAction(SemanticsAction.scrollUp))
config.onScrollUp = _performSemanticScrollUp;
if (_isValidAction(SemanticsAction.scrollDown))
config.onScrollDown = _performSemanticScrollDown;
}
}
bool _isValidAction(SemanticsAction action) {
return validActions == null || validActions.contains(action);
}
void _performSemanticScrollLeft() {
if (onHorizontalDragUpdate != null) {
final double primaryDelta = size.width * -scrollFactor;
onHorizontalDragUpdate(new DragUpdateDetails(
delta: new Offset(primaryDelta, 0.0), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Offset.zero)),
));
}
}
void _performSemanticScrollRight() {
if (onHorizontalDragUpdate != null) {
final double primaryDelta = size.width * scrollFactor;
onHorizontalDragUpdate(new DragUpdateDetails(
delta: new Offset(primaryDelta, 0.0), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Offset.zero)),
));
}
}
void _performSemanticScrollUp() {
if (onVerticalDragUpdate != null) {
final double primaryDelta = size.height * -scrollFactor;
onVerticalDragUpdate(new DragUpdateDetails(
delta: new Offset(0.0, primaryDelta), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Offset.zero)),
));
}
}
void _performSemanticScrollDown() {
if (onVerticalDragUpdate != null) {
final double primaryDelta = size.height * scrollFactor;
onVerticalDragUpdate(new DragUpdateDetails(
delta: new Offset(0.0, primaryDelta), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Offset.zero)),
));
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
final List<String> gestures = <String>[];
if (onTap != null)
gestures.add('tap');
if (onLongPress != null)
gestures.add('long press');
if (onHorizontalDragUpdate != null)
gestures.add('horizontal scroll');
if (onVerticalDragUpdate != null)
gestures.add('vertical scroll');
if (gestures.isEmpty)
gestures.add('<none>');
properties.add(new IterableProperty<String>('gestures', gestures));
}
}
/// Add annotations to the [SemanticsNode] for this subtree.
class RenderSemanticsAnnotations extends RenderProxyBox {
/// Creates a render object that attaches a semantic annotation.
///
/// The [container] argument must not be null.
///
/// If the [label] is not null, the [textDirection] must also not be null.
RenderSemanticsAnnotations({
RenderBox child,
bool container: false,
bool explicitChildNodes,
bool enabled,
bool checked,
bool selected,
bool button,
bool header,
bool textField,
bool focused,
bool inMutuallyExclusiveGroup,
bool obscured,
bool scopesRoute,
bool namesRoute,
bool hidden,
String label,
String value,
String increasedValue,
String decreasedValue,
String hint,
TextDirection textDirection,
SemanticsSortKey sortKey,
VoidCallback onTap,
VoidCallback onLongPress,
VoidCallback onScrollLeft,
VoidCallback onScrollRight,
VoidCallback onScrollUp,
VoidCallback onScrollDown,
VoidCallback onIncrease,
VoidCallback onDecrease,
VoidCallback onCopy,
VoidCallback onCut,
VoidCallback onPaste,
MoveCursorHandler onMoveCursorForwardByCharacter,
MoveCursorHandler onMoveCursorBackwardByCharacter,
SetSelectionHandler onSetSelection,
VoidCallback onDidGainAccessibilityFocus,
VoidCallback onDidLoseAccessibilityFocus,
}) : assert(container != null),
_container = container,
_explicitChildNodes = explicitChildNodes,
_enabled = enabled,
_checked = checked,
_selected = selected,
_button = button,
_header = header,
_textField = textField,
_focused = focused,
_inMutuallyExclusiveGroup = inMutuallyExclusiveGroup,
_obscured = obscured,
_scopesRoute = scopesRoute,
_namesRoute = namesRoute,
_hidden = hidden,
_label = label,
_value = value,
_increasedValue = increasedValue,
_decreasedValue = decreasedValue,
_hint = hint,
_textDirection = textDirection,
_sortKey = sortKey,
_onTap = onTap,
_onLongPress = onLongPress,
_onScrollLeft = onScrollLeft,
_onScrollRight = onScrollRight,
_onScrollUp = onScrollUp,
_onScrollDown = onScrollDown,
_onIncrease = onIncrease,
_onDecrease = onDecrease,
_onCopy = onCopy,
_onCut = onCut,