mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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
This commit is contained in:
parent
a69af9902c
commit
ffb24eda56
@ -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';
|
||||
|
||||
86
packages/flutter/lib/src/foundation/key.dart
Normal file
86
packages/flutter/lib/src/foundation/key.dart
Normal file
@ -0,0 +1,86 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' show hashValues;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s.
|
||||
///
|
||||
/// A new widget will only be used to update an existing element if its key is
|
||||
/// the same as the key of the current widget associated with the element.
|
||||
///
|
||||
/// Keys must be unique amongst the [Element]s with the same parent.
|
||||
///
|
||||
/// Subclasses of [Key] should either subclass [LocalKey] or [GlobalKey].
|
||||
///
|
||||
/// See also the discussion at [Widget.key].
|
||||
@immutable
|
||||
abstract class Key {
|
||||
/// Construct a [ValueKey<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;
|
||||
}
|
||||
|
||||
862
packages/flutter/lib/src/rendering/custom_paint.dart
Normal file
862
packages/flutter/lib/src/rendering/custom_paint.dart
Normal file
@ -0,0 +1,862 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'box.dart';
|
||||
import 'object.dart';
|
||||
import 'proxy_box.dart';
|
||||
|
||||
/// Signature of the function returned by [CustomPainter.semanticsBuilder].
|
||||
///
|
||||
/// Builds semantics information describing the picture drawn by a
|
||||
/// [CustomPainter]. Each [CustomPainterSemantics] in the returned list is
|
||||
/// converted into a [SemanticsNode] by copying its properties.
|
||||
///
|
||||
/// The returned list must not be mutated after this function completes. To
|
||||
/// change the semantic information, the function must return a new list
|
||||
/// instead.
|
||||
typedef List<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))
|
||||
|
||||
631
packages/flutter/test/widgets/custom_painter_test.dart
Normal file
631
packages/flutter/test/widgets/custom_painter_test.dart
Normal file
@ -0,0 +1,631 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
void main() {
|
||||
group(CustomPainter, () {
|
||||
setUp(() {
|
||||
debugResetSemanticsIdCounter();
|
||||
_PainterWithSemantics.shouldRebuildSemanticsCallCount = 0;
|
||||
_PainterWithSemantics.buildSemanticsCallCount = 0;
|
||||
_PainterWithSemantics.semanticsBuilderCallCount = 0;
|
||||
});
|
||||
|
||||
_defineTests();
|
||||
});
|
||||
}
|
||||
|
||||
void _defineTests() {
|
||||
testWidgets('builds no semantics by default', (WidgetTester tester) async {
|
||||
final SemanticsTester semanticsTester = new SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(new CustomPaint(
|
||||
painter: new _PainterWithoutSemantics(),
|
||||
));
|
||||
|
||||
expect(semanticsTester, hasSemantics(
|
||||
new TestSemantics.root(
|
||||
children: const <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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user