1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.accessibility.floatingmenu; 18 19 import android.graphics.PointF; 20 import android.view.MotionEvent; 21 import android.view.VelocityTracker; 22 import android.view.View; 23 import android.view.ViewConfiguration; 24 25 import androidx.annotation.NonNull; 26 import androidx.recyclerview.widget.RecyclerView; 27 28 import java.util.Optional; 29 30 /** 31 * Controls the all touch events of the accessibility target features view{@link RecyclerView} in 32 * the {@link MenuView}. And then compute the gestures' velocity for fling and spring 33 * animations. 34 */ 35 class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { 36 private static final int VELOCITY_UNIT_SECONDS = 1000; 37 private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); 38 private final MenuAnimationController mMenuAnimationController; 39 private final PointF mDown = new PointF(); 40 private final PointF mMenuTranslationDown = new PointF(); 41 private boolean mIsDragging = false; 42 private float mTouchSlop; 43 private final DismissAnimationController mDismissAnimationController; 44 private Optional<Runnable> mOnActionDownEnd = Optional.empty(); 45 MenuListViewTouchHandler(MenuAnimationController menuAnimationController, DismissAnimationController dismissAnimationController)46 MenuListViewTouchHandler(MenuAnimationController menuAnimationController, 47 DismissAnimationController dismissAnimationController) { 48 mMenuAnimationController = menuAnimationController; 49 mDismissAnimationController = dismissAnimationController; 50 } 51 52 @Override onInterceptTouchEvent(@onNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent)53 public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, 54 @NonNull MotionEvent motionEvent) { 55 56 final View menuView = (View) recyclerView.getParent(); 57 addMovement(motionEvent); 58 59 final float dx = motionEvent.getRawX() - mDown.x; 60 final float dy = motionEvent.getRawY() - mDown.y; 61 62 switch (motionEvent.getAction()) { 63 case MotionEvent.ACTION_DOWN: 64 mMenuAnimationController.fadeInNowIfEnabled(); 65 mTouchSlop = ViewConfiguration.get(recyclerView.getContext()).getScaledTouchSlop(); 66 mDown.set(motionEvent.getRawX(), motionEvent.getRawY()); 67 mMenuTranslationDown.set(menuView.getTranslationX(), menuView.getTranslationY()); 68 69 mMenuAnimationController.cancelAnimations(); 70 mDismissAnimationController.maybeConsumeDownMotionEvent(motionEvent); 71 72 mOnActionDownEnd.ifPresent(Runnable::run); 73 break; 74 case MotionEvent.ACTION_MOVE: 75 if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) { 76 if (!mIsDragging) { 77 mIsDragging = true; 78 mMenuAnimationController.onDraggingStart(); 79 } 80 81 mDismissAnimationController.showDismissView(/* show= */ true); 82 83 if (!mDismissAnimationController.maybeConsumeMoveMotionEvent(motionEvent)) { 84 mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx); 85 mMenuAnimationController.moveToPositionYIfNeeded( 86 mMenuTranslationDown.y + dy); 87 } 88 } 89 break; 90 case MotionEvent.ACTION_UP: 91 case MotionEvent.ACTION_CANCEL: 92 if (mIsDragging) { 93 final float endX = mMenuTranslationDown.x + dx; 94 mIsDragging = false; 95 96 if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) { 97 mDismissAnimationController.showDismissView(/* show= */ false); 98 mMenuAnimationController.fadeOutIfEnabled(); 99 100 return true; 101 } 102 103 if (!mDismissAnimationController.maybeConsumeUpMotionEvent(motionEvent)) { 104 mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS); 105 mMenuAnimationController.flingMenuThenSpringToEdge(endX, 106 mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); 107 mDismissAnimationController.showDismissView(/* show= */ false); 108 } 109 110 // Avoid triggering the listener of the item. 111 return true; 112 } 113 114 mMenuAnimationController.fadeOutIfEnabled(); 115 break; 116 default: // Do nothing 117 } 118 119 // not consume all the events here because keeping the scroll behavior of list view. 120 return false; 121 } 122 123 @Override onTouchEvent(@onNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent)124 public void onTouchEvent(@NonNull RecyclerView recyclerView, 125 @NonNull MotionEvent motionEvent) { 126 // Do nothing 127 } 128 129 @Override onRequestDisallowInterceptTouchEvent(boolean b)130 public void onRequestDisallowInterceptTouchEvent(boolean b) { 131 // Do nothing 132 } 133 setOnActionDownEndListener(Runnable onActionDownEndListener)134 void setOnActionDownEndListener(Runnable onActionDownEndListener) { 135 mOnActionDownEnd = Optional.ofNullable(onActionDownEndListener); 136 } 137 138 /** 139 * Adds a movement to the velocity tracker using raw screen coordinates. 140 */ addMovement(MotionEvent motionEvent)141 private void addMovement(MotionEvent motionEvent) { 142 final float deltaX = motionEvent.getRawX() - motionEvent.getX(); 143 final float deltaY = motionEvent.getRawY() - motionEvent.getY(); 144 motionEvent.offsetLocation(deltaX, deltaY); 145 mVelocityTracker.addMovement(motionEvent); 146 motionEvent.offsetLocation(-deltaX, -deltaY); 147 } 148 } 149