hit_test.dart 10.9 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
// @dart = 2.8

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

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

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

19 20 21 22
  /// 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.
23
  void hitTest(HitTestResult result, Offset position);
Ian Hickson's avatar
Ian Hickson committed
24
}
25

26
/// An object that can dispatch events.
27
abstract class HitTestDispatcher {
28 29 30
  // This class is intended to be used as an interface, and should not be
  // extended directly; this constructor prevents instantiation and extension.
  // ignore: unused_element
31 32
  factory HitTestDispatcher._() => null;

33
  /// Override this method to dispatch events.
34 35 36
  void dispatchEvent(PointerEvent event, HitTestResult result);
}

Adam Barth's avatar
Adam Barth committed
37
/// An object that can handle events.
38
abstract class HitTestTarget {
39 40 41
  // This class is intended to be used as an interface, and should not be
  // extended directly; this constructor prevents instantiation and extension.
  // ignore: unused_element
42 43
  factory HitTestTarget._() => null;

44
  /// Override this method to receive events.
Ian Hickson's avatar
Ian Hickson committed
45
  void handleEvent(PointerEvent event, HitTestEntry entry);
46 47
}

Adam Barth's avatar
Adam Barth committed
48 49 50 51
/// 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.
52
class HitTestEntry {
53
  /// Creates a hit test entry.
54
  HitTestEntry(this.target);
Adam Barth's avatar
Adam Barth committed
55 56

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

59
  @override
60
  String toString() => '${describeIdentity(this)}($target)';
61 62 63 64 65 66 67

  /// 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:
  ///
68 69
  ///  * [HitTestResult.addWithPaintTransform], which is used during hit testing
  ///    to build up this transform.
70 71
  Matrix4 get transform => _transform;
  Matrix4 _transform;
72 73
}

74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
// 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) {
    return matrix * rhs as Matrix4;
  }
}

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
112
/// The result of performing a hit test.
113
class HitTestResult {
114
  /// Creates an empty hit test result.
115 116
  HitTestResult()
     : _path = <HitTestEntry>[],
117 118
       _transforms = <Matrix4>[Matrix4.identity()],
       _localTransforms = <_TransformPart>[];
119 120 121

  /// Wraps `result` (usually a subtype of [HitTestResult]) to create a
  /// generic [HitTestResult].
122
  ///
123 124 125
  /// 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).
126 127
  HitTestResult.wrap(HitTestResult result)
     : _path = result._path,
128 129
       _transforms = result._transforms,
       _localTransforms = result._localTransforms;
130

131
  /// An unmodifiable list of [HitTestEntry] objects recorded during the hit test.
Adam Barth's avatar
Adam Barth committed
132
  ///
133 134 135
  /// 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
136
  Iterable<HitTestEntry> get path => _path;
137
  final List<HitTestEntry> _path;
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 170 171 172
  // 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;
  }
173

Adam Barth's avatar
Adam Barth committed
174 175 176
  /// 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
177 178
  /// 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
179
  void add(HitTestEntry entry) {
180
    assert(entry._transform == null);
181
    entry._transform = _lastTransform;
182
    _path.add(entry);
183
  }
Ian Hickson's avatar
Ian Hickson committed
184

185 186 187 188 189 190 191 192 193 194 195 196 197 198
  /// 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.
  ///
199 200 201
  /// If the provided `transform` is a translation matrix, it is much faster
  /// to use [pushOffset] with the translation offset instead.
  ///
202 203 204 205 206 207
  /// [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:
208
  ///
209 210
  ///  * [pushOffset], which is similar to [pushTransform] but is limited to
  ///    translations, and is faster in such cases.
211 212 213 214 215 216 217 218 219 220
  ///  * [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 '
221
      'point is directly under the pointing device. Did you forget to run the paint '
222
      'matrix through PointerEvent.removePerspectiveTransform? '
223 224
      'The provided matrix is:\n$transform'
    );
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 254 255 256
    _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));
257 258
  }

259
  /// Removes the last transform added via [pushTransform] or [pushOffset].
260 261 262 263 264 265
  ///
  /// 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
266
  /// required a call to [pushTransform] or [pushOffset].
267 268 269
  ///
  /// See also:
  ///
270 271
  ///  * [pushTransform] and [pushOffset], which describes the use case of this
  ///    function pair in more details.
272 273
  @protected
  void popTransform() {
274 275 276 277
    if (_localTransforms.isNotEmpty)
      _localTransforms.removeLast();
    else
      _transforms.removeLast();
278 279 280 281 282 283 284 285 286 287 288 289 290
    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;
  }

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