Text selection handles track scrolled text fields (#10805)
Introduce CompositedTransformTarget and CompositedTransformFollower widgets, corresponding render objects, and corresponding layers. Adjust the way text fields work to use this. Various changes I needed to debug the issues that came up.
This commit is contained in:
parent
40db1e4bc6
commit
6d32b33997
@ -4,23 +4,71 @@
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'print.dart';
|
||||
|
||||
/// A mixin that helps dump string representations of trees.
|
||||
abstract class TreeDiagnosticsMixin {
|
||||
// This class is intended to be used as a mixin, and should not be
|
||||
// extended directly.
|
||||
factory TreeDiagnosticsMixin._() => null;
|
||||
|
||||
/// A brief description of this object, usually just the [runtimeType] and the
|
||||
/// [hashCode].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [toStringShallow], for a detailed description of the object.
|
||||
/// * [toStringDeep], for a description of the subtree rooted at this object.
|
||||
@override
|
||||
String toString() => '$runtimeType#$hashCode';
|
||||
|
||||
/// Returns a one-line detailed description of the object.
|
||||
///
|
||||
/// This description includes everything from [debugFillDescription], but does
|
||||
/// not recurse to any children.
|
||||
///
|
||||
/// The [toStringShallow] method can take an argument, which is the string to
|
||||
/// place between each part obtained from [debugFillDescription]. Passing a
|
||||
/// string such as `'\n '` will result in a multiline string that indents the
|
||||
/// properties of the object below its name (as per [toString]).
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [toString], for a brief description of the object.
|
||||
/// * [toStringDeep], for a description of the subtree rooted at this object.
|
||||
String toStringShallow([String joiner = '; ']) {
|
||||
final StringBuffer result = new StringBuffer();
|
||||
result.write(toString());
|
||||
result.write(joiner);
|
||||
final List<String> description = <String>[];
|
||||
debugFillDescription(description);
|
||||
result.write(description.join(joiner));
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/// Returns a string representation of this node and its descendants.
|
||||
///
|
||||
/// This includes the information from [debugFillDescription], and then
|
||||
/// recurses into the children using [debugDescribeChildren].
|
||||
///
|
||||
/// The [toStringDeep] method takes arguments, but those are intended for
|
||||
/// internal use when recursing to the descendants, and so can be ignored.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [toString], for a brief description of the object but not its children.
|
||||
/// * [toStringShallow], for a detailed description of the object but not its
|
||||
/// children.
|
||||
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
|
||||
String result = '$prefixLineOne$this\n';
|
||||
final String childrenDescription = debugDescribeChildren(prefixOtherLines);
|
||||
final String descriptionPrefix = childrenDescription != '' ? '$prefixOtherLines \u2502 ' : '$prefixOtherLines ';
|
||||
final List<String> description = <String>[];
|
||||
debugFillDescription(description);
|
||||
result += description.map((String description) => '$descriptionPrefix$description\n').join();
|
||||
result += description
|
||||
.expand((String description) => debugWordWrap(description, 65, wrapIndent: ' '))
|
||||
.map<String>((String line) => "$descriptionPrefix$line\n")
|
||||
.join();
|
||||
if (childrenDescription == '') {
|
||||
final String prefix = prefixOtherLines.trimRight();
|
||||
if (prefix != '')
|
||||
@ -31,7 +79,8 @@ abstract class TreeDiagnosticsMixin {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Add additional information to the given description for use by [toStringDeep].
|
||||
/// Add additional information to the given description for use by
|
||||
/// [toStringDeep] and [toStringShallow].
|
||||
@protected
|
||||
@mustCallSuper
|
||||
void debugFillDescription(List<String> description) { }
|
||||
|
@ -6,6 +6,7 @@ import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'flat_button.dart';
|
||||
@ -90,8 +91,17 @@ class _TextSelectionToolbar extends StatelessWidget {
|
||||
/// Centers the toolbar around the given position, ensuring that it remains on
|
||||
/// screen.
|
||||
class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate {
|
||||
_TextSelectionToolbarLayout(this.position);
|
||||
_TextSelectionToolbarLayout(this.screenSize, this.globalEditableRegion, this.position);
|
||||
|
||||
/// The size of the screen at the time that the toolbar was last laid out.
|
||||
final Size screenSize;
|
||||
|
||||
/// Size and position of the editing region at the time the toolbar was last
|
||||
/// laid out, in global coordinates.
|
||||
final Rect globalEditableRegion;
|
||||
|
||||
/// Anchor position of the toolbar, relative to the top left of the
|
||||
/// [globalEditableRegion].
|
||||
final Offset position;
|
||||
|
||||
@override
|
||||
@ -101,17 +111,20 @@ class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate {
|
||||
|
||||
@override
|
||||
Offset getPositionForChild(Size size, Size childSize) {
|
||||
double x = position.dx - childSize.width / 2.0;
|
||||
double y = position.dy - childSize.height;
|
||||
final Offset globalPosition = globalEditableRegion.topLeft + position;
|
||||
|
||||
double x = globalPosition.dx - childSize.width / 2.0;
|
||||
double y = globalPosition.dy - childSize.height;
|
||||
|
||||
if (x < _kToolbarScreenPadding)
|
||||
x = _kToolbarScreenPadding;
|
||||
else if (x + childSize.width > size.width - 2 * _kToolbarScreenPadding)
|
||||
x = size.width - childSize.width - _kToolbarScreenPadding;
|
||||
else if (x + childSize.width > screenSize.width - _kToolbarScreenPadding)
|
||||
x = screenSize.width - childSize.width - _kToolbarScreenPadding;
|
||||
|
||||
if (y < _kToolbarScreenPadding)
|
||||
y = _kToolbarScreenPadding;
|
||||
else if (y + childSize.height > size.height - 2 * _kToolbarScreenPadding)
|
||||
y = size.height - childSize.height - _kToolbarScreenPadding;
|
||||
else if (y + childSize.height > screenSize.height - _kToolbarScreenPadding)
|
||||
y = screenSize.height - childSize.height - _kToolbarScreenPadding;
|
||||
|
||||
return new Offset(x, y);
|
||||
}
|
||||
@ -149,15 +162,17 @@ class _MaterialTextSelectionControls extends TextSelectionControls {
|
||||
|
||||
/// Builder for material-style copy/paste text selection toolbar.
|
||||
@override
|
||||
Widget buildToolbar(
|
||||
BuildContext context, Offset position, TextSelectionDelegate delegate) {
|
||||
Widget buildToolbar(BuildContext context, Rect globalEditableRegion, Offset position, TextSelectionDelegate delegate) {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
final Size screenSize = MediaQuery.of(context).size;
|
||||
return new ConstrainedBox(
|
||||
constraints: new BoxConstraints.loose(screenSize),
|
||||
constraints: new BoxConstraints.tight(globalEditableRegion.size),
|
||||
child: new CustomSingleChildLayout(
|
||||
delegate: new _TextSelectionToolbarLayout(position),
|
||||
child: new _TextSelectionToolbar(delegate)
|
||||
delegate: new _TextSelectionToolbarLayout(
|
||||
MediaQuery.of(context).size,
|
||||
globalEditableRegion,
|
||||
position,
|
||||
),
|
||||
child: new _TextSelectionToolbar(delegate),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1829,7 +1829,7 @@ abstract class RenderBox extends RenderObject {
|
||||
/// Subclasses that apply transforms during painting should override this
|
||||
/// function to factor those transforms into the calculation.
|
||||
///
|
||||
/// The RenderBox implementation takes care of adjusting the matrix for the
|
||||
/// The [RenderBox] implementation takes care of adjusting the matrix for the
|
||||
/// position of the given child as determined during layout and stored on the
|
||||
/// child's [parentData] in the [BoxParentData.offset] field.
|
||||
@override
|
||||
|
@ -123,8 +123,13 @@ bool debugCheckIntrinsicSizes = false;
|
||||
bool debugProfilePaintsEnabled = false;
|
||||
|
||||
|
||||
/// Returns a list of strings representing the given transform in a format useful for [RenderObject.debugFillDescription].
|
||||
/// Returns a list of strings representing the given transform in a format
|
||||
/// useful for [RenderObject.debugFillDescription].
|
||||
///
|
||||
/// If the argument is null, returns a list with the single string "null".
|
||||
List<String> debugDescribeTransform(Matrix4 transform) {
|
||||
if (transform == null)
|
||||
return const <String>['null'];
|
||||
final List<String> matrix = transform.toString().split('\n').map((String s) => ' $s').toList();
|
||||
matrix.removeLast();
|
||||
return matrix;
|
||||
|
@ -30,8 +30,9 @@ typedef void SelectionChangedHandler(TextSelection selection, RenderEditable ren
|
||||
/// Used by [RenderEditable.onCaretChanged].
|
||||
typedef void CaretChangedHandler(Rect caretRect);
|
||||
|
||||
/// Represents a global screen coordinate of the point in a selection, and the
|
||||
/// text direction at that point.
|
||||
/// Represents the coordinates of the point in a selection, and the text
|
||||
/// direction at that point, relative to top left of the [RenderEditable] that
|
||||
/// holds the selection.
|
||||
@immutable
|
||||
class TextSelectionPoint {
|
||||
/// Creates a description of a point in a text selection.
|
||||
@ -40,7 +41,8 @@ class TextSelectionPoint {
|
||||
const TextSelectionPoint(this.point, this.direction)
|
||||
: assert(point != null);
|
||||
|
||||
/// Screen coordinates of the lower left or lower right corner of the selection.
|
||||
/// Coordinates of the lower left or lower right corner of the selection,
|
||||
/// relative to the top left of the [RenderEditable] object.
|
||||
final Offset point;
|
||||
|
||||
/// Direction of the text at this edge of the selection.
|
||||
@ -316,7 +318,7 @@ class RenderEditable extends RenderBox {
|
||||
|
||||
bool _hasVisualOverflow = false;
|
||||
|
||||
/// Returns the global coordinates of the endpoints of the given selection.
|
||||
/// Returns the local coordinates of the endpoints of the given selection.
|
||||
///
|
||||
/// If the selection is collapsed (and therefore occupies a single point), the
|
||||
/// returned list is of length one. Otherwise, the selection is not collapsed
|
||||
@ -333,14 +335,14 @@ class RenderEditable extends RenderBox {
|
||||
// TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary.
|
||||
final Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype);
|
||||
final Offset start = new Offset(0.0, _preferredLineHeight) + caretOffset + paintOffset;
|
||||
return <TextSelectionPoint>[new TextSelectionPoint(localToGlobal(start), null)];
|
||||
return <TextSelectionPoint>[new TextSelectionPoint(start, null)];
|
||||
} else {
|
||||
final List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(selection);
|
||||
final Offset start = new Offset(boxes.first.start, boxes.first.bottom) + paintOffset;
|
||||
final Offset end = new Offset(boxes.last.end, boxes.last.bottom) + paintOffset;
|
||||
return <TextSelectionPoint>[
|
||||
new TextSelectionPoint(localToGlobal(start), boxes.first.direction),
|
||||
new TextSelectionPoint(localToGlobal(end), boxes.last.direction),
|
||||
new TextSelectionPoint(start, boxes.first.direction),
|
||||
new TextSelectionPoint(end, boxes.last.direction),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:ui' as ui show ImageFilter, Picture, SceneBuilder;
|
||||
import 'dart:ui' show Offset;
|
||||
|
||||
@ -90,7 +91,7 @@ abstract class Layer extends AbstractNode with TreeDiagnosticsMixin {
|
||||
|
||||
/// Override this method to upload this layer to the engine.
|
||||
///
|
||||
/// The layerOffset is the accumulated offset of this layer's parent from the
|
||||
/// The `layerOffset` is the accumulated offset of this layer's parent from the
|
||||
/// origin of the builder's coordinate system.
|
||||
void addToScene(ui.SceneBuilder builder, Offset layerOffset);
|
||||
|
||||
@ -117,6 +118,16 @@ abstract class Layer extends AbstractNode with TreeDiagnosticsMixin {
|
||||
///
|
||||
/// Picture layers are always leaves in the layer tree.
|
||||
class PictureLayer extends Layer {
|
||||
PictureLayer(this.canvasBounds);
|
||||
|
||||
/// The bounds that were used for the canvas that drew this layer's [picture].
|
||||
///
|
||||
/// This is purely advisory. It is included in the information dumped with
|
||||
/// [dumpLayerTree] (which can be triggered by pressing "L" when using
|
||||
/// "flutter run" at the console), which can help debug why certain drawing
|
||||
/// commands are being culled.
|
||||
final Rect canvasBounds;
|
||||
|
||||
/// The picture recorded for this layer.
|
||||
///
|
||||
/// The picture's coodinate system matches this layer's coodinate system.
|
||||
@ -150,6 +161,12 @@ class PictureLayer extends Layer {
|
||||
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
|
||||
builder.addPicture(layerOffset, picture, isComplexHint: isComplexHint, willChangeHint: willChangeHint);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
super.debugFillDescription(description);
|
||||
description.add('paint bounds: $canvasBounds');
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer that indicates to the compositor that it should display
|
||||
@ -357,6 +374,44 @@ class ContainerLayer extends Layer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the transform that would be applied when compositing the given
|
||||
/// child to the given matrix.
|
||||
///
|
||||
/// Specifically, this should apply the transform that is applied to child's
|
||||
/// _origin_. When using [applyTransform] with a chain of layers, results will
|
||||
/// be unreliable unless the deepest layer in the chain collapses the
|
||||
/// `layerOffset` in [addToScene] to zero, meaning that it passes
|
||||
/// [Offset.zero] to its children, and bakes any incoming `layerOffset` into
|
||||
/// the [SceneBuilder] as (for instance) a transform (which is then also
|
||||
/// included in the transformation applied by [applyTransform]).
|
||||
///
|
||||
/// For example, if [addToScene] applies the `layerOffset` and then
|
||||
/// passes [Offset.zero] to the children, then it should be included in the
|
||||
/// transform applied here, whereas if [addToScene] just passes the
|
||||
/// `layerOffset` to the child, then it should not be included in the
|
||||
/// transform applied here.
|
||||
///
|
||||
/// This method is only valid immediately after [addToScene] has been called,
|
||||
/// before any of the properties have been changed.
|
||||
///
|
||||
/// The default implementation does nothing, since [ContainerLayer], by
|
||||
/// default, composits its children at the origin of the [ContainerLayer]
|
||||
/// itself.
|
||||
///
|
||||
/// The `child` argument should generally not be null, since in principle a
|
||||
/// layer could transform each child independently. However, certain layers
|
||||
/// may explicitly allow null as a value, for example if they know that they
|
||||
/// transform all their children identically.
|
||||
///
|
||||
/// The `transform` argument must not be null.
|
||||
///
|
||||
/// Used by [FollowerLayer] to transform its child to a [LeaderLayer]'s
|
||||
/// position.
|
||||
void applyTransform(Layer child, Matrix4 transform) {
|
||||
assert(child != null);
|
||||
assert(transform != null);
|
||||
}
|
||||
|
||||
@override
|
||||
String debugDescribeChildren(String prefix) {
|
||||
if (firstChild == null)
|
||||
@ -391,13 +446,17 @@ class ContainerLayer extends Layer {
|
||||
class OffsetLayer extends ContainerLayer {
|
||||
/// Creates an offset layer.
|
||||
///
|
||||
/// By default, [offset] is zero.
|
||||
/// By default, [offset] is zero. It must be non-null before the compositing
|
||||
/// phase of the pipeline.
|
||||
OffsetLayer({ this.offset: Offset.zero });
|
||||
|
||||
/// Offset from parent in the parent's coordinate system.
|
||||
///
|
||||
/// The scene must be explicitly recomposited after this property is changed
|
||||
/// (as described at [Layer]).
|
||||
///
|
||||
/// The [offset] property must be non-null before the compositing phase of the
|
||||
/// pipeline.
|
||||
Offset offset;
|
||||
|
||||
@override
|
||||
@ -412,7 +471,6 @@ class OffsetLayer extends ContainerLayer {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A composite layer that clips its children using a rectangle.
|
||||
class ClipRectLayer extends ContainerLayer {
|
||||
/// Creates a layer with a rectangular clip.
|
||||
@ -497,35 +555,51 @@ class ClipPathLayer extends ContainerLayer {
|
||||
}
|
||||
}
|
||||
|
||||
/// A composited layer that applies a transformation matrix to its children.
|
||||
/// A composited layer that applies a given transformation matrix to its
|
||||
/// children.
|
||||
///
|
||||
/// This class inherits from [OffsetLayer] to make it one of the layers that
|
||||
/// can be used at the root of a [RenderObject] hierarchy.
|
||||
class TransformLayer extends OffsetLayer {
|
||||
/// Creates a transform layer.
|
||||
///
|
||||
/// The [transform] property must be non-null before the compositing phase of
|
||||
/// the pipeline.
|
||||
TransformLayer({
|
||||
this.transform
|
||||
});
|
||||
/// The [transform] and [offset] properties must be non-null before the
|
||||
/// compositing phase of the pipeline.
|
||||
TransformLayer({ this.transform, Offset offset: Offset.zero }) : super(offset: offset);
|
||||
|
||||
/// The matrix to apply.
|
||||
///
|
||||
/// The scene must be explicitly recomposited after this property is changed
|
||||
/// (as described at [Layer]).
|
||||
///
|
||||
/// This transform is applied before [offset], if both are set.
|
||||
///
|
||||
/// The [transform] property must be non-null before the compositing phase of
|
||||
/// the pipeline.
|
||||
Matrix4 transform;
|
||||
|
||||
Matrix4 _lastEffectiveTransform;
|
||||
|
||||
@override
|
||||
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
|
||||
assert(offset == Offset.zero);
|
||||
Matrix4 effectiveTransform = transform;
|
||||
if (layerOffset != Offset.zero) {
|
||||
effectiveTransform = new Matrix4.translationValues(layerOffset.dx, layerOffset.dy, 0.0)
|
||||
..multiply(transform);
|
||||
_lastEffectiveTransform = transform;
|
||||
final Offset totalOffset = offset + layerOffset;
|
||||
if (totalOffset != Offset.zero) {
|
||||
_lastEffectiveTransform = new Matrix4.translationValues(totalOffset.dx, totalOffset.dy, 0.0)
|
||||
..multiply(_lastEffectiveTransform);
|
||||
}
|
||||
builder.pushTransform(effectiveTransform.storage);
|
||||
builder.pushTransform(_lastEffectiveTransform.storage);
|
||||
addChildrenToScene(builder, Offset.zero);
|
||||
builder.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void applyTransform(Layer child, Matrix4 transform) {
|
||||
assert(child != null);
|
||||
assert(transform != null);
|
||||
transform.multiply(_lastEffectiveTransform);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
super.debugFillDescription(description);
|
||||
@ -565,7 +639,7 @@ class OpacityLayer extends ContainerLayer {
|
||||
}
|
||||
}
|
||||
|
||||
/// A composited layer that applies a shader to hits children.
|
||||
/// A composited layer that applies a shader to its children.
|
||||
class ShaderMaskLayer extends ContainerLayer {
|
||||
/// Creates a shader mask layer.
|
||||
///
|
||||
@ -682,3 +756,303 @@ class PhysicalModelLayer extends ContainerLayer {
|
||||
description.add('clipRRect: $clipRRect');
|
||||
}
|
||||
}
|
||||
|
||||
/// An object that a [LeaderLayer] can register with.
|
||||
///
|
||||
/// An instance of this class should be provided as the [LeaderLayer.link] and
|
||||
/// the [FollowerLayer.link] properties to cause the [FollowerLayer] to follow
|
||||
/// the [LeaderLayer].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CompositedTransformTarget], the widget that creates a [LeaderLayer].
|
||||
/// * [CompositedTransformFollower], the widget that creates a [FollowerLayer].
|
||||
/// * [RenderLeaderLayer] and [RenderFollowerLayer], the corresponding
|
||||
/// render objects.
|
||||
class LayerLink {
|
||||
/// The currently-registered [LeaderLayer], if any.
|
||||
LeaderLayer get leader => _leader;
|
||||
LeaderLayer _leader;
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#$hashCode(${ _leader != null ? "<linked>" : "<dangling>" })';
|
||||
}
|
||||
|
||||
/// A composited layer that can be followed by a [FollowerLayer].
|
||||
///
|
||||
/// This layer collapses the accumulated offset into a transform and passes
|
||||
/// [Offset.zero] to its child layers in the [addToScene]/[addChildrenToScene]
|
||||
/// methods, so that [applyTransform] will work reliably.
|
||||
class LeaderLayer extends ContainerLayer {
|
||||
/// Creates a leader layer.
|
||||
///
|
||||
/// The [link] property must not be null, and must not have been provided to
|
||||
/// any other [LeaderLayer] layers that are [attached] to the layer tree at
|
||||
/// the same time.
|
||||
///
|
||||
/// The [offset] property must be non-null before the compositing phase of the
|
||||
/// pipeline.
|
||||
LeaderLayer({ @required this.link, this.offset: Offset.zero }) : assert(link != null);
|
||||
|
||||
/// The object with which this layer should register.
|
||||
///
|
||||
/// The link will be established when this layer is [attach]ed, and will be
|
||||
/// cleared when this layer is [detach]ed.
|
||||
final LayerLink link;
|
||||
|
||||
/// Offset from parent in the parent's coordinate system.
|
||||
///
|
||||
/// The scene must be explicitly recomposited after this property is changed
|
||||
/// (as described at [Layer]).
|
||||
///
|
||||
/// The [offset] property must be non-null before the compositing phase of the
|
||||
/// pipeline.
|
||||
Offset offset;
|
||||
|
||||
@override
|
||||
void attach(Object owner) {
|
||||
super.attach(owner);
|
||||
assert(link.leader == null);
|
||||
_lastOffset = null;
|
||||
link._leader = this;
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
assert(link.leader == this);
|
||||
link._leader = null;
|
||||
_lastOffset = null;
|
||||
super.detach();
|
||||
}
|
||||
|
||||
/// The offset the last time this layer was composited.
|
||||
///
|
||||
/// This is reset to null when the layer is attached or detached, to help
|
||||
/// catch cases where the follower layer ends up before the leader layer, but
|
||||
/// not every case can be detected.
|
||||
Offset _lastOffset;
|
||||
|
||||
@override
|
||||
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
|
||||
assert(offset != null);
|
||||
_lastOffset = offset + layerOffset;
|
||||
if (_lastOffset != Offset.zero)
|
||||
builder.pushTransform(new Matrix4.translationValues(_lastOffset.dx, _lastOffset.dy, 0.0).storage);
|
||||
addChildrenToScene(builder, Offset.zero);
|
||||
if (_lastOffset != Offset.zero)
|
||||
builder.pop();
|
||||
}
|
||||
|
||||
/// Applies the transform that would be applied when compositing the given
|
||||
/// child to the given matrix.
|
||||
///
|
||||
/// See [ContainerLayer.applyTransform] for details.
|
||||
///
|
||||
/// The `child` argument may be null, as the same transform is applied to all
|
||||
/// children.
|
||||
@override
|
||||
void applyTransform(Layer child, Matrix4 transform) {
|
||||
assert(_lastOffset != null);
|
||||
if (_lastOffset != Offset.zero)
|
||||
transform.translate(_lastOffset.dx, _lastOffset.dy);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
super.debugFillDescription(description);
|
||||
description.add('offset: $offset');
|
||||
description.add('link: $link');
|
||||
}
|
||||
}
|
||||
|
||||
/// A composited layer that applies a transformation matrix to its children such
|
||||
/// that they are positioned to match a [LeaderLayer].
|
||||
///
|
||||
/// If any of the ancestors of this layer have a degenerate matrix (e.g. scaling
|
||||
/// by zero), then the [FollowerLayer] will not be able to transform its child
|
||||
/// to the coordinate space of the [Leader].
|
||||
///
|
||||
/// A [linkedOffset] property can be provided to further offset the child layer
|
||||
/// from the leader layer, for example if the child is to follow the linked
|
||||
/// layer at a distance rather than directly overlapping it.
|
||||
class FollowerLayer extends ContainerLayer {
|
||||
/// Creates a follower layer.
|
||||
///
|
||||
/// The [link] property must not be null.
|
||||
///
|
||||
/// The [unlinkedOffset], [linkedOffset], and [showWhenUnlinked] properties
|
||||
/// must be non-null before the compositing phase of the pipeline.
|
||||
FollowerLayer({
|
||||
@required this.link,
|
||||
this.showWhenUnlinked: true,
|
||||
this.unlinkedOffset: Offset.zero,
|
||||
this.linkedOffset: Offset.zero,
|
||||
}) : assert(link != null);
|
||||
|
||||
/// The link to the [LeaderLayer].
|
||||
///
|
||||
/// The same object should be provided to a [LeaderLayer] that is earlier in
|
||||
/// the layer tree. When this layer is composited, it will apply a transform
|
||||
/// that moves its children to match the position of the [LeaderLayer].
|
||||
final LayerLink link;
|
||||
|
||||
/// Whether to show the layer's contents when the [link] does not point to a
|
||||
/// [LeaderLayer].
|
||||
///
|
||||
/// When the layer is linked, children layers are positioned such that they
|
||||
/// have the same global position as the linked [LeaderLayer].
|
||||
///
|
||||
/// When the layer is not linked, then: if [showWhenUnlinked] is true,
|
||||
/// children are positioned as if the [FollowerLayer] was a [ContainerLayer];
|
||||
/// if it is false, then children are hidden.
|
||||
///
|
||||
/// The [showWhenUnlinked] property must be non-null before the compositing
|
||||
/// phase of the pipeline.
|
||||
bool showWhenUnlinked;
|
||||
|
||||
/// Offset from parent in the parent's coordinate system, used when the layer
|
||||
/// is not linked to a [LeaderLayer].
|
||||
///
|
||||
/// The scene must be explicitly recomposited after this property is changed
|
||||
/// (as described at [Layer]).
|
||||
///
|
||||
/// The [unlinkedOffset] property must be non-null before the compositing
|
||||
/// phase of the pipeline.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [linkedOffset], for when the layers are linked.
|
||||
Offset unlinkedOffset;
|
||||
|
||||
/// Offset from the origin of the leader layer to the origin of the child
|
||||
/// layers, used when the layer is linked to a [LeaderLayer].
|
||||
///
|
||||
/// The scene must be explicitly recomposited after this property is changed
|
||||
/// (as described at [Layer]).
|
||||
///
|
||||
/// The [linkedOffset] property must be non-null before the compositing phase
|
||||
/// of the pipeline.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [unlinkedOffset], for when the layer is not linked.
|
||||
Offset linkedOffset;
|
||||
|
||||
Offset _lastOffset;
|
||||
Matrix4 _lastTransform;
|
||||
|
||||
/// The transform that was used during the last composition phase.
|
||||
///
|
||||
/// If the [link] was not linked to a [LeaderLayer], or if this layer has
|
||||
/// a degerenate matrix applied, then this will be null.
|
||||
///
|
||||
/// This method returns a new [Matrix4] instance each time it is invoked.
|
||||
Matrix4 getLastTransform() {
|
||||
if (_lastTransform == null)
|
||||
return null;
|
||||
final Matrix4 result = new Matrix4.translationValues(-_lastOffset.dx, -_lastOffset.dy, 0.0);
|
||||
result.multiply(_lastTransform);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Call [applyTransform] for each layer in the provided list.
|
||||
///
|
||||
/// The list is in reverse order (deepest first). The first layer will be
|
||||
/// treated as the child of the second, and so forth. The first layer in the
|
||||
/// list won't have [applyTransform] called on it. The first layer may be
|
||||
/// null.
|
||||
Matrix4 _collectTransformForLayerChain(List<ContainerLayer> layers) {
|
||||
// Initialize our result matrix.
|
||||
final Matrix4 result = new Matrix4.identity();
|
||||
// Apply each layer to the matrix in turn, starting from the last layer,
|
||||
// and providing the previous layer as the child.
|
||||
for (int index = layers.length - 1; index > 0; index -= 1)
|
||||
layers[index].applyTransform(layers[index - 1], result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Populate [_lastTransform] given the current state of the tree.
|
||||
void _establishTransform() {
|
||||
assert(link != null);
|
||||
_lastTransform = null;
|
||||
// Check to see if we are linked.
|
||||
if (link.leader == null)
|
||||
return;
|
||||
// If we're linked, check the link is valid.
|
||||
assert(link.leader.owner == owner, 'Linked LeaderLayer anchor is not in the same layer tree as the FollowerLayer.');
|
||||
assert(link.leader._lastOffset != null, 'LeaderLayer anchor must come before FollowerLayer in paint order, but the reverse was true.');
|
||||
// Collect all our ancestors into a Set so we can recognize them.
|
||||
final Set<Layer> ancestors = new HashSet<Layer>();
|
||||
Layer ancestor = parent;
|
||||
while (ancestor != null) {
|
||||
ancestors.add(ancestor);
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
// Collect all the layers from a hypothetical child (null) of the target
|
||||
// layer up to the common ancestor layer.
|
||||
ContainerLayer layer = link.leader;
|
||||
final List<ContainerLayer> forwardLayers = <ContainerLayer>[null, layer];
|
||||
do {
|
||||
layer = layer.parent;
|
||||
forwardLayers.add(layer);
|
||||
} while (!ancestors.contains(layer));
|
||||
ancestor = layer;
|
||||
// Collect all the layers from this layer up to the common ancestor layer.
|
||||
layer = this;
|
||||
final List<ContainerLayer> inverseLayers = <ContainerLayer>[layer];
|
||||
do {
|
||||
layer = layer.parent;
|
||||
inverseLayers.add(layer);
|
||||
} while (layer != ancestor);
|
||||
// Establish the forward and backward matrices given these lists of layers.
|
||||
final Matrix4 forwardTransform = _collectTransformForLayerChain(forwardLayers);
|
||||
final Matrix4 inverseTransform = _collectTransformForLayerChain(inverseLayers);
|
||||
if (inverseTransform.invert() == 0.0) {
|
||||
// We are in a degenerate transform, so there's not much we can do.
|
||||
return;
|
||||
}
|
||||
// Combine the matrices and store the result.
|
||||
inverseTransform.multiply(forwardTransform);
|
||||
inverseTransform.translate(linkedOffset.dx, linkedOffset.dy);
|
||||
_lastTransform = inverseTransform;
|
||||
}
|
||||
|
||||
@override
|
||||
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
|
||||
assert(link != null);
|
||||
assert(showWhenUnlinked != null);
|
||||
if (link.leader == null && !showWhenUnlinked) {
|
||||
_lastTransform = null;
|
||||
_lastOffset = null;
|
||||
return;
|
||||
}
|
||||
_establishTransform();
|
||||
if (_lastTransform != null) {
|
||||
builder.pushTransform(_lastTransform.storage);
|
||||
addChildrenToScene(builder, Offset.zero);
|
||||
builder.pop();
|
||||
_lastOffset = unlinkedOffset + layerOffset;
|
||||
} else {
|
||||
_lastOffset = null;
|
||||
addChildrenToScene(builder, unlinkedOffset + layerOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void applyTransform(Layer child, Matrix4 transform) {
|
||||
assert(child != null);
|
||||
assert(transform != null);
|
||||
if (_lastTransform != null)
|
||||
transform.multiply(_lastTransform);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
super.debugFillDescription(description);
|
||||
description.add('link: $link');
|
||||
if (_lastTransform != null) {
|
||||
description.add('transform:');
|
||||
description.addAll(debugDescribeTransform(getLastTransform()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,13 +56,29 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset);
|
||||
/// child might be recorded in separate compositing layers. For this reason, do
|
||||
/// not hold a reference to the canvas across operations that might paint
|
||||
/// child render objects.
|
||||
///
|
||||
/// New [PaintingContext] objects are created automatically when using
|
||||
/// [PaintingContext.repaintCompositedChild] and [pushLayer].
|
||||
class PaintingContext {
|
||||
PaintingContext._(this._containerLayer, this._paintBounds)
|
||||
PaintingContext._(this._containerLayer, this.canvasBounds)
|
||||
: assert(_containerLayer != null),
|
||||
assert(_paintBounds != null);
|
||||
assert(canvasBounds != null);
|
||||
|
||||
final ContainerLayer _containerLayer;
|
||||
final Rect _paintBounds;
|
||||
|
||||
/// The bounds within which the painting context's [canvas] will record
|
||||
/// painting commands.
|
||||
///
|
||||
/// A render object provided with this [PaintingContext] (e.g. in its
|
||||
/// [RenderObject.paint] method) is permitted to paint outside the region that
|
||||
/// the render object occupies during layout, but is not permitted to paint
|
||||
/// outside these paints bounds. These paint bounds are used to construct
|
||||
/// memory-efficient composited layers, which means attempting to paint
|
||||
/// outside these bounds can attempt to write to pixels that do not exist in
|
||||
/// the composited layer.
|
||||
///
|
||||
/// The [paintBounds] rectangle is in the [canvas] coordinate system.
|
||||
final Rect canvasBounds;
|
||||
|
||||
/// Repaint the given render object.
|
||||
///
|
||||
@ -70,6 +86,11 @@ class PaintingContext {
|
||||
/// composited layer, and must be in need of painting. The render object's
|
||||
/// layer, if any, is re-used, along with any layers in the subtree that don't
|
||||
/// need to be repainted.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject]
|
||||
/// has a composited layer.
|
||||
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent: false }) {
|
||||
assert(child.isRepaintBoundary);
|
||||
assert(child._needsPaint);
|
||||
@ -97,7 +118,7 @@ class PaintingContext {
|
||||
childContext._stopRecordingIfNeeded();
|
||||
}
|
||||
|
||||
/// Paint a child render object.
|
||||
/// Paint a child [RenderObject].
|
||||
///
|
||||
/// If the child has its own composited layer, the child will be composited
|
||||
/// into the layer subtree associated with this painting context. Otherwise,
|
||||
@ -180,6 +201,8 @@ class PaintingContext {
|
||||
/// The current canvas can change whenever you paint a child using this
|
||||
/// context, which means it's fragile to hold a reference to the canvas
|
||||
/// returned by this getter.
|
||||
///
|
||||
/// Only calls within the [canvasBounds] will be recorded.
|
||||
Canvas get canvas {
|
||||
if (_canvas == null)
|
||||
_startRecording();
|
||||
@ -188,9 +211,9 @@ class PaintingContext {
|
||||
|
||||
void _startRecording() {
|
||||
assert(!_isRecording);
|
||||
_currentLayer = new PictureLayer();
|
||||
_currentLayer = new PictureLayer(canvasBounds);
|
||||
_recorder = new ui.PictureRecorder();
|
||||
_canvas = new Canvas(_recorder, _paintBounds);
|
||||
_canvas = new Canvas(_recorder, canvasBounds);
|
||||
_containerLayer.append(_currentLayer);
|
||||
}
|
||||
|
||||
@ -203,14 +226,14 @@ class PaintingContext {
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 6.0
|
||||
..color = debugCurrentRepaintColor.toColor();
|
||||
canvas.drawRect(_paintBounds.deflate(3.0), paint);
|
||||
canvas.drawRect(canvasBounds.deflate(3.0), paint);
|
||||
}
|
||||
if (debugPaintLayerBordersEnabled) {
|
||||
final Paint paint = new Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.0
|
||||
..color = debugPaintLayerBordersColor;
|
||||
canvas.drawRect(_paintBounds, paint);
|
||||
canvas.drawRect(canvasBounds, paint);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@ -262,7 +285,7 @@ class PaintingContext {
|
||||
}
|
||||
|
||||
/// Appends the given layer to the recording, and calls the `painter` callback
|
||||
/// with that layer, providing the [childPaintBounds] as the paint bounds of
|
||||
/// with that layer, providing the `childPaintBounds` as the paint bounds of
|
||||
/// the child. Canvas recording commands are not guaranteed to be stored
|
||||
/// outside of the paint bounds.
|
||||
///
|
||||
@ -272,9 +295,11 @@ class PaintingContext {
|
||||
///
|
||||
/// The `offset` is the offset to pass to the `painter`.
|
||||
///
|
||||
/// If the `childPaintBounds` are not specified then the current layer's
|
||||
/// If the `childPaintBounds` are not specified then the current layer's paint
|
||||
/// bounds are used. This is appropriate if the child layer does not apply any
|
||||
/// transformation or clipping to its contents.
|
||||
/// transformation or clipping to its contents. The `childPaintBounds`, if
|
||||
/// specified, must be in the coordinate system of the new layer, and should
|
||||
/// not go outside the current layer's paint bounds.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -285,7 +310,7 @@ class PaintingContext {
|
||||
assert(painter != null);
|
||||
_stopRecordingIfNeeded();
|
||||
_appendLayer(childLayer);
|
||||
final PaintingContext childContext = new PaintingContext._(childLayer, childPaintBounds ?? _paintBounds);
|
||||
final PaintingContext childContext = new PaintingContext._(childLayer, childPaintBounds ?? canvasBounds);
|
||||
painter(childContext, offset);
|
||||
childContext._stopRecordingIfNeeded();
|
||||
}
|
||||
@ -379,7 +404,7 @@ class PaintingContext {
|
||||
new TransformLayer(transform: effectiveTransform),
|
||||
painter,
|
||||
offset,
|
||||
childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, _paintBounds),
|
||||
childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, canvasBounds),
|
||||
);
|
||||
} else {
|
||||
canvas.save();
|
||||
@ -406,6 +431,9 @@ class PaintingContext {
|
||||
void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) {
|
||||
pushLayer(new OpacityLayer(alpha: alpha), painter, offset);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#$hashCode(layer: $_containerLayer, canvas bounds: $canvasBounds)';
|
||||
}
|
||||
|
||||
/// An abstract set of layout constraints.
|
||||
@ -1981,6 +2009,9 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
||||
/// frequently might want to repaint themselves without requiring their parent
|
||||
/// to repaint.
|
||||
///
|
||||
/// If this getter returns true, the [paintBounds] are applied to this object
|
||||
/// and all descendants.
|
||||
///
|
||||
/// Warning: This getter must not change value over the lifetime of this object.
|
||||
bool get isRepaintBoundary => false;
|
||||
|
||||
@ -2272,12 +2303,15 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
||||
|
||||
/// The bounds within which this render object will paint.
|
||||
///
|
||||
/// A render object is permitted to paint outside the region it occupies
|
||||
/// during layout but is not permitted to paint outside these paints bounds.
|
||||
/// These paint bounds are used to construct memory-efficient composited
|
||||
/// layers, which means attempting to paint outside these bounds can attempt
|
||||
/// to write to pixels that do not exist in this render object's composited
|
||||
/// layer.
|
||||
/// A render object and its descendants are permitted to paint outside the
|
||||
/// region it occupies during layout, but they are not permitted to paint
|
||||
/// outside these paints bounds. These paint bounds are used to construct
|
||||
/// memory-efficient composited layers, which means attempting to paint
|
||||
/// outside these bounds can attempt to write to pixels that do not exist in
|
||||
/// this render object's composited layer.
|
||||
///
|
||||
/// The [paintBounds] are only actually enforced when the render object is a
|
||||
/// repaint boundary; see [isRepaintBoundary].
|
||||
Rect get paintBounds;
|
||||
|
||||
/// Override this method to paint debugging information.
|
||||
|
@ -1636,7 +1636,7 @@ class RenderTransform extends RenderProxyBox {
|
||||
Matrix4 inverse;
|
||||
try {
|
||||
inverse = new Matrix4.inverted(_effectiveTransform);
|
||||
} catch (e) {
|
||||
} on ArgumentError {
|
||||
// We cannot invert the effective transform. That means the child
|
||||
// doesn't appear on screen and cannot be hit.
|
||||
return false;
|
||||
@ -1661,7 +1661,6 @@ class RenderTransform extends RenderProxyBox {
|
||||
@override
|
||||
void applyPaintTransform(RenderBox child, Matrix4 transform) {
|
||||
transform.multiply(_effectiveTransform);
|
||||
super.applyPaintTransform(child, transform);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1785,7 +1784,7 @@ class RenderFittedBox extends RenderProxyBox {
|
||||
Matrix4 inverse;
|
||||
try {
|
||||
inverse = new Matrix4.inverted(_transform);
|
||||
} catch (e) {
|
||||
} on ArgumentError {
|
||||
// We cannot invert the effective transform. That means the child
|
||||
// doesn't appear on screen and cannot be hit.
|
||||
return false;
|
||||
@ -1798,7 +1797,6 @@ class RenderFittedBox extends RenderProxyBox {
|
||||
void applyPaintTransform(RenderBox child, Matrix4 transform) {
|
||||
_updatePaintData();
|
||||
transform.multiply(_transform);
|
||||
super.applyPaintTransform(child, transform);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1864,7 +1862,6 @@ class RenderFractionalTranslation extends RenderProxyBox {
|
||||
@override
|
||||
void applyPaintTransform(RenderBox child, Matrix4 transform) {
|
||||
transform.translate(translation.dx * size.width, translation.dy * size.height);
|
||||
super.applyPaintTransform(child, transform);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -3046,3 +3043,194 @@ class RenderExcludeSemantics extends RenderProxyBox {
|
||||
description.add('excluding: $excluding');
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an anchor for a [RenderFollowerLayer].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CompositedTransformTarget], the corresponding widget.
|
||||
/// * [LeaderLayer], the layer that this render object creates.
|
||||
class RenderLeaderLayer extends RenderProxyBox {
|
||||
/// Creates a render object that uses a [LeaderLayer].
|
||||
///
|
||||
/// The [link] must not be null.
|
||||
RenderLeaderLayer({
|
||||
@required LayerLink link,
|
||||
RenderBox child,
|
||||
}) : assert(link != null),
|
||||
super(child) {
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
/// The link object that connects this [RenderLeaderLayer] with one or more
|
||||
/// [RenderFollowerLayer]s.
|
||||
///
|
||||
/// This property must not be null. The object must not be associated with
|
||||
/// another [RenderLeaderLayer] that is also being painted.
|
||||
LayerLink get link => _link;
|
||||
LayerLink _link;
|
||||
set link(LayerLink value) {
|
||||
assert(value != null);
|
||||
if (_link == value)
|
||||
return;
|
||||
_link = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get alwaysNeedsCompositing => true;
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
context.pushLayer(new LeaderLayer(link: link, offset: offset), super.paint, Offset.zero);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
super.debugFillDescription(description);
|
||||
description.add('link: $link');
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform the child so that its origin is [offset] from the orign of the
|
||||
/// [RenderLeaderLayer] with the same [LayerLink].
|
||||
///
|
||||
/// The [RenderLeaderLayer] in question must be earlier in the paint order.
|
||||
///
|
||||
/// Hit testing on descendants of this render object will only work if the
|
||||
/// target position is within the box that this render object's parent considers
|
||||
/// to be hitable.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CompositedTransformFollower], the corresponding widget.
|
||||
/// * [FollowerLayer], the layer that this render object creates.
|
||||
class RenderFollowerLayer extends RenderProxyBox {
|
||||
/// Creates a render object that uses a [FollowerLayer].
|
||||
///
|
||||
/// The [link] and [offset] arguments must not be null.
|
||||
RenderFollowerLayer({
|
||||
@required LayerLink link,
|
||||
bool showWhenUnlinked: true,
|
||||
Offset offset: Offset.zero,
|
||||
RenderBox child,
|
||||
}) : assert(link != null),
|
||||
assert(showWhenUnlinked != null),
|
||||
assert(offset != null),
|
||||
super(child) {
|
||||
this.link = link;
|
||||
this.showWhenUnlinked = showWhenUnlinked;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
/// The link object that connects this [RenderFollowerLayer] with a
|
||||
/// [RenderLeaderLayer] earlier in the paint order.
|
||||
LayerLink get link => _link;
|
||||
LayerLink _link;
|
||||
set link(LayerLink value) {
|
||||
assert(value != null);
|
||||
if (_link == value)
|
||||
return;
|
||||
_link = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
/// Whether to show the render object's contents when there is no
|
||||
/// corresponding [RenderLeaderLayer] with the same [link].
|
||||
///
|
||||
/// When the render object is linked, the child is positioned such that it has
|
||||
/// the same global position as the linked [RenderLeaderLayer].
|
||||
///
|
||||
/// When the render object is not linked, then: if [showWhenUnlinked] is true,
|
||||
/// the child is visible and not repositioned; if it is false, then child is
|
||||
/// hidden.
|
||||
bool get showWhenUnlinked => _showWhenUnlinked;
|
||||
bool _showWhenUnlinked;
|
||||
set showWhenUnlinked(bool value) {
|
||||
assert(value != null);
|
||||
if (_showWhenUnlinked == value)
|
||||
return;
|
||||
_showWhenUnlinked = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
/// The offset to apply to the origin of the linked [RenderLeaderLayer] to
|
||||
/// obtain this render object's origin.
|
||||
Offset get offset => _offset;
|
||||
Offset _offset;
|
||||
set offset(Offset value) {
|
||||
assert(value != null);
|
||||
if (_offset == value)
|
||||
return;
|
||||
_offset = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
_layer = null;
|
||||
super.detach();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get alwaysNeedsCompositing => true;
|
||||
|
||||
/// The layer we created when we were last painted.
|
||||
FollowerLayer _layer;
|
||||
|
||||
Matrix4 getCurrentTransform() {
|
||||
return _layer?.getLastTransform() ?? new Matrix4.identity();
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTest(HitTestResult result, { Offset position }) {
|
||||
Matrix4 inverse;
|
||||
try {
|
||||
inverse = new Matrix4.inverted(getCurrentTransform());
|
||||
} on ArgumentError {
|
||||
// We cannot invert the effective transform. That means the child
|
||||
// doesn't appear on screen and cannot be hit.
|
||||
return false;
|
||||
}
|
||||
position = MatrixUtils.transformPoint(inverse, position);
|
||||
return super.hitTest(result, position: position);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
assert(showWhenUnlinked != null);
|
||||
_layer = new FollowerLayer(
|
||||
link: link,
|
||||
showWhenUnlinked: showWhenUnlinked,
|
||||
linkedOffset: this.offset,
|
||||
unlinkedOffset: offset,
|
||||
);
|
||||
context.pushLayer(
|
||||
_layer,
|
||||
super.paint,
|
||||
Offset.zero,
|
||||
childPaintBounds: new Rect.fromLTRB(
|
||||
// We don't know where we'll end up, so we have no idea what our cull rect should be.
|
||||
double.NEGATIVE_INFINITY,
|
||||
double.NEGATIVE_INFINITY,
|
||||
double.INFINITY,
|
||||
double.INFINITY,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void applyPaintTransform(RenderBox child, Matrix4 transform) {
|
||||
transform.multiply(getCurrentTransform());
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
super.debugFillDescription(description);
|
||||
description.add('link: $link');
|
||||
description.add('showWhenUnlinked: $showWhenUnlinked');
|
||||
description.add('offset: $offset');
|
||||
description.add('current transform matrix:');
|
||||
description.addAll(debugDescribeTransform(getCurrentTransform()));
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ export 'package:flutter/rendering.dart' show
|
||||
FlowPaintingContext,
|
||||
FractionalOffsetTween,
|
||||
HitTestBehavior,
|
||||
LayerLink,
|
||||
MainAxisAlignment,
|
||||
MainAxisSize,
|
||||
MultiChildLayoutDelegate,
|
||||
@ -301,11 +302,13 @@ class CustomPaint extends SingleChildRenderObjectWidget {
|
||||
final Size size;
|
||||
|
||||
@override
|
||||
RenderCustomPaint createRenderObject(BuildContext context) => new RenderCustomPaint(
|
||||
painter: painter,
|
||||
foregroundPainter: foregroundPainter,
|
||||
preferredSize: size,
|
||||
);
|
||||
RenderCustomPaint createRenderObject(BuildContext context) {
|
||||
return new RenderCustomPaint(
|
||||
painter: painter,
|
||||
foregroundPainter: foregroundPainter,
|
||||
preferredSize: size,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderCustomPaint renderObject) {
|
||||
@ -711,12 +714,14 @@ class Transform extends SingleChildRenderObjectWidget {
|
||||
final bool transformHitTests;
|
||||
|
||||
@override
|
||||
RenderTransform createRenderObject(BuildContext context) => new RenderTransform(
|
||||
transform: transform,
|
||||
origin: origin,
|
||||
alignment: alignment,
|
||||
transformHitTests: transformHitTests
|
||||
);
|
||||
RenderTransform createRenderObject(BuildContext context) {
|
||||
return new RenderTransform(
|
||||
transform: transform,
|
||||
origin: origin,
|
||||
alignment: alignment,
|
||||
transformHitTests: transformHitTests
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderTransform renderObject) {
|
||||
@ -728,6 +733,140 @@ class Transform extends SingleChildRenderObjectWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that can be targetted by a [CompositedTransformFollower].
|
||||
///
|
||||
/// When this widget is composited during the compositing phase (which comes
|
||||
/// after the paint phase, as described in [WidgetsBinding.drawFrame]), it
|
||||
/// updates the [link] object so that any [CompositedTransformFollower] widgets
|
||||
/// that are subsequently composited in the same frame and were given the same
|
||||
/// [LayerLink] can position themselves at the same screen location.
|
||||
///
|
||||
/// A single [CompositedTransformTarget] can be followed by multiple
|
||||
/// [CompositedTransformFollower] widgets.
|
||||
///
|
||||
/// The [CompositedTransformTarget] must come earlier in the paint order than
|
||||
/// any linked [CompositedTransformFollower]s.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CompositedTransformFollower], the widget that can target this one.
|
||||
/// * [LeaderLayer], the layer that implements this widget's logic.
|
||||
class CompositedTransformTarget extends SingleChildRenderObjectWidget {
|
||||
/// Creates a composited transform target widget.
|
||||
///
|
||||
/// The [link] property must not be null, and must not be currently being used
|
||||
/// by any other [CompositedTransformTarget] object that is in the tree.
|
||||
const CompositedTransformTarget({
|
||||
Key key,
|
||||
@required this.link,
|
||||
Widget child,
|
||||
}) : assert(link != null),
|
||||
super(key: key, child: child);
|
||||
|
||||
/// The link object that connects this [CompositedTransformTarget] with one or
|
||||
/// more [CompositedTransformFollower]s.
|
||||
///
|
||||
/// This property must not be null. The object must not be associated with
|
||||
/// another [CompositedTransformTarget] that is also being painted.
|
||||
final LayerLink link;
|
||||
|
||||
@override
|
||||
RenderLeaderLayer createRenderObject(BuildContext context) {
|
||||
return new RenderLeaderLayer(
|
||||
link: link,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderLeaderLayer renderObject) {
|
||||
renderObject
|
||||
..link = link;
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that follows a [CompositedTransformTarget].
|
||||
///
|
||||
/// When this widget is composited during the compositing phase (which comes
|
||||
/// after the paint phase, as described in [WidgetsBinding.drawFrame]), it
|
||||
/// applies a transformation that causes it to provide its child with a
|
||||
/// coordinate space that matches that of the linked [CompositedTransformTarget]
|
||||
/// widget, offset by [offset].
|
||||
///
|
||||
/// The [LayerLink] object used as the [link] must be the same object as that
|
||||
/// provided to the matching [CompositedTransformTarget].
|
||||
///
|
||||
/// The [CompositedTransformTarget] must come earlier in the paint order than
|
||||
/// this [CompositedTransformFollower].
|
||||
///
|
||||
/// Hit testing on descendants of this widget will only work if the target
|
||||
/// position is within the box that this widget's parent considers to be
|
||||
/// hitable. If the parent covers the screen, this is trivially achievable, so
|
||||
/// this widget is usually used as the root of an [OverlayEntry] in an app-wide
|
||||
/// [Overlay] (e.g. as created by the [MaterialApp] widget's [Navigator]).
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CompositedTransformTarget], the widget that this widget can target.
|
||||
/// * [FollowerLayer], the layer that implements this widget's logic.
|
||||
/// * [Transform], which applies an arbitrary transform to a child.
|
||||
class CompositedTransformFollower extends SingleChildRenderObjectWidget {
|
||||
/// Creates a composited transform target widget.
|
||||
///
|
||||
/// The [link] property must not be null. If it was also provided to a
|
||||
/// [CompositedTransformTarget], that widget must come earlier in the paint
|
||||
/// order.
|
||||
///
|
||||
/// The [showWhenUnlinked] and [offset] properties must also not be null.
|
||||
const CompositedTransformFollower({
|
||||
Key key,
|
||||
@required this.link,
|
||||
this.showWhenUnlinked: true,
|
||||
this.offset: Offset.zero,
|
||||
Widget child,
|
||||
}) : assert(link != null),
|
||||
assert(showWhenUnlinked != null),
|
||||
assert(offset != null),
|
||||
super(key: key, child: child);
|
||||
|
||||
/// The link object that connects this [CompositedTransformFollower] with a
|
||||
/// [CompositedTransformTarget].
|
||||
///
|
||||
/// This property must not be null.
|
||||
final LayerLink link;
|
||||
|
||||
/// Whether to show the widget's contents when there is no corresponding
|
||||
/// [CompositedTransformTarget] with the same [link].
|
||||
///
|
||||
/// When the widget is linked, the child is positioned such that it has the
|
||||
/// same global position as the linked [CompositedTransformTarget].
|
||||
///
|
||||
/// When the widget is not linked, then: if [showWhenUnlinked] is true, the
|
||||
/// child is visible and not repositioned; if it is false, then child is
|
||||
/// hidden.
|
||||
final bool showWhenUnlinked;
|
||||
|
||||
/// The offset to apply to the origin of the linked
|
||||
/// [CompositedTransformTarget] to obtain this widget's origin.
|
||||
final Offset offset;
|
||||
|
||||
@override
|
||||
RenderFollowerLayer createRenderObject(BuildContext context) {
|
||||
return new RenderFollowerLayer(
|
||||
link: link,
|
||||
showWhenUnlinked: showWhenUnlinked,
|
||||
offset: offset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderFollowerLayer renderObject) {
|
||||
renderObject
|
||||
..link = link
|
||||
..showWhenUnlinked = showWhenUnlinked
|
||||
..offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
/// Scales and positions its child within itself according to [fit].
|
||||
///
|
||||
/// See also:
|
||||
@ -1207,9 +1346,11 @@ class SizedBox extends SingleChildRenderObjectWidget {
|
||||
final double height;
|
||||
|
||||
@override
|
||||
RenderConstrainedBox createRenderObject(BuildContext context) => new RenderConstrainedBox(
|
||||
additionalConstraints: _additionalConstraints,
|
||||
);
|
||||
RenderConstrainedBox createRenderObject(BuildContext context) {
|
||||
return new RenderConstrainedBox(
|
||||
additionalConstraints: _additionalConstraints,
|
||||
);
|
||||
}
|
||||
|
||||
BoxConstraints get _additionalConstraints {
|
||||
return new BoxConstraints.tightFor(width: width, height: height);
|
||||
@ -1353,11 +1494,13 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget {
|
||||
final FractionalOffset alignment;
|
||||
|
||||
@override
|
||||
RenderFractionallySizedOverflowBox createRenderObject(BuildContext context) => new RenderFractionallySizedOverflowBox(
|
||||
alignment: alignment,
|
||||
widthFactor: widthFactor,
|
||||
heightFactor: heightFactor
|
||||
);
|
||||
RenderFractionallySizedOverflowBox createRenderObject(BuildContext context) {
|
||||
return new RenderFractionallySizedOverflowBox(
|
||||
alignment: alignment,
|
||||
widthFactor: widthFactor,
|
||||
heightFactor: heightFactor
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderFractionallySizedOverflowBox renderObject) {
|
||||
@ -1423,10 +1566,12 @@ class LimitedBox extends SingleChildRenderObjectWidget {
|
||||
final double maxHeight;
|
||||
|
||||
@override
|
||||
RenderLimitedBox createRenderObject(BuildContext context) => new RenderLimitedBox(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight
|
||||
);
|
||||
RenderLimitedBox createRenderObject(BuildContext context) {
|
||||
return new RenderLimitedBox(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderLimitedBox renderObject) {
|
||||
@ -1489,13 +1634,15 @@ class OverflowBox extends SingleChildRenderObjectWidget {
|
||||
final double maxHeight;
|
||||
|
||||
@override
|
||||
RenderConstrainedOverflowBox createRenderObject(BuildContext context) => new RenderConstrainedOverflowBox(
|
||||
alignment: alignment,
|
||||
minWidth: minWidth,
|
||||
maxWidth: maxWidth,
|
||||
minHeight: minHeight,
|
||||
maxHeight: maxHeight
|
||||
);
|
||||
RenderConstrainedOverflowBox createRenderObject(BuildContext context) {
|
||||
return new RenderConstrainedOverflowBox(
|
||||
alignment: alignment,
|
||||
minWidth: minWidth,
|
||||
maxWidth: maxWidth,
|
||||
minHeight: minHeight,
|
||||
maxHeight: maxHeight
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderConstrainedOverflowBox renderObject) {
|
||||
@ -3196,18 +3343,20 @@ class RawImage extends LeafRenderObjectWidget {
|
||||
final Rect centerSlice;
|
||||
|
||||
@override
|
||||
RenderImage createRenderObject(BuildContext context) => new RenderImage(
|
||||
image: image,
|
||||
width: width,
|
||||
height: height,
|
||||
scale: scale,
|
||||
color: color,
|
||||
colorBlendMode: colorBlendMode,
|
||||
fit: fit,
|
||||
alignment: alignment,
|
||||
repeat: repeat,
|
||||
centerSlice: centerSlice
|
||||
);
|
||||
RenderImage createRenderObject(BuildContext context) {
|
||||
return new RenderImage(
|
||||
image: image,
|
||||
width: width,
|
||||
height: height,
|
||||
scale: scale,
|
||||
color: color,
|
||||
colorBlendMode: colorBlendMode,
|
||||
fit: fit,
|
||||
alignment: alignment,
|
||||
repeat: repeat,
|
||||
centerSlice: centerSlice
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderImage renderObject) {
|
||||
@ -3371,13 +3520,15 @@ class Listener extends SingleChildRenderObjectWidget {
|
||||
final HitTestBehavior behavior;
|
||||
|
||||
@override
|
||||
RenderPointerListener createRenderObject(BuildContext context) => new RenderPointerListener(
|
||||
onPointerDown: onPointerDown,
|
||||
onPointerMove: onPointerMove,
|
||||
onPointerUp: onPointerUp,
|
||||
onPointerCancel: onPointerCancel,
|
||||
behavior: behavior
|
||||
);
|
||||
RenderPointerListener createRenderObject(BuildContext context) {
|
||||
return new RenderPointerListener(
|
||||
onPointerDown: onPointerDown,
|
||||
onPointerMove: onPointerMove,
|
||||
onPointerUp: onPointerUp,
|
||||
onPointerCancel: onPointerCancel,
|
||||
behavior: behavior
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderPointerListener renderObject) {
|
||||
@ -3499,10 +3650,12 @@ class IgnorePointer extends SingleChildRenderObjectWidget {
|
||||
final bool ignoringSemantics;
|
||||
|
||||
@override
|
||||
RenderIgnorePointer createRenderObject(BuildContext context) => new RenderIgnorePointer(
|
||||
ignoring: ignoring,
|
||||
ignoringSemantics: ignoringSemantics
|
||||
);
|
||||
RenderIgnorePointer createRenderObject(BuildContext context) {
|
||||
return new RenderIgnorePointer(
|
||||
ignoring: ignoring,
|
||||
ignoringSemantics: ignoringSemantics
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderIgnorePointer renderObject) {
|
||||
@ -3583,10 +3736,12 @@ class MetaData extends SingleChildRenderObjectWidget {
|
||||
final HitTestBehavior behavior;
|
||||
|
||||
@override
|
||||
RenderMetaData createRenderObject(BuildContext context) => new RenderMetaData(
|
||||
metaData: metaData,
|
||||
behavior: behavior
|
||||
);
|
||||
RenderMetaData createRenderObject(BuildContext context) {
|
||||
return new RenderMetaData(
|
||||
metaData: metaData,
|
||||
behavior: behavior
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderMetaData renderObject) {
|
||||
@ -3668,12 +3823,14 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
final String label;
|
||||
|
||||
@override
|
||||
RenderSemanticsAnnotations createRenderObject(BuildContext context) => new RenderSemanticsAnnotations(
|
||||
container: container,
|
||||
checked: checked,
|
||||
selected: selected,
|
||||
label: label,
|
||||
);
|
||||
RenderSemanticsAnnotations createRenderObject(BuildContext context) {
|
||||
return new RenderSemanticsAnnotations(
|
||||
container: container,
|
||||
checked: checked,
|
||||
selected: selected,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderSemanticsAnnotations renderObject) {
|
||||
|
@ -263,6 +263,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
TextSelectionOverlay _selectionOverlay;
|
||||
|
||||
final ScrollController _scrollController = new ScrollController();
|
||||
final LayerLink _layerLink = new LayerLink();
|
||||
bool _didAutoFocus = false;
|
||||
|
||||
// State lifecycle:
|
||||
@ -272,6 +273,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
super.initState();
|
||||
widget.controller.addListener(_didChangeTextEditingValue);
|
||||
widget.focusNode.addListener(_handleFocusChanged);
|
||||
_scrollController.addListener(() { _selectionOverlay?.updateForScroll(); });
|
||||
}
|
||||
|
||||
@override
|
||||
@ -436,6 +438,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
context: context,
|
||||
value: _value,
|
||||
debugRequiredFor: widget,
|
||||
layerLink: _layerLink,
|
||||
renderObject: renderObject,
|
||||
onSelectionOverlayChanged: _handleSelectionOverlayChanged,
|
||||
selectionControls: widget.selectionControls,
|
||||
@ -538,19 +541,22 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
controller: _scrollController,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||
return new _Editable(
|
||||
value: _value,
|
||||
style: widget.style,
|
||||
cursorColor: widget.cursorColor,
|
||||
showCursor: _showCursor,
|
||||
maxLines: widget.maxLines,
|
||||
selectionColor: widget.selectionColor,
|
||||
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
|
||||
textAlign: widget.textAlign,
|
||||
obscureText: widget.obscureText,
|
||||
offset: offset,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
onCaretChanged: _handleCaretChanged,
|
||||
return new CompositedTransformTarget(
|
||||
link: _layerLink,
|
||||
child: new _Editable(
|
||||
value: _value,
|
||||
style: widget.style,
|
||||
cursorColor: widget.cursorColor,
|
||||
showCursor: _showCursor,
|
||||
maxLines: widget.maxLines,
|
||||
selectionColor: widget.selectionColor,
|
||||
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
|
||||
textAlign: widget.textAlign,
|
||||
obscureText: widget.obscureText,
|
||||
offset: offset,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
onCaretChanged: _handleCaretChanged,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -24,6 +24,10 @@ import 'scroll_position_with_single_context.dart';
|
||||
/// to an individual [Scrollable] widget. To use a custom [ScrollPosition],
|
||||
/// subclass [ScrollController] and override [createScrollPosition].
|
||||
///
|
||||
/// A [ScrollController] is a [Listenable]. It notifies its listeners whenever
|
||||
/// any of the attached [ScrollPosition]s notify _their_ listeners (i.e.
|
||||
/// whenever any of them scroll).
|
||||
///
|
||||
/// Typically used with [ListView], [GridView], [CustomScrollView].
|
||||
///
|
||||
/// See also:
|
||||
|
@ -73,8 +73,7 @@ abstract class TextSelectionControls {
|
||||
/// Builds a toolbar near a text selection.
|
||||
///
|
||||
/// Typically displays buttons for copying and pasting text.
|
||||
// TODO(mpcomplete): A single position is probably insufficient.
|
||||
Widget buildToolbar(BuildContext context, Offset position, TextSelectionDelegate delegate);
|
||||
Widget buildToolbar(BuildContext context, Rect globalEditableRegion, Offset position, TextSelectionDelegate delegate);
|
||||
|
||||
/// Returns the size of the selection handle.
|
||||
Size get handleSize;
|
||||
@ -92,7 +91,8 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
@required TextEditingValue value,
|
||||
@required this.context,
|
||||
this.debugRequiredFor,
|
||||
this.renderObject,
|
||||
@required this.layerLink,
|
||||
@required this.renderObject,
|
||||
this.onSelectionOverlayChanged,
|
||||
this.selectionControls,
|
||||
}): assert(value != null),
|
||||
@ -113,6 +113,10 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
/// Debugging information for explaining why the [Overlay] is required.
|
||||
final Widget debugRequiredFor;
|
||||
|
||||
/// The object supplied to the [CompositedTransformTarget] that wraps the text
|
||||
/// field.
|
||||
final LayerLink layerLink;
|
||||
|
||||
// TODO(mpcomplete): what if the renderObject is removed or replaced, or
|
||||
// moves? Not sure what cases I need to handle, or how to handle them.
|
||||
/// The editable line in which the selected text is being displayed.
|
||||
@ -149,8 +153,8 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
void showHandles() {
|
||||
assert(_handles == null);
|
||||
_handles = <OverlayEntry>[
|
||||
new OverlayEntry(builder: (BuildContext c) => _buildHandle(c, _TextSelectionHandlePosition.start)),
|
||||
new OverlayEntry(builder: (BuildContext c) => _buildHandle(c, _TextSelectionHandlePosition.end)),
|
||||
new OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.start)),
|
||||
new OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.end)),
|
||||
];
|
||||
Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles);
|
||||
_handleController.forward(from: 0.0);
|
||||
@ -184,6 +188,14 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Causes the overlay to update its rendering.
|
||||
///
|
||||
/// This is intended to be called when the [renderObject] may have changed its
|
||||
/// text metrics (e.g. because the text was scrolled).
|
||||
void updateForScroll() {
|
||||
_markNeedsBuild();
|
||||
}
|
||||
|
||||
void _markNeedsBuild([Duration duration]) {
|
||||
if (_handles != null) {
|
||||
_handles[0].markNeedsBuild();
|
||||
@ -223,10 +235,11 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
child: new _TextSelectionHandleOverlay(
|
||||
onSelectionHandleChanged: (TextSelection newSelection) { _handleSelectionHandleChanged(newSelection, position); },
|
||||
onSelectionHandleTapped: _handleSelectionHandleTapped,
|
||||
layerLink: layerLink,
|
||||
renderObject: renderObject,
|
||||
selection: _selection,
|
||||
selectionControls: selectionControls,
|
||||
position: position
|
||||
position: position,
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -241,12 +254,22 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
(endpoints.length == 1) ?
|
||||
endpoints[0].point.dx :
|
||||
(endpoints[0].point.dx + endpoints[1].point.dx) / 2.0,
|
||||
endpoints[0].point.dy - renderObject.size.height
|
||||
endpoints[0].point.dy - renderObject.size.height,
|
||||
);
|
||||
|
||||
final Rect editingRegion = new Rect.fromPoints(
|
||||
renderObject.localToGlobal(Offset.zero),
|
||||
renderObject.localToGlobal(renderObject.size.bottomRight(Offset.zero)),
|
||||
);
|
||||
|
||||
return new FadeTransition(
|
||||
opacity: _toolbarOpacity,
|
||||
child: selectionControls.buildToolbar(context, midpoint, this)
|
||||
child: new CompositedTransformFollower(
|
||||
link: layerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: -editingRegion.topLeft,
|
||||
child: selectionControls.buildToolbar(context, editingRegion, midpoint, this),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -298,16 +321,18 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
class _TextSelectionHandleOverlay extends StatefulWidget {
|
||||
const _TextSelectionHandleOverlay({
|
||||
Key key,
|
||||
this.selection,
|
||||
this.position,
|
||||
this.renderObject,
|
||||
this.onSelectionHandleChanged,
|
||||
this.onSelectionHandleTapped,
|
||||
this.selectionControls
|
||||
@required this.selection,
|
||||
@required this.position,
|
||||
@required this.layerLink,
|
||||
@required this.renderObject,
|
||||
@required this.onSelectionHandleChanged,
|
||||
@required this.onSelectionHandleTapped,
|
||||
@required this.selectionControls
|
||||
}) : super(key: key);
|
||||
|
||||
final TextSelection selection;
|
||||
final _TextSelectionHandlePosition position;
|
||||
final LayerLink layerLink;
|
||||
final RenderEditable renderObject;
|
||||
final ValueChanged<TextSelection> onSelectionHandleChanged;
|
||||
final VoidCallback onSelectionHandleTapped;
|
||||
@ -379,19 +404,23 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
|
||||
break;
|
||||
}
|
||||
|
||||
return new GestureDetector(
|
||||
onPanStart: _handleDragStart,
|
||||
onPanUpdate: _handleDragUpdate,
|
||||
onTap: _handleTap,
|
||||
child: new Stack(
|
||||
children: <Widget>[
|
||||
new Positioned(
|
||||
left: point.dx,
|
||||
top: point.dy,
|
||||
child: widget.selectionControls.buildHandle(context, type)
|
||||
)
|
||||
]
|
||||
)
|
||||
return new CompositedTransformFollower(
|
||||
link: widget.layerLink,
|
||||
showWhenUnlinked: false,
|
||||
child: new GestureDetector(
|
||||
onPanStart: _handleDragStart,
|
||||
onPanUpdate: _handleDragUpdate,
|
||||
onTap: _handleTap,
|
||||
child: new Stack(
|
||||
children: <Widget>[
|
||||
new Positioned(
|
||||
left: point.dx,
|
||||
top: point.dy,
|
||||
child: widget.selectionControls.buildHandle(context, type),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ class MockClipboard {
|
||||
|
||||
Widget overlay(Widget child) {
|
||||
return new MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
data: const MediaQueryData(size: const Size(800.0, 600.0)),
|
||||
child: new Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
new OverlayEntry(
|
||||
@ -73,10 +73,22 @@ void main() {
|
||||
return renderEditable;
|
||||
}
|
||||
|
||||
List<TextSelectionPoint> globalize(Iterable<TextSelectionPoint> points, RenderBox box) {
|
||||
return points.map((TextSelectionPoint point) {
|
||||
return new TextSelectionPoint(
|
||||
box.localToGlobal(point.point),
|
||||
point.direction,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Offset textOffsetToPosition(WidgetTester tester, int offset) {
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
||||
new TextSelection.collapsed(offset: offset),
|
||||
final List<TextSelectionPoint> endpoints = globalize(
|
||||
renderEditable.getEndpointsForSelection(
|
||||
new TextSelection.collapsed(offset: offset),
|
||||
),
|
||||
renderEditable,
|
||||
);
|
||||
expect(endpoints.length, 1);
|
||||
return endpoints[0].point + const Offset(0.0, -2.0);
|
||||
@ -309,15 +321,19 @@ void main() {
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
|
||||
|
||||
final TextSelection selection = controller.selection;
|
||||
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(selection);
|
||||
final List<TextSelectionPoint> endpoints = globalize(
|
||||
renderEditable.getEndpointsForSelection(selection),
|
||||
renderEditable,
|
||||
);
|
||||
expect(endpoints.length, 2);
|
||||
|
||||
// Drag the right handle 2 letters to the right.
|
||||
// Note: use a small offset because the endpoint is on the very corner
|
||||
// We use a small offset because the endpoint is on the very corner
|
||||
// of the handle.
|
||||
Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
|
||||
Offset newHandlePos = textOffsetToPosition(tester, selection.extentOffset+2);
|
||||
@ -368,10 +384,15 @@ void main() {
|
||||
// Tap the selection handle to bring up the "paste / select all" menu.
|
||||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||||
await tester.pumpWidget(builder());
|
||||
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
|
||||
RenderEditable renderEditable = findRenderEditable(tester);
|
||||
List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(controller.selection);
|
||||
List<TextSelectionPoint> endpoints = globalize(
|
||||
renderEditable.getEndpointsForSelection(controller.selection),
|
||||
renderEditable,
|
||||
);
|
||||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||||
await tester.pumpWidget(builder());
|
||||
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
|
||||
|
||||
// SELECT ALL should select all the text.
|
||||
await tester.tap(find.text('SELECT ALL'));
|
||||
@ -388,10 +409,15 @@ void main() {
|
||||
// Tap again to bring back the menu.
|
||||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||||
await tester.pumpWidget(builder());
|
||||
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
|
||||
renderEditable = findRenderEditable(tester);
|
||||
endpoints = renderEditable.getEndpointsForSelection(controller.selection);
|
||||
endpoints = globalize(
|
||||
renderEditable.getEndpointsForSelection(controller.selection),
|
||||
renderEditable,
|
||||
);
|
||||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||||
await tester.pumpWidget(builder());
|
||||
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
|
||||
|
||||
// PASTE right before the 'e'.
|
||||
await tester.tap(find.text('PASTE'));
|
||||
@ -422,8 +448,12 @@ void main() {
|
||||
// Tap the selection handle to bring up the "paste / select all" menu.
|
||||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||||
await tester.pumpWidget(builder());
|
||||
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(controller.selection);
|
||||
final List<TextSelectionPoint> endpoints = globalize(
|
||||
renderEditable.getEndpointsForSelection(controller.selection),
|
||||
renderEditable,
|
||||
);
|
||||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
@ -547,12 +577,16 @@ void main() {
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
|
||||
|
||||
expect(controller.selection.baseOffset, 39);
|
||||
expect(controller.selection.extentOffset, 44);
|
||||
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(controller.selection);
|
||||
final List<TextSelectionPoint> endpoints = globalize(
|
||||
renderEditable.getEndpointsForSelection(controller.selection),
|
||||
renderEditable,
|
||||
);
|
||||
expect(endpoints.length, 2);
|
||||
|
||||
// Drag the right handle to the third line, just after 'Third'.
|
||||
@ -653,7 +687,10 @@ void main() {
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(controller.selection);
|
||||
final List<TextSelectionPoint> endpoints = globalize(
|
||||
renderEditable.getEndpointsForSelection(controller.selection),
|
||||
renderEditable,
|
||||
);
|
||||
expect(endpoints.length, 2);
|
||||
|
||||
// Drag the left handle to the first line, just after 'First'.
|
||||
@ -1410,11 +1447,15 @@ void main() {
|
||||
|
||||
await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2')));
|
||||
await tester.pumpWidget(builder());
|
||||
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints =
|
||||
renderEditable.getEndpointsForSelection(textController.selection);
|
||||
final List<TextSelectionPoint> endpoints = globalize(
|
||||
renderEditable.getEndpointsForSelection(textController.selection),
|
||||
renderEditable,
|
||||
);
|
||||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||||
await tester.pumpWidget(builder());
|
||||
await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero
|
||||
|
||||
Clipboard.setData(const ClipboardData(text: '一4二\n5三6'));
|
||||
await tester.tap(find.text('PASTE'));
|
||||
|
166
packages/flutter/test/widgets/composited_transform_test.dart
Normal file
166
packages/flutter/test/widgets/composited_transform_test.dart
Normal file
@ -0,0 +1,166 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Composited transforms - only offsets', (WidgetTester tester) async {
|
||||
final LayerLink link = new LayerLink();
|
||||
final GlobalKey key = new GlobalKey();
|
||||
await tester.pumpWidget(
|
||||
new Stack(
|
||||
children: <Widget>[
|
||||
new Positioned(
|
||||
left: 123.0,
|
||||
top: 456.0,
|
||||
child: new CompositedTransformTarget(
|
||||
link: link,
|
||||
child: new Container(height: 10.0, width: 10.0),
|
||||
),
|
||||
),
|
||||
new Positioned(
|
||||
left: 787.0,
|
||||
top: 343.0,
|
||||
child: new CompositedTransformFollower(
|
||||
link: link,
|
||||
child: new Container(key: key, height: 10.0, width: 10.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
final RenderBox box = key.currentContext.findRenderObject();
|
||||
expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0));
|
||||
});
|
||||
|
||||
testWidgets('Composited transforms - with rotations', (WidgetTester tester) async {
|
||||
final LayerLink link = new LayerLink();
|
||||
final GlobalKey key1 = new GlobalKey();
|
||||
final GlobalKey key2 = new GlobalKey();
|
||||
await tester.pumpWidget(
|
||||
new Stack(
|
||||
children: <Widget>[
|
||||
new Positioned(
|
||||
top: 123.0,
|
||||
left: 456.0,
|
||||
child: new Transform.rotate(
|
||||
angle: 1.0, // radians
|
||||
child: new CompositedTransformTarget(
|
||||
link: link,
|
||||
child: new Container(key: key1, height: 10.0, width: 10.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
new Positioned(
|
||||
top: 787.0,
|
||||
left: 343.0,
|
||||
child: new Transform.rotate(
|
||||
angle: -0.3, // radians
|
||||
child: new CompositedTransformFollower(
|
||||
link: link,
|
||||
child: new Container(key: key2, height: 10.0, width: 10.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
final RenderBox box1 = key1.currentContext.findRenderObject();
|
||||
final RenderBox box2 = key2.currentContext.findRenderObject();
|
||||
final Offset position1 = box1.localToGlobal(Offset.zero);
|
||||
final Offset position2 = box2.localToGlobal(Offset.zero);
|
||||
expect(position1.dx, moreOrLessEquals(position2.dx));
|
||||
expect(position1.dy, moreOrLessEquals(position2.dy));
|
||||
});
|
||||
|
||||
testWidgets('Composited transforms - nested', (WidgetTester tester) async {
|
||||
final LayerLink link = new LayerLink();
|
||||
final GlobalKey key1 = new GlobalKey();
|
||||
final GlobalKey key2 = new GlobalKey();
|
||||
await tester.pumpWidget(
|
||||
new Stack(
|
||||
children: <Widget>[
|
||||
new Positioned(
|
||||
top: 123.0,
|
||||
left: 456.0,
|
||||
child: new Transform.rotate(
|
||||
angle: 1.0, // radians
|
||||
child: new CompositedTransformTarget(
|
||||
link: link,
|
||||
child: new Container(key: key1, height: 10.0, width: 10.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
new Positioned(
|
||||
top: 787.0,
|
||||
left: 343.0,
|
||||
child: new Transform.rotate(
|
||||
angle: -0.3, // radians
|
||||
child: new Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: new CompositedTransformFollower(
|
||||
link: new LayerLink(),
|
||||
child: new Transform(
|
||||
transform: new Matrix4.skew(0.9, 1.1),
|
||||
child: new Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: new CompositedTransformFollower(
|
||||
link: link,
|
||||
child: new Container(key: key2, height: 10.0, width: 10.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
final RenderBox box1 = key1.currentContext.findRenderObject();
|
||||
final RenderBox box2 = key2.currentContext.findRenderObject();
|
||||
final Offset position1 = box1.localToGlobal(Offset.zero);
|
||||
final Offset position2 = box2.localToGlobal(Offset.zero);
|
||||
expect(position1.dx, moreOrLessEquals(position2.dx));
|
||||
expect(position1.dy, moreOrLessEquals(position2.dy));
|
||||
});
|
||||
|
||||
testWidgets('Composited transforms - hit testing', (WidgetTester tester) async {
|
||||
final LayerLink link = new LayerLink();
|
||||
final GlobalKey key1 = new GlobalKey();
|
||||
final GlobalKey key2 = new GlobalKey();
|
||||
final GlobalKey key3 = new GlobalKey();
|
||||
bool _tapped = false;
|
||||
await tester.pumpWidget(
|
||||
new Stack(
|
||||
children: <Widget>[
|
||||
new Positioned(
|
||||
left: 123.0,
|
||||
top: 456.0,
|
||||
child: new CompositedTransformTarget(
|
||||
link: link,
|
||||
child: new Container(key: key1, height: 10.0, width: 10.0),
|
||||
),
|
||||
),
|
||||
new CompositedTransformFollower(
|
||||
link: link,
|
||||
child: new GestureDetector(
|
||||
key: key2,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () { _tapped = true; },
|
||||
child: new Container(key: key3, height: 10.0, width: 10.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
final RenderBox box2 = key2.currentContext.findRenderObject();
|
||||
expect(box2.size, const Size(10.0, 10.0));
|
||||
expect(_tapped, isFalse);
|
||||
await tester.tap(find.byKey(key1));
|
||||
expect(_tapped, isTrue);
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user