/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.accessibility; import android.annotation.DisplayContext; import android.annotation.NonNull; import android.content.Context; import android.graphics.PointF; import android.os.Handler; import android.view.Display; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; /** * Detects single tap and drag gestures using the supplied {@link MotionEvent}s. The {@link * OnGestureListener} callback will notify users when a particular motion event has occurred. This * class should only be used with {@link MotionEvent}s reported via touch (don't use for trackball * events). */ class MagnificationGestureDetector { interface OnGestureListener { /** * Called when a tap is completed within {@link ViewConfiguration#getLongPressTimeout()} and * the offset between {@link MotionEvent}s and the down event doesn't exceed {@link * ViewConfiguration#getScaledTouchSlop()}. * * @return {@code true} if this gesture is handled. */ boolean onSingleTap(View view); /** * Called when the user is performing dragging gesture. It is started after the offset * between the down location and the move event location exceed * {@link ViewConfiguration#getScaledTouchSlop()}. * * @param offsetX The X offset in screen coordinate. * @param offsetY The Y offset in screen coordinate. * @return {@code true} if this gesture is handled. */ boolean onDrag(View view, float offsetX, float offsetY); /** * Notified when a tap occurs with the down {@link MotionEvent} that triggered it. This will * be triggered immediately for every down event. All other events should be preceded by * this. * * @param x The X coordinate of the down event. * @param y The Y coordinate of the down event. * @return {@code true} if the down event is handled, otherwise the events won't be sent to * the view. */ boolean onStart(float x, float y); /** * Called when the detection is finished. In other words, it is called when up/cancel {@link * MotionEvent} is received. It will be triggered after single-tap * * @param x The X coordinate on the screen of the up event or the cancel event. * @param y The Y coordinate on the screen of the up event or the cancel event. * @return {@code true} if the event is handled. */ boolean onFinish(float x, float y); } private final PointF mPointerDown = new PointF(); private final PointF mPointerLocation = new PointF(Float.NaN, Float.NaN); private final Handler mHandler; private final Runnable mCancelTapGestureRunnable; private final OnGestureListener mOnGestureListener; private int mTouchSlopSquare; // Assume the gesture default is a single-tap. Set it to false if the gesture couldn't be a // single-tap anymore. private boolean mDetectSingleTap = true; private boolean mDraggingDetected = false; /** * @param context {@link Context} that is from {@link Context#createDisplayContext(Display)}. * @param handler The handler to post the runnable. * @param listener The listener invoked for all the callbacks. */ MagnificationGestureDetector(@DisplayContext Context context, @NonNull Handler handler, @NonNull OnGestureListener listener) { final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mTouchSlopSquare = touchSlop * touchSlop; mHandler = handler; mOnGestureListener = listener; mCancelTapGestureRunnable = () -> mDetectSingleTap = false; } /** * Analyzes the given motion event and if applicable to trigger the appropriate callbacks on the * {@link OnGestureListener} supplied. * * @param event The current motion event. * @return {@code True} if the {@link OnGestureListener} consumes the event, else false. */ boolean onTouch(View view, MotionEvent event) { final float rawX = event.getRawX(); final float rawY = event.getRawY(); boolean handled = false; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: mPointerDown.set(rawX, rawY); mHandler.postAtTime(mCancelTapGestureRunnable, event.getDownTime() + ViewConfiguration.getLongPressTimeout()); handled |= mOnGestureListener.onStart(rawX, rawY); break; case MotionEvent.ACTION_POINTER_DOWN: stopSingleTapDetection(); break; case MotionEvent.ACTION_MOVE: stopSingleTapDetectionIfNeeded(rawX, rawY); handled |= notifyDraggingGestureIfNeeded(view, rawX, rawY); break; case MotionEvent.ACTION_UP: stopSingleTapDetectionIfNeeded(rawX, rawY); if (mDetectSingleTap) { handled |= mOnGestureListener.onSingleTap(view); } // Fall through case MotionEvent.ACTION_CANCEL: handled |= mOnGestureListener.onFinish(rawX, rawY); reset(); break; } return handled; } private void stopSingleTapDetectionIfNeeded(float x, float y) { if (mDraggingDetected) { return; } if (!isLocationValid(mPointerDown)) { return; } final int deltaX = (int) (mPointerDown.x - x); final int deltaY = (int) (mPointerDown.y - y); final int distanceSquare = (deltaX * deltaX) + (deltaY * deltaY); if (distanceSquare > mTouchSlopSquare) { mDraggingDetected = true; stopSingleTapDetection(); } } private void stopSingleTapDetection() { mHandler.removeCallbacks(mCancelTapGestureRunnable); mDetectSingleTap = false; } private boolean notifyDraggingGestureIfNeeded(View view, float x, float y) { if (!mDraggingDetected) { return false; } if (!isLocationValid(mPointerLocation)) { mPointerLocation.set(mPointerDown); } final float offsetX = x - mPointerLocation.x; final float offsetY = y - mPointerLocation.y; mPointerLocation.set(x, y); return mOnGestureListener.onDrag(view, offsetX, offsetY); } private void reset() { resetPointF(mPointerDown); resetPointF(mPointerLocation); mHandler.removeCallbacks(mCancelTapGestureRunnable); mDetectSingleTap = true; mDraggingDetected = false; } private static void resetPointF(PointF pointF) { pointF.x = Float.NaN; pointF.y = Float.NaN; } private static boolean isLocationValid(PointF location) { return !Float.isNaN(location.x) && !Float.isNaN(location.y); } }