// 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 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter/semantics.dart'; import 'package:vector_math/vector_math_64.dart'; import 'box.dart'; import 'object.dart'; import 'proxy_box.dart'; /// Signature of the function returned by [CustomPainter.semanticsBuilder]. /// /// Builds semantics information describing the picture drawn by a /// [CustomPainter]. Each [CustomPainterSemantics] in the returned list is /// converted into a [SemanticsNode] by copying its properties. /// /// The returned list must not be mutated after this function completes. To /// change the semantic information, the function must return a new list /// instead. typedef SemanticsBuilderCallback = List<CustomPainterSemantics> Function(Size size); /// The interface used by [CustomPaint] (in the widgets library) and /// [RenderCustomPaint] (in the rendering library). /// /// To implement a custom painter, either subclass or implement this interface /// to define your custom paint delegate. [CustomPaint] subclasses must /// implement the [paint] and [shouldRepaint] methods, and may optionally also /// implement the [hitTest] and [shouldRebuildSemantics] methods, and the /// [semanticsBuilder] getter. /// /// The [paint] method is called whenever the custom object needs to be repainted. /// /// The [shouldRepaint] method is called when a new instance of the class /// is provided, to check if the new instance actually represents different /// information. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=vvI_NUXK00s} /// /// The most efficient way to trigger a repaint is to either: /// /// * Extend this class and supply a `repaint` argument to the constructor of /// the [CustomPainter], where that object notifies its listeners when it is /// time to repaint. /// * Extend [Listenable] (e.g. via [ChangeNotifier]) and implement /// [CustomPainter], so that the object itself provides the notifications /// directly. /// /// In either case, the [CustomPaint] widget or [RenderCustomPaint] /// render object will listen to the [Listenable] and repaint whenever the /// animation ticks, avoiding both the build and layout phases of the pipeline. /// /// The [hitTest] method is called when the user interacts with the underlying /// render object, to determine if the user hit the object or missed it. /// /// The [semanticsBuilder] is called whenever the custom object needs to rebuild /// its semantics information. /// /// The [shouldRebuildSemantics] method is called when a new instance of the /// class is provided, to check if the new instance contains different /// information that affects the semantics tree. /// /// {@tool sample} /// /// This sample extends the same code shown for [RadialGradient] to create a /// custom painter that paints a sky. /// /// ```dart /// class Sky extends CustomPainter { /// @override /// void paint(Canvas canvas, Size size) { /// var rect = Offset.zero & size; /// var gradient = RadialGradient( /// center: const Alignment(0.7, -0.6), /// radius: 0.2, /// colors: [const Color(0xFFFFFF00), const Color(0xFF0099FF)], /// stops: [0.4, 1.0], /// ); /// canvas.drawRect( /// rect, /// Paint()..shader = gradient.createShader(rect), /// ); /// } /// /// @override /// SemanticsBuilderCallback get semanticsBuilder { /// return (Size size) { /// // Annotate a rectangle containing the picture of the sun /// // with the label "Sun". When text to speech feature is enabled on the /// // device, a user will be able to locate the sun on this picture by /// // touch. /// var rect = Offset.zero & size; /// var width = size.shortestSide * 0.4; /// rect = const Alignment(0.8, -0.9).inscribe(Size(width, width), rect); /// return [ /// CustomPainterSemantics( /// rect: rect, /// properties: SemanticsProperties( /// label: 'Sun', /// textDirection: TextDirection.ltr, /// ), /// ), /// ]; /// }; /// } /// /// // Since this Sky painter has no fields, it always paints /// // the same thing and semantics information is the same. /// // Therefore we return false here. If we had fields (set /// // from the constructor) then we would return true if any /// // of them differed from the same fields on the oldDelegate. /// @override /// bool shouldRepaint(Sky oldDelegate) => false; /// @override /// bool shouldRebuildSemantics(Sky oldDelegate) => false; /// } /// ``` /// {@end-tool} /// /// See also: /// /// * [Canvas], the class that a custom painter uses to paint. /// * [CustomPaint], the widget that uses [CustomPainter], and whose sample /// code shows how to use the above `Sky` class. /// * [RadialGradient], whose sample code section shows a different take /// on the sample code above. abstract class CustomPainter extends Listenable { /// Creates a custom painter. /// /// The painter will repaint whenever `repaint` notifies its listeners. const CustomPainter({ Listenable repaint }) : _repaint = repaint; final Listenable _repaint; /// Register a closure to be notified when it is time to repaint. /// /// The [CustomPainter] implementation merely forwards to the same method on /// the [Listenable] provided to the constructor in the `repaint` argument, if /// it was not null. @override void addListener(VoidCallback listener) => _repaint?.addListener(listener); /// Remove a previously registered closure from the list of closures that the /// object notifies when it is time to repaint. /// /// The [CustomPainter] implementation merely forwards to the same method on /// the [Listenable] provided to the constructor in the `repaint` argument, if /// it was not null. @override void removeListener(VoidCallback listener) => _repaint?.removeListener(listener); /// Called whenever the object needs to paint. The given [Canvas] has its /// coordinate space configured such that the origin is at the top left of the /// box. The area of the box is the size of the [size] argument. /// /// Paint operations should remain inside the given area. Graphical /// operations outside the bounds may be silently ignored, clipped, or not /// clipped. It may sometimes be difficult to guarantee that a certain /// operation is inside the bounds (e.g., drawing a rectangle whose size is /// determined by user inputs). In that case, consider calling /// [Canvas.clipRect] at the beginning of [paint] so everything that follows /// will be guaranteed to only draw within the clipped area. /// /// Implementations should be wary of correctly pairing any calls to /// [Canvas.save]/[Canvas.saveLayer] and [Canvas.restore], otherwise all /// subsequent painting on this canvas may be affected, with potentially /// hilarious but confusing results. /// /// To paint text on a [Canvas], use a [TextPainter]. /// /// To paint an image on a [Canvas]: /// /// 1. Obtain an [ImageStream], for example by calling [ImageProvider.resolve] /// on an [AssetImage] or [NetworkImage] object. /// /// 2. Whenever the [ImageStream]'s underlying [ImageInfo] object changes /// (see [ImageStream.addListener]), create a new instance of your custom /// paint delegate, giving it the new [ImageInfo] object. /// /// 3. In your delegate's [paint] method, call the [Canvas.drawImage], /// [Canvas.drawImageRect], or [Canvas.drawImageNine] methods to paint the /// [ImageInfo.image] object, applying the [ImageInfo.scale] value to /// obtain the correct rendering size. void paint(Canvas canvas, Size size); /// Returns a function that builds semantic information for the picture drawn /// by this painter. /// /// If the returned function is null, this painter will not contribute new /// [SemanticsNode]s to the semantics tree and the [CustomPaint] corresponding /// to this painter will not create a semantics boundary. However, if /// [CustomPaint.child] is not null, the child may contribute [SemanticsNode]s /// to the tree. /// /// See also: /// /// * [SemanticsConfiguration.isSemanticBoundary], which causes new /// [SemanticsNode]s to be added to the semantics tree. /// * [RenderCustomPaint], which uses this getter to build semantics. SemanticsBuilderCallback get semanticsBuilder => null; /// Called whenever a new instance of the custom painter delegate class is /// provided to the [RenderCustomPaint] object, or any time that a new /// [CustomPaint] object is created with a new instance of the custom painter /// delegate class (which amounts to the same thing, because the latter is /// implemented in terms of the former). /// /// If the new instance would cause [semanticsBuilder] to create different /// semantics information, then this method should return true, otherwise it /// should return false. /// /// If the method returns false, then the [semanticsBuilder] call might be /// optimized away. /// /// It's possible that the [semanticsBuilder] will get called even if /// [shouldRebuildSemantics] would return false. For example, it is called /// when the [CustomPaint] is rendered for the very first time, or when the /// box changes its size. /// /// By default this method delegates to [shouldRepaint] under the assumption /// that in most cases semantics change when something new is drawn. bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => shouldRepaint(oldDelegate); /// Called whenever a new instance of the custom painter delegate class is /// provided to the [RenderCustomPaint] object, or any time that a new /// [CustomPaint] object is created with a new instance of the custom painter /// delegate class (which amounts to the same thing, because the latter is /// implemented in terms of the former). /// /// If the new instance represents different information than the old /// instance, then the method should return true, otherwise it should return /// false. /// /// If the method returns false, then the [paint] call might be optimized /// away. /// /// It's possible that the [paint] method will get called even if /// [shouldRepaint] returns false (e.g. if an ancestor or descendant needed to /// be repainted). It's also possible that the [paint] method will get called /// without [shouldRepaint] being called at all (e.g. if the box changes /// size). /// /// If a custom delegate has a particularly expensive paint function such that /// repaints should be avoided as much as possible, a [RepaintBoundary] or /// [RenderRepaintBoundary] (or other render object with /// [RenderObject.isRepaintBoundary] set to true) might be helpful. /// /// The `oldDelegate` argument will never be null. bool shouldRepaint(covariant CustomPainter oldDelegate); /// Called whenever a hit test is being performed on an object that is using /// this custom paint delegate. /// /// The given point is relative to the same coordinate space as the last /// [paint] call. /// /// The default behavior is to consider all points to be hits for /// background painters, and no points to be hits for foreground painters. /// /// Return true if the given position corresponds to a point on the drawn /// image that should be considered a "hit", false if it corresponds to a /// point that should be considered outside the painted image, and null to use /// the default behavior. bool hitTest(Offset position) => null; @override String toString() => '${describeIdentity(this)}(${ _repaint?.toString() ?? "" })'; } /// Contains properties describing information drawn in a rectangle contained by /// the [Canvas] used by a [CustomPaint]. /// /// This information is used, for example, by assistive technologies to improve /// the accessibility of applications. /// /// Implement [CustomPainter.semanticsBuilder] to build the semantic /// description of the whole picture drawn by a [CustomPaint], rather that one /// particular rectangle. /// /// See also: /// /// * [SemanticsNode], which is created using the properties of this class. /// * [CustomPainter], which creates instances of this class. @immutable class CustomPainterSemantics { /// Creates semantics information describing a rectangle on a canvas. /// /// Arguments `rect` and `properties` must not be null. const CustomPainterSemantics({ this.key, @required this.rect, @required this.properties, this.transform, this.tags, }) : assert(rect != null), assert(properties != null); /// Identifies this object in a list of siblings. /// /// [SemanticsNode] inherits this key, so that when the list of nodes is /// updated, its nodes are updated from [CustomPainterSemantics] with matching /// keys. /// /// If this is null, the update algorithm does not guarantee which /// [SemanticsNode] will be updated using this instance. /// /// This value is assigned to [SemanticsNode.key] during update. final Key key; /// The location and size of the box on the canvas where this piece of semantic /// information applies. /// /// This value is assigned to [SemanticsNode.rect] during update. final Rect rect; /// The transform from the canvas' coordinate system to its parent's /// coordinate system. /// /// This value is assigned to [SemanticsNode.transform] during update. final Matrix4 transform; /// Contains properties that are assigned to the [SemanticsNode] created or /// updated from this object. /// /// See also: /// /// * [Semantics], which is a widget that also uses [SemanticsProperties] to /// annotate. final SemanticsProperties properties; /// Tags used by the parent [SemanticsNode] to determine the layout of the /// semantics tree. /// /// This value is assigned to [SemanticsNode.tags] during update. final Set<SemanticsTag> tags; } /// Provides a canvas on which to draw during the paint phase. /// /// When asked to paint, [RenderCustomPaint] first asks its [painter] to paint /// on the current canvas, then it paints its child, and then, after painting /// its child, it asks its [foregroundPainter] to paint. The coordinate system of /// the canvas matches the coordinate system of the [CustomPaint] object. The /// painters are expected to paint within a rectangle starting at the origin and /// encompassing a region of the given size. (If the painters paint outside /// those bounds, there might be insufficient memory allocated to rasterize the /// painting commands and the resulting behavior is undefined.) /// /// Painters are implemented by subclassing or implementing [CustomPainter]. /// /// Because custom paint calls its painters during paint, you cannot mark the /// tree as needing a new layout during the callback (the layout for this frame /// has already happened). /// /// Custom painters normally size themselves to their child. If they do not have /// a child, they attempt to size themselves to the [preferredSize], which /// defaults to [Size.zero]. /// /// See also: /// /// * [CustomPainter], the class that custom painter delegates should extend. /// * [Canvas], the API provided to custom painter delegates. class RenderCustomPaint extends RenderProxyBox { /// Creates a render object that delegates its painting. RenderCustomPaint({ CustomPainter painter, CustomPainter foregroundPainter, Size preferredSize = Size.zero, this.isComplex = false, this.willChange = false, RenderBox child, }) : assert(preferredSize != null), _painter = painter, _foregroundPainter = foregroundPainter, _preferredSize = preferredSize, super(child); /// The background custom paint delegate. /// /// This painter, if non-null, is called to paint behind the children. CustomPainter get painter => _painter; CustomPainter _painter; /// Set a new background custom paint delegate. /// /// If the new delegate is the same as the previous one, this does nothing. /// /// If the new delegate is the same class as the previous one, then the new /// delegate has its [CustomPainter.shouldRepaint] called; if the result is /// true, then the delegate will be called. /// /// If the new delegate is a different class than the previous one, then the /// delegate will be called. /// /// If the new value is null, then there is no background custom painter. set painter(CustomPainter value) { if (_painter == value) return; final CustomPainter oldPainter = _painter; _painter = value; _didUpdatePainter(_painter, oldPainter); } /// The foreground custom paint delegate. /// /// This painter, if non-null, is called to paint in front of the children. CustomPainter get foregroundPainter => _foregroundPainter; CustomPainter _foregroundPainter; /// Set a new foreground custom paint delegate. /// /// If the new delegate is the same as the previous one, this does nothing. /// /// If the new delegate is the same class as the previous one, then the new /// delegate has its [CustomPainter.shouldRepaint] called; if the result is /// true, then the delegate will be called. /// /// If the new delegate is a different class than the previous one, then the /// delegate will be called. /// /// If the new value is null, then there is no foreground custom painter. set foregroundPainter(CustomPainter value) { if (_foregroundPainter == value) return; final CustomPainter oldPainter = _foregroundPainter; _foregroundPainter = value; _didUpdatePainter(_foregroundPainter, oldPainter); } void _didUpdatePainter(CustomPainter newPainter, CustomPainter oldPainter) { // Check if we need to repaint. if (newPainter == null) { assert(oldPainter != null); // We should be called only for changes. markNeedsPaint(); } else if (oldPainter == null || newPainter.runtimeType != oldPainter.runtimeType || newPainter.shouldRepaint(oldPainter)) { markNeedsPaint(); } if (attached) { oldPainter?.removeListener(markNeedsPaint); newPainter?.addListener(markNeedsPaint); } // Check if we need to rebuild semantics. if (newPainter == null) { assert(oldPainter != null); // We should be called only for changes. if (attached) markNeedsSemanticsUpdate(); } else if (oldPainter == null || newPainter.runtimeType != oldPainter.runtimeType || newPainter.shouldRebuildSemantics(oldPainter)) { markNeedsSemanticsUpdate(); } } /// The size that this [RenderCustomPaint] should aim for, given the layout /// constraints, if there is no child. /// /// Defaults to [Size.zero]. /// /// If there's a child, this is ignored, and the size of the child is used /// instead. Size get preferredSize => _preferredSize; Size _preferredSize; set preferredSize(Size value) { assert(value != null); if (preferredSize == value) return; _preferredSize = value; markNeedsLayout(); } /// Whether to hint that this layer's painting should be cached. /// /// The compositor contains a raster cache that holds bitmaps of layers in /// order to avoid the cost of repeatedly rendering those layers on each /// frame. If this flag is not set, then the compositor will apply its own /// heuristics to decide whether the this layer is complex enough to benefit /// from caching. bool isComplex; /// Whether the raster cache should be told that this painting is likely /// to change in the next frame. bool willChange; @override void attach(PipelineOwner owner) { super.attach(owner); _painter?.addListener(markNeedsPaint); _foregroundPainter?.addListener(markNeedsPaint); } @override void detach() { _painter?.removeListener(markNeedsPaint); _foregroundPainter?.removeListener(markNeedsPaint); super.detach(); } @override bool hitTestChildren(BoxHitTestResult result, { Offset position }) { if (_foregroundPainter != null && (_foregroundPainter.hitTest(position) ?? false)) return true; return super.hitTestChildren(result, position: position); } @override bool hitTestSelf(Offset position) { return _painter != null && (_painter.hitTest(position) ?? true); } @override void performResize() { size = constraints.constrain(preferredSize); markNeedsSemanticsUpdate(); } void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) { int debugPreviousCanvasSaveCount; canvas.save(); assert(() { debugPreviousCanvasSaveCount = canvas.getSaveCount(); return true; }()); if (offset != Offset.zero) canvas.translate(offset.dx, offset.dy); painter.paint(canvas, size); assert(() { // This isn't perfect. For example, we can't catch the case of // someone first restoring, then setting a transform or whatnot, // then saving. // If this becomes a real problem, we could add logic to the // Canvas class to lock the canvas at a particular save count // such that restore() fails if it would take the lock count // below that number. final int debugNewCanvasSaveCount = canvas.getSaveCount(); if (debugNewCanvasSaveCount > debugPreviousCanvasSaveCount) { throw FlutterError( 'The $painter custom painter called canvas.save() or canvas.saveLayer() at least ' '${debugNewCanvasSaveCount - debugPreviousCanvasSaveCount} more ' 'time${debugNewCanvasSaveCount - debugPreviousCanvasSaveCount == 1 ? '' : 's' } ' 'than it called canvas.restore().\n' 'This leaves the canvas in an inconsistent state and will probably result in a broken display.\n' 'You must pair each call to save()/saveLayer() with a later matching call to restore().' ); } if (debugNewCanvasSaveCount < debugPreviousCanvasSaveCount) { throw FlutterError( 'The $painter custom painter called canvas.restore() ' '${debugPreviousCanvasSaveCount - debugNewCanvasSaveCount} more ' 'time${debugPreviousCanvasSaveCount - debugNewCanvasSaveCount == 1 ? '' : 's' } ' 'than it called canvas.save() or canvas.saveLayer().\n' 'This leaves the canvas in an inconsistent state and will result in a broken display.\n' 'You should only call restore() if you first called save() or saveLayer().' ); } return debugNewCanvasSaveCount == debugPreviousCanvasSaveCount; }()); canvas.restore(); } @override void paint(PaintingContext context, Offset offset) { if (_painter != null) { _paintWithPainter(context.canvas, offset, _painter); _setRasterCacheHints(context); } super.paint(context, offset); if (_foregroundPainter != null) { _paintWithPainter(context.canvas, offset, _foregroundPainter); _setRasterCacheHints(context); } } void _setRasterCacheHints(PaintingContext context) { if (isComplex) context.setIsComplexHint(); if (willChange) context.setWillChangeHint(); } /// Builds semantics for the picture drawn by [painter]. SemanticsBuilderCallback _backgroundSemanticsBuilder; /// Builds semantics for the picture drawn by [foregroundPainter]. SemanticsBuilderCallback _foregroundSemanticsBuilder; @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); _backgroundSemanticsBuilder = painter?.semanticsBuilder; _foregroundSemanticsBuilder = foregroundPainter?.semanticsBuilder; config.isSemanticBoundary = _backgroundSemanticsBuilder != null || _foregroundSemanticsBuilder != null; } /// Describe the semantics of the picture painted by the [painter]. List<SemanticsNode> _backgroundSemanticsNodes; /// Describe the semantics of the picture painted by the [foregroundPainter]. List<SemanticsNode> _foregroundSemanticsNodes; @override void assembleSemanticsNode( SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children, ) { assert(() { if (child == null && children.isNotEmpty) { throw FlutterError( '$runtimeType does not have a child widget but received a non-empty list of child SemanticsNode:\n' '${children.join('\n')}' ); } return true; }()); final List<CustomPainterSemantics> backgroundSemantics = _backgroundSemanticsBuilder != null ? _backgroundSemanticsBuilder(size) : const <CustomPainterSemantics>[]; _backgroundSemanticsNodes = _updateSemanticsChildren(_backgroundSemanticsNodes, backgroundSemantics); final List<CustomPainterSemantics> foregroundSemantics = _foregroundSemanticsBuilder != null ? _foregroundSemanticsBuilder(size) : const <CustomPainterSemantics>[]; _foregroundSemanticsNodes = _updateSemanticsChildren(_foregroundSemanticsNodes, foregroundSemantics); final bool hasBackgroundSemantics = _backgroundSemanticsNodes != null && _backgroundSemanticsNodes.isNotEmpty; final bool hasForegroundSemantics = _foregroundSemanticsNodes != null && _foregroundSemanticsNodes.isNotEmpty; final List<SemanticsNode> finalChildren = <SemanticsNode>[ if (hasBackgroundSemantics) ..._backgroundSemanticsNodes, ...children, if (hasForegroundSemantics) ..._foregroundSemanticsNodes, ]; super.assembleSemanticsNode(node, config, finalChildren); } @override void clearSemantics() { super.clearSemantics(); _backgroundSemanticsNodes = null; _foregroundSemanticsNodes = null; } /// Updates the nodes of `oldSemantics` using data in `newChildSemantics`, and /// returns a new list containing child nodes sorted according to the order /// specified by `newChildSemantics`. /// /// [SemanticsNode]s that match [CustomPainterSemantics] by [Key]s preserve /// their [SemanticsNode.key] field. If a node with the same key appears in /// a different position in the list, it is moved to the new position, but the /// same object is reused. /// /// [SemanticsNode]s whose `key` is null may be updated from /// [CustomPainterSemantics] whose `key` is also null. However, the algorithm /// does not guarantee it. If your semantics require that specific nodes are /// updated from specific [CustomPainterSemantics], it is recommended to match /// them by specifying non-null keys. /// /// The algorithm tries to be as close to [RenderObjectElement.updateChildren] /// as possible, deviating only where the concepts diverge between widgets and /// semantics. For example, a [SemanticsNode] can be updated from a /// [CustomPainterSemantics] based on `Key` alone; their types are not /// considered because there is only one type of [SemanticsNode]. There is no /// concept of a "forgotten" node in semantics, deactivated nodes, or global /// keys. static List<SemanticsNode> _updateSemanticsChildren( List<SemanticsNode> oldSemantics, List<CustomPainterSemantics> newChildSemantics, ) { oldSemantics = oldSemantics ?? const <SemanticsNode>[]; newChildSemantics = newChildSemantics ?? const <CustomPainterSemantics>[]; assert(() { final Map<Key, int> keys = HashMap<Key, int>(); final StringBuffer errors = StringBuffer(); for (int i = 0; i < newChildSemantics.length; i += 1) { final CustomPainterSemantics child = newChildSemantics[i]; if (child.key != null) { if (keys.containsKey(child.key)) { errors.writeln( '- duplicate key ${child.key} found at position $i', ); } keys[child.key] = i; } } if (errors.isNotEmpty) { throw FlutterError( 'Failed to update the list of CustomPainterSemantics:\n' '$errors' ); } return true; }()); int newChildrenTop = 0; int oldChildrenTop = 0; int newChildrenBottom = newChildSemantics.length - 1; int oldChildrenBottom = oldSemantics.length - 1; final List<SemanticsNode> newChildren = List<SemanticsNode>(newChildSemantics.length); // Update the top of the list. while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { final SemanticsNode oldChild = oldSemantics[oldChildrenTop]; final CustomPainterSemantics newSemantics = newChildSemantics[newChildrenTop]; if (!_canUpdateSemanticsChild(oldChild, newSemantics)) break; final SemanticsNode newChild = _updateSemanticsChild(oldChild, newSemantics); newChildren[newChildrenTop] = newChild; newChildrenTop += 1; oldChildrenTop += 1; } // Scan the bottom of the list. while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { final SemanticsNode oldChild = oldSemantics[oldChildrenBottom]; final CustomPainterSemantics newChild = newChildSemantics[newChildrenBottom]; if (!_canUpdateSemanticsChild(oldChild, newChild)) break; oldChildrenBottom -= 1; newChildrenBottom -= 1; } // Scan the old children in the middle of the list. final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom; Map<Key, SemanticsNode> oldKeyedChildren; if (haveOldChildren) { oldKeyedChildren = <Key, SemanticsNode>{}; while (oldChildrenTop <= oldChildrenBottom) { final SemanticsNode oldChild = oldSemantics[oldChildrenTop]; if (oldChild.key != null) oldKeyedChildren[oldChild.key] = oldChild; oldChildrenTop += 1; } } // Update the middle of the list. while (newChildrenTop <= newChildrenBottom) { SemanticsNode oldChild; final CustomPainterSemantics newSemantics = newChildSemantics[newChildrenTop]; if (haveOldChildren) { final Key key = newSemantics.key; if (key != null) { oldChild = oldKeyedChildren[key]; if (oldChild != null) { if (_canUpdateSemanticsChild(oldChild, newSemantics)) { // we found a match! // remove it from oldKeyedChildren so we don't unsync it later oldKeyedChildren.remove(key); } else { // Not a match, let's pretend we didn't see it for now. oldChild = null; } } } } assert(oldChild == null || _canUpdateSemanticsChild(oldChild, newSemantics)); final SemanticsNode newChild = _updateSemanticsChild(oldChild, newSemantics); assert(oldChild == newChild || oldChild == null); newChildren[newChildrenTop] = newChild; newChildrenTop += 1; } // We've scanned the whole list. assert(oldChildrenTop == oldChildrenBottom + 1); assert(newChildrenTop == newChildrenBottom + 1); assert(newChildSemantics.length - newChildrenTop == oldSemantics.length - oldChildrenTop); newChildrenBottom = newChildSemantics.length - 1; oldChildrenBottom = oldSemantics.length - 1; // Update the bottom of the list. while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { final SemanticsNode oldChild = oldSemantics[oldChildrenTop]; final CustomPainterSemantics newSemantics = newChildSemantics[newChildrenTop]; assert(_canUpdateSemanticsChild(oldChild, newSemantics)); final SemanticsNode newChild = _updateSemanticsChild(oldChild, newSemantics); assert(oldChild == newChild); newChildren[newChildrenTop] = newChild; newChildrenTop += 1; oldChildrenTop += 1; } assert(() { for (SemanticsNode node in newChildren) { assert(node != null); } return true; }()); return newChildren; } /// Whether `oldChild` can be updated with properties from `newSemantics`. /// /// If `oldChild` can be updated, it is updated using [_updateSemanticsChild]. /// Otherwise, the node is replaced by a new instance of [SemanticsNode]. static bool _canUpdateSemanticsChild(SemanticsNode oldChild, CustomPainterSemantics newSemantics) { return oldChild.key == newSemantics.key; } /// Updates `oldChild` using the properties of `newSemantics`. /// /// This method requires that `_canUpdateSemanticsChild(oldChild, newSemantics)` /// is true prior to calling it. static SemanticsNode _updateSemanticsChild(SemanticsNode oldChild, CustomPainterSemantics newSemantics) { assert(oldChild == null || _canUpdateSemanticsChild(oldChild, newSemantics)); final SemanticsNode newChild = oldChild ?? SemanticsNode( key: newSemantics.key, ); final SemanticsProperties properties = newSemantics.properties; final SemanticsConfiguration config = SemanticsConfiguration(); if (properties.sortKey != null) { config.sortKey = properties.sortKey; } if (properties.checked != null) { config.isChecked = properties.checked; } if (properties.selected != null) { config.isSelected = properties.selected; } if (properties.button != null) { config.isButton = properties.button; } if (properties.link != null) { config.isLink = properties.link; } if (properties.textField != null) { config.isTextField = properties.textField; } if (properties.readOnly != null) { config.isReadOnly = properties.readOnly; } if (properties.focusable != null) { config.isFocusable = properties.focusable; } if (properties.focused != null) { config.isFocused = properties.focused; } if (properties.enabled != null) { config.isEnabled = properties.enabled; } if (properties.inMutuallyExclusiveGroup != null) { config.isInMutuallyExclusiveGroup = properties.inMutuallyExclusiveGroup; } if (properties.obscured != null) { config.isObscured = properties.obscured; } if (properties.multiline != null) { config.isMultiline = properties.multiline; } if (properties.hidden != null) { config.isHidden = properties.hidden; } if (properties.header != null) { config.isHeader = properties.header; } if (properties.scopesRoute != null) { config.scopesRoute = properties.scopesRoute; } if (properties.namesRoute != null) { config.namesRoute = properties.namesRoute; } if (properties.liveRegion != null) { config.liveRegion = properties.liveRegion; } if (properties.maxValueLength != null) { config.maxValueLength = properties.maxValueLength; } if (properties.currentValueLength != null) { config.currentValueLength = properties.currentValueLength; } if (properties.toggled != null) { config.isToggled = properties.toggled; } if (properties.image != null) { config.isImage = properties.image; } if (properties.label != null) { config.label = properties.label; } if (properties.value != null) { config.value = properties.value; } if (properties.increasedValue != null) { config.increasedValue = properties.increasedValue; } if (properties.decreasedValue != null) { config.decreasedValue = properties.decreasedValue; } if (properties.hint != null) { config.hint = properties.hint; } if (properties.textDirection != null) { config.textDirection = properties.textDirection; } if (properties.onTap != null) { config.onTap = properties.onTap; } if (properties.onLongPress != null) { config.onLongPress = properties.onLongPress; } if (properties.onScrollLeft != null) { config.onScrollLeft = properties.onScrollLeft; } if (properties.onScrollRight != null) { config.onScrollRight = properties.onScrollRight; } if (properties.onScrollUp != null) { config.onScrollUp = properties.onScrollUp; } if (properties.onScrollDown != null) { config.onScrollDown = properties.onScrollDown; } if (properties.onIncrease != null) { config.onIncrease = properties.onIncrease; } if (properties.onDecrease != null) { config.onDecrease = properties.onDecrease; } if (properties.onCopy != null) { config.onCopy = properties.onCopy; } if (properties.onCut != null) { config.onCut = properties.onCut; } if (properties.onPaste != null) { config.onPaste = properties.onPaste; } if (properties.onMoveCursorForwardByCharacter != null) { config.onMoveCursorForwardByCharacter = properties.onMoveCursorForwardByCharacter; } if (properties.onMoveCursorBackwardByCharacter != null) { config.onMoveCursorBackwardByCharacter = properties.onMoveCursorBackwardByCharacter; } if (properties.onMoveCursorForwardByWord != null) { config.onMoveCursorForwardByWord = properties.onMoveCursorForwardByWord; } if (properties.onMoveCursorBackwardByWord != null) { config.onMoveCursorBackwardByWord = properties.onMoveCursorBackwardByWord; } if (properties.onSetSelection != null) { config.onSetSelection = properties.onSetSelection; } if (properties.onDidGainAccessibilityFocus != null) { config.onDidGainAccessibilityFocus = properties.onDidGainAccessibilityFocus; } if (properties.onDidLoseAccessibilityFocus != null) { config.onDidLoseAccessibilityFocus = properties.onDidLoseAccessibilityFocus; } if (properties.onDismiss != null) { config.onDismiss = properties.onDismiss; } newChild.updateWith( config: config, // As of now CustomPainter does not support multiple tree levels. childrenInInversePaintOrder: const <SemanticsNode>[], ); newChild ..rect = newSemantics.rect ..transform = newSemantics.transform ..tags = newSemantics.tags; return newChild; } }