| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart: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 snippet} |
| /// |
| /// 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.fromParts(<DiagnosticsNode>[ |
| ErrorSummary( |
| '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().' |
| ), |
| ErrorDescription('This leaves the canvas in an inconsistent state and will probably result in a broken display.'), |
| ErrorHint('You must pair each call to save()/saveLayer() with a later matching call to restore().') |
| ]); |
| } |
| if (debugNewCanvasSaveCount < debugPreviousCanvasSaveCount) { |
| throw FlutterError.fromParts(<DiagnosticsNode>[ |
| ErrorSummary('The $painter custom painter called canvas.restore() ' |
| '${debugPreviousCanvasSaveCount - debugNewCanvasSaveCount} more ' |
| 'time${debugPreviousCanvasSaveCount - debugNewCanvasSaveCount == 1 ? '' : 's' } ' |
| 'than it called canvas.save() or canvas.saveLayer().' |
| ), |
| ErrorDescription('This leaves the canvas in an inconsistent state and will result in a broken display.'), |
| ErrorHint('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.fromParts(<DiagnosticsNode>[ |
| ErrorSummary( |
| '$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 List<DiagnosticsNode> information = <DiagnosticsNode>[]; |
| for (int i = 0; i < newChildSemantics.length; i += 1) { |
| final CustomPainterSemantics child = newChildSemantics[i]; |
| if (child.key != null) { |
| if (keys.containsKey(child.key)) { |
| information.add(ErrorDescription('- duplicate key ${child.key} found at position $i')); |
| } |
| keys[child.key] = i; |
| } |
| } |
| |
| if (information.isNotEmpty) { |
| information.insert(0, ErrorSummary('Failed to update the list of CustomPainterSemantics:')); |
| throw FlutterError.fromParts(information); |
| } |
| |
| 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 (final 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; |
| } |
| } |