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