Unverified Commit ffb24eda authored by Yegor's avatar Yegor Committed by GitHub

Accessibility API for CustomPainter (#13313)

Summary:

- Add `key` field to `SemanticsNode`, while moving key into `foundation` library so it can be used by the render layer.
- Introduce `SemanticsProperties` and move many of the `Semantics` fields into it.
- Introduce `CustomPaintSemantics` - a `SemanticsNode` prototype created by `CustomPainter`.
- Introduce `semanticsBuilder` and `shouldRebuildSemantics` in `CustomerPainter`

**Breaking change**

The default `Semantics` constructor becomes non-const (due to https://github.com/dart-lang/sdk/issues/20962). However, a new `const Semantics.fromProperties` is added that still allowed creating constant `Semantics` widgets ([mailing list announcement](https://groups.google.com/forum/#!topic/flutter-dev/KQXBl2_1sws)).

Fixes https://github.com/flutter/flutter/issues/11791
Fixes https://github.com/flutter/flutter/issues/1666
parent a69af990
......@@ -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';
......
......@@ -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';
......
// 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<String>] with the given [String].
///
/// This is the simplest way to create keys.
const factory Key(String value) = ValueKey<String>;
/// 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<T>] is equal to another [ValueKey<T>] 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<T> 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<T> 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<ValueKey<T>>().type)
return '[$valueString]';
return '[$T $valueString]';
}
}
class _TypeLiteral<T> { Type get type => T; }
......@@ -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;
}
// 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<CustomPainterSemantics> 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<SemanticsTag> tags;
}
/// Provides a canvas on which to draw during the paint phase.
///
/// When asked to paint, [RenderCustomPaint] first asks its [painter] to paint
/// on the current canvas, then it paints its child, and then, after painting
/// its child, it asks its [foregroundPainter] to paint. The coordinate system of
/// the canvas matches the coordinate system of the [CustomPaint] object. The
/// painters are expected to paint within a rectangle starting at the origin and
/// encompassing a region of the given size. (If the painters paint outside
/// those bounds, there might be insufficient memory allocated to rasterize the
/// painting commands and the resulting behavior is undefined.)
///
/// Painters are implemented by subclassing or implementing [CustomPainter].
///
/// Because custom paint calls its painters during paint, you cannot mark the
/// tree as needing a new layout during the callback (the layout for this frame
/// has already happened).
///
/// Custom painters normally size themselves to their child. If they do not have
/// a child, they attempt to size themselves to the [preferredSize], which
/// defaults to [Size.zero].
///
/// See also:
///
/// * [CustomPainter], the class that custom painter delegates should extend.
/// * [Canvas], the API provided to custom painter delegates.
class RenderCustomPaint extends RenderProxyBox {
/// Creates a render object that delegates its painting.
RenderCustomPaint({
CustomPainter painter,
CustomPainter foregroundPainter,
Size preferredSize: Size.zero,
this.isComplex: false,
this.willChange: false,
RenderBox child,
}) : assert(preferredSize != null),
_painter = painter,
_foregroundPainter = foregroundPainter,
_preferredSize = preferredSize,
super(child);
/// The background custom paint delegate.
///
/// This painter, if non-null, is called to paint behind the children.
CustomPainter get painter => _painter;
CustomPainter _painter;
/// Set a new background custom paint delegate.
///
/// If the new delegate is the same as the previous one, this does nothing.
///
/// If the new delegate is the same class as the previous one, then the new
/// delegate has its [CustomPainter.shouldRepaint] called; if the result is
/// true, then the delegate will be called.
///
/// If the new delegate is a different class than the previous one, then the
/// delegate will be called.
///
/// If the new value is null, then there is no background custom painter.
set painter(CustomPainter value) {
if (_painter == value)
return;
final CustomPainter oldPainter = _painter;
_painter = value;
_didUpdatePainter(_painter, oldPainter);
}
/// The foreground custom paint delegate.
///
/// This painter, if non-null, is called to paint in front of the children.
CustomPainter get foregroundPainter => _foregroundPainter;
CustomPainter _foregroundPainter;
/// Set a new foreground custom paint delegate.
///
/// If the new delegate is the same as the previous one, this does nothing.
///
/// If the new delegate is the same class as the previous one, then the new
/// delegate has its [CustomPainter.shouldRepaint] called; if the result is
/// true, then the delegate will be called.
///
/// If the new delegate is a different class than the previous one, then the
/// delegate will be called.
///
/// If the new value is null, then there is no foreground custom painter.
set foregroundPainter(CustomPainter value) {
if (_foregroundPainter == value)
return;
final CustomPainter oldPainter = _foregroundPainter;
_foregroundPainter = value;
_didUpdatePainter(_foregroundPainter, oldPainter);
}
void _didUpdatePainter(CustomPainter newPainter, CustomPainter oldPainter) {
// Check if we need to repaint.
if (newPainter == null) {
assert(oldPainter != null); // We should be called only for changes.
markNeedsPaint();
} else if (oldPainter == null ||
newPainter.runtimeType != oldPainter.runtimeType ||
newPainter.shouldRepaint(oldPainter)) {
markNeedsPaint();
}
if (attached) {
oldPainter?.removeListener(markNeedsPaint);
newPainter?.addListener(markNeedsPaint);
}
// Check if we need to rebuild semantics.
if (newPainter == null) {
assert(oldPainter != null); // We should be called only for changes.
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<SemanticsNode> _backgroundSemanticsNodes;
/// Describe the semantics of the picture painted by the [foregroundPainter].
List<SemanticsNode> _foregroundSemanticsNodes;
@override
void assembleSemanticsNode(
SemanticsNode node,
SemanticsConfiguration config,
Iterable<SemanticsNode> children,
) {
assert(() {
if (child == null && children.isNotEmpty) {
throw 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<CustomPainterSemantics> backgroundSemantics = _backgroundSemanticsBuilder != null
? _backgroundSemanticsBuilder(size)
: const <CustomPainterSemantics>[];
_backgroundSemanticsNodes = _updateSemanticsChildren(_backgroundSemanticsNodes, backgroundSemantics);
final List<CustomPainterSemantics> foregroundSemantics = _foregroundSemanticsBuilder != null
? _foregroundSemanticsBuilder(size)
: const <CustomPainterSemantics>[];
_foregroundSemanticsNodes = _updateSemanticsChildren(_foregroundSemanticsNodes, foregroundSemantics);
final bool hasBackgroundSemantics = _backgroundSemanticsNodes != null && _backgroundSemanticsNodes.isNotEmpty;
final bool hasForegroundSemantics = _foregroundSemanticsNodes != null && _foregroundSemanticsNodes.isNotEmpty;
final List<SemanticsNode> finalChildren = <SemanticsNode>[];
if (hasBackgroundSemantics)
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<SemanticsNode> _updateSemanticsChildren(
List<SemanticsNode> oldSemantics,
List<CustomPainterSemantics> newChildSemantics,
) {
oldSemantics = oldSemantics ?? const <SemanticsNode>[];
newChildSemantics = newChildSemantics ?? const <CustomPainterSemantics>[];
assert(() {
final Map<Key, int> keys = new HashMap<Key, int>();
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<SemanticsNode> newChildren = new List<SemanticsNode>(newChildSemantics.length);
// Update the top of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final SemanticsNode oldChild = oldSemantics[oldChildrenTop];
final CustomPainterSemantics newSemantics = newChildSemantics[newChildrenTop];
if (!_canUpdateSemanticsChild(oldChild, newSemantics))
break;
final SemanticsNode newChild = _updateSemanticsChild(oldChild, newSemantics);
newChildren[newChildrenTop] = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
// Scan the bottom of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final SemanticsNode oldChild = oldSemantics[oldChildrenBottom];
final CustomPainterSemantics newChild = newChildSemantics[newChildrenBottom];
if (!_canUpdateSemanticsChild(oldChild, newChild))
break;
oldChildrenBottom -= 1;
newChildrenBottom -= 1;
}
// Scan the old children in the middle of the list.
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map<Key, SemanticsNode> oldKeyedChildren;
if (haveOldChildren) {
oldKeyedChildren = <Key, SemanticsNode>{};
while (oldChildrenTop <= oldChildrenBottom) {
final SemanticsNode oldChild = oldSemantics[oldChildrenTop];
if (oldChild.key != null)
oldKeyedChildren[oldChild.key] = oldChild;
oldChildrenTop += 1;
}
}
// Update the middle of the list.
while (newChildrenTop <= newChildrenBottom) {
SemanticsNode oldChild;
final CustomPainterSemantics newSemantics = newChildSemantics[newChildrenTop];
if (haveOldChildren) {
final Key key = newSemantics.key;
if (key != null) {
oldChild = oldKeyedChildren[key];
if (oldChild != null) {
if (_canUpdateSemanticsChild(oldChild, newSemantics)) {
// we found a match!
// remove it from oldKeyedChildren so we don't unsync it later
oldKeyedChildren.remove(key);
} else {
// Not a match, let's pretend we didn't see it for now.
oldChild = null;
}
}
}
}
assert(oldChild == null || _canUpdateSemanticsChild(oldChild, newSemantics));
final SemanticsNode newChild = _updateSemanticsChild(oldChild, newSemantics);
assert(oldChild == newChild || oldChild == null);
newChildren[newChildrenTop] = newChild;
newChildrenTop += 1;
}
// We've scanned the whole list.
assert(oldChildrenTop == oldChildrenBottom + 1);
assert(newChildrenTop == newChildrenBottom + 1);
assert(newChildSemantics.length - newChildrenTop == oldSemantics.length - oldChildrenTop);
newChildrenBottom = newChildSemantics.length - 1;
oldChildrenBottom = oldSemantics.length - 1;
// Update the bottom of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final SemanticsNode oldChild = oldSemantics[oldChildrenTop];
final CustomPainterSemantics newSemantics = newChildSemantics[newChildrenTop];
assert(_canUpdateSemanticsChild(oldChild, newSemantics));
final SemanticsNode newChild = _updateSemanticsChild(oldChild, newSemantics);
assert(oldChild == newChild);
newChildren[newChildrenTop] = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
assert(() {
for (SemanticsNode node in newChildren) {
assert(node != null);
}
return true;
}());
return newChildren;
}
/// Whether `oldChild` can be updated with properties from `newSemantics`.
///
/// If `oldChild` can be updated, it is updated using [_updateSemanticsChild].
/// Otherwise, the node is replaced by a new instance of [SemanticsNode].
static bool _canUpdateSemanticsChild(SemanticsNode oldChild, CustomPainterSemantics newSemantics) {
return oldChild.key == newSemantics.key;
}
/// Updates `oldChild` using the properties of `newSemantics`.
///
/// This method requires that `_canUpdateSemanticsChild(oldChild, newSemantics)`
/// is true prior to calling it.
static SemanticsNode _updateSemanticsChild(SemanticsNode oldChild, CustomPainterSemantics newSemantics) {
assert(oldChild == null || _canUpdateSemanticsChild(oldChild, newSemantics));
final SemanticsNode newChild = oldChild ?? 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 <SemanticsNode>[],
);
newChild
..rect = newSemantics.rect
..transform = newSemantics.transform
..tags = newSemantics.tags;
return newChild;
}
}
......@@ -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
......
......@@ -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].
......
......@@ -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<SemanticsNode> {
}
}
/// 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<bool>('checked', checked, defaultValue: null));
description.add(new DiagnosticsProperty<bool>('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', 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<SemanticsNode> _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<SemanticsNode> _debugPreviousSnapshot;
void _replaceChildren(List<SemanticsNode> 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<SemanticsNode>.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<SemanticsNode> 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.
///
......
......@@ -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<bool>('container', container));
description.add(new DiagnosticsProperty<bool>('checked', checked, defaultValue: null));
description.add(new DiagnosticsProperty<bool>('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', textDirection, defaultValue: null));
description.add(new DiagnosticsProperty<SemanticsProperties>('properties', properties));
properties.debugFillProperties(description);
}
}
......
......@@ -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<String>] with the given [String].
///
/// This is the simplest way to create keys.
const factory Key(String value) = ValueKey<String>;
/// 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<T>] is equal to another [ValueKey<T>] 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<T> 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<T> 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<ValueKey<T>>().type)
return '[$valueString]';
return '[$T $valueString]';
}
}
class _TypeLiteral<T> { 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<T extends State<StatefulWidget>> 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<GlobalKey, Element> _registry = <GlobalKey, Element>{};
static final Set<GlobalKey> _removedKeys = new HashSet<GlobalKey>();
......@@ -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))
......
// 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 <TestSemantics>[],
),
));
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: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
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: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
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: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
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<int>(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: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
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<int>(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: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
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(<String>[
'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: <String>[],
to: <String>['a'],
);
});
testDiff('removes the last item from the list', (_DiffTester tester) async {
await tester.diff(
from: <String>['a'],
to: <String>[],
);
});
testDiff('appends one item at the end of a non-empty list', (_DiffTester tester) async {
await tester.diff(
from: <String>['a'],
to: <String>['a', 'b'],
);
});
testDiff('prepends one item at the beginning of a non-empty list', (_DiffTester tester) async {
await tester.diff(
from: <String>['b'],
to: <String>['a', 'b'],
);
});
testDiff('inserts one item in the middle of a list', (_DiffTester tester) async {
await tester.diff(
from: <String>[
'a-k',
'c-k',
],
to: <String>[
'a-k',
'b-k',
'c-k',
],
);
});
testDiff('removes one item from the middle of a list', (_DiffTester tester) async {
await tester.diff(
from: <String>[
'a-k',
'b-k',
'c-k',
],
to: <String>[
'a-k',
'c-k',
],
);
});
testDiff('swaps two items', (_DiffTester tester) async {
await tester.diff(
from: <String>[
'a-k',
'b-k',
],
to: <String>[
'b-k',
'a-k',
],
);
});
testDiff('finds and moved one keyed item', (_DiffTester tester) async {
await tester.diff(
from: <String>[
'a-k',
'b',
'c',
],
to: <String>[
'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<Null> 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<Null> diff({List<String> from, List<String> to}) async {
final SemanticsTester semanticsTester = new SemanticsTester(tester);
TestSemantics createExpectations(List<String> labels) {
final List<TestSemantics> children = <TestSemantics>[];
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: <TestSemantics>[
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<Key, int> idAssignments = <Key, int>{};
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<String> data;
@override
void paint(Canvas canvas, Size size) {
// We don't test painting.
}
@override
SemanticsBuilderCallback get semanticsBuilder => buildSemantics;
List<CustomPainterSemantics> buildSemantics(Size size) {
final List<CustomPainterSemantics> semantics = <CustomPainterSemantics>[];
for (String label in data) {
Key key;
if (label.endsWith('-k')) {
key = new ValueKey<String>(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<CustomPainterSemantics> buildSemantics(Size size) {
buildSemanticsCallCount += 1;
return <CustomPainterSemantics>[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;
}
......@@ -47,10 +47,10 @@ Widget buildTestWidgets({bool excludeSemantics, String label, bool isSemanticsBo
isSemanticBoundary: isSemanticsBoundary,
child: new Column(
children: <Widget>[
const Semantics(
new Semantics(
label: 'child1',
),
const Semantics(
new Semantics(
label: 'child2',
),
],
......
......@@ -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
......
......@@ -50,7 +50,7 @@ void main() {
children: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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,
......
......@@ -28,7 +28,7 @@ void main() {
children: <Widget>[
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: <Widget>[
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: <Widget>[
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,
......
......@@ -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,
),
......
......@@ -26,7 +26,7 @@ void main() {
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
const Semantics(
new Semantics(
container: true,
label: 'L1',
),
......@@ -36,10 +36,10 @@ void main() {
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
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: <Widget>[
const Semantics(
new Semantics(
label: 'L1',
container: true,
),
......@@ -98,10 +98,10 @@ void main() {
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
const Semantics(
new Semantics(
checked: true,
),
const Semantics(),
new Semantics(),
],
),
),
......@@ -136,17 +136,17 @@ void main() {
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
const Semantics(),
new Semantics(),
new Semantics(
label: 'L2',
container: true,
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
const Semantics(
new Semantics(
checked: true,
),
const Semantics(),
new Semantics(),
],
),
),
......
......@@ -17,14 +17,14 @@ void main() {
textDirection: TextDirection.ltr,
fit: StackFit.expand,
children: <Widget>[
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,
),
......
......@@ -37,7 +37,7 @@ void main() {
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
const Semantics(
new Semantics(
checked: true,
),
new Semantics(
......@@ -93,7 +93,7 @@ void main() {
child: new Stack(
fit: StackFit.expand,
children: <Widget>[
const Semantics(
new Semantics(
checked: true,
),
new Semantics(
......
......@@ -23,10 +23,10 @@ void main() {
child: new Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
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: <Widget>[
const Semantics(
new Semantics(
label: 'label',
textDirection: TextDirection.ltr,
),
const Semantics(
new Semantics(
checked: true
)
]
......
......@@ -16,11 +16,11 @@ void main() {
textDirection: TextDirection.ltr,
child: new Stack(
children: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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),
],
),
),
......
......@@ -262,10 +262,10 @@ void main() {
container: true,
child: new Column(
children: <Widget>[
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: <Widget>[
const Semantics(
new Semantics(
hint: 'hint',
),
const Semantics(
new Semantics(
value: 'value',
),
],
......
......@@ -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<dynamic, dynamic> 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));
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment