1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';
import 'events.dart';
/// An object that can hit-test pointers.
abstract class HitTestable {
// This class is intended to be used as an interface, and should not be
// extended directly; this constructor prevents instantiation and extension.
HitTestable._();
/// 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.
void hitTest(HitTestResult result, Offset position);
}
/// An object that can dispatch events.
abstract class HitTestDispatcher {
// This class is intended to be used as an interface, and should not be
// extended directly; this constructor prevents instantiation and extension.
HitTestDispatcher._();
/// Override this method to dispatch events.
void dispatchEvent(PointerEvent event, HitTestResult result);
}
/// An object that can handle events.
abstract class HitTestTarget {
// This class is intended to be used as an interface, and should not be
// extended directly; this constructor prevents instantiation and extension.
HitTestTarget._();
/// Override this method to receive events.
void handleEvent(PointerEvent event, HitTestEntry entry);
}
/// 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.
class HitTestEntry {
/// Creates a hit test entry.
HitTestEntry(this.target);
/// The [HitTestTarget] encountered during the hit test.
final HitTestTarget target;
@override
String toString() => '${describeIdentity(this)}($target)';
/// 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:
///
/// * [BoxHitTestResult.addWithPaintTransform], which is used during hit testing
/// to build up this transform.
Matrix4? get transform => _transform;
Matrix4? _transform;
}
// 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);
}
}
/// The result of performing a hit test.
class HitTestResult {
/// Creates an empty hit test result.
HitTestResult()
: _path = <HitTestEntry>[],
_transforms = <Matrix4>[Matrix4.identity()],
_localTransforms = <_TransformPart>[];
/// Wraps `result` (usually a subtype of [HitTestResult]) to create a
/// generic [HitTestResult].
///
/// 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).
HitTestResult.wrap(HitTestResult result)
: _path = result._path,
_transforms = result._transforms,
_localTransforms = result._localTransforms;
/// An unmodifiable list of [HitTestEntry] objects recorded during the hit test.
///
/// 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.
Iterable<HitTestEntry> get path => _path;
final List<HitTestEntry> _path;
// 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;
}
/// Add a [HitTestEntry] to the path.
///
/// The new entry is added at the end of the path, which means entries should
/// be added in order from most specific to least specific, typically during an
/// upward walk of the tree being hit tested.
void add(HitTestEntry entry) {
assert(entry._transform == null);
entry._transform = _lastTransform;
_path.add(entry);
}
/// 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.
///
/// If the provided `transform` is a translation matrix, it is much faster
/// to use [pushOffset] with the translation offset instead.
///
/// [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:
///
/// * [pushOffset], which is similar to [pushTransform] but is limited to
/// translations, and is faster in such cases.
/// * [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 '
'point is directly under the pointing device. Did you forget to run the paint '
'matrix through PointerEvent.removePerspectiveTransform? '
'The provided matrix is:\n$transform',
);
_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));
}
/// Removes the last transform added via [pushTransform] or [pushOffset].
///
/// 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
/// required a call to [pushTransform] or [pushOffset].
///
/// See also:
///
/// * [pushTransform] and [pushOffset], which describes the use case of this
/// function pair in more details.
@protected
void popTransform() {
if (_localTransforms.isNotEmpty)
_localTransforms.removeLast();
else
_transforms.removeLast();
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;
}
@override
String toString() => 'HitTestResult(${_path.isEmpty ? "<empty path>" : _path.join(", ")})';
}