hit_test.dart 10.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5

6 7 8
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';

Ian Hickson's avatar
Ian Hickson committed
9 10 11
import 'events.dart';

/// An object that can hit-test pointers.
12
abstract class HitTestable {
13 14
  // This class is intended to be used as an interface, and should not be
  // extended directly; this constructor prevents instantiation and extension.
15
  HitTestable._();
16

17 18 19 20
  /// Check whether the given position hits this object.
  ///
  /// If this given position hits this object, consider adding a [HitTestEntry]
  /// to the given hit test result.
21
  void hitTest(HitTestResult result, Offset position);
Ian Hickson's avatar
Ian Hickson committed
22
}
23

24
/// An object that can dispatch events.
25
abstract class HitTestDispatcher {
26 27
  // This class is intended to be used as an interface, and should not be
  // extended directly; this constructor prevents instantiation and extension.
28
  HitTestDispatcher._();
29

30
  /// Override this method to dispatch events.
31 32 33
  void dispatchEvent(PointerEvent event, HitTestResult result);
}

Adam Barth's avatar
Adam Barth committed
34
/// An object that can handle events.
35
abstract class HitTestTarget {
36 37
  // This class is intended to be used as an interface, and should not be
  // extended directly; this constructor prevents instantiation and extension.
38
  HitTestTarget._();
39

40
  /// Override this method to receive events.
41
  void handleEvent(PointerEvent event, HitTestEntry<HitTestTarget> entry);
42 43
}

Adam Barth's avatar
Adam Barth committed
44 45 46 47
/// Data collected during a hit test about a specific [HitTestTarget].
///
/// Subclass this object to pass additional information from the hit test phase
/// to the event propagation phase.
48 49
@optionalTypeArgs
class HitTestEntry<T extends HitTestTarget> {
50
  /// Creates a hit test entry.
51
  HitTestEntry(this.target);
Adam Barth's avatar
Adam Barth committed
52 53

  /// The [HitTestTarget] encountered during the hit test.
54
  final T target;
Ian Hickson's avatar
Ian Hickson committed
55

56
  @override
57
  String toString() => '${describeIdentity(this)}($target)';
58 59 60 61 62 63 64

  /// Returns a matrix describing how [PointerEvent]s delivered to this
  /// [HitTestEntry] should be transformed from the global coordinate space of
  /// the screen to the local coordinate space of [target].
  ///
  /// See also:
  ///
65
  ///  * [BoxHitTestResult.addWithPaintTransform], which is used during hit testing
66
  ///    to build up this transform.
67 68
  Matrix4? get transform => _transform;
  Matrix4? _transform;
69 70
}

71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
// A type of data that can be applied to a matrix by left-multiplication.
@immutable
abstract class _TransformPart {
  const _TransformPart();

  // Apply this transform part to `rhs` from the left.
  //
  // This should work as if this transform part is first converted to a matrix
  // and then left-multiplied to `rhs`.
  //
  // For example, if this transform part is a vector `v1`, whose corresponding
  // matrix is `m1 = Matrix4.translation(v1)`, then the result of
  // `_VectorTransformPart(v1).multiply(rhs)` should equal to `m1 * rhs`.
  Matrix4 multiply(Matrix4 rhs);
}

class _MatrixTransformPart extends _TransformPart {
  const _MatrixTransformPart(this.matrix);

  final Matrix4 matrix;

  @override
  Matrix4 multiply(Matrix4 rhs) {
94
    return matrix.multiplied(rhs);
95 96 97 98 99 100 101 102 103 104 105 106 107 108
  }
}

class _OffsetTransformPart extends _TransformPart {
  const _OffsetTransformPart(this.offset);

  final Offset offset;

  @override
  Matrix4 multiply(Matrix4 rhs) {
    return rhs.clone()..leftTranslate(offset.dx, offset.dy);
  }
}

Adam Barth's avatar
Adam Barth committed
109
/// The result of performing a hit test.
110
class HitTestResult {
111
  /// Creates an empty hit test result.
112 113
  HitTestResult()
     : _path = <HitTestEntry>[],
114 115
       _transforms = <Matrix4>[Matrix4.identity()],
       _localTransforms = <_TransformPart>[];
116 117 118

  /// Wraps `result` (usually a subtype of [HitTestResult]) to create a
  /// generic [HitTestResult].
119
  ///
120 121 122
  /// The [HitTestEntry]s added to the returned [HitTestResult] are also
  /// added to the wrapped `result` (both share the same underlying data
  /// structure to store [HitTestEntry]s).
123 124
  HitTestResult.wrap(HitTestResult result)
     : _path = result._path,
125 126
       _transforms = result._transforms,
       _localTransforms = result._localTransforms;
127

128
  /// An unmodifiable list of [HitTestEntry] objects recorded during the hit test.
Adam Barth's avatar
Adam Barth committed
129
  ///
130 131 132
  /// The first entry in the path is the most specific, typically the one at
  /// the leaf of tree being hit tested. Event propagation starts with the most
  /// specific (i.e., first) entry and proceeds in order through the path.
Ian Hickson's avatar
Ian Hickson committed
133
  Iterable<HitTestEntry> get path => _path;
134
  final List<HitTestEntry> _path;
135

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
  // A stack of transform parts.
  //
  // The transform part stack leading from global to the current object is stored
  // in 2 parts:
  //
  //  * `_transforms` are globalized matrices, meaning they have been multiplied
  //    by the ancestors and are thus relative to the global coordinate space.
  //  * `_localTransforms` are local transform parts, which are relative to the
  //    parent's coordinate space.
  //
  // When new transform parts are added they're appended to `_localTransforms`,
  // and are converted to global ones and moved to `_transforms` only when used.
  final List<Matrix4> _transforms;
  final List<_TransformPart> _localTransforms;

  // Globalize all transform parts in `_localTransforms` and move them to
  // _transforms.
  void _globalizeTransforms() {
    if (_localTransforms.isEmpty) {
      return;
    }
    Matrix4 last = _transforms.last;
    for (final _TransformPart part in _localTransforms) {
      last = part.multiply(last);
      _transforms.add(last);
    }
    _localTransforms.clear();
  }

  Matrix4 get _lastTransform {
    _globalizeTransforms();
    assert(_localTransforms.isEmpty);
    return _transforms.last;
  }
170

Adam Barth's avatar
Adam Barth committed
171 172 173
  /// Add a [HitTestEntry] to the path.
  ///
  /// The new entry is added at the end of the path, which means entries should
Ian Hickson's avatar
Ian Hickson committed
174 175
  /// be added in order from most specific to least specific, typically during an
  /// upward walk of the tree being hit tested.
Adam Barth's avatar
Adam Barth committed
176
  void add(HitTestEntry entry) {
177
    assert(entry._transform == null);
178
    entry._transform = _lastTransform;
179
    _path.add(entry);
180
  }
Ian Hickson's avatar
Ian Hickson committed
181

182 183 184 185 186 187 188 189 190 191 192 193 194 195
  /// Pushes a new transform matrix that is to be applied to all future
  /// [HitTestEntry]s added via [add] until it is removed via [popTransform].
  ///
  /// This method is only to be used by subclasses, which must provide
  /// coordinate space specific public wrappers around this function for their
  /// users (see [BoxHitTestResult.addWithPaintTransform] for such an example).
  ///
  /// The provided `transform` matrix should describe how to transform
  /// [PointerEvent]s from the coordinate space of the method caller to the
  /// coordinate space of its children. In most cases `transform` is derived
  /// from running the inverted result of [RenderObject.applyPaintTransform]
  /// through [PointerEvent.removePerspectiveTransform] to remove
  /// the perspective component.
  ///
196 197 198
  /// If the provided `transform` is a translation matrix, it is much faster
  /// to use [pushOffset] with the translation offset instead.
  ///
199 200 201 202 203 204
  /// [HitTestable]s need to call this method indirectly through a convenience
  /// method defined on a subclass before hit testing a child that does not
  /// have the same origin as the parent. After hit testing the child,
  /// [popTransform] has to be called to remove the child-specific `transform`.
  ///
  /// See also:
205
  ///
206 207
  ///  * [pushOffset], which is similar to [pushTransform] but is limited to
  ///    translations, and is faster in such cases.
208 209 210 211 212 213 214 215 216 217
  ///  * [BoxHitTestResult.addWithPaintTransform], which is a public wrapper
  ///    around this function for hit testing on [RenderBox]s.
  @protected
  void pushTransform(Matrix4 transform) {
    assert(transform != null);
    assert(
      _debugVectorMoreOrLessEquals(transform.getRow(2), Vector4(0, 0, 1, 0)) &&
      _debugVectorMoreOrLessEquals(transform.getColumn(2), Vector4(0, 0, 1, 0)),
      'The third row and third column of a transform matrix for pointer '
      'events must be Vector4(0, 0, 1, 0) to ensure that a transformed '
218
      'point is directly under the pointing device. Did you forget to run the paint '
219
      'matrix through PointerEvent.removePerspectiveTransform? '
220
      'The provided matrix is:\n$transform',
221
    );
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
    _localTransforms.add(_MatrixTransformPart(transform));
  }

  /// Pushes a new translation offset that is to be applied to all future
  /// [HitTestEntry]s added via [add] until it is removed via [popTransform].
  ///
  /// This method is only to be used by subclasses, which must provide
  /// coordinate space specific public wrappers around this function for their
  /// users (see [BoxHitTestResult.addWithPaintOffset] for such an example).
  ///
  /// The provided `offset` should describe how to transform [PointerEvent]s from
  /// the coordinate space of the method caller to the coordinate space of its
  /// children. Usually `offset` is the inverse of the offset of the child
  /// relative to the parent.
  ///
  /// [HitTestable]s need to call this method indirectly through a convenience
  /// method defined on a subclass before hit testing a child that does not
  /// have the same origin as the parent. After hit testing the child,
  /// [popTransform] has to be called to remove the child-specific `transform`.
  ///
  /// See also:
  ///
  ///  * [pushTransform], which is similar to [pushOffset] but allows general
  ///    transform besides translation.
  ///  * [BoxHitTestResult.addWithPaintOffset], which is a public wrapper
  ///    around this function for hit testing on [RenderBox]s.
  ///  * [SliverHitTestResult.addWithAxisOffset], which is a public wrapper
  ///    around this function for hit testing on [RenderSliver]s.
  @protected
  void pushOffset(Offset offset) {
    assert(offset != null);
    _localTransforms.add(_OffsetTransformPart(offset));
254 255
  }

256
  /// Removes the last transform added via [pushTransform] or [pushOffset].
257 258 259 260 261 262
  ///
  /// This method is only to be used by subclasses, which must provide
  /// coordinate space specific public wrappers around this function for their
  /// users (see [BoxHitTestResult.addWithPaintTransform] for such an example).
  ///
  /// This method must be called after hit testing is done on a child that
263
  /// required a call to [pushTransform] or [pushOffset].
264 265 266
  ///
  /// See also:
  ///
267 268
  ///  * [pushTransform] and [pushOffset], which describes the use case of this
  ///    function pair in more details.
269 270
  @protected
  void popTransform() {
271 272 273 274
    if (_localTransforms.isNotEmpty)
      _localTransforms.removeLast();
    else
      _transforms.removeLast();
275 276 277 278 279 280 281 282 283 284 285 286 287
    assert(_transforms.isNotEmpty);
  }

  bool _debugVectorMoreOrLessEquals(Vector4 a, Vector4 b, { double epsilon = precisionErrorTolerance }) {
    bool result = true;
    assert(() {
      final Vector4 difference = a - b;
      result = difference.storage.every((double component) => component.abs() < epsilon);
      return true;
    }());
    return result;
  }

288
  @override
289
  String toString() => 'HitTestResult(${_path.isEmpty ? "<empty path>" : _path.join(", ")})';
290
}