hit_test.dart 6.84 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7 8 9
import 'dart:collection';

import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';

Ian Hickson's avatar
Ian Hickson committed
10 11 12
import 'events.dart';

/// An object that can hit-test pointers.
13
abstract class HitTestable {
14 15 16 17
  // This class is intended to be used as an interface with the implements
  // keyword, and should not be extended directly.
  factory HitTestable._() => null;

18 19 20 21
  /// 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.
22
  void hitTest(HitTestResult result, Offset position);
Ian Hickson's avatar
Ian Hickson committed
23
}
24

25
/// An object that can dispatch events.
26
abstract class HitTestDispatcher {
27 28 29 30
  // This class is intended to be used as an interface with the implements
  // keyword, and should not be extended directly.
  factory HitTestDispatcher._() => null;

31
  /// Override this method to dispatch events.
32 33 34
  void dispatchEvent(PointerEvent event, HitTestResult result);
}

Adam Barth's avatar
Adam Barth committed
35
/// An object that can handle events.
36
abstract class HitTestTarget {
37 38 39 40
  // This class is intended to be used as an interface with the implements
  // keyword, and should not be extended directly.
  factory HitTestTarget._() => null;

41
  /// Override this method to receive events.
Ian Hickson's avatar
Ian Hickson committed
42
  void handleEvent(PointerEvent event, HitTestEntry entry);
43 44
}

Adam Barth's avatar
Adam Barth committed
45 46 47 48
/// 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.
49
class HitTestEntry {
50
  /// Creates a hit test entry.
51
  HitTestEntry(this.target);
Adam Barth's avatar
Adam Barth committed
52 53

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

56
  @override
Ian Hickson's avatar
Ian Hickson committed
57
  String toString() => '$target';
58 59 60 61 62 63 64 65 66 67 68

  /// 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:
  ///
  ///  * [HitTestResult.addWithPaintTransform], which is used during hit testing
  ///    to build up the transform returned by this method.
  Matrix4 get transform => _transform;
  Matrix4 _transform;
69 70
}

Adam Barth's avatar
Adam Barth committed
71
/// The result of performing a hit test.
72
class HitTestResult {
73
  /// Creates an empty hit test result.
74 75 76
  HitTestResult()
     : _path = <HitTestEntry>[],
       _transforms = Queue<Matrix4>();
77 78 79

  /// Wraps `result` (usually a subtype of [HitTestResult]) to create a
  /// generic [HitTestResult].
80
  ///
81 82 83
  /// 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).
84 85 86
  HitTestResult.wrap(HitTestResult result)
     : _path = result._path,
       _transforms = result._transforms;
87

88
  /// An unmodifiable list of [HitTestEntry] objects recorded during the hit test.
Adam Barth's avatar
Adam Barth committed
89
  ///
90 91 92
  /// 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
93
  Iterable<HitTestEntry> get path => _path;
94
  final List<HitTestEntry> _path;
95

96 97
  final Queue<Matrix4> _transforms;

Adam Barth's avatar
Adam Barth committed
98 99 100
  /// 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
101 102
  /// 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
103
  void add(HitTestEntry entry) {
104 105
    assert(entry._transform == null);
    entry._transform = _transforms.isEmpty ? null : _transforms.last;
106
    _path.add(entry);
107
  }
Ian Hickson's avatar
Ian Hickson committed
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
  /// 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.
  ///
  /// [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:
  ///  * [BoxHitTestResult.addWithPaintTransform], 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 [RenderSlivers]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 pointer device. Did you forget to run the paint '
      'matrix through PointerEvent.removePerspectiveTransform?'
      'The provided matrix is:\n$transform'
    );
    _transforms.add(_transforms.isEmpty ? transform :  transform * _transforms.last);
  }

  /// Removes the last transform added via [pushTransform].
  ///
  /// 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].
  ///
  /// See also:
  ///
  ///  * [pushTransform], which describes the use case of this function pair in
  ///    more details.
  @protected
  void popTransform() {
    assert(_transforms.isNotEmpty);
    _transforms.removeLast();
  }

  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;
  }

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