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.
diff --git a/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart b/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart
index 3c63414..cb69a74 100644
--- a/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart
+++ b/packages/flutter/lib/src/foundation/tree_diagnostics_mixin.dart
@@ -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 @@
     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) { }
diff --git a/packages/flutter/lib/src/material/text_selection.dart b/packages/flutter/lib/src/material/text_selection.dart
index f696c41..719f466 100644
--- a/packages/flutter/lib/src/material/text_selection.dart
+++ b/packages/flutter/lib/src/material/text_selection.dart
@@ -6,6 +6,7 @@
 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 @@
 /// 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 @@
 
   @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 @@
 
   /// 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),
       )
     );
   }
diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart
index 065e5da..270820f 100644
--- a/packages/flutter/lib/src/rendering/box.dart
+++ b/packages/flutter/lib/src/rendering/box.dart
@@ -1829,7 +1829,7 @@
   /// 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
diff --git a/packages/flutter/lib/src/rendering/debug.dart b/packages/flutter/lib/src/rendering/debug.dart
index 54610e1..6183b4b 100644
--- a/packages/flutter/lib/src/rendering/debug.dart
+++ b/packages/flutter/lib/src/rendering/debug.dart
@@ -123,8 +123,13 @@
 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;
diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart
index aa5c516..b28f39d 100644
--- a/packages/flutter/lib/src/rendering/editable.dart
+++ b/packages/flutter/lib/src/rendering/editable.dart
@@ -30,8 +30,9 @@
 /// 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 @@
   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 @@
 
   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 @@
       // 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),
       ];
     }
   }
diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart
index da25813..453fe50 100644
--- a/packages/flutter/lib/src/rendering/layer.dart
+++ b/packages/flutter/lib/src/rendering/layer.dart
@@ -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 @@
 
   /// 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 @@
 ///
 /// 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 @@
   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 @@
     }
   }
 
+  /// 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 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 @@
   }
 }
 
-
 /// A composite layer that clips its children using a rectangle.
 class ClipRectLayer extends ContainerLayer {
   /// Creates a layer with a rectangular clip.
@@ -497,36 +555,52 @@
   }
 }
 
-/// 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);
     description.add('transform:');
@@ -565,7 +639,7 @@
   }
 }
 
-/// 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 @@
     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()));
+    }
+  }
+}
diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart
index 1cad998..6e248e0 100644
--- a/packages/flutter/lib/src/rendering/object.dart
+++ b/packages/flutter/lib/src/rendering/object.dart
@@ -56,13 +56,29 @@
 /// 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 @@
   /// 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 @@
     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 @@
   /// 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 @@
 
   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 @@
           ..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 @@
   }
 
   /// 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 @@
   ///
   /// 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 @@
     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 @@
         new TransformLayer(transform: effectiveTransform),
         painter,
         offset,
-        childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, _paintBounds),
+        childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, canvasBounds),
       );
     } else {
       canvas.save();
@@ -406,6 +431,9 @@
   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 @@
   /// 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 @@
 
   /// 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.
diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart
index bb4b7e7..4453c87 100644
--- a/packages/flutter/lib/src/rendering/proxy_box.dart
+++ b/packages/flutter/lib/src/rendering/proxy_box.dart
@@ -1636,7 +1636,7 @@
       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 @@
   @override
   void applyPaintTransform(RenderBox child, Matrix4 transform) {
     transform.multiply(_effectiveTransform);
-    super.applyPaintTransform(child, transform);
   }
 
   @override
@@ -1785,7 +1784,7 @@
     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 @@
   void applyPaintTransform(RenderBox child, Matrix4 transform) {
     _updatePaintData();
     transform.multiply(_transform);
-    super.applyPaintTransform(child, transform);
   }
 
   @override
@@ -1864,7 +1862,6 @@
   @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 @@
     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()));
+  }
+}
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index bf462de..6444b41 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -26,6 +26,7 @@
   FlowPaintingContext,
   FractionalOffsetTween,
   HitTestBehavior,
+  LayerLink,
   MainAxisAlignment,
   MainAxisSize,
   MultiChildLayoutDelegate,
@@ -301,11 +302,13 @@
   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 @@
   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 @@
   }
 }
 
+/// 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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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 @@
   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) {
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index eb08c06..7ac573f 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -263,6 +263,7 @@
   TextSelectionOverlay _selectionOverlay;
 
   final ScrollController _scrollController = new ScrollController();
+  final LayerLink _layerLink = new LayerLink();
   bool _didAutoFocus = false;
 
   // State lifecycle:
@@ -272,6 +273,7 @@
     super.initState();
     widget.controller.addListener(_didChangeTextEditingValue);
     widget.focusNode.addListener(_handleFocusChanged);
+    _scrollController.addListener(() { _selectionOverlay?.updateForScroll(); });
   }
 
   @override
@@ -436,6 +438,7 @@
         context: context,
         value: _value,
         debugRequiredFor: widget,
+        layerLink: _layerLink,
         renderObject: renderObject,
         onSelectionOverlayChanged: _handleSelectionOverlayChanged,
         selectionControls: widget.selectionControls,
@@ -538,19 +541,22 @@
       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,
+          ),
         );
       },
     );
diff --git a/packages/flutter/lib/src/widgets/scroll_controller.dart b/packages/flutter/lib/src/widgets/scroll_controller.dart
index 4f8b268..aeb1dea 100644
--- a/packages/flutter/lib/src/widgets/scroll_controller.dart
+++ b/packages/flutter/lib/src/widgets/scroll_controller.dart
@@ -24,6 +24,10 @@
 /// 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:
diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart
index d589eb5..e5a8846 100644
--- a/packages/flutter/lib/src/widgets/text_selection.dart
+++ b/packages/flutter/lib/src/widgets/text_selection.dart
@@ -73,8 +73,7 @@
   /// 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 @@
     @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 @@
   /// 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 @@
   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 @@
     }
   }
 
+  /// 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 @@
       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 @@
       (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 _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 @@
         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),
+            ),
+          ],
+        ),
+      ),
     );
   }
 
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 7382e83..530e546 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -27,7 +27,7 @@
 
 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 @@
     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 @@
     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 @@
     // 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 @@
     // 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 @@
     // 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 @@
     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 @@
     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 @@
 
       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'));
diff --git a/packages/flutter/test/widgets/composited_transform_test.dart b/packages/flutter/test/widgets/composited_transform_test.dart
new file mode 100644
index 0000000..e293b8c
--- /dev/null
+++ b/packages/flutter/test/widgets/composited_transform_test.dart
@@ -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);
+  });
+}