diff --git a/lib/src/custom_follower.dart b/lib/src/custom_follower.dart index 4c0f12e..b018d0a 100644 --- a/lib/src/custom_follower.dart +++ b/lib/src/custom_follower.dart @@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:vector_math/vector_math_64.dart'; import 'anchor.dart'; +import 'flutter_src/layer.dart'; import 'portal.dart'; /// @nodoc @@ -24,7 +25,7 @@ class CustomCompositedTransformFollower extends SingleChildRenderObjectWidget { final Anchor anchor; /// @nodoc - final LayerLink link; + final MyLayerLink link; /// @nodoc final OverlayLink overlayLink; @@ -58,7 +59,7 @@ class CustomCompositedTransformFollower extends SingleChildRenderObjectWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('anchor', anchor)); - properties.add(DiagnosticsProperty('link', link)); + properties.add(DiagnosticsProperty('link', link)); properties .add(DiagnosticsProperty('overlayLink', overlayLink)); properties.add(DiagnosticsProperty('targetSize', targetSize)); @@ -70,7 +71,7 @@ class CustomCompositedTransformFollower extends SingleChildRenderObjectWidget { class CustomRenderFollowerLayer extends RenderProxyBox { /// @nodoc CustomRenderFollowerLayer({ - required LayerLink link, + required MyLayerLink link, required OverlayLink overlayLink, required Size targetSize, required Anchor anchor, @@ -93,12 +94,12 @@ class CustomRenderFollowerLayer extends RenderProxyBox { } } - LayerLink _link; + MyLayerLink _link; /// @nodoc - LayerLink get link => _link; + MyLayerLink get link => _link; - set link(LayerLink value) { + set link(MyLayerLink value) { if (_link == value) { return; } @@ -170,7 +171,7 @@ class CustomRenderFollowerLayer extends RenderProxyBox { /// [PortalTarget]. /// /// The reason we cannot simply access the [link]'s leader in [paint] is that - /// the leader is only attached to the [LayerLink] in [LeaderLayer.attach], + /// the leader is only attached to the [MyLayerLink] in [LeaderLayer.attach], /// which is called in the compositing phase which is after the paint phase. Offset _computeLinkedOffset(Offset leaderOffset) { assert( @@ -234,7 +235,7 @@ class CustomRenderFollowerLayer extends RenderProxyBox { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('link', link)); + properties.add(DiagnosticsProperty('link', link)); properties .add(DiagnosticsProperty('overlayLink', overlayLink)); properties.add( @@ -266,7 +267,7 @@ class _CustomFollowerLayer extends ContainerLayer { required this.linkedOffsetCallback, }); - LayerLink link; + MyLayerLink link; /// Callback that is called to compute the linked offset of the follower layer /// based on the `leaderOffset` of the leader layer. @@ -457,7 +458,7 @@ class _CustomFollowerLayer extends ContainerLayer { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('link', link)); + properties.add(DiagnosticsProperty('link', link)); properties.add( TransformProperty('transform', getLastTransform(), defaultValue: null)); properties.add(DiagnosticsProperty( diff --git a/lib/src/flutter_src/basic.dart b/lib/src/flutter_src/basic.dart new file mode 100644 index 0000000..e54660d --- /dev/null +++ b/lib/src/flutter_src/basic.dart @@ -0,0 +1,57 @@ +// ignore_for_file: unnecessary_null_comparison, diagnostic_describe_all_properties + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'layer.dart'; +import 'proxy_box.dart'; + +/// A widget that can be targeted 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 +/// [MyLayerLink] can position themselves at the same screen location. +/// +/// A single [MyCompositedTransformTarget] can be followed by multiple +/// [CompositedTransformFollower] widgets. +/// +/// The [MyCompositedTransformTarget] 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 MyCompositedTransformTarget 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 [MyCompositedTransformTarget] object that is in the tree. + const MyCompositedTransformTarget({ + Key? key, + required this.link, + Widget? child, + }) : assert(link != null), + super(key: key, child: child); + + /// The link object that connects this [MyCompositedTransformTarget] with one or + /// more [CompositedTransformFollower]s. + /// + /// This property must not be null. The object must not be associated with + /// another [MyCompositedTransformTarget] that is also being painted. + final MyLayerLink link; + + @override + MyRenderLeaderLayer createRenderObject(BuildContext context) { + return MyRenderLeaderLayer( + link: link, + ); + } + + @override + void updateRenderObject( + BuildContext context, MyRenderLeaderLayer renderObject) { + renderObject.link = link; + } +} diff --git a/lib/src/flutter_src/layer.dart b/lib/src/flutter_src/layer.dart new file mode 100644 index 0000000..a729754 --- /dev/null +++ b/lib/src/flutter_src/layer.dart @@ -0,0 +1,191 @@ +// ignore_for_file: comment_references, unnecessary_null_comparison, curly_braces_in_flow_control_structures, prefer_int_literals, diagnostic_describe_all_properties, omit_local_variable_types, avoid_types_on_closure_parameters, always_put_control_body_on_new_line + +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; + +/// An object that a [MyLeaderLayer] can register with. +/// +/// An instance of this class should be provided as the [MyLeaderLayer.link] and +/// the [FollowerLayer.link] properties to cause the [FollowerLayer] to follow +/// the [MyLeaderLayer]. +/// +/// See also: +/// +/// * [CompositedTransformTarget], the widget that creates a [MyLeaderLayer]. +/// * [CompositedTransformFollower], the widget that creates a [FollowerLayer]. +/// * [RenderMyLeaderLayer] and [RenderFollowerLayer], the corresponding +/// render objects. +class MyLayerLink { + /// The [MyLeaderLayer] connected to this link. + MyLeaderLayer? get leader => _leader; + MyLeaderLayer? _leader; + + void _registerLeader(MyLeaderLayer leader) { + assert(_leader != leader); + assert(() { + if (_leader != null) { + _debugPreviousLeaders ??= {}; + _debugScheduleLeadersCleanUpCheck(); + return _debugPreviousLeaders!.add(_leader!); + } + return true; + }()); + _leader = leader; + } + + void _unregisterLeader(MyLeaderLayer leader) { + if (_leader == leader) { + _leader = null; + } else { + assert(_debugPreviousLeaders!.remove(leader)); + } + } + + /// Stores the previous leaders that were replaced by the current [_leader] + /// in the current frame. + /// + /// These leaders need to give up their leaderships of this link by the end of + /// the current frame. + Set? _debugPreviousLeaders; + bool _debugLeaderCheckScheduled = false; + + /// Schedules the check as post frame callback to make sure the + /// [_debugPreviousLeaders] is empty. + void _debugScheduleLeadersCleanUpCheck() { + assert(_debugPreviousLeaders != null); + assert(() { + if (_debugLeaderCheckScheduled) return true; + _debugLeaderCheckScheduled = true; + SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) { + _debugLeaderCheckScheduled = false; + assert(_debugPreviousLeaders!.isEmpty); + }); + return true; + }()); + } + + /// The total size of the content of the connected [MyLeaderLayer]. + /// + /// Generally this should be set by the [RenderObject] that paints on the + /// registered [MyLeaderLayer] (for instance a [RenderMyLeaderLayer] that shares + /// this link with its followers). This size may be outdated before and during + /// layout. + Size? leaderSize; + + @override + String toString() => + '${describeIdentity(this)}(${_leader != null ? "" : ""})'; +} + +/// 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 MyLeaderLayer extends ContainerLayer { + /// Creates a leader layer. + /// + /// The [link] property must not be null, and must not have been provided to + /// any other [MyLeaderLayer] 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. + MyLeaderLayer({required MyLayerLink link, Offset offset = Offset.zero}) + : assert(link != null), + _link = link, + _offset = offset; + + /// 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. + MyLayerLink get link => _link; + MyLayerLink _link; + set link(MyLayerLink value) { + assert(value != null); + if (_link == value) { + return; + } + if (attached) { + _link._unregisterLeader(this); + value._registerLeader(this); + } + _link = value; + } + + /// 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 get offset => _offset; + Offset _offset; + set offset(Offset value) { + assert(value != null); + if (value == _offset) { + return; + } + _offset = value; + if (!alwaysNeedsAddToScene) { + markNeedsAddToScene(); + } + } + + @override + void attach(Object owner) { + super.attach(owner); + _link._registerLeader(this); + } + + @override + void detach() { + _link._unregisterLeader(this); + super.detach(); + } + + @override + bool findAnnotations( + AnnotationResult result, Offset localPosition, + {required bool onlyFirst}) { + return super.findAnnotations(result, localPosition - offset, + onlyFirst: onlyFirst); + } + + @override + void addToScene(ui.SceneBuilder builder) { + assert(offset != null); + if (offset != Offset.zero) + engineLayer = builder.pushTransform( + Matrix4.translationValues(offset.dx, offset.dy, 0.0).storage, + // NOTE XXX edit + oldLayer: engineLayer as ui.TransformEngineLayer?, + ); + addChildrenToScene(builder); + if (offset != 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) { + if (offset != Offset.zero) transform.translate(offset.dx, offset.dy); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('offset', offset)); + properties.add(DiagnosticsProperty('link', link)); + } +} diff --git a/lib/src/flutter_src/proxy_box.dart b/lib/src/flutter_src/proxy_box.dart new file mode 100644 index 0000000..89050dc --- /dev/null +++ b/lib/src/flutter_src/proxy_box.dart @@ -0,0 +1,76 @@ +// ignore_for_file: unnecessary_null_comparison, curly_braces_in_flow_control_structures, omit_local_variable_types, comment_references, always_put_control_body_on_new_line + +import 'package:flutter/rendering.dart'; + +import 'layer.dart'; + +/// Provides an anchor for a [RenderFollowerLayer]. +/// +/// See also: +/// +/// * [CompositedTransformTarget], the corresponding widget. +/// * [MyLeaderLayer], the layer that this render object creates. +class MyRenderLeaderLayer extends RenderProxyBox { + /// Creates a render object that uses a [MyLeaderLayer]. + /// + /// The [link] must not be null. + MyRenderLeaderLayer({ + required MyLayerLink link, + RenderBox? child, + }) : assert(link != null), + _link = link, + super(child); + + /// The link object that connects this [MyRenderLeaderLayer] with one or more + /// [RenderFollowerLayer]s. + /// + /// This property must not be null. The object must not be associated with + /// another [MyRenderLeaderLayer] that is also being painted. + MyLayerLink get link => _link; + MyLayerLink _link; + set link(MyLayerLink value) { + assert(value != null); + if (_link == value) return; + _link.leaderSize = null; + _link = value; + if (_previousLayoutSize != null) { + _link.leaderSize = _previousLayoutSize; + } + markNeedsPaint(); + } + + @override + bool get alwaysNeedsCompositing => true; + + // The latest size of this [RenderBox], computed during the previous layout + // pass. It should always be equal to [size], but can be accessed even when + // [debugDoingThisResize] and [debugDoingThisLayout] are false. + Size? _previousLayoutSize; + + @override + void performLayout() { + super.performLayout(); + _previousLayoutSize = size; + link.leaderSize = size; + } + + @override + void paint(PaintingContext context, Offset offset) { + if (layer == null) { + layer = MyLeaderLayer(link: link, offset: offset); + } else { + final MyLeaderLayer leaderLayer = layer! as MyLeaderLayer; + leaderLayer + ..link = link + ..offset = offset; + } + context.pushLayer(layer!, super.paint, Offset.zero); + assert(layer != null); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('link', link)); + } +} diff --git a/lib/src/portal.dart b/lib/src/portal.dart index d8e08ac..53b1997 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -6,6 +6,8 @@ import 'package:flutter/rendering.dart'; import 'anchor.dart'; import 'custom_follower.dart'; +import 'flutter_src/basic.dart'; +import 'flutter_src/layer.dart'; /// The widget where a [PortalTarget] and its [PortalFollower] are rendered. /// @@ -400,7 +402,7 @@ class PortalTarget extends StatefulWidget { } class _PortalTargetState extends State { - final _link = LayerLink(); + final _link = MyLayerLink(); late bool _visible = widget.visible; Timer? _timer; @@ -445,7 +447,7 @@ class _PortalTargetState extends State { return Stack( children: [ - CompositedTransformTarget( + MyCompositedTransformTarget( link: _link, child: widget.child, ),