blob: d2ba9725fd28aa9a51020a86f77704b2b6d3769e [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:ui' as ui show ImageFilter, Gradient;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.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 {
/// 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
abstract class RenderProxyBoxMixin extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
// 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 description) {
super.debugFillProperties(description);
description.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 description) {
super.debugFillProperties(description);
description.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 description) {
super.debugFillProperties(description);
description.add(new DoubleProperty('maxWidth', maxWidth, defaultValue: double.INFINITY));
description.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 description) {
super.debugFillProperties(description);
description.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 description) {
super.debugFillProperties(description);
description.add(new DoubleProperty('stepWidth', stepWidth));
description.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;
_opacity = value;
_alpha = _getAlphaFromOpacity(_opacity);
markNeedsCompositingBitsUpdate();
markNeedsPaint();
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 description) {
super.debugFillProperties(description);
description.add(new DoubleProperty('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';
}
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;
}());
}
}
/// Creates a physical model layer that clips its children to a rounded
/// rectangle.
///
/// A physical model layer casts a shadow based on its [elevation].
class RenderPhysicalModel extends _RenderCustomClip<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,
_elevation = elevation,
_color = color,
_shadowColor = shadowColor,
super(child: child);
/// 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();
}
/// 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;
_elevation = value;
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();
}
@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);
}
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 paint(PaintingContext context, Offset offset) {
if (child != null) {
_updateClip();
final RRect offsetClipRRect = _clip.shift(offset);
final Rect offsetBounds = offsetClipRRect.outerRect;
if (needsCompositing) {
final PhysicalModelLayer physicalModel = new PhysicalModelLayer(
clipRRect: offsetClipRRect,
elevation: elevation,
color: color,
);
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),
_transparentPaint,
);
canvas.drawShadow(
new Path()..addRRect(offsetClipRRect),
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, _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));
description.add(new DoubleProperty('elevation', elevation));
description.add(new DiagnosticsProperty<Color>('color', color));
}
}
/// 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();
}
@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 description) {
super.debugFillProperties(description);
description.add(_decoration.toDiagnosticsNode(name: 'decoration'));
description.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 corder 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();
}
/// 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();
}
/// 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();
}
/// 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();
}
/// Sets the transform to the identity matrix.
void setIdentity() {
_transform.setIdentity();
markNeedsPaint();
}
/// Concatenates a rotation about the x axis into the transform.
void rotateX(double radians) {
_transform.rotateX(radians);
markNeedsPaint();
}
/// Concatenates a rotation about the y axis into the transform.
void rotateY(double radians) {
_transform.rotateY(radians);
markNeedsPaint();
}
/// Concatenates a rotation about the z axis into the transform.
void rotateZ(double radians) {
_transform.rotateZ(radians);
markNeedsPaint();
}
/// 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();
}
/// Concatenates a scale into the transform.
void scale(double x, [double y, double z]) {
_transform.scale(x, y, z);
markNeedsPaint();
}
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) {
Matrix4 inverse;
try {
inverse = new Matrix4.inverted(_effectiveTransform);
} on ArgumentError {
// 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 description) {
super.debugFillProperties(description);
description.add(new TransformProperty('transform matrix', _transform));
description.add(new DiagnosticsProperty<Offset>('origin', origin));
description.add(new DiagnosticsProperty<Alignment>('alignment', alignment));
description.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
description.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.width;
_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();
Matrix4 inverse;
try {
inverse = new Matrix4.inverted(_transform);
} on ArgumentError {
// 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 description) {
super.debugFillProperties(description);
description.add(new EnumProperty<BoxFit>('fit', fit));
description.add(new DiagnosticsProperty<Alignment>('alignment', alignment));
description.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 description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<Offset>('translation', translation));
description.add(new DiagnosticsProperty<bool>('transformHitTests', transformHitTests));
}
}
/// The interface used by [CustomPaint] (in the widgets library) and
/// [RenderCustomPaint] (in the rendering library).
///
/// To implement a custom painter, either subclass or implement this interface
/// to define your custom paint delegate. [CustomPaint] subclasses must
/// implement the [paint] and [shouldRepaint] methods, and may optionally also
/// implement the [hitTest] method.
///
/// The [paint] method is called whenever the custom object needs to be repainted.
///
/// The [shouldRepaint] 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 trigger a repaint is to either extend this class
/// and supply a `repaint` argument to the constructor of the [CustomPainter],
/// where that object notifies its listeners when it is time to repaint, or to
/// extend [Listenable] (e.g. via [ChangeNotifier]) and implement
/// [CustomPainter], so that the object itself provides the notifications
/// directly. In either case, the [CustomPaint] widget or [RenderCustomPaint]
/// render object will listen to the [Listenable] and repaint whenever the
/// animation ticks, avoiding both the build and layout phases of the pipeline.
///
/// The [hitTest] method is called when the user interacts with the underlying
/// render object, to determine if the user hit the object or missed it.
///
/// ## Sample code
///
/// This sample extends the same code shown for [RadialGradient] to create a
/// custom painter that paints a sky.
///
/// ```dart
/// class Sky extends CustomPainter {
/// @override
/// void paint(Canvas canvas, Size size) {
/// var rect = Offset.zero & size;
/// var gradient = new RadialGradient(
/// center: const Alignment(0.7, -0.6),
/// radius: 0.2,
/// colors: [const Color(0xFFFFFF00), const Color(0xFF0099FF)],
/// stops: [0.4, 1.0],
/// );
/// canvas.drawRect(
/// rect,
/// new Paint()..shader = gradient.createShader(rect),
/// );
/// }
///
/// @override
/// bool shouldRepaint(Sky oldDelegate) {
/// // Since this Sky painter has no fields, it always paints
/// // the same thing, and therefore we return false here. If
/// // we had fields (set from the constructor) then we would
/// // return true if any of them differed from the same
/// // fields on the oldDelegate.
/// return false;
/// }
/// }
/// ```
///
/// See also:
///
/// * [Canvas], the class that a custom painter uses to paint.
/// * [CustomPaint], the widget that uses [CustomPainter], and whose sample
/// code shows how to use the above `Sky` class.
/// * [RadialGradient], whose sample code section shows a different take
/// on the sample code above.
abstract class CustomPainter extends Listenable {
/// Creates a custom painter.
///
/// The painter will repaint whenever `repaint` notifies its listeners.
const CustomPainter({ Listenable repaint }) : _repaint = repaint;
final Listenable _repaint;
/// Register a closure to be notified when it is time to repaint.
///
/// The [CustomPainter] implementation merely forwards to the same method on
/// the [Listenable] provided to the constructor in the `repaint` argument, if
/// it was not null.
@override
void addListener(VoidCallback listener) => _repaint?.addListener(listener);
/// Remove a previously registered closure from the list of closures that the
/// object notifies when it is time to repaint.
///
/// The [CustomPainter] implementation merely forwards to the same method on
/// the [Listenable] provided to the constructor in the `repaint` argument, if
/// it was not null.
@override
void removeListener(VoidCallback listener) => _repaint?.removeListener(listener);
/// Called whenever the object needs to paint. The given [Canvas] has its
/// coordinate space configured such that the origin is at the top left of the
/// box. The area of the box is the size of the [size] argument.
///
/// Paint operations should remain inside the given area. Graphical operations
/// outside the bounds may be silently ignored, clipped, or not clipped.
///
/// Implementations should be wary of correctly pairing any calls to
/// [Canvas.save]/[Canvas.saveLayer] and [Canvas.restore], otherwise all
/// subsequent painting on this canvas may be affected, with potentially
/// hilarious but confusing results.
///
/// To paint text on a [Canvas], use a [TextPainter].
///
/// To paint an image on a [Canvas]:
///
/// 1. Obtain an [ImageStream], for example by calling [ImageProvider.resolve]
/// on an [AssetImage] or [NetworkImage] object.
///
/// 2. Whenever the [ImageStream]'s underlying [ImageInfo] object changes
/// (see [ImageStream.addListener]), create a new instance of your custom
/// paint delegate, giving it the new [ImageInfo] object.
///
/// 3. In your delegate's [paint] method, call the [Canvas.drawImage],
/// [Canvas.drawImageRect], or [Canvas.drawImageNine] methods to paint the
/// [ImageInfo.image] object, applying the [ImageInfo.scale] value to
/// obtain the correct rendering size.
void paint(Canvas canvas, Size size);
/// Called whenever a new instance of the custom painter delegate class is
/// provided to the [RenderCustomPaint] object, or any time that a new
/// [CustomPaint] 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 [paint] call might be optimized
/// away.
///
/// It's possible that the [paint] method will get called even if
/// [shouldRepaint] returns false (e.g. if an ancestor or descendant needed to
/// be repainted). It's also possible that the [paint] method will get called
/// without [shouldRepaint] being called at all (e.g. if the box changes
/// size).
///
/// If a custom delegate has a particularly expensive paint function such that
/// repaints should be avoided as much as possible, a [RepaintBoundary] or
/// [RenderRepaintBoundary] (or other render object with
/// [RenderObject.isRepaintBoundary] set to true) might be helpful.
bool shouldRepaint(covariant CustomPainter oldDelegate);
/// Called whenever a hit test is being performed on an object that is using
/// this custom paint delegate.
///
/// The given point is relative to the same coordinate space as the last
/// [paint] call.
///
/// The default behavior is to consider all points to be hits for
/// background painters, and no points to be hits for foreground painters.
///
/// Return true if the given position corresponds to a point on the drawn
/// image that should be considered a "hit", false if it corresponds to a
/// point that should be considered outside the painted image, and null to use
/// the default behavior.
bool hitTest(Offset position) => null;
@override
String toString() => '${describeIdentity(this)}(${ _repaint?.toString() ?? "" })';
}
/// Provides a canvas on which to draw during the paint phase.
///
/// When asked to paint, [RenderCustomPaint] first asks its [painter] to paint
/// on the current canvas, then it paints its child, and then, after painting
/// its child, it asks its [foregroundPainter] to paint. The coordinate system of
/// the canvas matches the coordinate system of the [CustomPaint] object. The
/// painters are expected to paint within a rectangle starting at the origin and
/// encompassing a region of the given size. (If the painters paint outside
/// those bounds, there might be insufficient memory allocated to rasterize the
/// painting commands and the resulting behavior is undefined.)
///
/// Painters are implemented by subclassing or implementing [CustomPainter].
///
/// Because custom paint calls its painters during paint, you cannot mark the
/// tree as needing a new layout during the callback (the layout for this frame
/// has already happened).
///
/// Custom painters normally size themselves to their child. If they do not have
/// a child, they attempt to size themselves to the [preferredSize], which
/// defaults to [Size.zero].
///
/// See also:
///
/// * [CustomPainter], the class that custom painter delegates should extend.
/// * [Canvas], the API provided to custom painter delegates.
class RenderCustomPaint extends RenderProxyBox {
/// Creates a render object that delegates its painting.
RenderCustomPaint({
CustomPainter painter,
CustomPainter foregroundPainter,
Size preferredSize: Size.zero,
this.isComplex: false,
this.willChange: false,
RenderBox child,
}) : assert(preferredSize != null),
_painter = painter,
_foregroundPainter = foregroundPainter,
_preferredSize = preferredSize,
super(child);
/// The background custom paint delegate.
///
/// This painter, if non-null, is called to paint behind the children.
CustomPainter get painter => _painter;
CustomPainter _painter;
/// Set a new background custom paint delegate.
///
/// If the new delegate is the same as the previous one, this does nothing.
///
/// If the new delegate is the same class as the previous one, then the new
/// delegate has its [CustomPainter.shouldRepaint] called; if the result is
/// true, then the delegate will be called.
///
/// If the new delegate is a different class than the previous one, then the
/// delegate will be called.
///
/// If the new value is null, then there is no background custom painter.
set painter(CustomPainter value) {
if (_painter == value)
return;
final CustomPainter oldPainter = _painter;
_painter = value;
_didUpdatePainter(_painter, oldPainter);
}
/// The foreground custom paint delegate.
///
/// This painter, if non-null, is called to paint in front of the children.
CustomPainter get foregroundPainter => _foregroundPainter;
CustomPainter _foregroundPainter;
/// Set a new foreground custom paint delegate.
///
/// If the new delegate is the same as the previous one, this does nothing.
///
/// If the new delegate is the same class as the previous one, then the new
/// delegate has its [CustomPainter.shouldRepaint] called; if the result is
/// true, then the delegate will be called.
///
/// If the new delegate is a different class than the previous one, then the
/// delegate will be called.
///
/// If the new value is null, then there is no foreground custom painter.
set foregroundPainter(CustomPainter value) {
if (_foregroundPainter == value)
return;
final CustomPainter oldPainter = _foregroundPainter;
_foregroundPainter = value;
_didUpdatePainter(_foregroundPainter, oldPainter);
}
void _didUpdatePainter(CustomPainter newPainter, CustomPainter oldPainter) {
if (newPainter == null) {
assert(oldPainter != null); // We should be called only for changes.
markNeedsPaint();
} else if (oldPainter == null ||
newPainter.runtimeType != oldPainter.runtimeType ||
newPainter.shouldRepaint(oldPainter)) {
markNeedsPaint();
}
if (attached) {
oldPainter?.removeListener(markNeedsPaint);
newPainter?.addListener(markNeedsPaint);
}
}
/// The size that this [RenderCustomPaint] should aim for, given the layout
/// constraints, if there is no child.
///
/// Defaults to [Size.zero].
///
/// If there's a child, this is ignored, and the size of the child is used
/// instead.
Size get preferredSize => _preferredSize;
Size _preferredSize;
set preferredSize(Size value) {
assert(value != null);
if (preferredSize == value)
return;
_preferredSize = value;
markNeedsLayout();
}
/// Whether to hint that this layer's painting should be cached.
///
/// The compositor contains a raster cache that holds bitmaps of layers in
/// order to avoid the cost of repeatedly rendering those layers on each
/// frame. If this flag is not set, then the compositor will apply its own
/// heuristics to decide whether the this layer is complex enough to benefit
/// from caching.
bool isComplex;
/// Whether the raster cache should be told that this painting is likely
/// to change in the next frame.
bool willChange;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_painter?.addListener(markNeedsPaint);
_foregroundPainter?.addListener(markNeedsPaint);
}
@override
void detach() {
_painter?.removeListener(markNeedsPaint);
_foregroundPainter?.removeListener(markNeedsPaint);
super.detach();
}
@override
bool hitTestChildren(HitTestResult result, { Offset position }) {
if (_foregroundPainter != null && (_foregroundPainter.hitTest(position) ?? false))
return true;
return super.hitTestChildren(result, position: position);
}
@override
bool hitTestSelf(Offset position) {
return _painter != null && (_painter.hitTest(position) ?? true);
}
@override
void performResize() {
size = constraints.constrain(preferredSize);
}
void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) {
int debugPreviousCanvasSaveCount;
canvas.save();
assert(() { debugPreviousCanvasSaveCount = canvas.getSaveCount(); return true; }());
if (offset != Offset.zero)
canvas.translate(offset.dx, offset.dy);
painter.paint(canvas, size);
assert(() {
// This isn't perfect. For example, we can't catch the case of
// someone first restoring, then setting a transform or whatnot,
// then saving.
// If this becomes a real problem, we could add logic to the
// Canvas class to lock the canvas at a particular save count
// such that restore() fails if it would take the lock count
// below that number.
final int debugNewCanvasSaveCount = canvas.getSaveCount();
if (debugNewCanvasSaveCount > debugPreviousCanvasSaveCount) {
throw new FlutterError(
'The $painter custom painter called canvas.save() or canvas.saveLayer() at least '
'${debugNewCanvasSaveCount - debugPreviousCanvasSaveCount} more '
'time${debugNewCanvasSaveCount - debugPreviousCanvasSaveCount == 1 ? '' : 's' } '
'than it called canvas.restore().\n'
'This leaves the canvas in an inconsistent state and will probably result in a broken display.\n'
'You must pair each call to save()/saveLayer() with a later matching call to restore().'
);
}
if (debugNewCanvasSaveCount < debugPreviousCanvasSaveCount) {
throw new FlutterError(
'The $painter custom painter called canvas.restore() '
'${debugPreviousCanvasSaveCount - debugNewCanvasSaveCount} more '
'time${debugPreviousCanvasSaveCount - debugNewCanvasSaveCount == 1 ? '' : 's' } '
'than it called canvas.save() or canvas.saveLayer().\n'
'This leaves the canvas in an inconsistent state and will result in a broken display.\n'
'You should only call restore() if you first called save() or saveLayer().'
);
}
return debugNewCanvasSaveCount == debugPreviousCanvasSaveCount;
}());
canvas.restore();
}
@override
void paint(PaintingContext context, Offset offset) {
if (_painter != null) {
_paintWithPainter(context.canvas, offset, _painter);
_setRasterCacheHints(context);
}
super.paint(context, offset);
if (_foregroundPainter != null) {
_paintWithPainter(context.canvas, offset, _foregroundPainter);
_setRasterCacheHints(context);
}
}
void _setRasterCacheHints(PaintingContext context) {
if (isComplex)
context.setIsComplexHint();
if (willChange)
context.setWillChangeHint();
}
}
/// 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 description) {
super.debugFillProperties(description);
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>');
description.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;
/// 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 description) {
super.debugFillProperties(description);
bool inReleaseMode = true;
assert(() {
inReleaseMode = false;
if (debugSymmetricPaintCount + debugAsymmetricPaintCount == 0) {
description.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';
}
description.add(new PercentProperty('metrics', fraction, unit: 'useful', tooltip: '$debugSymmetricPaintCount bad vs $debugAsymmetricPaintCount good'));
description.add(new MessageProperty('diagnosis', diagnosis));
}
return true;
}());
if (inReleaseMode)
description.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 description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<bool>('ignoring', ignoring));
description.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 description) {
super.debugFillProperties(description);
description.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 ? true : super.hitTest(result, position: position);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.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 description) {
super.debugFillProperties(description);
description.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);
/// When a [SemanticsNode] that is a direct child of this object's
/// [SemanticsNode] is tagged with [excludeFromScrolling] it will not be
/// part of the scrolling area for semantic purposes.
///
/// This behavior is only active if the [SemanticsNode] of this
/// [RenderSemanticsGestureHandler] is tagged with [useTwoPaneSemantics].
/// Otherwise, the [excludeFromScrolling] tag is ignored.
///
/// As an example, a [RenderSliver] that stays on the screen within a
/// [Scrollable] even though the user has scrolled past it (e.g. a pinned app
/// bar) can tag its [SemanticsNode] with [excludeFromScrolling] to indicate
/// that it should no longer be considered for semantic actions related to
/// scrolling.
static const SemanticsTag excludeFromScrolling = const SemanticsTag('RenderSemanticsGestureHandler.excludeFromScrolling');
/// If the [SemanticsNode] of this [RenderSemanticsGestureHandler] is tagged
/// with [useTwoPaneSemantics], two semantics nodes will be used to represent
/// this render object in the semantics tree.
///
/// Two semantics nodes are necessary to exclude certain child nodes (via the
/// [excludeFromScrolling] tag) from the scrollable area for semantic
/// purposes.
///
/// If this tag is used, the first "outer" semantics node is the regular node
/// of this object. The second "inner" node is introduces as a child to that
/// node. All scrollable children are now a child of the inner node, which has
/// the semantic scrolling logic enabled. All children that have been
/// excluded from scrolling with [excludeFromScrolling] are turned into
/// children of the outer node.
static const SemanticsTag useTwoPaneSemantics = const SemanticsTag('RenderSemanticsGestureHandler.twoPane');
/// 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;
bool get _hasHandlers {
return onTap != null
|| onLongPress != null
|| onHorizontalDragUpdate != null
|| onVerticalDragUpdate != null;
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = _hasHandlers;
// TODO(goderbauer): this needs to be set even when there is only potential
// for this to become a scroll view.
config.explicitChildNodes = onHorizontalDragUpdate != null
|| onVerticalDragUpdate != null;
final Map<SemanticsAction, VoidCallback> actions = <SemanticsAction, VoidCallback>{};
if (onTap != null)
actions[SemanticsAction.tap] = onTap;
if (onLongPress != null)
actions[SemanticsAction.longPress] = onLongPress;
if (onHorizontalDragUpdate != null) {
actions[SemanticsAction.scrollRight] = _performSemanticScrollRight;
actions[SemanticsAction.scrollLeft] = _performSemanticScrollLeft;
}
if (onVerticalDragUpdate != null) {
actions[SemanticsAction.scrollUp] = _performSemanticScrollUp;
actions[SemanticsAction.scrollDown] = _performSemanticScrollDown;
}
final Iterable<SemanticsAction> actionsToAdd = validActions ?? actions.keys;
for (SemanticsAction action in actionsToAdd) {
final VoidCallback handler = actions[action];
if (handler != null)
config.addAction(action, handler);
}
}
SemanticsNode _innerNode;
SemanticsNode _annotatedNode;
/// Sends a [SemanticsEvent] in the context of the [SemanticsNode] that is
/// annotated with this object's semantics information.
void sendSemanticsEvent(SemanticsEvent event) {
_annotatedNode?.sendEvent(event);
}
@override
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
if (children.isEmpty || !children.first.isTagged(useTwoPaneSemantics)) {
_annotatedNode = node;
super.assembleSemanticsNode(node, config, children);
return;
}
_innerNode ??= new SemanticsNode(showOnScreen: showOnScreen);
_innerNode
..isMergedIntoParent = node.isPartOfNodeMerging
..rect = Offset.zero & node.rect.size;
_annotatedNode = _innerNode;
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode];
final List<SemanticsNode> included = <SemanticsNode>[];
for (SemanticsNode child in children) {
assert(child.isTagged(useTwoPaneSemantics));
if (child.isTagged(excludeFromScrolling))
excluded.add(child);
else
included.add(child);
}
node.updateWith(config: null, childrenInInversePaintOrder: excluded);
_innerNode.updateWith(config: config, childrenInInversePaintOrder: included);
}
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() {