1 package io.flutter.embedding.android; 2 3 import android.os.Build; 4 import android.support.annotation.IntDef; 5 import android.support.annotation.NonNull; 6 import android.view.InputDevice; 7 import android.view.MotionEvent; 8 9 import java.nio.ByteBuffer; 10 import java.nio.ByteOrder; 11 12 import io.flutter.embedding.engine.renderer.FlutterRenderer; 13 14 /** 15 * Sends touch information from Android to Flutter in a format that Flutter 16 * understands. 17 */ 18 public class AndroidTouchProcessor { 19 20 // Must match the PointerChange enum in pointer.dart. 21 @IntDef({ 22 PointerChange.CANCEL, 23 PointerChange.ADD, 24 PointerChange.REMOVE, 25 PointerChange.HOVER, 26 PointerChange.DOWN, 27 PointerChange.MOVE, 28 PointerChange.UP 29 }) 30 private @interface PointerChange { 31 int CANCEL = 0; 32 int ADD = 1; 33 int REMOVE = 2; 34 int HOVER = 3; 35 int DOWN = 4; 36 int MOVE = 5; 37 int UP = 6; 38 } 39 40 // Must match the PointerDeviceKind enum in pointer.dart. 41 @IntDef({ 42 PointerDeviceKind.TOUCH, 43 PointerDeviceKind.MOUSE, 44 PointerDeviceKind.STYLUS, 45 PointerDeviceKind.INVERTED_STYLUS, 46 PointerDeviceKind.UNKNOWN 47 }) 48 private @interface PointerDeviceKind { 49 int TOUCH = 0; 50 int MOUSE = 1; 51 int STYLUS = 2; 52 int INVERTED_STYLUS = 3; 53 int UNKNOWN = 4; 54 } 55 56 // Must match the PointerSignalKind enum in pointer.dart. 57 @IntDef({ 58 PointerSignalKind.NONE, 59 PointerSignalKind.SCROLL, 60 PointerSignalKind.UNKNOWN 61 }) 62 private @interface PointerSignalKind { 63 int NONE = 0; 64 int SCROLL = 1; 65 int UNKNOWN = 2; 66 } 67 68 // Must match the unpacking code in hooks.dart. 69 private static final int POINTER_DATA_FIELD_COUNT = 24; 70 private static final int BYTES_PER_FIELD = 8; 71 72 // This value must match the value in framework's platform_view.dart. 73 // This flag indicates whether the original Android pointer events were batched together. 74 private static final int POINTER_DATA_FLAG_BATCHED = 1; 75 76 @NonNull 77 private final FlutterRenderer renderer; 78 79 /** 80 * Constructs an {@code AndroidTouchProcessor} that will send touch event data 81 * to the Flutter execution context represented by the given {@link FlutterRenderer}. 82 */ 83 // TODO(mattcarroll): consider moving packet behavior to a FlutterInteractionSurface instead of FlutterRenderer AndroidTouchProcessor(@onNull FlutterRenderer renderer)84 public AndroidTouchProcessor(@NonNull FlutterRenderer renderer) { 85 this.renderer = renderer; 86 } 87 88 /** 89 * Sends the given {@link MotionEvent} data to Flutter in a format that 90 * Flutter understands. 91 */ onTouchEvent(@onNull MotionEvent event)92 public boolean onTouchEvent(@NonNull MotionEvent event) { 93 int pointerCount = event.getPointerCount(); 94 95 // Prepare a data packet of the appropriate size and order. 96 ByteBuffer packet = ByteBuffer.allocateDirect( 97 pointerCount * POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD 98 ); 99 packet.order(ByteOrder.LITTLE_ENDIAN); 100 101 int maskedAction = event.getActionMasked(); 102 int pointerChange = getPointerChangeForAction(event.getActionMasked()); 103 boolean updateForSinglePointer = maskedAction == MotionEvent.ACTION_DOWN || maskedAction == MotionEvent.ACTION_POINTER_DOWN; 104 boolean updateForMultiplePointers = !updateForSinglePointer && (maskedAction == MotionEvent.ACTION_UP || maskedAction == MotionEvent.ACTION_POINTER_UP); 105 if (updateForSinglePointer) { 106 // ACTION_DOWN and ACTION_POINTER_DOWN always apply to a single pointer only. 107 addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, packet); 108 } else if (updateForMultiplePointers) { 109 // ACTION_UP and ACTION_POINTER_UP may contain position updates for other pointers. 110 // We are converting these updates to move events here in order to preserve this data. 111 // We also mark these events with a flag in order to help the framework reassemble 112 // the original Android event later, should it need to forward it to a PlatformView. 113 for (int p = 0; p < pointerCount; p++) { 114 if (p != event.getActionIndex() && event.getToolType(p) == MotionEvent.TOOL_TYPE_FINGER) { 115 addPointerForIndex(event, p, PointerChange.MOVE, POINTER_DATA_FLAG_BATCHED, packet); 116 } 117 } 118 // It's important that we're sending the UP event last. This allows PlatformView 119 // to correctly batch everything back into the original Android event if needed. 120 addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, packet); 121 } else { 122 // ACTION_MOVE may not actually mean all pointers have moved 123 // but it's the responsibility of a later part of the system to 124 // ignore 0-deltas if desired. 125 for (int p = 0; p < pointerCount; p++) { 126 addPointerForIndex(event, p, pointerChange, 0, packet); 127 } 128 } 129 130 // Verify that the packet is the expected size. 131 if (packet.position() % (POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD) != 0) { 132 throw new AssertionError("Packet position is not on field boundary"); 133 } 134 135 // Send the packet to Flutter. 136 renderer.dispatchPointerDataPacket(packet, packet.position()); 137 138 return true; 139 } 140 141 /** 142 * Sends the given generic {@link MotionEvent} data to Flutter in a format that Flutter 143 * understands. 144 * 145 * Generic motion events include joystick movement, mouse hover, track pad touches, scroll wheel 146 * movements, etc. 147 */ onGenericMotionEvent(@onNull MotionEvent event)148 public boolean onGenericMotionEvent(@NonNull MotionEvent event) { 149 // Method isFromSource is only available in API 18+ (Jelly Bean MR2) 150 // Mouse hover support is not implemented for API < 18. 151 boolean isPointerEvent = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 152 && event.isFromSource(InputDevice.SOURCE_CLASS_POINTER); 153 boolean isMovementEvent = (event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE 154 || event.getActionMasked() == MotionEvent.ACTION_SCROLL); 155 if (!isPointerEvent || !isMovementEvent) { 156 return false; 157 } 158 159 int pointerChange = getPointerChangeForAction(event.getActionMasked()); 160 ByteBuffer packet = ByteBuffer.allocateDirect( 161 event.getPointerCount() * POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD 162 ); 163 packet.order(ByteOrder.LITTLE_ENDIAN); 164 165 // ACTION_HOVER_MOVE always applies to a single pointer only. 166 addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, packet); 167 if (packet.position() % (POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD) != 0) { 168 throw new AssertionError("Packet position is not on field boundary."); 169 } 170 renderer.dispatchPointerDataPacket(packet, packet.position()); 171 return true; 172 } 173 174 // TODO(mattcarroll): consider creating a PointerPacket class instead of using a procedure that mutates inputs. addPointerForIndex( MotionEvent event, int pointerIndex, int pointerChange, int pointerData, ByteBuffer packet )175 private void addPointerForIndex( 176 MotionEvent event, 177 int pointerIndex, 178 int pointerChange, 179 int pointerData, 180 ByteBuffer packet 181 ) { 182 if (pointerChange == -1) { 183 return; 184 } 185 186 int pointerKind = getPointerDeviceTypeForToolType(event.getToolType(pointerIndex)); 187 188 int signalKind = event.getActionMasked() == MotionEvent.ACTION_SCROLL 189 ? PointerSignalKind.SCROLL 190 : PointerSignalKind.NONE; 191 192 long timeStamp = event.getEventTime() * 1000; // Convert from milliseconds to microseconds. 193 194 packet.putLong(timeStamp); // time_stamp 195 packet.putLong(pointerChange); // change 196 packet.putLong(pointerKind); // kind 197 packet.putLong(signalKind); // signal_kind 198 packet.putLong(event.getPointerId(pointerIndex)); // device 199 packet.putDouble(event.getX(pointerIndex)); // physical_x 200 packet.putDouble(event.getY(pointerIndex)); // physical_y 201 202 if (pointerKind == PointerDeviceKind.MOUSE) { 203 packet.putLong(event.getButtonState() & 0x1F); // buttons 204 } else if (pointerKind == PointerDeviceKind.STYLUS) { 205 packet.putLong((event.getButtonState() >> 4) & 0xF); // buttons 206 } else { 207 packet.putLong(0); // buttons 208 } 209 210 packet.putLong(0); // obscured 211 212 packet.putDouble(event.getPressure(pointerIndex)); // pressure 213 double pressureMin = 0.0; 214 double pressureMax = 1.0; 215 if (event.getDevice() != null) { 216 InputDevice.MotionRange pressureRange = event.getDevice().getMotionRange(MotionEvent.AXIS_PRESSURE); 217 if (pressureRange != null) { 218 pressureMin = pressureRange.getMin(); 219 pressureMax = pressureRange.getMax(); 220 } 221 } 222 packet.putDouble(pressureMin); // pressure_min 223 packet.putDouble(pressureMax); // pressure_max 224 225 if (pointerKind == PointerDeviceKind.STYLUS) { 226 packet.putDouble(event.getAxisValue(MotionEvent.AXIS_DISTANCE, pointerIndex)); // distance 227 packet.putDouble(0.0); // distance_max 228 } else { 229 packet.putDouble(0.0); // distance 230 packet.putDouble(0.0); // distance_max 231 } 232 233 packet.putDouble(event.getSize(pointerIndex)); // size 234 235 packet.putDouble(event.getToolMajor(pointerIndex)); // radius_major 236 packet.putDouble(event.getToolMinor(pointerIndex)); // radius_minor 237 238 packet.putDouble(0.0); // radius_min 239 packet.putDouble(0.0); // radius_max 240 241 packet.putDouble(event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex)); // orientation 242 243 if (pointerKind == PointerDeviceKind.STYLUS) { 244 packet.putDouble(event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex)); // tilt 245 } else { 246 packet.putDouble(0.0); // tilt 247 } 248 249 packet.putLong(pointerData); // platformData 250 251 if (signalKind == PointerSignalKind.SCROLL) { 252 packet.putDouble(-event.getAxisValue(MotionEvent.AXIS_HSCROLL)); // scroll_delta_x 253 packet.putDouble(-event.getAxisValue(MotionEvent.AXIS_VSCROLL)); // scroll_delta_y 254 } else { 255 packet.putDouble(0.0); // scroll_delta_x 256 packet.putDouble(0.0); // scroll_delta_x 257 } 258 } 259 260 @PointerChange getPointerChangeForAction(int maskedAction)261 private int getPointerChangeForAction(int maskedAction) { 262 // Primary pointer: 263 if (maskedAction == MotionEvent.ACTION_DOWN) { 264 return PointerChange.DOWN; 265 } 266 if (maskedAction == MotionEvent.ACTION_UP) { 267 return PointerChange.UP; 268 } 269 // Secondary pointer: 270 if (maskedAction == MotionEvent.ACTION_POINTER_DOWN) { 271 return PointerChange.DOWN; 272 } 273 if (maskedAction == MotionEvent.ACTION_POINTER_UP) { 274 return PointerChange.UP; 275 } 276 // All pointers: 277 if (maskedAction == MotionEvent.ACTION_MOVE) { 278 return PointerChange.MOVE; 279 } 280 if (maskedAction == MotionEvent.ACTION_HOVER_MOVE) { 281 return PointerChange.HOVER; 282 } 283 if (maskedAction == MotionEvent.ACTION_CANCEL) { 284 return PointerChange.CANCEL; 285 } 286 if (maskedAction == MotionEvent.ACTION_SCROLL) { 287 return PointerChange.HOVER; 288 } 289 return -1; 290 } 291 292 @PointerDeviceKind getPointerDeviceTypeForToolType(int toolType)293 private int getPointerDeviceTypeForToolType(int toolType) { 294 switch (toolType) { 295 case MotionEvent.TOOL_TYPE_FINGER: 296 return PointerDeviceKind.TOUCH; 297 case MotionEvent.TOOL_TYPE_STYLUS: 298 return PointerDeviceKind.STYLUS; 299 case MotionEvent.TOOL_TYPE_MOUSE: 300 return PointerDeviceKind.MOUSE; 301 case MotionEvent.TOOL_TYPE_ERASER: 302 return PointerDeviceKind.INVERTED_STYLUS; 303 default: 304 // MotionEvent.TOOL_TYPE_UNKNOWN will reach here. 305 return PointerDeviceKind.UNKNOWN; 306 } 307 } 308 } 309