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.
Ian Hickson's avatar
Ian Hickson committed
41
  void handleEvent(PointerEvent event, HitTestEntry 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
class HitTestEntry {
49
  /// Creates a hit test entry.
50
  HitTestEntry(this.target);
Adam Barth's avatar
Adam Barth committed
51 52

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

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

  /// 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:
  ///
64
  ///  * [BoxHitTestResult.addWithPaintTransform], which is used during hit testing
65
  ///    to build up this transform.
66 67
  Matrix4? get transform => _transform;
  Matrix4? _transform;
68 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 94 95 96 97 98 99 100 101 102 103 104 105 106 107
// 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
108
/// The result of performing a hit test.
109
class HitTestResult {
110
  /// Creates an empty hit test result.
111 112
  HitTestResult()
     : _path = <HitTestEntry>[],
113 114
       _transforms = <Matrix4>[Matrix4.identity()],
       _localTransforms = <_TransformPart>[];
115 116 117

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

127
  /// An unmodifiable list of [HitTestEntry] objects recorded during the hit test.
Adam Barth's avatar
Adam Barth committed
128
  ///
129 130 131
  /// 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
132
  Iterable<HitTestEntry> get path => _path;
133
  final List<HitTestEntry> _path;
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 164 165 166 167 168
  // 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;
  }
169

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

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

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

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