hit_test.dart 11 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
import 'events.dart';

11 12 13 14 15 16
export 'dart:ui' show Offset;

export 'package:vector_math/vector_math_64.dart' show Matrix4;

export 'events.dart' show PointerEvent;

Ian Hickson's avatar
Ian Hickson committed
17
/// An object that can hit-test pointers.
18
abstract class HitTestable {
19 20
  // This class is intended to be used as an interface, and should not be
  // extended directly; this constructor prevents instantiation and extension.
21
  HitTestable._();
22

23 24 25 26
  /// 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.
27
  void hitTest(HitTestResult result, Offset position);
Ian Hickson's avatar
Ian Hickson committed
28
}
29

30
/// An object that can dispatch events.
31
abstract class HitTestDispatcher {
32 33
  // This class is intended to be used as an interface, and should not be
  // extended directly; this constructor prevents instantiation and extension.
34
  HitTestDispatcher._();
35

36
  /// Override this method to dispatch events.
37 38 39
  void dispatchEvent(PointerEvent event, HitTestResult result);
}

Adam Barth's avatar
Adam Barth committed
40
/// An object that can handle events.
41
abstract class HitTestTarget {
42 43
  // This class is intended to be used as an interface, and should not be
  // extended directly; this constructor prevents instantiation and extension.
44
  HitTestTarget._();
45

46
  /// Override this method to receive events.
47
  void handleEvent(PointerEvent event, HitTestEntry<HitTestTarget> entry);
48 49
}

Adam Barth's avatar
Adam Barth committed
50 51 52 53
/// 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.
54 55
@optionalTypeArgs
class HitTestEntry<T extends HitTestTarget> {
56
  /// Creates a hit test entry.
57
  HitTestEntry(this.target);
Adam Barth's avatar
Adam Barth committed
58 59

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

62
  @override
63
  String toString() => '${describeIdentity(this)}($target)';
64 65 66 67 68 69 70

  /// 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:
  ///
71
  ///  * [BoxHitTestResult.addWithPaintTransform], which is used during hit testing
72
  ///    to build up this transform.
73 74
  Matrix4? get transform => _transform;
  Matrix4? _transform;
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
// 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) {
100
    return matrix.multiplied(rhs);
101 102 103 104 105 106 107 108 109 110 111 112 113 114
  }
}

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

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

134
  /// An unmodifiable list of [HitTestEntry] objects recorded during the hit test.
Adam Barth's avatar
Adam Barth committed
135
  ///
136 137 138
  /// 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
139
  Iterable<HitTestEntry> get path => _path;
140
  final List<HitTestEntry> _path;
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 173 174 175
  // 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;
  }
176

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

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

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

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