diff --git a/packages/flutter/lib/foundation.dart b/packages/flutter/lib/foundation.dart index ae738b57ea5..0fc5096db4d 100644 --- a/packages/flutter/lib/foundation.dart +++ b/packages/flutter/lib/foundation.dart @@ -39,6 +39,7 @@ export 'src/foundation/change_notifier.dart'; export 'src/foundation/collections.dart'; export 'src/foundation/debug.dart'; export 'src/foundation/diagnostics.dart'; +export 'src/foundation/key.dart'; export 'src/foundation/licenses.dart'; export 'src/foundation/node.dart'; export 'src/foundation/observer_list.dart'; diff --git a/packages/flutter/lib/rendering.dart b/packages/flutter/lib/rendering.dart index ee4dbb27cdf..4e4e97d76a1 100644 --- a/packages/flutter/lib/rendering.dart +++ b/packages/flutter/lib/rendering.dart @@ -35,6 +35,7 @@ export 'src/rendering/animated_size.dart'; export 'src/rendering/binding.dart'; export 'src/rendering/box.dart'; export 'src/rendering/custom_layout.dart'; +export 'src/rendering/custom_paint.dart'; export 'src/rendering/debug.dart'; export 'src/rendering/debug_overflow_indicator.dart'; export 'src/rendering/editable.dart'; diff --git a/packages/flutter/lib/src/foundation/key.dart b/packages/flutter/lib/src/foundation/key.dart new file mode 100644 index 00000000000..1969775f202 --- /dev/null +++ b/packages/flutter/lib/src/foundation/key.dart @@ -0,0 +1,86 @@ +// 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:ui' show hashValues; + +import 'package:meta/meta.dart'; + +/// A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s. +/// +/// A new widget will only be used to update an existing element if its key is +/// the same as the key of the current widget associated with the element. +/// +/// Keys must be unique amongst the [Element]s with the same parent. +/// +/// Subclasses of [Key] should either subclass [LocalKey] or [GlobalKey]. +/// +/// See also the discussion at [Widget.key]. +@immutable +abstract class Key { + /// Construct a [ValueKey] with the given [String]. + /// + /// This is the simplest way to create keys. + const factory Key(String value) = ValueKey; + + /// Default constructor, used by subclasses. + /// + /// Useful so that subclasses can call us, because the [new Key] factory + /// constructor shadows the implicit constructor. + @protected + const Key.empty(); +} + +/// A key that is not a [GlobalKey]. +/// +/// Keys must be unique amongst the [Element]s with the same parent. By +/// contrast, [GlobalKey]s must be unique across the entire app. +/// +/// See also the discussion at [Widget.key]. +abstract class LocalKey extends Key { + /// Default constructor, used by subclasses. + const LocalKey() : super.empty(); +} + +/// A key that uses a value of a particular type to identify itself. +/// +/// A [ValueKey] is equal to another [ValueKey] if, and only if, their +/// values are [operator==]. +/// +/// This class can be subclassed to create value keys that will not be equal to +/// other value keys that happen to use the same value. If the subclass is +/// private, this results in a value key type that cannot collide with keys from +/// other sources, which could be useful, for example, if the keys are being +/// used as fallbacks in the same scope as keys supplied from another widget. +/// +/// See also the discussion at [Widget.key]. +class ValueKey extends LocalKey { + /// Creates a key that delegates its [operator==] to the given value. + const ValueKey(this.value); + + /// The value to which this key delegates its [operator==] + final T value; + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) + return false; + final ValueKey typedOther = other; + return value == typedOther.value; + } + + @override + int get hashCode => hashValues(runtimeType, value); + + @override + String toString() { + final String valueString = T == String ? '<\'$value\'>' : '<$value>'; + // The crazy on the next line is a workaround for + // https://github.com/dart-lang/sdk/issues/28548 + if (runtimeType == new _TypeLiteral>().type) + return '[$valueString]'; + return '[$T $valueString]'; + } +} + +class _TypeLiteral { Type get type => T; } diff --git a/packages/flutter/lib/src/material/scrollbar.dart b/packages/flutter/lib/src/material/scrollbar.dart index f7519c8fa80..5b02201c473 100644 --- a/packages/flutter/lib/src/material/scrollbar.dart +++ b/packages/flutter/lib/src/material/scrollbar.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'theme.dart'; @@ -214,4 +215,10 @@ class _ScrollbarPainter extends ChangeNotifier implements CustomPainter { @override bool shouldRepaint(_ScrollbarPainter oldDelegate) => false; + + @override + bool shouldRebuildSemantics(CustomPainter oldDelegate) => false; + + @override + SemanticsBuilderCallback get semanticsBuilder => null; } diff --git a/packages/flutter/lib/src/rendering/custom_paint.dart b/packages/flutter/lib/src/rendering/custom_paint.dart new file mode 100644 index 00000000000..4a703757678 --- /dev/null +++ b/packages/flutter/lib/src/rendering/custom_paint.dart @@ -0,0 +1,862 @@ +// 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 List SemanticsBuilderCallback(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. +/// +/// 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, or to +/// 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. +/// +/// ## Sample code +/// +/// 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 = new 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, +/// new 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(new Size(width, width), rect); +/// return [ +/// new CustomPainterSemantics( +/// rect: rect, +/// properties: new 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. +/// bool shouldRepaint(Sky oldDelegate) => false; +/// bool shouldRebuildSemantics(Sky oldDelegate) => false; +/// } +/// ``` +/// +/// 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. + /// + /// 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. + 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 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. + 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(HitTestResult 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 new 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 new 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 _backgroundSemanticsNodes; + + /// Describe the semantics of the picture painted by the [foregroundPainter]. + List _foregroundSemanticsNodes; + + @override + void assembleSemanticsNode( + SemanticsNode node, + SemanticsConfiguration config, + Iterable children, + ) { + assert(() { + if (child == null && children.isNotEmpty) { + throw new 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 backgroundSemantics = _backgroundSemanticsBuilder != null + ? _backgroundSemanticsBuilder(size) + : const []; + _backgroundSemanticsNodes = _updateSemanticsChildren(_backgroundSemanticsNodes, backgroundSemantics); + + final List foregroundSemantics = _foregroundSemanticsBuilder != null + ? _foregroundSemanticsBuilder(size) + : const []; + _foregroundSemanticsNodes = _updateSemanticsChildren(_foregroundSemanticsNodes, foregroundSemantics); + + final bool hasBackgroundSemantics = _backgroundSemanticsNodes != null && _backgroundSemanticsNodes.isNotEmpty; + final bool hasForegroundSemantics = _foregroundSemanticsNodes != null && _foregroundSemanticsNodes.isNotEmpty; + final List finalChildren = []; + if (hasBackgroundSemantics) + finalChildren.addAll(_backgroundSemanticsNodes); + finalChildren.addAll(children); + if (hasForegroundSemantics) + finalChildren.addAll(_foregroundSemanticsNodes); + super.assembleSemanticsNode(node, config, finalChildren); + } + + /// 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 _updateSemanticsChildren( + List oldSemantics, + List newChildSemantics, + ) { + oldSemantics = oldSemantics ?? const []; + newChildSemantics = newChildSemantics ?? const []; + + assert(() { + final Map keys = new HashMap(); + final StringBuffer errors = new 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 new 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 newChildren = new List(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 oldKeyedChildren; + if (haveOldChildren) { + oldKeyedChildren = {}; + 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 ?? new SemanticsNode( + key: newSemantics.key, + ); + + final SemanticsProperties properties = newSemantics.properties; + final SemanticsConfiguration config = new SemanticsConfiguration(); + + 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.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.addAction(SemanticsAction.tap, properties.onTap); + } + if (properties.onLongPress != null) { + config.addAction(SemanticsAction.longPress, properties.onLongPress); + } + if (properties.onScrollLeft != null) { + config.addAction(SemanticsAction.scrollLeft, properties.onScrollLeft); + } + if (properties.onScrollRight != null) { + config.addAction(SemanticsAction.scrollRight, properties.onScrollRight); + } + if (properties.onScrollUp != null) { + config.addAction(SemanticsAction.scrollUp, properties.onScrollUp); + } + if (properties.onScrollDown != null) { + config.addAction(SemanticsAction.scrollDown, properties.onScrollDown); + } + if (properties.onIncrease != null) { + config.addAction(SemanticsAction.increase, properties.onIncrease); + } + if (properties.onDecrease != null) { + config.addAction(SemanticsAction.decrease, properties.onDecrease); + } + + newChild.updateWith( + config: config, + // As of now CustomPainter does not support multiple tree levels. + childrenInInversePaintOrder: const [], + ); + + newChild + ..rect = newSemantics.rect + ..transform = newSemantics.transform + ..tags = newSemantics.tags; + + return newChild; + } +} diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 232cfdb46c8..07fabe82b28 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -2362,7 +2362,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im /// /// If [isSemanticBoundary] is true, this method is called with the `node` /// created for this [RenderObject], the `config` to be applied to that node - /// and the `children` [SemanticNode]s that decedents of this RenderObject + /// and the `children` [SemanticNode]s that descendants of this RenderObject /// have generated. /// /// By default, the method will annotate `node` with `config` and add the diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index d2ba9725fd2..66320f05e9a 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2054,399 +2054,6 @@ class RenderFractionalTranslation extends RenderProxyBox { } } -/// 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] method. -/// -/// 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. -/// -/// 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, or to -/// 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. -/// -/// ## Sample code -/// -/// 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 = new 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, -/// new Paint()..shader = gradient.createShader(rect), -/// ); -/// } -/// -/// @override -/// bool shouldRepaint(Sky oldDelegate) { -/// // Since this Sky painter has no fields, it always paints -/// // the same thing, and 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. -/// return false; -/// } -/// } -/// ``` -/// -/// 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. - /// - /// 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); - - /// 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. - 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() ?? "" })'; -} - -/// 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) { - 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); - } - } - - /// 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(HitTestResult 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); - } - - 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 new 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 new 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(); - } -} - /// Signature for listening to [PointerDownEvent] events. /// /// Used by [Listener] and [RenderPointerListener]. diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index af9453e617d..e3f83d67e73 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -29,7 +29,7 @@ typedef bool SemanticsNodeVisitor(SemanticsNode node); /// /// Tags can be interpreted by the parent of a [SemanticsNode] /// and depending on the presence of a tag the parent can for example decide -/// how to add the tagged note as a child. Tags are not sent to the engine. +/// how to add the tagged node as a child. Tags are not sent to the engine. /// /// As an example, the [RenderSemanticsGestureHandler] uses tags to determine /// if a child node should be excluded from the scrollable area for semantic @@ -224,6 +224,236 @@ class _SemanticsDiagnosticableNode extends DiagnosticableNode { } } +/// Contains properties used by assistive technologies to make the application +/// more accessible. +/// +/// The properties of this class are used to generate a [SemanticsNode]s in the +/// semantics tree. +@immutable +class SemanticsProperties extends DiagnosticableTree { + /// Creates a semantic annotation. + /// + /// The [container] argument must not be null. + const SemanticsProperties({ + this.checked, + this.selected, + this.button, + this.label, + this.value, + this.increasedValue, + this.decreasedValue, + this.hint, + this.textDirection, + this.onTap, + this.onLongPress, + this.onScrollLeft, + this.onScrollRight, + this.onScrollUp, + this.onScrollDown, + this.onIncrease, + this.onDecrease, + }); + + /// If non-null, indicates that this subtree represents a checkbox + /// or similar widget with a "checked" state, and what its current + /// state is. + final bool checked; + + /// If non-null indicates that this subtree represents something that can be + /// in a selected or unselected state, and what its current state is. + /// + /// The active tab in a tab bar for example is considered "selected", whereas + /// all other tabs are unselected. + final bool selected; + + /// If non-null, indicates that this subtree represents a button. + /// + /// TalkBack/VoiceOver provides users with the hint "button" when a button + /// is focused. + final bool button; + + /// Provides a textual description of the widget. + /// + /// If a label is provided, there must either by an ambient [Directionality] + /// or an explicit [textDirection] should be provided. + /// + /// See also: + /// + /// * [SemanticsConfiguration.label] for a description of how this is exposed + /// in TalkBack and VoiceOver. + final String label; + + /// Provides a textual description of the value of the widget. + /// + /// If a value is provided, there must either by an ambient [Directionality] + /// or an explicit [textDirection] should be provided. + /// + /// See also: + /// + /// * [SemanticsConfiguration.value] for a description of how this is exposed + /// in TalkBack and VoiceOver. + final String value; + + /// The value that [value] will become after a [SemanticsAction.increase] + /// action has been performed on this widget. + /// + /// If a value is provided, [onIncrease] must also be set and there must + /// either be an ambient [Directionality] or an explicit [textDirection] + /// must be provided. + /// + /// See also: + /// + /// * [SemanticsConfiguration.increasedValue] for a description of how this + /// is exposed in TalkBack and VoiceOver. + final String increasedValue; + + /// The value that [value] will become after a [SemanticsAction.decrease] + /// action has been performed on this widget. + /// + /// If a value is provided, [onDecrease] must also be set and there must + /// either be an ambient [Directionality] or an explicit [textDirection] + /// must be provided. + /// + /// See also: + /// + /// * [SemanticsConfiguration.decreasedValue] for a description of how this + /// is exposed in TalkBack and VoiceOver. + final String decreasedValue; + + /// Provides a brief textual description of the result of an action performed + /// on the widget. + /// + /// If a hint is provided, there must either by an ambient [Directionality] + /// or an explicit [textDirection] should be provided. + /// + /// See also: + /// + /// * [SemanticsConfiguration.hint] for a description of how this is exposed + /// in TalkBack and VoiceOver. + final String hint; + + /// The reading direction of the [label], [value], [hint], [increasedValue], + /// and [decreasedValue]. + /// + /// Defaults to the ambient [Directionality]. + final TextDirection textDirection; + + /// The handler for [SemanticsAction.tap]. + /// + /// This is the semantic equivalent of a user briefly tapping the screen with + /// the finger without moving it. For example, a button should implement this + /// action. + /// + /// VoiceOver users on iOS and TalkBack users on Android can trigger this + /// action by double-tapping the screen while an element is focused. + final VoidCallback onTap; + + /// The handler for [SemanticsAction.longPress]. + /// + /// This is the semantic equivalent of a user pressing and holding the screen + /// with the finger for a few seconds without moving it. + /// + /// VoiceOver users on iOS and TalkBack users on Android can trigger this + /// action by double-tapping the screen without lifting the finger after the + /// second tap. + final VoidCallback onLongPress; + + /// The handler for [SemanticsAction.scrollLeft]. + /// + /// This is the semantic equivalent of a user moving their finger across the + /// screen from right to left. It should be recognized by controls that are + /// horizontally scrollable. + /// + /// VoiceOver users on iOS can trigger this action by swiping left with three + /// fingers. TalkBack users on Android can trigger this action by swiping + /// right and then left in one motion path. On Android, [onScrollUp] and + /// [onScrollLeft] share the same gesture. Therefore, only on of them should + /// be provided. + final VoidCallback onScrollLeft; + + /// The handler for [SemanticsAction.scrollRight]. + /// + /// This is the semantic equivalent of a user moving their finger across the + /// screen from left to right. It should be recognized by controls that are + /// horizontally scrollable. + /// + /// VoiceOver users on iOS can trigger this action by swiping right with three + /// fingers. TalkBack users on Android can trigger this action by swiping + /// left and then right in one motion path. On Android, [onScrollDown] and + /// [onScrollRight] share the same gesture. Therefore, only on of them should + /// be provided. + final VoidCallback onScrollRight; + + /// The handler for [SemanticsAction.scrollUp]. + /// + /// This is the semantic equivalent of a user moving their finger across the + /// screen from bottom to top. It should be recognized by controls that are + /// vertically scrollable. + /// + /// VoiceOver users on iOS can trigger this action by swiping up with three + /// fingers. TalkBack users on Android can trigger this action by swiping + /// right and then left in one motion path. On Android, [onScrollUp] and + /// [onScrollLeft] share the same gesture. Therefore, only on of them should + /// be provided. + final VoidCallback onScrollUp; + + /// The handler for [SemanticsAction.scrollDown]. + /// + /// This is the semantic equivalent of a user moving their finger across the + /// screen from top to bottom. It should be recognized by controls that are + /// vertically scrollable. + /// + /// VoiceOver users on iOS can trigger this action by swiping down with three + /// fingers. TalkBack users on Android can trigger this action by swiping + /// left and then right in one motion path. On Android, [onScrollDown] and + /// [onScrollRight] share the same gesture. Therefore, only on of them should + /// be provided. + final VoidCallback onScrollDown; + + /// The handler for [SemanticsAction.increase]. + /// + /// This is a request to increase the value represented by the widget. For + /// example, this action might be recognized by a slider control. + /// + /// If a [value] is set, [increasedValue] must also be provided and + /// [onIncrease] must ensure that [value] will be set to [increasedValue]. + /// + /// VoiceOver users on iOS can trigger this action by swiping up with one + /// finger. TalkBack users on Android can trigger this action by pressing the + /// volume up button. + final VoidCallback onIncrease; + + /// The handler for [SemanticsAction.decrease]. + /// + /// This is a request to decrease the value represented by the widget. For + /// example, this action might be recognized by a slider control. + /// + /// If a [value] is set, [decreasedValue] must also be provided and + /// [onDecrease] must ensure that [value] will be set to [decreasedValue]. + /// + /// VoiceOver users on iOS can trigger this action by swiping down with one + /// finger. TalkBack users on Android can trigger this action by pressing the + /// volume down button. + final VoidCallback onDecrease; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(new DiagnosticsProperty('checked', checked, defaultValue: null)); + description.add(new DiagnosticsProperty('selected', selected, defaultValue: null)); + description.add(new StringProperty('label', label, defaultValue: '')); + description.add(new StringProperty('value', value)); + description.add(new StringProperty('hint', hint)); + description.add(new EnumProperty('textDirection', textDirection, defaultValue: null)); + } +} + +/// In tests use this function to reset the counter used to generate +/// [SemanticsNode.id]. +void debugResetSemanticsIdCounter() { + SemanticsNode._lastIdentifier = 0; +} + /// A node that represents some semantic data. /// /// The semantics tree is maintained during the semantics phase of the pipeline @@ -236,6 +466,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { /// Each semantic node has a unique identifier that is assigned when the node /// is created. SemanticsNode({ + this.key, VoidCallback showOnScreen, }) : id = _generateNewId(), _showOnScreen = showOnScreen; @@ -244,6 +475,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { /// /// The root node is assigned an identifier of zero. SemanticsNode.root({ + this.key, VoidCallback showOnScreen, SemanticsOwner owner, }) : id = 0, @@ -257,6 +489,12 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { return _lastIdentifier; } + /// Uniquely identifies this node in the list of sibling nodes. + /// + /// Keys are used during the construction of the semantics tree. They are not + /// transferred to the engine. + final Key key; + /// The unique identifier for this node. /// /// The root node has an id of zero. Other nodes are given a unique id when @@ -344,9 +582,44 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { /// Contains the children in inverse hit test order (i.e. paint order). List _children; + /// A snapshot of `newChildren` passed to [_replaceChildren] that we keep in + /// debug mode. It supports the assertion that user does not mutate the list + /// of children. + List _debugPreviousSnapshot; + void _replaceChildren(List newChildren) { assert(!newChildren.any((SemanticsNode child) => child == this)); assert(() { + if (identical(newChildren, _children)) { + final StringBuffer mutationErrors = new StringBuffer(); + if (newChildren.length != _debugPreviousSnapshot.length) { + mutationErrors.writeln( + 'The list\'s length has changed from ${_debugPreviousSnapshot.length} ' + 'to ${newChildren.length}.' + ); + } else { + for (int i = 0; i < newChildren.length; i++) { + if (!identical(newChildren[i], _debugPreviousSnapshot[i])) { + mutationErrors.writeln( + 'Child node at position $i was replaced:\n' + 'Previous child: ${newChildren[i]}\n' + 'New child: ${_debugPreviousSnapshot[i]}\n' + ); + } + } + } + if (mutationErrors.isNotEmpty) { + throw new FlutterError( + 'Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.\n' + 'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.\n' + 'Error details:\n' + '$mutationErrors' + ); + } + } + + _debugPreviousSnapshot = new List.from(newChildren); + SemanticsNode ancestor = this; while (ancestor.parent is SemanticsNode) ancestor = ancestor.parent; @@ -412,10 +685,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { } } } - final List oldChildren = _children; _children = newChildren; - oldChildren?.clear(); - newChildren = oldChildren; if (sawChange) _markDirty(); } @@ -1017,7 +1287,7 @@ class SemanticsConfiguration { /// own [SemanticsNode]. /// /// When set to true semantic information associated with the [RenderObject] - /// owner of this configuration or any of its defendants will not leak into + /// owner of this configuration or any of its descendants will not leak into /// parents. The [SemanticsNode] generated out of this configuration will /// act as a boundary. /// diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 4376372173d..390b879c746 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -26,6 +26,7 @@ export 'package:flutter/rendering.dart' show CrossAxisAlignment, CustomClipper, CustomPainter, + CustomPainterSemantics, DecorationPosition, FlexFit, FlowDelegate, @@ -48,6 +49,7 @@ export 'package:flutter/rendering.dart' show PointerUpEvent, PointerUpEventListener, RelativeRect, + SemanticsBuilderCallback, ShaderCallback, SingleChildLayoutDelegate, StackFit, @@ -348,7 +350,7 @@ class CustomPaint extends SingleChildRenderObjectWidget { this.size: Size.zero, this.isComplex: false, this.willChange: false, - Widget child + Widget child, }) : assert(size != null), assert(isComplex != null), assert(willChange != null), @@ -4696,35 +4698,77 @@ class MetaData extends SingleChildRenderObjectWidget { /// * [SemanticsDebugger], an overlay to help visualize the semantics tree. Can /// be enabled using [WidgetsApp.showSemanticsDebugger] or /// [MaterialApp.showSemanticsDebugger]. +@immutable class Semantics extends SingleChildRenderObjectWidget { /// Creates a semantic annotation. /// - /// The [container] argument must not be null. - const Semantics({ + /// The [container] argument must not be null. To create a `const` instance + /// of [Semantics], use the [new Semantics.fromProperties] constructor. + Semantics({ + Key key, + Widget child, + bool container: false, + bool explicitChildNodes: false, + bool checked, + bool selected, + bool button, + String label, + String value, + String increasedValue, + String decreasedValue, + String hint, + TextDirection textDirection, + VoidCallback onTap, + VoidCallback onLongPress, + VoidCallback onScrollLeft, + VoidCallback onScrollRight, + VoidCallback onScrollUp, + VoidCallback onScrollDown, + VoidCallback onIncrease, + VoidCallback onDecrease, + }) : this.fromProperties( + key: key, + child: child, + container: container, + explicitChildNodes: explicitChildNodes, + properties: new SemanticsProperties( + checked: checked, + selected: selected, + button: button, + label: label, + value: value, + increasedValue: increasedValue, + decreasedValue: decreasedValue, + hint: hint, + textDirection: textDirection, + onTap: onTap, + onLongPress: onLongPress, + onScrollLeft: onScrollLeft, + onScrollRight: onScrollRight, + onScrollUp: onScrollUp, + onScrollDown: onScrollDown, + onIncrease: onIncrease, + onDecrease: onDecrease, + ), + ); + + /// Creates a semantic annotation using [SemanticsProperties]. + /// + /// The [container] and [properties] arguments must not be null. + const Semantics.fromProperties({ Key key, Widget child, this.container: false, this.explicitChildNodes: false, - this.checked, - this.selected, - this.button, - this.label, - this.value, - this.increasedValue, - this.decreasedValue, - this.hint, - this.textDirection, - this.onTap, - this.onLongPress, - this.onScrollLeft, - this.onScrollRight, - this.onScrollUp, - this.onScrollDown, - this.onIncrease, - this.onDecrease, + @required this.properties, }) : assert(container != null), + assert(properties != null), super(key: key, child: child); + /// Contains properties used by assistive technologies to make the application + /// more accessible. + final SemanticsProperties properties; + /// If 'container' is true, this widget will introduce a new /// node in the semantics tree. Otherwise, the semantics will be /// merged with the semantics of any ancestors (if the ancestor allows that). @@ -4748,250 +4792,72 @@ class Semantics extends SingleChildRenderObjectWidget { /// create semantic boundaries that are either writable or not for children. final bool explicitChildNodes; - /// If non-null, indicates that this subtree represents a checkbox - /// or similar widget with a "checked" state, and what its current - /// state is. - final bool checked; - - /// If non-null indicates that this subtree represents something that can be - /// in a selected or unselected state, and what its current state is. - /// - /// The active tab in a tab bar for example is considered "selected", whereas - /// all other tabs are unselected. - final bool selected; - - /// If non-null, indicates that this subtree represents a button. - /// - /// TalkBack/VoiceOver provides users with the hint "button" when a button - /// is focused. - final bool button; - - /// Provides a textual description of the widget. - /// - /// If a label is provided, there must either by an ambient [Directionality] - /// or an explicit [textDirection] should be provided. - /// - /// See also: - /// - /// * [SemanticsConfiguration.label] for a description of how this is exposed - /// in TalkBack and VoiceOver. - final String label; - - /// Provides a textual description of the value of the widget. - /// - /// If a value is provided, there must either by an ambient [Directionality] - /// or an explicit [textDirection] should be provided. - /// - /// See also: - /// - /// * [SemanticsConfiguration.value] for a description of how this is exposed - /// in TalkBack and VoiceOver. - final String value; - - /// The value that [value] will become after a [SemanticsAction.increase] - /// action has been performed on this widget. - /// - /// If a value is provided, [onIncrease] must also be set and there must - /// either be an ambient [Directionality] or an explicit [textDirection] - /// must be provided. - /// - /// See also: - /// - /// * [SemanticsConfiguration.increasedValue] for a description of how this - /// is exposed in TalkBack and VoiceOver. - final String increasedValue; - - /// The value that [value] will become after a [SemanticsAction.decrease] - /// action has been performed on this widget. - /// - /// If a value is provided, [onDecrease] must also be set and there must - /// either be an ambient [Directionality] or an explicit [textDirection] - /// must be provided. - /// - /// See also: - /// - /// * [SemanticsConfiguration.decreasedValue] for a description of how this - /// is exposed in TalkBack and VoiceOver. - final String decreasedValue; - - /// Provides a brief textual description of the result of an action performed - /// on the widget. - /// - /// If a hint is provided, there must either by an ambient [Directionality] - /// or an explicit [textDirection] should be provided. - /// - /// See also: - /// - /// * [SemanticsConfiguration.hint] for a description of how this is exposed - /// in TalkBack and VoiceOver. - final String hint; - - /// The reading direction of the [label], [value], [hint], [increasedValue], - /// and [decreasedValue]. - /// - /// Defaults to the ambient [Directionality]. - final TextDirection textDirection; - - TextDirection _getTextDirection(BuildContext context) { - return textDirection ?? (label != null || value != null || hint != null ? Directionality.of(context) : null); - } - - /// The handler for [SemanticsAction.tap]. - /// - /// This is the semantic equivalent of a user briefly tapping the screen with - /// the finger without moving it. For example, a button should implement this - /// action. - /// - /// VoiceOver users on iOS and TalkBack users on Android can trigger this - /// action by double-tapping the screen while an element is focused. - final VoidCallback onTap; - - /// The handler for [SemanticsAction.longPress]. - /// - /// This is the semantic equivalent of a user pressing and holding the screen - /// with the finger for a few seconds without moving it. - /// - /// VoiceOver users on iOS and TalkBack users on Android can trigger this - /// action by double-tapping the screen without lifting the finger after the - /// second tap. - final VoidCallback onLongPress; - - /// The handler for [SemanticsAction.scrollLeft]. - /// - /// This is the semantic equivalent of a user moving their finger across the - /// screen from right to left. It should be recognized by controls that are - /// horizontally scrollable. - /// - /// VoiceOver users on iOS can trigger this action by swiping left with three - /// fingers. TalkBack users on Android can trigger this action by swiping - /// right and then left in one motion path. On Android, [onScrollUp] and - /// [onScrollLeft] share the same gesture. Therefore, only on of them should - /// be provided. - final VoidCallback onScrollLeft; - - /// The handler for [SemanticsAction.scrollRight]. - /// - /// This is the semantic equivalent of a user moving their finger across the - /// screen from left to right. It should be recognized by controls that are - /// horizontally scrollable. - /// - /// VoiceOver users on iOS can trigger this action by swiping right with three - /// fingers. TalkBack users on Android can trigger this action by swiping - /// left and then right in one motion path. On Android, [onScrollDown] and - /// [onScrollRight] share the same gesture. Therefore, only on of them should - /// be provided. - final VoidCallback onScrollRight; - - /// The handler for [SemanticsAction.scrollUp]. - /// - /// This is the semantic equivalent of a user moving their finger across the - /// screen from bottom to top. It should be recognized by controls that are - /// vertically scrollable. - /// - /// VoiceOver users on iOS can trigger this action by swiping up with three - /// fingers. TalkBack users on Android can trigger this action by swiping - /// right and then left in one motion path. On Android, [onScrollUp] and - /// [onScrollLeft] share the same gesture. Therefore, only on of them should - /// be provided. - final VoidCallback onScrollUp; - - /// The handler for [SemanticsAction.scrollDown]. - /// - /// This is the semantic equivalent of a user moving their finger across the - /// screen from top to bottom. It should be recognized by controls that are - /// vertically scrollable. - /// - /// VoiceOver users on iOS can trigger this action by swiping down with three - /// fingers. TalkBack users on Android can trigger this action by swiping - /// left and then right in one motion path. On Android, [onScrollDown] and - /// [onScrollRight] share the same gesture. Therefore, only on of them should - /// be provided. - final VoidCallback onScrollDown; - - /// The handler for [SemanticsAction.increase]. - /// - /// This is a request to increase the value represented by the widget. For - /// example, this action might be recognized by a slider control. - /// - /// If a [value] is set, [increasedValue] must also be provided and - /// [onIncrease] must ensure that [value] will be set to [increasedValue]. - /// - /// VoiceOver users on iOS can trigger this action by swiping up with one - /// finger. TalkBack users on Android can trigger this action by pressing the - /// volume up button. - final VoidCallback onIncrease; - - /// The handler for [SemanticsAction.decrease]. - /// - /// This is a request to decrease the value represented by the widget. For - /// example, this action might be recognized by a slider control. - /// - /// If a [value] is set, [decreasedValue] must also be provided and - /// [onDecrease] must ensure that [value] will be set to [decreasedValue]. - /// - /// VoiceOver users on iOS can trigger this action by swiping down with one - /// finger. TalkBack users on Android can trigger this action by pressing the - /// volume down button. - final VoidCallback onDecrease; - @override RenderSemanticsAnnotations createRenderObject(BuildContext context) { return new RenderSemanticsAnnotations( container: container, explicitChildNodes: explicitChildNodes, - checked: checked, - selected: selected, - button: button, - label: label, - value: value, - increasedValue: increasedValue, - decreasedValue: decreasedValue, - hint: hint, + checked: properties.checked, + selected: properties.selected, + button: properties.button, + label: properties.label, + value: properties.value, + increasedValue: properties.increasedValue, + decreasedValue: properties.decreasedValue, + hint: properties.hint, textDirection: _getTextDirection(context), - onTap: onTap, - onLongPress: onLongPress, - onScrollLeft: onScrollLeft, - onScrollRight: onScrollRight, - onScrollUp: onScrollUp, - onScrollDown: onScrollDown, - onIncrease: onIncrease, - onDecrease: onDecrease, + onTap: properties.onTap, + onLongPress: properties.onLongPress, + onScrollLeft: properties.onScrollLeft, + onScrollRight: properties.onScrollRight, + onScrollUp: properties.onScrollUp, + onScrollDown: properties.onScrollDown, + onIncrease: properties.onIncrease, + onDecrease: properties.onDecrease, ); } + TextDirection _getTextDirection(BuildContext context) { + if (properties.textDirection != null) + return properties.textDirection; + + final bool containsText = properties.label != null || properties.value != null || properties.hint != null; + + if (!containsText) + return null; + + return Directionality.of(context); + } + @override void updateRenderObject(BuildContext context, RenderSemanticsAnnotations renderObject) { renderObject ..container = container ..explicitChildNodes = explicitChildNodes - ..checked = checked - ..selected = selected - ..label = label - ..value = value - ..increasedValue = increasedValue - ..decreasedValue = decreasedValue - ..hint = hint + ..checked = properties.checked + ..selected = properties.selected + ..label = properties.label + ..value = properties.value + ..increasedValue = properties.increasedValue + ..decreasedValue = properties.decreasedValue + ..hint = properties.hint ..textDirection = _getTextDirection(context) - ..onTap = onTap - ..onLongPress = onLongPress - ..onScrollLeft = onScrollLeft - ..onScrollRight = onScrollRight - ..onScrollUp = onScrollUp - ..onScrollDown = onScrollDown - ..onIncrease = onIncrease - ..onDecrease = onDecrease; + ..onTap = properties.onTap + ..onLongPress = properties.onLongPress + ..onScrollLeft = properties.onScrollLeft + ..onScrollRight = properties.onScrollRight + ..onScrollUp = properties.onScrollUp + ..onScrollDown = properties.onScrollDown + ..onIncrease = properties.onIncrease + ..onDecrease = properties.onDecrease; } @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description.add(new DiagnosticsProperty('container', container)); - description.add(new DiagnosticsProperty('checked', checked, defaultValue: null)); - description.add(new DiagnosticsProperty('selected', selected, defaultValue: null)); - description.add(new StringProperty('label', label, defaultValue: '')); - description.add(new StringProperty('value', value)); - description.add(new StringProperty('hint', hint)); - description.add(new EnumProperty('textDirection', textDirection, defaultValue: null)); + description.add(new DiagnosticsProperty('properties', properties)); + properties.debugFillProperties(description); } } diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 4532530245d..8dae92fea38 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -17,6 +17,7 @@ export 'dart:ui' show hashValues, hashList; export 'package:flutter/foundation.dart' show FlutterError, debugPrint, debugPrintStack; export 'package:flutter/foundation.dart' show VoidCallback, ValueChanged, ValueGetter, ValueSetter; export 'package:flutter/foundation.dart' show DiagnosticLevel; +export 'package:flutter/foundation.dart' show Key, LocalKey, ValueKey; export 'package:flutter/rendering.dart' show RenderObject, RenderBox, debugDumpRenderTree, debugDumpLayerTree; // Examples can assume: @@ -30,84 +31,6 @@ export 'package:flutter/rendering.dart' show RenderObject, RenderBox, debugDumpR // KEYS -/// A [Key] is an identifier for [Widget]s and [Element]s. -/// -/// A new widget will only be used to update an existing element if its key is -/// the same as the key of the current widget associated with the element. -/// -/// Keys must be unique amongst the [Element]s with the same parent. -/// -/// Subclasses of [Key] should either subclass [LocalKey] or [GlobalKey]. -/// -/// See also the discussion at [Widget.key]. -@immutable -abstract class Key { - /// Construct a [ValueKey] with the given [String]. - /// - /// This is the simplest way to create keys. - const factory Key(String value) = ValueKey; - - /// Default constructor, used by subclasses. - /// - /// Useful so that subclasses can call us, because the Key() factory - /// constructor shadows the implicit constructor. - const Key._(); -} - -/// A key that is not a [GlobalKey]. -/// -/// Keys must be unique amongst the [Element]s with the same parent. By -/// contrast, [GlobalKey]s must be unique across the entire app. -/// -/// See also the discussion at [Widget.key]. -abstract class LocalKey extends Key { - /// Default constructor, used by subclasses. - const LocalKey() : super._(); -} - -/// A key that uses a value of a particular type to identify itself. -/// -/// A [ValueKey] is equal to another [ValueKey] if, and only if, their -/// values are [operator==]. -/// -/// This class can be subclassed to create value keys that will not be equal to -/// other value keys that happen to use the same value. If the subclass is -/// private, this results in a value key type that cannot collide with keys from -/// other sources, which could be useful, for example, if the keys are being -/// used as fallbacks in the same scope as keys supplied from another widget. -/// -/// See also the discussion at [Widget.key]. -class ValueKey extends LocalKey { - /// Creates a key that delegates its [operator==] to the given value. - const ValueKey(this.value); - - /// The value to which this key delegates its [operator==] - final T value; - - @override - bool operator ==(dynamic other) { - if (other.runtimeType != runtimeType) - return false; - final ValueKey typedOther = other; - return value == typedOther.value; - } - - @override - int get hashCode => hashValues(runtimeType, value); - - @override - String toString() { - final String valueString = T == String ? '<\'$value\'>' : '<$value>'; - // The crazy on the next line is a workaround for - // https://github.com/dart-lang/sdk/issues/28548 - if (runtimeType == new _TypeLiteral>().type) - return '[$valueString]'; - return '[$T $valueString]'; - } -} - -class _TypeLiteral { Type get type => T; } - /// A key that is only equal to itself. class UniqueKey extends LocalKey { /// Creates a key that is equal only to itself. @@ -183,7 +106,7 @@ abstract class GlobalKey> extends Key { /// /// Used by subclasses because the factory constructor shadows the implicit /// constructor. - const GlobalKey.constructor() : super._(); + const GlobalKey.constructor() : super.empty(); static final Map _registry = {}; static final Set _removedKeys = new HashSet(); @@ -4287,9 +4210,10 @@ abstract class RenderObjectElement extends Element { return forgottenChildren != null && forgottenChildren.contains(child) ? null : child; } - // This attempts to diff the new child list (this.children) with - // the old child list (old.children), and update our renderObject - // accordingly. + // This attempts to diff the new child list (newWidgets) with + // the old child list (oldChildren), and produce a new list of elements to + // be the new list of child elements of this element. The called of this + // method is expected to update this render object accordingly. // The cases it tries to optimize for are: // - the old list is empty @@ -4305,13 +4229,13 @@ abstract class RenderObjectElement extends Element { // 2. Walk the lists from the bottom, without syncing nodes, until you no // longer have matching nodes. We'll sync these nodes at the end. We // don't sync them now because we want to sync all the nodes in order - // from beginning ot end. + // from beginning to end. // At this point we narrowed the old and new lists to the point // where the nodes no longer match. // 3. Walk the narrowed part of the old list to get the list of // keys and sync null with non-keyed items. // 4. Walk the narrowed part of the new list forwards: - // * Sync unkeyed items with null + // * Sync non-keyed items with null // * Sync keyed items with the source if it exists, else with null. // 5. Walk the bottom of the list again, syncing the nodes. // 6. Sync null with any items in the list of keys that are still @@ -4378,7 +4302,7 @@ abstract class RenderObjectElement extends Element { if (haveOldChildren) { final Key key = newWidget.key; if (key != null) { - oldChild = oldKeyedChildren[newWidget.key]; + oldChild = oldKeyedChildren[key]; if (oldChild != null) { if (Widget.canUpdate(oldChild.widget, newWidget)) { // we found a match! @@ -4400,7 +4324,7 @@ abstract class RenderObjectElement extends Element { newChildrenTop += 1; } - // We've scaned the whole list. + // We've scanned the whole list. assert(oldChildrenTop == oldChildrenBottom + 1); assert(newChildrenTop == newChildrenBottom + 1); assert(newWidgets.length - newChildrenTop == oldChildren.length - oldChildrenTop); @@ -4423,7 +4347,7 @@ abstract class RenderObjectElement extends Element { oldChildrenTop += 1; } - // clean up any of the remaining middle nodes from the old list + // Clean up any of the remaining middle nodes from the old list. if (haveOldChildren && oldKeyedChildren.isNotEmpty) { for (Element oldChild in oldKeyedChildren.values) { if (forgottenChildren == null || !forgottenChildren.contains(oldChild)) diff --git a/packages/flutter/test/widgets/custom_painter_test.dart b/packages/flutter/test/widgets/custom_painter_test.dart new file mode 100644 index 00000000000..f8ea9eb0dd3 --- /dev/null +++ b/packages/flutter/test/widgets/custom_painter_test.dart @@ -0,0 +1,631 @@ +// 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:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'semantics_tester.dart'; + +void main() { + group(CustomPainter, () { + setUp(() { + debugResetSemanticsIdCounter(); + _PainterWithSemantics.shouldRebuildSemanticsCallCount = 0; + _PainterWithSemantics.buildSemanticsCallCount = 0; + _PainterWithSemantics.semanticsBuilderCallCount = 0; + }); + + _defineTests(); + }); +} + +void _defineTests() { + testWidgets('builds no semantics by default', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + + await tester.pumpWidget(new CustomPaint( + painter: new _PainterWithoutSemantics(), + )); + + expect(semanticsTester, hasSemantics( + new TestSemantics.root( + children: const [], + ), + )); + + semanticsTester.dispose(); + }); + + testWidgets('provides foreground semantics', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + + await tester.pumpWidget(new CustomPaint( + foregroundPainter: new _PainterWithSemantics( + semantics: new CustomPainterSemantics( + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + properties: const SemanticsProperties( + label: 'foreground', + textDirection: TextDirection.rtl, + ), + ), + ), + )); + + expect(semanticsTester, hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + rect: TestSemantics.fullScreen, + children: [ + new TestSemantics( + id: 2, + label: 'foreground', + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + ), + ], + ), + ], + ), + )); + + semanticsTester.dispose(); + }); + + testWidgets('provides background semantics', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + + await tester.pumpWidget(new CustomPaint( + painter: new _PainterWithSemantics( + semantics: new CustomPainterSemantics( + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + properties: const SemanticsProperties( + label: 'background', + textDirection: TextDirection.rtl, + ), + ), + ), + )); + + expect(semanticsTester, hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + rect: TestSemantics.fullScreen, + children: [ + new TestSemantics( + id: 2, + label: 'background', + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + ), + ], + ), + ], + ), + )); + + semanticsTester.dispose(); + }); + + testWidgets('combines background, child and foreground semantics', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + + await tester.pumpWidget(new CustomPaint( + painter: new _PainterWithSemantics( + semantics: new CustomPainterSemantics( + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + properties: const SemanticsProperties( + label: 'background', + textDirection: TextDirection.rtl, + ), + ), + ), + child: new Semantics( + container: true, + child: const Text('Hello', textDirection: TextDirection.ltr), + ), + foregroundPainter: new _PainterWithSemantics( + semantics: new CustomPainterSemantics( + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + properties: const SemanticsProperties( + label: 'foreground', + textDirection: TextDirection.rtl, + ), + ), + ), + )); + + expect(semanticsTester, hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + rect: TestSemantics.fullScreen, + children: [ + new TestSemantics( + id: 3, + label: 'background', + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + ), + new TestSemantics( + id: 2, + label: 'Hello', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), + ), + new TestSemantics( + id: 4, + label: 'foreground', + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + ), + ], + ), + ], + ), + )); + + semanticsTester.dispose(); + }); + + testWidgets('applies $SemanticsProperties', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + + await tester.pumpWidget(new CustomPaint( + painter: new _PainterWithSemantics( + semantics: new CustomPainterSemantics( + key: const ValueKey(1), + rect: new Rect.fromLTRB(1.0, 2.0, 3.0, 4.0), + properties: const SemanticsProperties( + checked: false, + selected: false, + button: false, + label: 'label-before', + value: 'value-before', + increasedValue: 'increase-before', + decreasedValue: 'decrease-before', + hint: 'hint-before', + textDirection: TextDirection.rtl, + ), + ), + ), + )); + + expect(semanticsTester, hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + rect: TestSemantics.fullScreen, + children: [ + new TestSemantics( + rect: new Rect.fromLTRB(1.0, 2.0, 3.0, 4.0), + id: 2, + flags: 1, + label: 'label-before', + value: 'value-before', + increasedValue: 'increase-before', + decreasedValue: 'decrease-before', + hint: 'hint-before', + textDirection: TextDirection.rtl, + ), + ], + ), + ], + ), + )); + + await tester.pumpWidget(new CustomPaint( + painter: new _PainterWithSemantics( + semantics: new CustomPainterSemantics( + key: const ValueKey(1), + rect: new Rect.fromLTRB(5.0, 6.0, 7.0, 8.0), + properties: new SemanticsProperties( + checked: true, + selected: true, + button: true, + label: 'label-after', + value: 'value-after', + increasedValue: 'increase-after', + decreasedValue: 'decrease-after', + hint: 'hint-after', + textDirection: TextDirection.ltr, + onScrollDown: () {}, + onLongPress: () {}, + onDecrease: () {}, + onIncrease: () {}, + onScrollLeft: () {}, + onScrollRight: () {}, + onScrollUp: () {}, + onTap: () {}, + ), + ), + ), + )); + + expect(semanticsTester, hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + rect: TestSemantics.fullScreen, + children: [ + new TestSemantics( + rect: new Rect.fromLTRB(5.0, 6.0, 7.0, 8.0), + actions: 255, + id: 2, + flags: 15, + label: 'label-after', + value: 'value-after', + increasedValue: 'increase-after', + decreasedValue: 'decrease-after', + hint: 'hint-after', + textDirection: TextDirection.ltr, + ), + ], + ), + ], + ), + )); + + semanticsTester.dispose(); + }); + + group('diffing', () { + testWidgets('complains about duplicate keys', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + await tester.pumpWidget(new CustomPaint( + painter: new _SemanticsDiffTest([ + 'a-k', + 'a-k', + ]), + )); + expect(tester.takeException(), isFlutterError); + semanticsTester.dispose(); + }); + + testDiff('adds one item to an empty list', (_DiffTester tester) async { + await tester.diff( + from: [], + to: ['a'], + ); + }); + + testDiff('removes the last item from the list', (_DiffTester tester) async { + await tester.diff( + from: ['a'], + to: [], + ); + }); + + testDiff('appends one item at the end of a non-empty list', (_DiffTester tester) async { + await tester.diff( + from: ['a'], + to: ['a', 'b'], + ); + }); + + testDiff('prepends one item at the beginning of a non-empty list', (_DiffTester tester) async { + await tester.diff( + from: ['b'], + to: ['a', 'b'], + ); + }); + + testDiff('inserts one item in the middle of a list', (_DiffTester tester) async { + await tester.diff( + from: [ + 'a-k', + 'c-k', + ], + to: [ + 'a-k', + 'b-k', + 'c-k', + ], + ); + }); + + testDiff('removes one item from the middle of a list', (_DiffTester tester) async { + await tester.diff( + from: [ + 'a-k', + 'b-k', + 'c-k', + ], + to: [ + 'a-k', + 'c-k', + ], + ); + }); + + testDiff('swaps two items', (_DiffTester tester) async { + await tester.diff( + from: [ + 'a-k', + 'b-k', + ], + to: [ + 'b-k', + 'a-k', + ], + ); + }); + + testDiff('finds and moved one keyed item', (_DiffTester tester) async { + await tester.diff( + from: [ + 'a-k', + 'b', + 'c', + ], + to: [ + 'b', + 'c', + 'a-k', + ], + ); + }); + }); + + testWidgets('rebuilds semantics upon resize', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + + final _PainterWithSemantics painter = new _PainterWithSemantics( + semantics: new CustomPainterSemantics( + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + properties: const SemanticsProperties( + label: 'background', + textDirection: TextDirection.rtl, + ), + ), + ); + + final CustomPaint paint = new CustomPaint(painter: painter); + + await tester.pumpWidget(new SizedBox( + height: 20.0, + width: 20.0, + child: paint, + )); + expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0); + expect(_PainterWithSemantics.buildSemanticsCallCount, 1); + expect(_PainterWithSemantics.semanticsBuilderCallCount, 4); + + await tester.pumpWidget(new SizedBox( + height: 20.0, + width: 20.0, + child: paint, + )); + expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0); + expect(_PainterWithSemantics.buildSemanticsCallCount, 1); + expect(_PainterWithSemantics.semanticsBuilderCallCount, 4); + + await tester.pumpWidget(new SizedBox( + height: 40.0, + width: 40.0, + child: paint, + )); + expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0); + expect(_PainterWithSemantics.buildSemanticsCallCount, 2); + expect(_PainterWithSemantics.semanticsBuilderCallCount, 4); + + semanticsTester.dispose(); + }); + + testWidgets('does not rebuild when shouldRebuildSemantics is false', (WidgetTester tester) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + + final CustomPainterSemantics testSemantics = new CustomPainterSemantics( + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + properties: const SemanticsProperties( + label: 'background', + textDirection: TextDirection.rtl, + ), + ); + + await tester.pumpWidget(new CustomPaint(painter: new _PainterWithSemantics( + semantics: testSemantics, + ))); + expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0); + expect(_PainterWithSemantics.buildSemanticsCallCount, 1); + expect(_PainterWithSemantics.semanticsBuilderCallCount, 4); + + await tester.pumpWidget(new CustomPaint(painter: new _PainterWithSemantics( + semantics: testSemantics, + ))); + expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 1); + expect(_PainterWithSemantics.buildSemanticsCallCount, 1); + expect(_PainterWithSemantics.semanticsBuilderCallCount, 4); + + final CustomPainterSemantics testSemantics2 = new CustomPainterSemantics( + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + properties: const SemanticsProperties( + label: 'background', + textDirection: TextDirection.rtl, + ), + ); + + await tester.pumpWidget(new CustomPaint(painter: new _PainterWithSemantics( + semantics: testSemantics2, + ))); + expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 2); + expect(_PainterWithSemantics.buildSemanticsCallCount, 2); + expect(_PainterWithSemantics.semanticsBuilderCallCount, 5); + + semanticsTester.dispose(); + }); +} + +void testDiff(String description, Future Function(_DiffTester tester) testFunction) { + testWidgets(description, (WidgetTester tester) async { + await testFunction(new _DiffTester(tester)); + }); +} + +class _DiffTester { + _DiffTester(this.tester); + + final WidgetTester tester; + + /// Creates an initial semantics list using the `from` list, then updates the + /// list to the `to` list. This causes [RenderCustomPaint] to diff the two + /// lists and apply the changes. This method asserts the the changes were + /// applied correctly, specifically: + /// + /// - checks that initial and final configurations are in the desired states. + /// - checks that keyed nodes have stable IDs. + Future diff({List from, List to}) async { + final SemanticsTester semanticsTester = new SemanticsTester(tester); + + TestSemantics createExpectations(List labels) { + final List children = []; + for (String label in labels) { + children.add( + new TestSemantics( + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + label: label, + ), + ); + } + + return new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + rect: TestSemantics.fullScreen, + children: children, + ), + ], + ); + } + + await tester.pumpWidget(new CustomPaint( + painter: new _SemanticsDiffTest(from), + )); + expect(semanticsTester, hasSemantics(createExpectations(from), ignoreId: true)); + + SemanticsNode root = RendererBinding.instance?.renderView?.debugSemantics; + final Map idAssignments = {}; + root.visitChildren((SemanticsNode firstChild) { + firstChild.visitChildren((SemanticsNode node) { + if (node.key != null) { + idAssignments[node.key] = node.id; + } + return true; + }); + return true; + }); + + await tester.pumpWidget(new CustomPaint( + painter: new _SemanticsDiffTest(to), + )); + await tester.pumpAndSettle(); + expect(semanticsTester, hasSemantics(createExpectations(to), ignoreId: true)); + + root = RendererBinding.instance?.renderView?.debugSemantics; + root.visitChildren((SemanticsNode firstChild) { + firstChild.visitChildren((SemanticsNode node) { + if (node.key != null && idAssignments[node.key] != null) { + expect(idAssignments[node.key], node.id, reason: + 'Node with key ${node.key} was previously assigned id ${idAssignments[node.key]}. ' + 'After diffing the child list, its id changed to ${node.id}. Ids must be stable.'); + } + return true; + }); + return true; + }); + + semanticsTester.dispose(); + } +} + +class _SemanticsDiffTest extends CustomPainter { + _SemanticsDiffTest(this.data); + + final List data; + + @override + void paint(Canvas canvas, Size size) { + // We don't test painting. + } + + @override + SemanticsBuilderCallback get semanticsBuilder => buildSemantics; + + List buildSemantics(Size size) { + final List semantics = []; + for (String label in data) { + Key key; + if (label.endsWith('-k')) { + key = new ValueKey(label); + } + semantics.add( + new CustomPainterSemantics( + rect: new Rect.fromLTRB(1.0, 1.0, 2.0, 2.0), + key: key, + properties: new SemanticsProperties( + label: label, + textDirection: TextDirection.rtl, + ), + ), + ); + } + return semantics; + } + + @override + bool shouldRepaint(_SemanticsDiffTest oldPainter) => true; +} + +class _PainterWithSemantics extends CustomPainter { + _PainterWithSemantics({ this.semantics }); + + final CustomPainterSemantics semantics; + + static int semanticsBuilderCallCount = 0; + static int buildSemanticsCallCount = 0; + static int shouldRebuildSemanticsCallCount = 0; + + @override + void paint(Canvas canvas, Size size) { + // We don't test painting. + } + + @override + SemanticsBuilderCallback get semanticsBuilder { + semanticsBuilderCallCount += 1; + return buildSemantics; + } + + List buildSemantics(Size size) { + buildSemanticsCallCount += 1; + return [semantics]; + } + + @override + bool shouldRepaint(_PainterWithSemantics oldPainter) { + return true; + } + + @override + bool shouldRebuildSemantics(_PainterWithSemantics oldPainter) { + shouldRebuildSemanticsCallCount += 1; + return !identical(oldPainter.semantics, semantics); + } +} + +class _PainterWithoutSemantics extends CustomPainter { + _PainterWithoutSemantics(); + + @override + void paint(Canvas canvas, Size size) { + // We don't test painting. + } + + @override + bool shouldRepaint(_PainterWithSemantics oldPainter) => true; +} diff --git a/packages/flutter/test/widgets/semantics_10_test.dart b/packages/flutter/test/widgets/semantics_10_test.dart index 4cfabf476fe..0612fd7bb0e 100644 --- a/packages/flutter/test/widgets/semantics_10_test.dart +++ b/packages/flutter/test/widgets/semantics_10_test.dart @@ -47,10 +47,10 @@ Widget buildTestWidgets({bool excludeSemantics, String label, bool isSemanticsBo isSemanticBoundary: isSemanticsBoundary, child: new Column( children: [ - const Semantics( + new Semantics( label: 'child1', ), - const Semantics( + new Semantics( label: 'child2', ), ], diff --git a/packages/flutter/test/widgets/semantics_11_test.dart b/packages/flutter/test/widgets/semantics_11_test.dart index ef1bb3f1099..4b75eb6041a 100644 --- a/packages/flutter/test/widgets/semantics_11_test.dart +++ b/packages/flutter/test/widgets/semantics_11_test.dart @@ -14,12 +14,12 @@ void main() { final SemanticsTester semantics = new SemanticsTester(tester); await tester.pumpWidget( - const Semantics( + new Semantics( container: true, onTap: dummyTapHandler, - child: const Semantics( + child: new Semantics( onTap: dummyTapHandler, - child: const Semantics( + child: new Semantics( onTap: dummyTapHandler, textDirection: TextDirection.ltr, label: 'foo', @@ -54,12 +54,12 @@ void main() { // This should not throw an assert. await tester.pumpWidget( - const Semantics( + new Semantics( container: true, onTap: dummyTapHandler, - child: const Semantics( + child: new Semantics( onTap: dummyTapHandler, - child: const Semantics( + child: new Semantics( onTap: dummyTapHandler, textDirection: TextDirection.ltr, label: 'bar', // <-- only change diff --git a/packages/flutter/test/widgets/semantics_1_test.dart b/packages/flutter/test/widgets/semantics_1_test.dart index 2263cd612e3..516e08194f9 100644 --- a/packages/flutter/test/widgets/semantics_1_test.dart +++ b/packages/flutter/test/widgets/semantics_1_test.dart @@ -50,7 +50,7 @@ void main() { children: [ new Container( height: 10.0, - child: const Semantics( + child: new Semantics( label: 'child1', textDirection: TextDirection.ltr, selected: true, @@ -58,9 +58,9 @@ void main() { ), new Container( height: 10.0, - child: const IgnorePointer( + child: new IgnorePointer( ignoring: true, - child: const Semantics( + child: new Semantics( label: 'child1', textDirection: TextDirection.ltr, selected: true, @@ -92,7 +92,7 @@ void main() { children: [ new Container( height: 10.0, - child: const Semantics( + child: new Semantics( label: 'child1', textDirection: TextDirection.ltr, selected: true, @@ -100,9 +100,9 @@ void main() { ), new Container( height: 10.0, - child: const IgnorePointer( + child: new IgnorePointer( ignoring: false, - child: const Semantics( + child: new Semantics( label: 'child2', textDirection: TextDirection.ltr, selected: true, @@ -146,7 +146,7 @@ void main() { children: [ new Container( height: 10.0, - child: const Semantics( + child: new Semantics( label: 'child1', textDirection: TextDirection.ltr, selected: true, @@ -154,9 +154,9 @@ void main() { ), new Container( height: 10.0, - child: const IgnorePointer( + child: new IgnorePointer( ignoring: true, - child: const Semantics( + child: new Semantics( label: 'child2', textDirection: TextDirection.ltr, selected: true, @@ -188,7 +188,7 @@ void main() { children: [ new Container( height: 10.0, - child: const Semantics( + child: new Semantics( label: 'child1', textDirection: TextDirection.ltr, selected: true, @@ -196,9 +196,9 @@ void main() { ), new Container( height: 10.0, - child: const IgnorePointer( + child: new IgnorePointer( ignoring: false, - child: const Semantics( + child: new Semantics( label: 'child2', textDirection: TextDirection.ltr, selected: true, diff --git a/packages/flutter/test/widgets/semantics_2_test.dart b/packages/flutter/test/widgets/semantics_2_test.dart index e9fc35e8111..71c3830f5a9 100644 --- a/packages/flutter/test/widgets/semantics_2_test.dart +++ b/packages/flutter/test/widgets/semantics_2_test.dart @@ -28,7 +28,7 @@ void main() { children: [ new Container( height: 10.0, - child: const Semantics( + child: new Semantics( label: 'child1', textDirection: TextDirection.ltr, selected: true, @@ -36,9 +36,9 @@ void main() { ), new Container( height: 10.0, - child: const IgnorePointer( + child: new IgnorePointer( ignoring: false, - child: const Semantics( + child: new Semantics( label: 'child2', textDirection: TextDirection.ltr, selected: true, @@ -82,7 +82,7 @@ void main() { children: [ new Container( height: 10.0, - child: const Semantics( + child: new Semantics( label: 'child1', textDirection: TextDirection.ltr, selected: true, @@ -90,9 +90,9 @@ void main() { ), new Container( height: 10.0, - child: const IgnorePointer( + child: new IgnorePointer( ignoring: true, - child: const Semantics( + child: new Semantics( label: 'child2', textDirection: TextDirection.ltr, selected: true, @@ -124,7 +124,7 @@ void main() { children: [ new Container( height: 10.0, - child: const Semantics( + child: new Semantics( label: 'child1', textDirection: TextDirection.ltr, selected: true, @@ -132,9 +132,9 @@ void main() { ), new Container( height: 10.0, - child: const IgnorePointer( + child: new IgnorePointer( ignoring: false, - child: const Semantics( + child: new Semantics( label: 'child2', textDirection: TextDirection.ltr, selected: true, diff --git a/packages/flutter/test/widgets/semantics_3_test.dart b/packages/flutter/test/widgets/semantics_3_test.dart index c899eae58ba..ded37091880 100644 --- a/packages/flutter/test/widgets/semantics_3_test.dart +++ b/packages/flutter/test/widgets/semantics_3_test.dart @@ -23,7 +23,7 @@ void main() { label: 'test', textDirection: TextDirection.ltr, child: new Container( - child: const Semantics( + child: new Semantics( checked: true ), ), @@ -50,7 +50,7 @@ void main() { new Semantics( container: true, child: new Container( - child: const Semantics( + child: new Semantics( checked: true, ), ), @@ -74,7 +74,7 @@ void main() { new Semantics( container: true, child: new Container( - child: const Semantics( + child: new Semantics( label: 'test', textDirection: TextDirection.ltr, ), @@ -100,9 +100,9 @@ void main() { new Semantics( container: true, child: new Container( - child: const Semantics( + child: new Semantics( checked: true, - child: const Semantics( + child: new Semantics( label: 'test', textDirection: TextDirection.ltr, ), @@ -134,9 +134,9 @@ void main() { new Semantics( container: true, child: new Container( - child: const Semantics( + child: new Semantics( checked: true, - child: const Semantics( + child: new Semantics( label: 'test', textDirection: TextDirection.ltr, ), diff --git a/packages/flutter/test/widgets/semantics_4_test.dart b/packages/flutter/test/widgets/semantics_4_test.dart index 293f5b2be57..ab1cd279b4a 100644 --- a/packages/flutter/test/widgets/semantics_4_test.dart +++ b/packages/flutter/test/widgets/semantics_4_test.dart @@ -26,7 +26,7 @@ void main() { child: new Stack( fit: StackFit.expand, children: [ - const Semantics( + new Semantics( container: true, label: 'L1', ), @@ -36,10 +36,10 @@ void main() { child: new Stack( fit: StackFit.expand, children: [ - const Semantics( + new Semantics( checked: true, ), - const Semantics( + new Semantics( checked: false, ), ], @@ -88,7 +88,7 @@ void main() { child: new Stack( fit: StackFit.expand, children: [ - const Semantics( + new Semantics( label: 'L1', container: true, ), @@ -98,10 +98,10 @@ void main() { child: new Stack( fit: StackFit.expand, children: [ - const Semantics( + new Semantics( checked: true, ), - const Semantics(), + new Semantics(), ], ), ), @@ -136,17 +136,17 @@ void main() { child: new Stack( fit: StackFit.expand, children: [ - const Semantics(), + new Semantics(), new Semantics( label: 'L2', container: true, child: new Stack( fit: StackFit.expand, children: [ - const Semantics( + new Semantics( checked: true, ), - const Semantics(), + new Semantics(), ], ), ), diff --git a/packages/flutter/test/widgets/semantics_5_test.dart b/packages/flutter/test/widgets/semantics_5_test.dart index 3284e78776c..25fb24700b2 100644 --- a/packages/flutter/test/widgets/semantics_5_test.dart +++ b/packages/flutter/test/widgets/semantics_5_test.dart @@ -17,14 +17,14 @@ void main() { textDirection: TextDirection.ltr, fit: StackFit.expand, children: [ - const Semantics( + new Semantics( // this tests that empty nodes disappear ), - const Semantics( + new Semantics( // this tests whether you can have a container with no other semantics container: true, ), - const Semantics( + new Semantics( label: 'label', // (force a fork) textDirection: TextDirection.ltr, ), diff --git a/packages/flutter/test/widgets/semantics_7_test.dart b/packages/flutter/test/widgets/semantics_7_test.dart index e5b8377e5ab..981667a02fe 100644 --- a/packages/flutter/test/widgets/semantics_7_test.dart +++ b/packages/flutter/test/widgets/semantics_7_test.dart @@ -37,7 +37,7 @@ void main() { child: new Stack( fit: StackFit.expand, children: [ - const Semantics( + new Semantics( checked: true, ), new Semantics( @@ -93,7 +93,7 @@ void main() { child: new Stack( fit: StackFit.expand, children: [ - const Semantics( + new Semantics( checked: true, ), new Semantics( diff --git a/packages/flutter/test/widgets/semantics_8_test.dart b/packages/flutter/test/widgets/semantics_8_test.dart index 03429a54248..81f3f45c31d 100644 --- a/packages/flutter/test/widgets/semantics_8_test.dart +++ b/packages/flutter/test/widgets/semantics_8_test.dart @@ -23,10 +23,10 @@ void main() { child: new Stack( textDirection: TextDirection.ltr, children: [ - const Semantics( + new Semantics( checked: true, ), - const Semantics( + new Semantics( label: 'label', textDirection: TextDirection.ltr, ) @@ -61,11 +61,11 @@ void main() { child: new Stack( textDirection: TextDirection.ltr, children: [ - const Semantics( + new Semantics( label: 'label', textDirection: TextDirection.ltr, ), - const Semantics( + new Semantics( checked: true ) ] diff --git a/packages/flutter/test/widgets/semantics_debugger_test.dart b/packages/flutter/test/widgets/semantics_debugger_test.dart index 9be6e32b814..f25dd4d5b1d 100644 --- a/packages/flutter/test/widgets/semantics_debugger_test.dart +++ b/packages/flutter/test/widgets/semantics_debugger_test.dart @@ -16,11 +16,11 @@ void main() { textDirection: TextDirection.ltr, child: new Stack( children: [ - const Semantics(), - const Semantics( + new Semantics(), + new Semantics( container: true, ), - const Semantics( + new Semantics( label: 'label', textDirection: TextDirection.ltr, ), @@ -35,11 +35,11 @@ void main() { child: new SemanticsDebugger( child: new Stack( children: [ - const Semantics(), - const Semantics( + new Semantics(), + new Semantics( container: true, ), - const Semantics( + new Semantics( label: 'label', textDirection: TextDirection.ltr, ), @@ -62,14 +62,14 @@ void main() { child: new SemanticsDebugger( child: new Stack( children: [ - const Semantics(label: 'label1', textDirection: TextDirection.ltr), + new Semantics(label: 'label1', textDirection: TextDirection.ltr), new Positioned( key: key, left: 0.0, top: 0.0, width: 100.0, height: 100.0, - child: const Semantics(label: 'label2', textDirection: TextDirection.ltr), + child: new Semantics(label: 'label2', textDirection: TextDirection.ltr), ), ], ), @@ -83,7 +83,7 @@ void main() { child: new SemanticsDebugger( child: new Stack( children: [ - const Semantics(label: 'label1', textDirection: TextDirection.ltr), + new Semantics(label: 'label1', textDirection: TextDirection.ltr), new Semantics( container: true, child: new Stack( @@ -94,9 +94,9 @@ void main() { top: 0.0, width: 100.0, height: 100.0, - child: const Semantics(label: 'label2', textDirection: TextDirection.ltr), + child: new Semantics(label: 'label2', textDirection: TextDirection.ltr), ), - const Semantics(label: 'label3', textDirection: TextDirection.ltr), + new Semantics(label: 'label3', textDirection: TextDirection.ltr), ], ), ), @@ -112,7 +112,7 @@ void main() { child: new SemanticsDebugger( child: new Stack( children: [ - const Semantics(label: 'label1', textDirection: TextDirection.ltr), + new Semantics(label: 'label1', textDirection: TextDirection.ltr), new Semantics( container: true, child: new Stack( @@ -123,9 +123,9 @@ void main() { top: 0.0, width: 100.0, height: 100.0, - child: const Semantics(label: 'label2', textDirection: TextDirection.ltr)), - const Semantics(label: 'label3', textDirection: TextDirection.ltr), - const Semantics(label: 'label4', textDirection: TextDirection.ltr), + child: new Semantics(label: 'label2', textDirection: TextDirection.ltr)), + new Semantics(label: 'label3', textDirection: TextDirection.ltr), + new Semantics(label: 'label4', textDirection: TextDirection.ltr), ], ), ), diff --git a/packages/flutter/test/widgets/semantics_test.dart b/packages/flutter/test/widgets/semantics_test.dart index a92a4b14caa..40ebc221fe9 100644 --- a/packages/flutter/test/widgets/semantics_test.dart +++ b/packages/flutter/test/widgets/semantics_test.dart @@ -262,10 +262,10 @@ void main() { container: true, child: new Column( children: [ - const Semantics( + new Semantics( hint: 'hint one', ), - const Semantics( + new Semantics( hint: 'hint two', ) @@ -347,10 +347,10 @@ void main() { container: true, child: new Column( children: [ - const Semantics( + new Semantics( hint: 'hint', ), - const Semantics( + new Semantics( value: 'value', ), ], diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart index 918ec62b8c8..99c0702c612 100644 --- a/packages/flutter/test/widgets/semantics_tester.dart +++ b/packages/flutter/test/widgets/semantics_tester.dart @@ -196,7 +196,7 @@ class TestSemantics { final SemanticsData nodeData = node.getSemanticsData(); bool fail(String message) { - matchState[TestSemantics] = '$message\n$_matcherHelp'; + matchState[TestSemantics] = '$message'; return false; } @@ -246,8 +246,29 @@ class TestSemantics { } @override - String toString() { - return 'node $id, flags=$flags, actions=$actions, label="$label", textDirection=$textDirection, rect=$rect, transform=$transform, ${children.length} child${ children.length == 1 ? "" : "ren" }'; + String toString([int indentAmount = 0]) { + final String indent = ' ' * indentAmount; + final StringBuffer buf = new StringBuffer(); + buf.writeln('$indent$runtimeType {'); + if (id != null) + buf.writeln('$indent id: $id'); + buf.writeln('$indent flags: $flags'); + buf.writeln('$indent actions: $actions'); + if (label != null) + buf.writeln('$indent label: "$label"'); + if (textDirection != null) + buf.writeln('$indent textDirection: $textDirection'); + if (rect != null) + buf.writeln('$indent rect: $rect'); + if (transform != null) + buf.writeln('$indent transform:\n${transform.toString().trim().split('\n').map((String line) => '$indent $line').join('\n')}'); + buf.writeln('$indent children: ['); + for (TestSemantics child in children) { + buf.writeln(child.toString(indentAmount + 2)); + } + buf.writeln('$indent ]'); + buf.write('$indent}'); + return buf.toString(); } } @@ -295,12 +316,17 @@ class _HasSemantics extends Matcher { @override Description describe(Description description) { - return description.add('semantics node matching: $_semantics'); + return description.add('semantics node matching:\n$_semantics'); } @override Description describeMismatch(dynamic item, Description mismatchDescription, Map matchState, bool verbose) { - return mismatchDescription.add(matchState[TestSemantics]); + return mismatchDescription + .add('${matchState[TestSemantics]}\n') + .add( + 'Current SemanticsNode tree:\n' + ) + .add(RendererBinding.instance?.renderView?.debugSemantics?.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest)); } }