• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:collection';
6
7import 'package:flutter/foundation.dart';
8import 'package:vector_math/vector_math_64.dart';
9
10import 'events.dart';
11
12/// An object that can hit-test pointers.
13abstract class HitTestable {
14  // This class is intended to be used as an interface with the implements
15  // keyword, and should not be extended directly.
16  factory HitTestable._() => null;
17
18  /// Check whether the given position hits this object.
19  ///
20  /// If this given position hits this object, consider adding a [HitTestEntry]
21  /// to the given hit test result.
22  void hitTest(HitTestResult result, Offset position);
23}
24
25/// An object that can dispatch events.
26abstract class HitTestDispatcher {
27  // This class is intended to be used as an interface with the implements
28  // keyword, and should not be extended directly.
29  factory HitTestDispatcher._() => null;
30
31  /// Override this method to dispatch events.
32  void dispatchEvent(PointerEvent event, HitTestResult result);
33}
34
35/// An object that can handle events.
36abstract class HitTestTarget {
37  // This class is intended to be used as an interface with the implements
38  // keyword, and should not be extended directly.
39  factory HitTestTarget._() => null;
40
41  /// Override this method to receive events.
42  void handleEvent(PointerEvent event, HitTestEntry entry);
43}
44
45/// Data collected during a hit test about a specific [HitTestTarget].
46///
47/// Subclass this object to pass additional information from the hit test phase
48/// to the event propagation phase.
49class HitTestEntry {
50  /// Creates a hit test entry.
51  HitTestEntry(this.target);
52
53  /// The [HitTestTarget] encountered during the hit test.
54  final HitTestTarget target;
55
56  @override
57  String toString() => '$target';
58
59  /// Returns a matrix describing how [PointerEvent]s delivered to this
60  /// [HitTestEntry] should be transformed from the global coordinate space of
61  /// the screen to the local coordinate space of [target].
62  ///
63  /// See also:
64  ///
65  ///  * [HitTestResult.addWithPaintTransform], which is used during hit testing
66  ///    to build up the transform returned by this method.
67  Matrix4 get transform => _transform;
68  Matrix4 _transform;
69}
70
71/// The result of performing a hit test.
72class HitTestResult {
73  /// Creates an empty hit test result.
74  HitTestResult()
75     : _path = <HitTestEntry>[],
76       _transforms = Queue<Matrix4>();
77
78  /// Wraps `result` (usually a subtype of [HitTestResult]) to create a
79  /// generic [HitTestResult].
80  ///
81  /// The [HitTestEntry]s added to the returned [HitTestResult] are also
82  /// added to the wrapped `result` (both share the same underlying data
83  /// structure to store [HitTestEntry]s).
84  HitTestResult.wrap(HitTestResult result)
85     : _path = result._path,
86       _transforms = result._transforms;
87
88  /// An unmodifiable list of [HitTestEntry] objects recorded during the hit test.
89  ///
90  /// The first entry in the path is the most specific, typically the one at
91  /// the leaf of tree being hit tested. Event propagation starts with the most
92  /// specific (i.e., first) entry and proceeds in order through the path.
93  Iterable<HitTestEntry> get path => _path;
94  final List<HitTestEntry> _path;
95
96  final Queue<Matrix4> _transforms;
97
98  /// Add a [HitTestEntry] to the path.
99  ///
100  /// The new entry is added at the end of the path, which means entries should
101  /// be added in order from most specific to least specific, typically during an
102  /// upward walk of the tree being hit tested.
103  void add(HitTestEntry entry) {
104    assert(entry._transform == null);
105    entry._transform = _transforms.isEmpty ? null : _transforms.last;
106    _path.add(entry);
107  }
108
109  /// Pushes a new transform matrix that is to be applied to all future
110  /// [HitTestEntry]s added via [add] until it is removed via [popTransform].
111  ///
112  /// This method is only to be used by subclasses, which must provide
113  /// coordinate space specific public wrappers around this function for their
114  /// users (see [BoxHitTestResult.addWithPaintTransform] for such an example).
115  ///
116  /// The provided `transform` matrix should describe how to transform
117  /// [PointerEvent]s from the coordinate space of the method caller to the
118  /// coordinate space of its children. In most cases `transform` is derived
119  /// from running the inverted result of [RenderObject.applyPaintTransform]
120  /// through [PointerEvent.removePerspectiveTransform] to remove
121  /// the perspective component.
122  ///
123  /// [HitTestable]s need to call this method indirectly through a convenience
124  /// method defined on a subclass before hit testing a child that does not
125  /// have the same origin as the parent. After hit testing the child,
126  /// [popTransform] has to be called to remove the child-specific `transform`.
127  ///
128  /// See also:
129  ///  * [BoxHitTestResult.addWithPaintTransform], which is a public wrapper
130  ///    around this function for hit testing on [RenderBox]s.
131  ///  * [SliverHitTestResult.addWithAxisOffset], which is a public wrapper
132  ///    around this function for hit testing on [RenderSlivers]s.
133  @protected
134  void pushTransform(Matrix4 transform) {
135    assert(transform != null);
136    assert(
137      _debugVectorMoreOrLessEquals(transform.getRow(2), Vector4(0, 0, 1, 0)) &&
138      _debugVectorMoreOrLessEquals(transform.getColumn(2), Vector4(0, 0, 1, 0)),
139      'The third row and third column of a transform matrix for pointer '
140      'events must be Vector4(0, 0, 1, 0) to ensure that a transformed '
141      'point is directly under the pointer device. Did you forget to run the paint '
142      'matrix through PointerEvent.removePerspectiveTransform?'
143      'The provided matrix is:\n$transform'
144    );
145    _transforms.add(_transforms.isEmpty ? transform :  transform * _transforms.last);
146  }
147
148  /// Removes the last transform added via [pushTransform].
149  ///
150  /// This method is only to be used by subclasses, which must provide
151  /// coordinate space specific public wrappers around this function for their
152  /// users (see [BoxHitTestResult.addWithPaintTransform] for such an example).
153  ///
154  /// This method must be called after hit testing is done on a child that
155  /// required a call to [pushTransform].
156  ///
157  /// See also:
158  ///
159  ///  * [pushTransform], which describes the use case of this function pair in
160  ///    more details.
161  @protected
162  void popTransform() {
163    assert(_transforms.isNotEmpty);
164    _transforms.removeLast();
165  }
166
167  bool _debugVectorMoreOrLessEquals(Vector4 a, Vector4 b, { double epsilon = precisionErrorTolerance }) {
168    bool result = true;
169    assert(() {
170      final Vector4 difference = a - b;
171      result = difference.storage.every((double component) => component.abs() < epsilon);
172      return true;
173    }());
174    return result;
175  }
176
177  @override
178  String toString() => 'HitTestResult(${_path.isEmpty ? "<empty path>" : _path.join(", ")})';
179}
180