| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'dart:ui' as ui show ImageFilter, Gradient, Image, Color; |
| |
| import 'package:flutter/animation.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/painting.dart'; |
| import 'package:flutter/semantics.dart'; |
| |
| import 'package:vector_math/vector_math_64.dart'; |
| |
| import 'binding.dart'; |
| import 'box.dart'; |
| import 'layer.dart'; |
| import 'mouse_tracking.dart'; |
| import 'object.dart'; |
| |
| export 'package:flutter/gestures.dart' show |
| PointerEvent, |
| PointerDownEvent, |
| PointerMoveEvent, |
| PointerUpEvent, |
| PointerCancelEvent; |
| |
| /// A base class for render boxes 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. |
| /// |
| /// See also: |
| /// |
| /// * [RenderProxySliver], a base class for render slivers that resemble their |
| /// children. |
| class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin<RenderBox> { |
| /// Creates a proxy render box. |
| /// |
| /// Proxy render boxes are rarely created directly because they simply proxy |
| /// the render box protocol to [child]. Instead, consider using one of the |
| /// subclasses. |
| RenderProxyBox([RenderBox child]) { |
| this.child = child; |
| } |
| } |
| |
| /// Implementation of [RenderProxyBox]. |
| /// |
| /// Use this mixin in 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/31543 is fixed |
| @optionalTypeArgs |
| mixin RenderProxyBoxMixin<T extends RenderBox> on RenderBox, RenderObjectWithChildMixin<T> { |
| @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 = 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(BoxHitTestResult 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(BoxHitTestResult result, { Offset position }) { |
| bool hitTarget = false; |
| if (size.contains(position)) { |
| hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position); |
| if (hitTarget || behavior == HitTestBehavior.translucent) |
| result.add(BoxHitTestEntry(this, position)); |
| } |
| return hitTarget; |
| } |
| |
| @override |
| bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque; |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(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); |
| assert(width.isFinite); |
| if (!_additionalConstraints.hasInfiniteWidth) |
| 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); |
| assert(width.isFinite); |
| if (!_additionalConstraints.hasInfiniteWidth) |
| 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); |
| assert(height.isFinite); |
| if (!_additionalConstraints.hasInfiniteHeight) |
| 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); |
| assert(height.isFinite); |
| if (!_additionalConstraints.hasInfiniteHeight) |
| return _additionalConstraints.constrainHeight(height); |
| return height; |
| } |
| |
| @override |
| void performLayout() { |
| final BoxConstraints constraints = this.constraints; |
| 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 = Paint() |
| ..color = const Color(0x90909090); |
| context.canvas.drawRect(offset & size, paint); |
| } |
| return true; |
| }()); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(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 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) { |
| final BoxConstraints constraints = this.constraints; |
| child.layout(_limitConstraints(constraints), parentUsesSize: true); |
| size = constraints.constrain(child.size); |
| } else { |
| size = _limitConstraints(constraints).constrain(Size.zero); |
| } |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DoubleProperty('maxWidth', maxWidth, defaultValue: double.infinity)); |
| properties.add(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 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(Size(width, height)); |
| } |
| |
| @override |
| void performLayout() { |
| size = _applyAspectRatio(constraints); |
| if (child != null) |
| child.layout(BoxConstraints.tight(size)); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(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. |
| /// |
| /// If [stepWidth] is non-null it must be > 0.0. Similarly If [stepHeight] is |
| /// non-null it must be > 0.0. |
| RenderIntrinsicWidth({ |
| double stepWidth, |
| double stepHeight, |
| RenderBox child, |
| }) : assert(stepWidth == null || stepWidth > 0.0), |
| assert(stepHeight == null || stepHeight > 0.0), |
| _stepWidth = stepWidth, |
| _stepHeight = stepHeight, |
| super(child); |
| |
| /// If non-null, force the child's width to be a multiple of this value. |
| /// |
| /// This value must be null or > 0.0. |
| double get stepWidth => _stepWidth; |
| double _stepWidth; |
| set stepWidth(double value) { |
| assert(value == null || value > 0.0); |
| if (value == _stepWidth) |
| return; |
| _stepWidth = value; |
| markNeedsLayout(); |
| } |
| |
| /// If non-null, force the child's height to be a multiple of this value. |
| /// |
| /// This value must be null or > 0.0. |
| double get stepHeight => _stepHeight; |
| double _stepHeight; |
| set stepHeight(double value) { |
| assert(value == null || value > 0.0); |
| if (value == _stepHeight) |
| return; |
| _stepHeight = value; |
| markNeedsLayout(); |
| } |
| |
| static double _applyStep(double input, double step) { |
| assert(input.isFinite); |
| if (step == null) |
| return input; |
| return (input / step).ceil() * step; |
| } |
| |
| @override |
| double computeMinIntrinsicWidth(double height) { |
| return computeMaxIntrinsicWidth(height); |
| } |
| |
| @override |
| double computeMaxIntrinsicWidth(double height) { |
| if (child == null) |
| return 0.0; |
| final double width = child.getMaxIntrinsicWidth(height); |
| return _applyStep(width, _stepWidth); |
| } |
| |
| @override |
| double computeMinIntrinsicHeight(double width) { |
| if (child == null) |
| return 0.0; |
| if (!width.isFinite) |
| width = computeMaxIntrinsicWidth(double.infinity); |
| assert(width.isFinite); |
| final double height = child.getMinIntrinsicHeight(width); |
| return _applyStep(height, _stepHeight); |
| } |
| |
| @override |
| double computeMaxIntrinsicHeight(double width) { |
| if (child == null) |
| return 0.0; |
| if (!width.isFinite) |
| width = computeMaxIntrinsicWidth(double.infinity); |
| assert(width.isFinite); |
| final double height = child.getMaxIntrinsicHeight(width); |
| return _applyStep(height, _stepHeight); |
| } |
| |
| @override |
| void performLayout() { |
| if (child != null) { |
| BoxConstraints childConstraints = constraints; |
| if (!childConstraints.hasTightWidth) { |
| final double width = child.getMaxIntrinsicWidth(childConstraints.maxHeight); |
| assert(width.isFinite); |
| childConstraints = childConstraints.tighten(width: _applyStep(width, _stepWidth)); |
| } |
| if (_stepHeight != null) { |
| final double height = child.getMaxIntrinsicHeight(childConstraints.maxWidth); |
| assert(height.isFinite); |
| childConstraints = childConstraints.tighten(height: _applyStep(height, _stepHeight)); |
| } |
| child.layout(childConstraints, parentUsesSize: true); |
| size = child.size; |
| } else { |
| performResize(); |
| } |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DoubleProperty('stepWidth', stepWidth)); |
| properties.add(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(); |
| } |
| } |
| |
| } |
| |
| /// 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, |
| bool alwaysIncludeSemantics = false, |
| RenderBox child, |
| }) : assert(opacity != null), |
| assert(opacity >= 0.0 && opacity <= 1.0), |
| assert(alwaysIncludeSemantics != null), |
| _opacity = opacity, |
| _alwaysIncludeSemantics = alwaysIncludeSemantics, |
| _alpha = ui.Color.getAlphaFromOpacity(opacity), |
| super(child); |
| |
| @override |
| bool get alwaysNeedsCompositing => child != null && (_alpha != 0 && _alpha != 255); |
| |
| int _alpha; |
| |
| /// The fraction to scale the child's alpha value. |
| /// |
| /// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent |
| /// (i.e., invisible). |
| /// |
| /// The opacity must not be null. |
| /// |
| /// Values 1.0 and 0.0 are painted with a fast path. Other values |
| /// require painting the child into an intermediate buffer, which is |
| /// expensive. |
| double get opacity => _opacity; |
| double _opacity; |
| set opacity(double value) { |
| assert(value != null); |
| assert(value >= 0.0 && value <= 1.0); |
| if (_opacity == value) |
| return; |
| final bool didNeedCompositing = alwaysNeedsCompositing; |
| final bool wasVisible = _alpha != 0; |
| _opacity = value; |
| _alpha = ui.Color.getAlphaFromOpacity(_opacity); |
| if (didNeedCompositing != alwaysNeedsCompositing) |
| markNeedsCompositingBitsUpdate(); |
| markNeedsPaint(); |
| if (wasVisible != (_alpha != 0) && !alwaysIncludeSemantics) |
| markNeedsSemanticsUpdate(); |
| } |
| |
| /// Whether child semantics are included regardless of the opacity. |
| /// |
| /// If false, semantics are excluded when [opacity] is 0.0. |
| /// |
| /// Defaults to false. |
| bool get alwaysIncludeSemantics => _alwaysIncludeSemantics; |
| bool _alwaysIncludeSemantics; |
| set alwaysIncludeSemantics(bool value) { |
| if (value == _alwaysIncludeSemantics) |
| return; |
| _alwaysIncludeSemantics = value; |
| markNeedsSemanticsUpdate(); |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| if (child != null) { |
| if (_alpha == 0) { |
| // No need to keep the layer. We'll create a new one if necessary. |
| layer = null; |
| return; |
| } |
| if (_alpha == 255) { |
| // No need to keep the layer. We'll create a new one if necessary. |
| layer = null; |
| context.paintChild(child, offset); |
| return; |
| } |
| assert(needsCompositing); |
| layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer); |
| } |
| } |
| |
| @override |
| void visitChildrenForSemantics(RenderObjectVisitor visitor) { |
| if (child != null && (_alpha != 0 || alwaysIncludeSemantics)) |
| visitor(child); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DoubleProperty('opacity', opacity)); |
| properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics')); |
| } |
| } |
| |
| /// Implementation of [RenderAnimatedOpacity] and [RenderSliverAnimatedOpacity]. |
| /// |
| /// Use this mixin in situations where the proxying behavior |
| /// of [RenderProxyBox] or [RenderProxySliver] is desired for animating opacity, |
| /// but would like to use the same methods for both types of render objects. |
| mixin RenderAnimatedOpacityMixin<T extends RenderObject> on RenderObjectWithChildMixin<T> { |
| int _alpha; |
| |
| @override |
| bool get alwaysNeedsCompositing => child != null && _currentlyNeedsCompositing; |
| bool _currentlyNeedsCompositing; |
| |
| /// The animation that drives this render object's opacity. |
| /// |
| /// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent |
| /// (i.e., invisible). |
| /// |
| /// To change the opacity of a child in a static manner, not animated, |
| /// consider [RenderOpacity] instead. |
| Animation<double> get opacity => _opacity; |
| Animation<double> _opacity; |
| set opacity(Animation<double> value) { |
| assert(value != null); |
| if (_opacity == value) |
| return; |
| if (attached && _opacity != null) |
| _opacity.removeListener(_updateOpacity); |
| _opacity = value; |
| if (attached) |
| _opacity.addListener(_updateOpacity); |
| _updateOpacity(); |
| } |
| |
| /// Whether child semantics are included regardless of the opacity. |
| /// |
| /// If false, semantics are excluded when [opacity] is 0.0. |
| /// |
| /// Defaults to false. |
| bool get alwaysIncludeSemantics => _alwaysIncludeSemantics; |
| bool _alwaysIncludeSemantics; |
| set alwaysIncludeSemantics(bool value) { |
| if (value == _alwaysIncludeSemantics) |
| return; |
| _alwaysIncludeSemantics = value; |
| markNeedsSemanticsUpdate(); |
| } |
| |
| @override |
| void attach(PipelineOwner owner) { |
| super.attach(owner); |
| _opacity.addListener(_updateOpacity); |
| _updateOpacity(); // in case it changed while we weren't listening |
| } |
| |
| @override |
| void detach() { |
| _opacity.removeListener(_updateOpacity); |
| super.detach(); |
| } |
| |
| void _updateOpacity() { |
| final int oldAlpha = _alpha; |
| _alpha = ui.Color.getAlphaFromOpacity(_opacity.value); |
| if (oldAlpha != _alpha) { |
| final bool didNeedCompositing = _currentlyNeedsCompositing; |
| _currentlyNeedsCompositing = _alpha > 0 && _alpha < 255; |
| if (child != null && didNeedCompositing != _currentlyNeedsCompositing) |
| markNeedsCompositingBitsUpdate(); |
| markNeedsPaint(); |
| if (oldAlpha == 0 || _alpha == 0) |
| markNeedsSemanticsUpdate(); |
| } |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| if (child != null) { |
| if (_alpha == 0) { |
| // No need to keep the layer. We'll create a new one if necessary. |
| layer = null; |
| return; |
| } |
| if (_alpha == 255) { |
| // No need to keep the layer. We'll create a new one if necessary. |
| layer = null; |
| context.paintChild(child, offset); |
| return; |
| } |
| assert(needsCompositing); |
| layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer); |
| } |
| } |
| |
| @override |
| void visitChildrenForSemantics(RenderObjectVisitor visitor) { |
| if (child != null && (_alpha != 0 || alwaysIncludeSemantics)) |
| visitor(child); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<Animation<double>>('opacity', opacity)); |
| properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics')); |
| } |
| } |
| |
| /// Makes its child partially transparent, driven from an [Animation]. |
| /// |
| /// This is a variant of [RenderOpacity] that uses an [Animation<double>] rather |
| /// than a [double] to control the opacity. |
| class RenderAnimatedOpacity extends RenderProxyBox with RenderProxyBoxMixin, RenderAnimatedOpacityMixin<RenderBox> { |
| /// Creates a partially transparent render object. |
| /// |
| /// The [opacity] argument must not be null. |
| RenderAnimatedOpacity({ |
| @required Animation<double> opacity, |
| bool alwaysIncludeSemantics = false, |
| RenderBox child, |
| }) : assert(opacity != null), |
| assert(alwaysIncludeSemantics != null), |
| super(child) { |
| this.opacity = opacity; |
| this.alwaysIncludeSemantics = alwaysIncludeSemantics; |
| } |
| } |
| |
| /// Signature for a function that creates a [Shader] for a given [Rect]. |
| /// |
| /// Used by [RenderShaderMask] and the [ShaderMask] widget. |
| typedef ShaderCallback = Shader Function(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); |
| |
| @override |
| ShaderMaskLayer get layer => super.layer as ShaderMaskLayer; |
| |
| /// 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. |
| /// |
| /// The rectangle will always be at the origin when called by |
| /// [RenderShaderMask]. |
| // 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); |
| layer ??= ShaderMaskLayer(); |
| layer |
| ..shader = _shaderCallback(Offset.zero & size) |
| ..maskRect = offset & size |
| ..blendMode = _blendMode; |
| context.pushLayer(layer, super.paint, offset); |
| } else { |
| layer = null; |
| } |
| } |
| } |
| |
| /// 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); |
| |
| @override |
| BackdropFilterLayer get layer => super.layer as BackdropFilterLayer; |
| |
| /// 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); |
| layer ??= BackdropFilterLayer(); |
| layer.filter = _filter; |
| context.pushLayer(layer, super.paint, offset); |
| } else { |
| layer = null; |
| } |
| } |
| } |
| |
| /// 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<Rect>]. |
| /// * [ClipRRect], which can be customized with a [CustomClipper<RRect>]. |
| /// * [ClipOval], which can be customized with a [CustomClipper<Rect>]. |
| /// * [ClipPath], which can be customized with a [CustomClipper<Path>]. |
| /// * [ShapeBorderClipper], for specifying a clip path using a [ShapeBorder]. |
| 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() => objectRuntimeType(this, 'CustomClipper'); |
| } |
| |
| /// A [CustomClipper] that clips to the outer path of a [ShapeBorder]. |
| class ShapeBorderClipper extends CustomClipper<Path> { |
| /// Creates a [ShapeBorder] clipper. |
| /// |
| /// The [shape] argument must not be null. |
| /// |
| /// The [textDirection] argument must be provided non-null if [shape] |
| /// has a text direction dependency (for example if it is expressed in terms |
| /// of "start" and "end" instead of "left" and "right"). It may be null if |
| /// the border will not need the text direction to paint itself. |
| const ShapeBorderClipper({ |
| @required this.shape, |
| this.textDirection, |
| }) : assert(shape != null); |
| |
| /// The shape border whose outer path this clipper clips to. |
| final ShapeBorder shape; |
| |
| /// The text direction to use for getting the outer path for [shape]. |
| /// |
| /// [ShapeBorder]s can depend on the text direction (e.g having a "dent" |
| /// towards the start of the shape). |
| final TextDirection textDirection; |
| |
| /// Returns the outer path of [shape] as the clip. |
| @override |
| Path getClip(Size size) { |
| return shape.getOuterPath(Offset.zero & size, textDirection: textDirection); |
| } |
| |
| @override |
| bool shouldReclip(CustomClipper<Path> oldClipper) { |
| if (oldClipper.runtimeType != ShapeBorderClipper) |
| return true; |
| final ShapeBorderClipper typedOldClipper = oldClipper as ShapeBorderClipper; |
| return typedOldClipper.shape != shape |
| || typedOldClipper.textDirection != textDirection; |
| } |
| } |
| |
| abstract class _RenderCustomClip<T> extends RenderProxyBox { |
| _RenderCustomClip({ |
| RenderBox child, |
| CustomClipper<T> clipper, |
| Clip clipBehavior = Clip.antiAlias, |
| }) : assert(clipBehavior != null), |
| _clipper = clipper, |
| _clipBehavior = clipBehavior, |
| 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 || |
| newClipper.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; |
| |
| Clip get clipBehavior => _clipBehavior; |
| set clipBehavior(Clip value) { |
| if (value != _clipBehavior) { |
| _clipBehavior = value; |
| markNeedsPaint(); |
| } |
| } |
| Clip _clipBehavior; |
| |
| @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 ??= Paint() |
| ..shader = 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 ??= TextPainter( |
| text: const TextSpan( |
| text: '✂', |
| style: TextStyle( |
| color: 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. |
| /// |
| /// The [clipBehavior] must not be null or [Clip.none]. |
| RenderClipRect({ |
| RenderBox child, |
| CustomClipper<Rect> clipper, |
| Clip clipBehavior = Clip.antiAlias, |
| }) : assert(clipBehavior != null), |
| assert(clipBehavior != Clip.none), |
| super(child: child, clipper: clipper, clipBehavior: clipBehavior); |
| |
| @override |
| Rect get _defaultClip => Offset.zero & size; |
| |
| @override |
| bool hitTest(BoxHitTestResult 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(); |
| layer = context.pushClipRect( |
| needsCompositing, |
| offset, |
| _clip, |
| super.paint, |
| clipBehavior: clipBehavior, |
| oldLayer: layer as ClipRectLayer, |
| ); |
| } else { |
| layer = null; |
| } |
| } |
| |
| @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 + 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. |
| /// |
| /// The [clipBehavior] argument must not be null or [Clip.none]. |
| RenderClipRRect({ |
| RenderBox child, |
| BorderRadius borderRadius = BorderRadius.zero, |
| CustomClipper<RRect> clipper, |
| Clip clipBehavior = Clip.antiAlias, |
| }) : assert(clipBehavior != null), |
| assert(clipBehavior != Clip.none), |
| _borderRadius = borderRadius, |
| super(child: child, clipper: clipper, clipBehavior: clipBehavior) { |
| 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(BoxHitTestResult 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(); |
| layer = context.pushClipRRect( |
| needsCompositing, |
| offset, |
| _clip.outerRect, |
| _clip, |
| super.paint, clipBehavior: clipBehavior, oldLayer: layer as ClipRRectLayer, |
| ); |
| } else { |
| layer = null; |
| } |
| } |
| |
| @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 + 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. |
| /// |
| /// The [clipBehavior] argument must not be null or [Clip.none]. |
| RenderClipOval({ |
| RenderBox child, |
| CustomClipper<Rect> clipper, |
| Clip clipBehavior = Clip.antiAlias, |
| }) : assert(clipBehavior != null), |
| assert(clipBehavior != Clip.none), |
| super(child: child, clipper: clipper, clipBehavior: clipBehavior); |
| |
| Rect _cachedRect; |
| Path _cachedPath; |
| |
| Path _getClipPath(Rect rect) { |
| if (rect != _cachedRect) { |
| _cachedRect = rect; |
| _cachedPath = Path()..addOval(_cachedRect); |
| } |
| return _cachedPath; |
| } |
| |
| @override |
| Rect get _defaultClip => Offset.zero & size; |
| |
| @override |
| bool hitTest(BoxHitTestResult 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 = 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(); |
| layer = context.pushClipPath( |
| needsCompositing, |
| offset, |
| _clip, |
| _getClipPath(_clip), |
| super.paint, |
| clipBehavior: clipBehavior, |
| oldLayer: layer as ClipPathLayer, |
| ); |
| } else { |
| layer = null; |
| } |
| } |
| |
| @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 + 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. |
| /// |
| /// The [clipBehavior] argument must not be null or [Clip.none]. |
| RenderClipPath({ |
| RenderBox child, |
| CustomClipper<Path> clipper, |
| Clip clipBehavior = Clip.antiAlias, |
| }) : assert(clipBehavior != null), |
| assert(clipBehavior != Clip.none), |
| super(child: child, clipper: clipper, clipBehavior: clipBehavior); |
| |
| @override |
| Path get _defaultClip => Path()..addRect(Offset.zero & size); |
| |
| @override |
| bool hitTest(BoxHitTestResult 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(); |
| layer = context.pushClipPath( |
| needsCompositing, |
| offset, |
| Offset.zero & size, |
| _clip, |
| super.paint, |
| clipBehavior: clipBehavior, |
| oldLayer: layer as ClipPathLayer, |
| ); |
| } else { |
| layer = null; |
| } |
| } |
| |
| @override |
| void debugPaintSize(PaintingContext context, Offset offset) { |
| assert(() { |
| if (child != null) { |
| super.debugPaintSize(context, offset); |
| context.canvas.drawPath(_clip.shift(offset), _debugPaint); |
| _debugText.paint(context.canvas, offset); |
| } |
| return true; |
| }()); |
| } |
| } |
| |
| /// A physical model layer casts a shadow based on its [elevation]. |
| /// |
| /// The concrete implementations [RenderPhysicalModel] and [RenderPhysicalShape] |
| /// determine the actual shape of the physical model. |
| abstract class _RenderPhysicalModelBase<T> extends _RenderCustomClip<T> { |
| /// The [shape], [elevation], [color], and [shadowColor] must not be null. |
| /// Additionally, the [elevation] must be non-negative. |
| _RenderPhysicalModelBase({ |
| @required RenderBox child, |
| @required double elevation, |
| @required Color color, |
| @required Color shadowColor, |
| Clip clipBehavior = Clip.none, |
| CustomClipper<T> clipper, |
| }) : assert(elevation != null && elevation >= 0.0), |
| assert(color != null), |
| assert(shadowColor != null), |
| assert(clipBehavior != null), |
| _elevation = elevation, |
| _color = color, |
| _shadowColor = shadowColor, |
| super(child: child, clipBehavior: clipBehavior, clipper: clipper); |
| |
| /// The z-coordinate relative to the parent at which to place this material. |
| /// |
| /// The value is non-negative. |
| /// |
| /// If [debugDisableShadows] is set, this value is ignored and no shadow is |
| /// drawn (an outline is rendered instead). |
| double get elevation => _elevation; |
| double _elevation; |
| set elevation(double value) { |
| assert(value != null && value >= 0.0); |
| if (elevation == value) |
| return; |
| final bool didNeedCompositing = alwaysNeedsCompositing; |
| _elevation = value; |
| if (didNeedCompositing != alwaysNeedsCompositing) |
| markNeedsCompositingBitsUpdate(); |
| markNeedsPaint(); |
| } |
| |
| /// The shadow color. |
| Color get shadowColor => _shadowColor; |
| Color _shadowColor; |
| set shadowColor(Color value) { |
| assert(value != null); |
| if (shadowColor == value) |
| return; |
| _shadowColor = value; |
| markNeedsPaint(); |
| } |
| |
| /// The background color. |
| Color get color => _color; |
| Color _color; |
| set color(Color value) { |
| assert(value != null); |
| if (color == value) |
| return; |
| _color = value; |
| markNeedsPaint(); |
| } |
| |
| @override |
| bool get alwaysNeedsCompositing => true; |
| |
| @override |
| void describeSemanticsConfiguration(SemanticsConfiguration config) { |
| super.describeSemanticsConfiguration(config); |
| config.elevation = elevation; |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder description) { |
| super.debugFillProperties(description); |
| description.add(DoubleProperty('elevation', elevation)); |
| description.add(ColorProperty('color', color)); |
| description.add(ColorProperty('shadowColor', color)); |
| } |
| } |
| |
| /// Creates a physical model layer that clips its child to a rounded |
| /// rectangle. |
| /// |
| /// A physical model layer casts a shadow based on its [elevation]. |
| class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> { |
| /// Creates a rounded-rectangular clip. |
| /// |
| /// The [color] is required. |
| /// |
| /// The [shape], [elevation], [color], [clipBehavior], and [shadowColor] |
| /// arguments must not be null. Additionally, the [elevation] must be |
| /// non-negative. |
| RenderPhysicalModel({ |
| RenderBox child, |
| BoxShape shape = BoxShape.rectangle, |
| Clip clipBehavior = Clip.none, |
| BorderRadius borderRadius, |
| double elevation = 0.0, |
| @required Color color, |
| Color shadowColor = const Color(0xFF000000), |
| }) : assert(shape != null), |
| assert(clipBehavior != null), |
| assert(elevation != null && elevation >= 0.0), |
| assert(color != null), |
| assert(shadowColor != null), |
| _shape = shape, |
| _borderRadius = borderRadius, |
| super( |
| clipBehavior: clipBehavior, |
| child: child, |
| elevation: elevation, |
| color: color, |
| shadowColor: shadowColor, |
| ); |
| |
| @override |
| PhysicalModelLayer get layer => super.layer as PhysicalModelLayer; |
| |
| /// The shape of the layer. |
| /// |
| /// Defaults to [BoxShape.rectangle]. The [borderRadius] affects the corners |
| /// of the rectangle. |
| BoxShape get shape => _shape; |
| BoxShape _shape; |
| set shape(BoxShape value) { |
| assert(value != null); |
| if (shape == value) |
| return; |
| _shape = value; |
| _markNeedsClip(); |
| } |
| |
| /// The border radius of the rounded corners. |
| /// |
| /// Values are clamped so that horizontal and vertical radii sums do not |
| /// exceed width/height. |
| /// |
| /// This property is ignored if the [shape] is not [BoxShape.rectangle]. |
| /// |
| /// The value null is treated like [BorderRadius.zero]. |
| BorderRadius get borderRadius => _borderRadius; |
| BorderRadius _borderRadius; |
| set borderRadius(BorderRadius value) { |
| if (borderRadius == value) |
| return; |
| _borderRadius = value; |
| _markNeedsClip(); |
| } |
| |
| @override |
| RRect get _defaultClip { |
| assert(hasSize); |
| assert(_shape != null); |
| switch (_shape) { |
| case BoxShape.rectangle: |
| return (borderRadius ?? BorderRadius.zero).toRRect(Offset.zero & size); |
| case BoxShape.circle: |
| final Rect rect = Offset.zero & size; |
| return RRect.fromRectXY(rect, rect.width / 2, rect.height / 2); |
| } |
| return null; |
| } |
| |
| @override |
| bool hitTest(BoxHitTestResult result, { Offset position }) { |
| if (_clipper != null) { |
| _updateClip(); |
| assert(_clip != null); |
| if (!_clip.contains(position)) |
| return false; |
| } |
| return super.hitTest(result, position: position); |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| if (child != null) { |
| _updateClip(); |
| final RRect offsetRRect = _clip.shift(offset); |
| final Rect offsetBounds = offsetRRect.outerRect; |
| final Path offsetRRectAsPath = Path()..addRRect(offsetRRect); |
| bool paintShadows = true; |
| assert(() { |
| if (debugDisableShadows) { |
| if (elevation > 0.0) { |
| context.canvas.drawRRect( |
| offsetRRect, |
| Paint() |
| ..color = shadowColor |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = elevation * 2.0, |
| ); |
| } |
| paintShadows = false; |
| } |
| return true; |
| }()); |
| layer ??= PhysicalModelLayer(); |
| layer |
| ..clipPath = offsetRRectAsPath |
| ..clipBehavior = clipBehavior |
| ..elevation = paintShadows ? elevation : 0.0 |
| ..color = color |
| ..shadowColor = shadowColor; |
| context.pushLayer(layer, super.paint, offset, childPaintBounds: offsetBounds); |
| assert(() { |
| layer.debugCreator = debugCreator; |
| return true; |
| }()); |
| } else { |
| layer = null; |
| } |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder description) { |
| super.debugFillProperties(description); |
| description.add(DiagnosticsProperty<BoxShape>('shape', shape)); |
| description.add(DiagnosticsProperty<BorderRadius>('borderRadius', borderRadius)); |
| } |
| } |
| |
| /// Creates a physical shape layer that clips its child to a [Path]. |
| /// |
| /// A physical shape layer casts a shadow based on its [elevation]. |
| /// |
| /// See also: |
| /// |
| /// * [RenderPhysicalModel], which is optimized for rounded rectangles and |
| /// circles. |
| class RenderPhysicalShape extends _RenderPhysicalModelBase<Path> { |
| /// Creates an arbitrary shape clip. |
| /// |
| /// The [color] and [shape] parameters are required. |
| /// |
| /// The [clipper], [elevation], [color] and [shadowColor] must not be null. |
| /// Additionally, the [elevation] must be non-negative. |
| RenderPhysicalShape({ |
| RenderBox child, |
| @required CustomClipper<Path> clipper, |
| Clip clipBehavior = Clip.none, |
| double elevation = 0.0, |
| @required Color color, |
| Color shadowColor = const Color(0xFF000000), |
| }) : assert(clipper != null), |
| assert(elevation != null && elevation >= 0.0), |
| assert(color != null), |
| assert(shadowColor != null), |
| super( |
| child: child, |
| elevation: elevation, |
| color: color, |
| shadowColor: shadowColor, |
| clipper: clipper, |
| clipBehavior: clipBehavior, |
| ); |
| |
| @override |
| PhysicalModelLayer get layer => super.layer as PhysicalModelLayer; |
| |
| @override |
| Path get _defaultClip => Path()..addRect(Offset.zero & size); |
| |
| @override |
| bool hitTest(BoxHitTestResult result, { Offset position }) { |
| if (_clipper != null) { |
| _updateClip(); |
| assert(_clip != null); |
| if (!_clip.contains(position)) |
| return false; |
| } |
| return super.hitTest(result, position: position); |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| if (child != null) { |
| _updateClip(); |
| final Rect offsetBounds = offset & size; |
| final Path offsetPath = _clip.shift(offset); |
| bool paintShadows = true; |
| assert(() { |
| if (debugDisableShadows) { |
| if (elevation > 0.0) { |
| context.canvas.drawPath( |
| offsetPath, |
| Paint() |
| ..color = shadowColor |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = elevation * 2.0, |
| ); |
| } |
| paintShadows = false; |
| } |
| return true; |
| }()); |
| layer ??= PhysicalModelLayer(); |
| layer |
| ..clipPath = offsetPath |
| ..clipBehavior = clipBehavior |
| ..elevation = paintShadows ? elevation : 0.0 |
| ..color = color |
| ..shadowColor = shadowColor; |
| context.pushLayer(layer, super.paint, offset, childPaintBounds: offsetBounds); |
| assert(() { |
| layer.debugCreator = debugCreator; |
| return true; |
| }()); |
| } else { |
| layer = null; |
| } |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder description) { |
| super.debugFillProperties(description); |
| description.add(DiagnosticsProperty<CustomClipper<Path>>('clipper', clipper)); |
| } |
| } |
| |
| /// Where to paint a box decoration. |
| enum DecorationPosition { |
| /// Paint the box decoration behind the children. |
| background, |
| |
| /// Paint the box decoration in front of the children. |
| foreground, |
| } |
| |
| /// Paints a [Decoration] either before or after its child paints. |
| class RenderDecoratedBox extends RenderProxyBox { |
| /// Creates a decorated box. |
| /// |
| /// The [decoration], [position], and [configuration] arguments must not be |
| /// null. By default the decoration paints behind the child. |
| /// |
| /// The [ImageConfiguration] will be passed to the decoration (with the size |
| /// filled in) to let it resolve images. |
| RenderDecoratedBox({ |
| @required Decoration decoration, |
| DecorationPosition position = DecorationPosition.background, |
| ImageConfiguration configuration = ImageConfiguration.empty, |
| RenderBox child, |
| }) : assert(decoration != null), |
| assert(position != null), |
| assert(configuration != null), |
| _decoration = decoration, |
| _position = position, |
| _configuration = configuration, |
| super(child); |
| |
| BoxPainter _painter; |
| |
| /// What decoration to paint. |
| /// |
| /// Commonly a [BoxDecoration]. |
| Decoration get decoration => _decoration; |
| Decoration _decoration; |
| set decoration(Decoration value) { |
| assert(value != null); |
| if (value == _decoration) |
| return; |
| _painter?.dispose(); |
| _painter = null; |
| _decoration = value; |
| markNeedsPaint(); |
| } |
| |
| /// Whether to paint the box decoration behind or in front of the child. |
| DecorationPosition get position => _position; |
| DecorationPosition _position; |
| set position(DecorationPosition value) { |
| assert(value != null); |
| if (value == _position) |
| return; |
| _position = value; |
| markNeedsPaint(); |
| } |
| |
| /// The settings to pass to the decoration when painting, so that it can |
| /// resolve images appropriately. See [ImageProvider.resolve] and |
| /// [BoxPainter.paint]. |
| /// |
| /// The [ImageConfiguration.textDirection] field is also used by |
| /// direction-sensitive [Decoration]s for painting and hit-testing. |
| ImageConfiguration get configuration => _configuration; |
| ImageConfiguration _configuration; |
| set configuration(ImageConfiguration value) { |
| assert(value != null); |
| if (value == _configuration) |
| return; |
| _configuration = value; |
| markNeedsPaint(); |
| } |
| |
| @override |
| void detach() { |
| _painter?.dispose(); |
| _painter = null; |
| super.detach(); |
| // Since we're disposing of our painter, we won't receive change |
| // notifications. We mark ourselves as needing paint so that we will |
| // resubscribe to change notifications. If we didn't do this, then, for |
| // example, animated GIFs would stop animating when a DecoratedBox gets |
| // moved around the tree due to GlobalKey reparenting. |
| markNeedsPaint(); |
| } |
| |
| @override |
| bool hitTestSelf(Offset position) { |
| return _decoration.hitTest(size, position, textDirection: configuration.textDirection); |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| assert(size.width != null); |
| assert(size.height != null); |
| _painter ??= _decoration.createBoxPainter(markNeedsPaint); |
| final ImageConfiguration filledConfiguration = configuration.copyWith(size: size); |
| if (position == DecorationPosition.background) { |
| int debugSaveCount; |
| assert(() { |
| debugSaveCount = context.canvas.getSaveCount(); |
| return true; |
| }()); |
| _painter.paint(context.canvas, offset, filledConfiguration); |
| assert(() { |
| if (debugSaveCount != context.canvas.getSaveCount()) { |
| throw FlutterError.fromParts(<DiagnosticsNode>[ |
| ErrorSummary('${_decoration.runtimeType} painter had mismatching save and restore calls.'), |
| ErrorDescription( |
| '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().' |
| ), |
| DiagnosticsProperty<Decoration>('The decoration was', decoration, style: DiagnosticsTreeStyle.errorProperty), |
| DiagnosticsProperty<BoxPainter>('The painter was', _painter, style: DiagnosticsTreeStyle.errorProperty), |
| ]); |
| } |
| return true; |
| }()); |
| if (decoration.isComplex) |
| context.setIsComplexHint(); |
| } |
| super.paint(context, offset); |
| if (position == DecorationPosition.foreground) { |
| _painter.paint(context.canvas, offset, filledConfiguration); |
| if (decoration.isComplex) |
| context.setIsComplexHint(); |
| } |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(_decoration.toDiagnosticsNode(name: 'decoration')); |
| properties.add(DiagnosticsProperty<ImageConfiguration>('configuration', configuration)); |
| } |
| } |
| |
| /// Applies a transformation before painting its child. |
| class RenderTransform extends RenderProxyBox { |
| /// Creates a render object that transforms its child. |
| /// |
| /// The [transform] argument must not be null. |
| RenderTransform({ |
| @required Matrix4 transform, |
| Offset origin, |
| AlignmentGeometry alignment, |
| TextDirection textDirection, |
| this.transformHitTests = true, |
| RenderBox child, |
| }) : assert(transform != null), |
| super(child) { |
| this.transform = transform; |
| this.alignment = alignment; |
| this.textDirection = textDirection; |
| this.origin = origin; |
| } |
| |
| /// The origin of the coordinate system (relative to the upper left corner of |
| /// this render object) in which to apply the matrix. |
| /// |
| /// Setting an origin is equivalent to conjugating the transform matrix by a |
| /// translation. This property is provided just for convenience. |
| Offset get origin => _origin; |
| Offset _origin; |
| set origin(Offset value) { |
| if (_origin == value) |
| return; |
| _origin = value; |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| /// The alignment of the origin, relative to the size of the box. |
| /// |
| /// This is equivalent to setting an origin based on the size of the box. |
| /// If it is specified at the same time as an offset, both are applied. |
| /// |
| /// An [AlignmentDirectional.start] value is the same as an [Alignment] |
| /// whose [Alignment.x] value is `-1.0` if [textDirection] is |
| /// [TextDirection.ltr], and `1.0` if [textDirection] is [TextDirection.rtl]. |
| /// Similarly [AlignmentDirectional.end] is the same as an [Alignment] |
| /// whose [Alignment.x] value is `1.0` if [textDirection] is |
| /// [TextDirection.ltr], and `-1.0` if [textDirection] is [TextDirection.rtl]. |
| AlignmentGeometry get alignment => _alignment; |
| AlignmentGeometry _alignment; |
| set alignment(AlignmentGeometry value) { |
| if (_alignment == value) |
| return; |
| _alignment = value; |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| /// The text direction with which to resolve [alignment]. |
| /// |
| /// This may be changed to null, but only after [alignment] has been changed |
| /// to a value that does not depend on the direction. |
| TextDirection get textDirection => _textDirection; |
| TextDirection _textDirection; |
| set textDirection(TextDirection value) { |
| if (_textDirection == value) |
| return; |
| _textDirection = value; |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| /// When set to true, hit tests are performed based on the position of the |
| /// child as it is painted. When set to false, hit tests are performed |
| /// ignoring the transformation. |
| /// |
| /// [applyPaintTransform], and therefore [localToGlobal] and [globalToLocal], |
| /// always honor the transformation, regardless of the value of this property. |
| bool transformHitTests; |
| |
| // Note the lack of a getter for transform because Matrix4 is not immutable |
| Matrix4 _transform; |
| |
| /// The matrix to transform the child by during painting. |
| set transform(Matrix4 value) { |
| assert(value != null); |
| if (_transform == value) |
| return; |
| _transform = Matrix4.copy(value); |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| /// Sets the transform to the identity matrix. |
| void setIdentity() { |
| _transform.setIdentity(); |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| /// Concatenates a rotation about the x axis into the transform. |
| void rotateX(double radians) { |
| _transform.rotateX(radians); |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| /// Concatenates a rotation about the y axis into the transform. |
| void rotateY(double radians) { |
| _transform.rotateY(radians); |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| /// Concatenates a rotation about the z axis into the transform. |
| void rotateZ(double radians) { |
| _transform.rotateZ(radians); |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| /// Concatenates a translation by (x, y, z) into the transform. |
| void translate(double x, [ double y = 0.0, double z = 0.0 ]) { |
| _transform.translate(x, y, z); |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| /// Concatenates a scale into the transform. |
| void scale(double x, [ double y, double z ]) { |
| _transform.scale(x, y, z); |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| Matrix4 get _effectiveTransform { |
| final Alignment resolvedAlignment = alignment?.resolve(textDirection); |
| if (_origin == null && resolvedAlignment == null) |
| return _transform; |
| final Matrix4 result = 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(BoxHitTestResult result, { Offset position }) { |
| // RenderTransform objects don't check if they are |
| // themselves hit, because it's confusing to think about |
| // how the untransformed size and the child's transformed |
| // position interact. |
| return hitTestChildren(result, position: position); |
| } |
| |
| @override |
| bool hitTestChildren(BoxHitTestResult result, { Offset position }) { |
| assert(!transformHitTests || _effectiveTransform != null); |
| return result.addWithPaintTransform( |
| transform: transformHitTests ? _effectiveTransform : null, |
| position: position, |
| hitTest: (BoxHitTestResult result, Offset position) { |
| return super.hitTestChildren(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) { |
| layer = context.pushTransform( |
| needsCompositing, |
| offset, |
| transform, |
| super.paint, |
| oldLayer: layer as TransformLayer, |
| ); |
| } else { |
| super.paint(context, offset + childOffset); |
| layer = null; |
| } |
| } |
| } |
| |
| @override |
| void applyPaintTransform(RenderBox child, Matrix4 transform) { |
| transform.multiply(_effectiveTransform); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(TransformProperty('transform matrix', _transform)); |
| properties.add(DiagnosticsProperty<Offset>('origin', origin)); |
| properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment)); |
| properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); |
| properties.add(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 = Matrix4.identity(); |
| } else { |
| _resolve(); |
| final Size childSize = child.size; |
| final FittedSizes sizes = applyBoxFit(_fit, childSize, size); |
| final double scaleX = sizes.destination.width / sizes.source.width; |
| final double scaleY = sizes.destination.height / sizes.source.height; |
| final Rect sourceRect = _resolvedAlignment.inscribe(sizes.source, Offset.zero & childSize); |
| final Rect destinationRect = _resolvedAlignment.inscribe(sizes.destination, Offset.zero & size); |
| _hasVisualOverflow = sourceRect.width < childSize.width || sourceRect.height < childSize.height; |
| assert(scaleX.isFinite && scaleY.isFinite); |
| _transform = Matrix4.translationValues(destinationRect.left, destinationRect.top, 0.0) |
| ..scale(scaleX, scaleY, 1.0) |
| ..translate(-sourceRect.left, -sourceRect.top); |
| assert(_transform.storage.every((double value) => value.isFinite)); |
| } |
| } |
| |
| TransformLayer _paintChildWithTransform(PaintingContext context, Offset offset) { |
| final Offset childOffset = MatrixUtils.getAsTranslation(_transform); |
| if (childOffset == null) |
| return context.pushTransform(needsCompositing, offset, _transform, super.paint, |
| oldLayer: layer is TransformLayer ? layer as TransformLayer : null); |
| else |
| super.paint(context, offset + childOffset); |
| return null; |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| if (size.isEmpty || child.size.isEmpty) |
| return; |
| _updatePaintData(); |
| if (child != null) { |
| if (_hasVisualOverflow) |
| layer = context.pushClipRect(needsCompositing, offset, Offset.zero & size, _paintChildWithTransform, |
| oldLayer: layer is ClipRectLayer ? layer as ClipRectLayer : null); |
| else |
| layer = _paintChildWithTransform(context, offset); |
| } |
| } |
| |
| @override |
| bool hitTestChildren(BoxHitTestResult result, { Offset position }) { |
| if (size.isEmpty || child?.size?.isEmpty == true) |
| return false; |
| _updatePaintData(); |
| return result.addWithPaintTransform( |
| transform: _transform, |
| position: position, |
| hitTest: (BoxHitTestResult result, Offset position) { |
| return super.hitTestChildren(result, position: position); |
| }, |
| ); |
| } |
| |
| @override |
| void applyPaintTransform(RenderBox child, Matrix4 transform) { |
| if (size.isEmpty || child.size.isEmpty) { |
| transform.setZero(); |
| } else { |
| _updatePaintData(); |
| transform.multiply(_transform); |
| } |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(EnumProperty<BoxFit>('fit', fit)); |
| properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment)); |
| properties.add(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(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| @override |
| bool hitTest(BoxHitTestResult result, { Offset position }) { |
| // RenderFractionalTranslation objects don't check if they are |
| // themselves hit, because it's confusing to think about |
| // how the untransformed size and the child's transformed |
| // position interact. |
| return hitTestChildren(result, position: position); |
| } |
| |
| /// 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 hitTestChildren(BoxHitTestResult result, { Offset position }) { |
| assert(!debugNeedsLayout); |
| return result.addWithPaintOffset( |
| offset: transformHitTests |
| ? Offset(translation.dx * size.width, translation.dy * size.height) |
| : null, |
| position: position, |
| hitTest: (BoxHitTestResult result, Offset position) { |
| return super.hitTestChildren(result, position: position); |
| }, |
| ); |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| assert(!debugNeedsLayout); |
| if (child != null) { |
| super.paint(context, Offset( |
| offset.dx + translation.dx * size.width, |
| offset.dy + translation.dy * size.height, |
| )); |
| } |
| } |
| |
| @override |
| void applyPaintTransform(RenderBox child, Matrix4 transform) { |
| transform.translate( |
| translation.dx * size.width, |
| translation.dy * size.height, |
| ); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<Offset>('translation', translation)); |
| properties.add(DiagnosticsProperty<bool>('transformHitTests', transformHitTests)); |
| } |
| } |
| |
| /// Signature for listening to [PointerDownEvent] events. |
| /// |
| /// Used by [Listener] and [RenderPointerListener]. |
| typedef PointerDownEventListener = void Function(PointerDownEvent event); |
| |
| /// Signature for listening to [PointerMoveEvent] events. |
| /// |
| /// Used by [Listener] and [RenderPointerListener]. |
| typedef PointerMoveEventListener = void Function(PointerMoveEvent event); |
| |
| /// Signature for listening to [PointerUpEvent] events. |
| /// |
| /// Used by [Listener] and [RenderPointerListener]. |
| typedef PointerUpEventListener = void Function(PointerUpEvent event); |
| |
| /// Signature for listening to [PointerCancelEvent] events. |
| /// |
| /// Used by [Listener] and [RenderPointerListener]. |
| typedef PointerCancelEventListener = void Function(PointerCancelEvent event); |
| |
| /// Signature for listening to [PointerSignalEvent] events. |
| /// |
| /// Used by [Listener] and [RenderPointerListener]. |
| typedef PointerSignalEventListener = void Function(PointerSignalEvent event); |
| |
| /// Calls callbacks in response to common pointer events. |
| /// |
| /// It responds to events that can construct gestures, such as when the |
| /// pointer is pressed, moved, then released or canceled. |
| /// |
| /// It does not respond to events that are exclusive to mouse, such as when the |
| /// mouse enters, exits or hovers a region without pressing any buttons. For |
| /// these events, use [RenderMouseRegion]. |
| /// |
| /// 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 pointer events to callbacks. |
| /// |
| /// The [behavior] argument defaults to [HitTestBehavior.deferToChild]. |
| RenderPointerListener({ |
| this.onPointerDown, |
| this.onPointerMove, |
| this.onPointerUp, |
| this.onPointerCancel, |
| this.onPointerSignal, |
| HitTestBehavior behavior = HitTestBehavior.deferToChild, |
| RenderBox child, |
| }) : super(behavior: behavior, child: child); |
| |
| /// Called when a pointer comes into contact with the screen (for touch |
| /// pointers), or has its button pressed (for mouse pointers) at this widget's |
| /// location. |
| 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; |
| |
| /// Called when a pointer signal occurs over this object. |
| PointerSignalEventListener onPointerSignal; |
| |
| @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); |
| if (onPointerSignal != null && event is PointerSignalEvent) |
| return onPointerSignal(event); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(FlagsSummary<Function>( |
| 'listeners', |
| <String, Function>{ |
| 'down': onPointerDown, |
| 'move': onPointerMove, |
| 'up': onPointerUp, |
| 'cancel': onPointerCancel, |
| 'signal': onPointerSignal, |
| }, |
| ifEmpty: '<none>', |
| )); |
| } |
| } |
| |
| /// Calls callbacks in response to pointer events that are exclusive to mice. |
| /// |
| /// It responds to events that are related to hovering, i.e. when the mouse |
| /// enters, exits (with or without pressing buttons), or moves over a region |
| /// without pressing buttons. |
| /// |
| /// It does not respond to common events that construct gestures, such as when |
| /// the pointer is pressed, moved, then released or canceled. For these events, |
| /// use [RenderPointerListener]. |
| /// |
| /// If it has a child, it defers to the child for sizing behavior. |
| /// |
| /// If it does not have a child, it grows to fit the parent-provided constraints. |
| /// |
| /// See also: |
| /// |
| /// * [MouseRegion], a widget that listens to hover events using |
| /// [RenderMouseRegion]. |
| class RenderMouseRegion extends RenderProxyBox { |
| /// Creates a render object that forwards pointer events to callbacks. |
| /// |
| /// All parameters are optional. By default this method creates an opaque |
| /// mouse region with no callbacks. |
| RenderMouseRegion({ |
| PointerEnterEventListener onEnter, |
| PointerHoverEventListener onHover, |
| PointerExitEventListener onExit, |
| bool opaque = true, |
| RenderBox child, |
| }) : assert(opaque != null), |
| _onEnter = onEnter, |
| _onHover = onHover, |
| _onExit = onExit, |
| _opaque = opaque, |
| _annotationIsActive = false, |
| super(child) { |
| _hoverAnnotation = MouseTrackerAnnotation( |
| onEnter: _handleEnter, |
| onHover: _handleHover, |
| onExit: _handleExit, |
| ); |
| } |
| |
| /// Whether this object should prevent [RenderMouseRegion]s visually behind it |
| /// from detecting the pointer, thus affecting how their [onHover], [onEnter], |
| /// and [onExit] behave. |
| /// |
| /// If [opaque] is true, this object will absorb the mouse pointer and |
| /// prevent this object's siblings (or any other objects that are not |
| /// ancestors or descendants of this object) from detecting the mouse |
| /// pointer even when the pointer is within their areas. |
| /// |
| /// If [opaque] is false, this object will not affect how [RenderMouseRegion]s |
| /// behind it behave, which will detect the mouse pointer as long as the |
| /// pointer is within their areas. |
| /// |
| /// This defaults to true. |
| bool get opaque => _opaque; |
| bool _opaque; |
| set opaque(bool value) { |
| if (_opaque != value) { |
| _opaque = value; |
| _markPropertyUpdated(mustRepaint: true); |
| } |
| } |
| |
| /// Called when a mouse pointer starts being contained by the region (with or |
| /// without buttons pressed) for any reason. |
| /// |
| /// This callback is always matched by a later [onExit]. |
| /// |
| /// See also: |
| /// |
| /// * [MouseRegion.onEnter], which uses this callback. |
| PointerEnterEventListener get onEnter => _onEnter; |
| set onEnter(PointerEnterEventListener value) { |
| if (_onEnter != value) { |
| _onEnter = value; |
| _markPropertyUpdated(mustRepaint: false); |
| } |
| } |
| PointerEnterEventListener _onEnter; |
| void _handleEnter(PointerEnterEvent event) { |
| if (_onEnter != null) |
| _onEnter(event); |
| } |
| |
| /// Called when a pointer changes position without buttons pressed and the end |
| /// position is within the region. |
| PointerHoverEventListener get onHover => _onHover; |
| set onHover(PointerHoverEventListener value) { |
| if (_onHover != value) { |
| _onHover = value; |
| _markPropertyUpdated(mustRepaint: false); |
| } |
| } |
| PointerHoverEventListener _onHover; |
| void _handleHover(PointerHoverEvent event) { |
| if (_onHover != null) |
| _onHover(event); |
| } |
| |
| /// Called when a pointer is no longer contained by the region (with or |
| /// without buttons pressed) for any reason. |
| /// |
| /// This callback is always matched by an earlier [onEnter]. |
| /// |
| /// See also: |
| /// |
| /// * [MouseRegion.onExit], which uses this callback, but is not triggered in |
| /// certain cases and does not always match its earier [MouseRegion.onEnter]. |
| PointerExitEventListener get onExit => _onExit; |
| set onExit(PointerExitEventListener value) { |
| if (_onExit != value) { |
| _onExit = value; |
| _markPropertyUpdated(mustRepaint: false); |
| } |
| } |
| PointerExitEventListener _onExit; |
| void _handleExit(PointerExitEvent event) { |
| if (_onExit != null) |
| _onExit(event); |
| } |
| |
| // Object used for annotation of the layer used for hover hit detection. |
| MouseTrackerAnnotation _hoverAnnotation; |
| |
| /// Object used for annotation of the layer used for hover hit detection. |
| /// |
| /// This is only public to allow for testing of Listener widgets. Do not call |
| /// in other contexts. |
| @visibleForTesting |
| MouseTrackerAnnotation get hoverAnnotation => _hoverAnnotation; |
| |
| // Call this method when a property has changed and might affect the |
| // `_annotationIsActive` bit. |
| // |
| // If `mustRepaint` is false, this method does NOT call `markNeedsPaint` |
| // unless the `_annotationIsActive` bit is changed. If there is a property |
| // that needs updating while `_annotationIsActive` stays true, make |
| // `mustRepaint` true. |
| // |
| // This method must not be called during `paint`. |
| void _markPropertyUpdated({@required bool mustRepaint}) { |
| assert(owner == null || !owner.debugDoingPaint); |
| final bool newAnnotationIsActive = ( |
| _onEnter != null || |
| _onHover != null || |
| _onExit != null || |
| opaque |
| ) && RendererBinding.instance.mouseTracker.mouseIsConnected; |
| _setAnnotationIsActive(newAnnotationIsActive); |
| if (mustRepaint) |
| markNeedsPaint(); |
| } |
| |
| void _setAnnotationIsActive(bool value) { |
| final bool annotationWasActive = _annotationIsActive; |
| _annotationIsActive = value; |
| if (annotationWasActive != value) { |
| markNeedsPaint(); |
| markNeedsCompositingBitsUpdate(); |
| } |
| } |
| |
| void _handleUpdatedMouseIsConnected() { |
| _markPropertyUpdated(mustRepaint: false); |
| } |
| |
| @override |
| void attach(PipelineOwner owner) { |
| super.attach(owner); |
| // Add a listener to listen for changes in mouseIsConnected. |
| RendererBinding.instance.mouseTracker.addListener(_handleUpdatedMouseIsConnected); |
| _markPropertyUpdated(mustRepaint: false); |
| } |
| |
| @override |
| void detach() { |
| RendererBinding.instance.mouseTracker.removeListener(_handleUpdatedMouseIsConnected); |
| super.detach(); |
| } |
| |
| bool _annotationIsActive; |
| |
| @override |
| bool get needsCompositing => super.needsCompositing || _annotationIsActive; |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| if (_annotationIsActive) { |
| // Annotated region layers are not retained because they do not create engine layers. |
| final AnnotatedRegionLayer<MouseTrackerAnnotation> layer = AnnotatedRegionLayer<MouseTrackerAnnotation>( |
| _hoverAnnotation, |
| size: size, |
| offset: offset, |
| opaque: opaque, |
| ); |
| context.pushLayer(layer, super.paint, offset); |
| } else { |
| super.paint(context, offset); |
| } |
| } |
| |
| @override |
| void performResize() { |
| size = constraints.biggest; |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(FlagsSummary<Function>( |
| 'listeners', |
| <String, Function>{ |
| 'enter': onEnter, |
| 'hover': onHover, |
| 'exit': onExit, |
| }, |
| ifEmpty: '<none>', |
| )); |
| properties.add(DiagnosticsProperty<bool>('opaque', opaque, defaultValue: true)); |
| } |
| } |
| |
| /// Creates a separate display list for its child. |
| /// |
| /// This render object creates a separate display list for its child, which |
| /// can improve performance if the subtree repaints at different times than |
| /// the surrounding parts of the tree. Specifically, when the child does not |
| /// repaint but its parent does, we can re-use the display list we recorded |
| /// previously. Similarly, when the child repaints but the surround tree does |
| /// not, we can re-record its display list without re-recording the display list |
| /// for the surround tree. |
| /// |
| /// In some cases, it is necessary to place _two_ (or more) repaint boundaries |
| /// to get a useful effect. Consider, for example, an e-mail application that |
| /// shows an unread count and a list of e-mails. Whenever a new e-mail comes in, |
| /// the list would update, but so would the unread count. If only one of these |
| /// two parts of the application was behind a repaint boundary, the entire |
| /// application would repaint each time. On the other hand, if both were behind |
| /// a repaint boundary, a new e-mail would only change those two parts of the |
| /// application and the rest of the application would not repaint. |
| /// |
| /// To tell if a particular RenderRepaintBoundary is useful, run your |
| /// application in checked mode, interacting with it in typical ways, and then |
| /// call [debugDumpRenderTree]. Each RenderRepaintBoundary will include the |
| /// ratio of cases where the repaint boundary was useful vs the cases where it |
| /// was not. These counts can also be inspected programmatically using |
| /// [debugAsymmetricPaintCount] and [debugSymmetricPaintCount] respectively. |
| class RenderRepaintBoundary extends RenderProxyBox { |
| /// Creates a repaint boundary around [child]. |
| RenderRepaintBoundary({ RenderBox child }) : super(child); |
| |
| @override |
| bool get isRepaintBoundary => true; |
| |
| /// Capture an image of the current state of this render object and its |
| /// children. |
| /// |
| /// The returned [ui.Image] has uncompressed raw RGBA bytes in the dimensions |
| /// of the render object, multiplied by the [pixelRatio]. |
| /// |
| /// To use [toImage], the render object must have gone through the paint phase |
| /// (i.e. [debugNeedsPaint] must be false). |
| /// |
| /// The [pixelRatio] describes the scale between the logical pixels and the |
| /// size of the output image. It is independent of the |
| /// [window.devicePixelRatio] for the device, so specifying 1.0 (the default) |
| /// will give you a 1:1 mapping between logical pixels and the output pixels |
| /// in the image. |
| /// |
| /// {@tool snippet} |
| /// |
| /// The following is an example of how to go from a `GlobalKey` on a |
| /// `RepaintBoundary` to a PNG: |
| /// |
| /// ```dart |
| /// class PngHome extends StatefulWidget { |
| /// PngHome({Key key}) : super(key: key); |
| /// |
| /// @override |
| /// _PngHomeState createState() => _PngHomeState(); |
| /// } |
| /// |
| /// class _PngHomeState extends State<PngHome> { |
| /// GlobalKey globalKey = GlobalKey(); |
| /// |
| /// Future<void> _capturePng() async { |
| /// RenderRepaintBoundary boundary = globalKey.currentContext.findRenderObject(); |
| /// ui.Image image = await boundary.toImage(); |
| /// ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png); |
| /// Uint8List pngBytes = byteData.buffer.asUint8List(); |
| /// print(pngBytes); |
| /// } |
| /// |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// return RepaintBoundary( |
| /// key: globalKey, |
| /// child: Center( |
| /// child: FlatButton( |
| /// child: Text('Hello World', textDirection: TextDirection.ltr), |
| /// onPressed: _capturePng, |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [OffsetLayer.toImage] for a similar API at the layer level. |
| /// * [dart:ui.Scene.toImage] for more information about the image returned. |
| Future<ui.Image> toImage({ double pixelRatio = 1.0 }) { |
| assert(!debugNeedsPaint); |
| final OffsetLayer offsetLayer = layer as OffsetLayer; |
| return offsetLayer.toImage(Offset.zero & size, pixelRatio: pixelRatio); |
| } |
| |
| |
| /// The number of times that this render object repainted at the same time as |
| /// its parent. Repaint boundaries are only useful when the parent and child |
| /// paint at different times. When both paint at the same time, the repaint |
| /// boundary is redundant, and may be actually making performance worse. |
| /// |
| /// Only valid when asserts are enabled. In release builds, always returns |
| /// zero. |
| /// |
| /// Can be reset using [debugResetMetrics]. See [debugAsymmetricPaintCount] |
| /// for the corresponding count of times where only the parent or only the |
| /// child painted. |
| int get debugSymmetricPaintCount => _debugSymmetricPaintCount; |
| int _debugSymmetricPaintCount = 0; |
| |
| /// The number of times that either this render object repainted without the |
| /// parent being painted, or the parent repainted without this object being |
| /// painted. When a repaint boundary is used at a seam in the render tree |
| /// where the parent tends to repaint at entirely different times than the |
| /// child, it can improve performance by reducing the number of paint |
| /// operations that have to be recorded each frame. |
| /// |
| /// Only valid when asserts are enabled. In release builds, always returns |
| /// zero. |
| /// |
| /// Can be reset using [debugResetMetrics]. See [debugSymmetricPaintCount] for |
| /// the corresponding count of times where both the parent and the child |
| /// painted together. |
| int get debugAsymmetricPaintCount => _debugAsymmetricPaintCount; |
| int _debugAsymmetricPaintCount = 0; |
| |
| /// Resets the [debugSymmetricPaintCount] and [debugAsymmetricPaintCount] |
| /// counts to zero. |
| /// |
| /// Only valid when asserts are enabled. Does nothing in release builds. |
| void debugResetMetrics() { |
| assert(() { |
| _debugSymmetricPaintCount = 0; |
| _debugAsymmetricPaintCount = 0; |
| return true; |
| }()); |
| } |
| |
| @override |
| void debugRegisterRepaintBoundaryPaint({ bool includedParent = true, bool includedChild = false }) { |
| assert(() { |
| if (includedParent && includedChild) |
| _debugSymmetricPaintCount += 1; |
| else |
| _debugAsymmetricPaintCount += 1; |
| return true; |
| }()); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| bool inReleaseMode = true; |
| assert(() { |
| inReleaseMode = false; |
| if (debugSymmetricPaintCount + debugAsymmetricPaintCount == 0) { |
| properties.add(MessageProperty('usefulness ratio', 'no metrics collected yet (never painted)')); |
| } else { |
| final double fraction = debugAsymmetricPaintCount / (debugSymmetricPaintCount + debugAsymmetricPaintCount); |
| String diagnosis; |
| if (debugSymmetricPaintCount + debugAsymmetricPaintCount < 5) { |
| diagnosis = 'insufficient data to draw conclusion (less than five repaints)'; |
| } else if (fraction > 0.9) { |
| diagnosis = 'this is an outstandingly useful repaint boundary and should definitely be kept'; |
| } else if (fraction > 0.5) { |
| diagnosis = 'this is a useful repaint boundary and should be kept'; |
| } else if (fraction > 0.30) { |
| diagnosis = 'this repaint boundary is probably useful, but maybe it would be more useful in tandem with adding more repaint boundaries elsewhere'; |
| } else if (fraction > 0.1) { |
| diagnosis = 'this repaint boundary does sometimes show value, though currently not that often'; |
| } else if (debugAsymmetricPaintCount == 0) { |
| diagnosis = 'this repaint boundary is astoundingly ineffectual and should be removed'; |
| } else { |
| diagnosis = 'this repaint boundary is not very effective and should probably be removed'; |
| } |
| properties.add(PercentProperty('metrics', fraction, unit: 'useful', tooltip: '$debugSymmetricPaintCount bad vs $debugAsymmetricPaintCount good')); |
| properties.add(MessageProperty('diagnosis', diagnosis)); |
| } |
| return true; |
| }()); |
| if (inReleaseMode) |
| properties.add(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] is null, |
| /// 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 || !_ignoringSemantics) |
| 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 ?? ignoring; |
| |
| @override |
| bool hitTest(BoxHitTestResult result, { Offset position }) { |
| return !ignoring && super.hitTest(result, position: position); |
| } |
| |
| // TODO(ianh): figure out a way to still include labels and flags in |
| // descendants, just make them non-interactive, even when |
| // _effectiveIgnoringSemantics is true |
| @override |
| void visitChildrenForSemantics(RenderObjectVisitor visitor) { |
| if (child != null && !_effectiveIgnoringSemantics) |
| visitor(child); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<bool>('ignoring', ignoring)); |
| properties.add( |
| 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; |
| |