hit_test.dart 10.4 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 interface class HitTestable {
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 interface class HitTestDispatcher {
28
  /// Override this method to dispatch events.
29 30 31
  void dispatchEvent(PointerEvent event, HitTestResult result);
}

Adam Barth's avatar
Adam Barth committed
32
/// An object that can handle events.
33
abstract interface class HitTestTarget {
34
  /// Override this method to receive events.
35
  void handleEvent(PointerEvent event, HitTestEntry<HitTestTarget> entry);
36 37
}

Adam Barth's avatar
Adam Barth committed
38 39 40 41
/// 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.
42 43
@optionalTypeArgs
class HitTestEntry<T extends HitTestTarget> {
44
  /// Creates a hit test entry.
45
  HitTestEntry(this.target);
Adam Barth's avatar
Adam Barth committed
46 47

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

50
  @override
51
  String toString() => '${describeIdentity(this)}($target)';
52 53 54 55 56 57 58

  /// 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:
  ///
59
  ///  * [BoxHitTestResult.addWithPaintTransform], which is used during hit testing
60
  ///    to build up this transform.
61 62
  Matrix4? get transform => _transform;
  Matrix4? _transform;
63 64
}

65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
// 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) {
88
    return matrix.multiplied(rhs);
89 90 91 92 93 94 95 96 97 98 99 100 101 102
  }
}

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
103
/// The result of performing a hit test.
104
class HitTestResult {
105
  /// Creates an empty hit test result.
106 107
  HitTestResult()
     : _path = <HitTestEntry>[],
108 109
       _transforms = <Matrix4>[Matrix4.identity()],
       _localTransforms = <_TransformPart>[];
110 111 112

  /// Wraps `result` (usually a subtype of [HitTestResult]) to create a
  /// generic [HitTestResult].
113
  ///
114 115 116
  /// 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).
117 118
  HitTestResult.wrap(HitTestResult result)
     : _path = result._path,
119 120
       _transforms = result._transforms,
       _localTransforms = result._localTransforms;
121

122
  /// An unmodifiable list of [HitTestEntry] objects recorded during the hit test.
Adam Barth's avatar
Adam Barth committed
123
  ///
124 125 126
  /// 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
127
  Iterable<HitTestEntry> get path => _path;
128
  final List<HitTestEntry> _path;
129

130 131 132 133 134 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
  // 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;
  }
164

Adam Barth's avatar
Adam Barth committed
165 166 167
  /// 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
168 169
  /// 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
170
  void add(HitTestEntry entry) {
171
    assert(entry._transform == null);
172
    entry._transform = _lastTransform;
173
    _path.add(entry);
174
  }
Ian Hickson's avatar
Ian Hickson committed
175

176 177 178 179 180 181 182 183 184 185 186 187 188 189
  /// 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.
  ///
190 191 192
  /// If the provided `transform` is a translation matrix, it is much faster
  /// to use [pushOffset] with the translation offset instead.
  ///
193 194 195 196 197 198
  /// [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:
199
  ///
200 201
  ///  * [pushOffset], which is similar to [pushTransform] but is limited to
  ///    translations, and is faster in such cases.
202 203 204 205 206 207 208 209 210
  ///  * [BoxHitTestResult.addWithPaintTransform], which is a public wrapper
  ///    around this function for hit testing on [RenderBox]s.
  @protected
  void pushTransform(Matrix4 transform) {
    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 '
211
      'point is directly under the pointing device. Did you forget to run the paint '
212
      'matrix through PointerEvent.removePerspectiveTransform? '
213
      'The provided matrix is:\n$transform',
214
    );
215 216 217 218 219 220 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
    _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) {
    _localTransforms.add(_OffsetTransformPart(offset));
246 247
  }

248
  /// Removes the last transform added via [pushTransform] or [pushOffset].
249 250 251 252 253 254
  ///
  /// 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
255
  /// required a call to [pushTransform] or [pushOffset].
256 257 258
  ///
  /// See also:
  ///
259 260
  ///  * [pushTransform] and [pushOffset], which describes the use case of this
  ///    function pair in more details.
261 262
  @protected
  void popTransform() {
263
    if (_localTransforms.isNotEmpty) {
264
      _localTransforms.removeLast();
265
    } else {
266
      _transforms.removeLast();
267
    }
268 269 270 271 272 273 274 275 276 277 278 279 280
    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;
  }

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