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 static android.R.id.empty;
20 
21 import android.graphics.PointF;
22 import android.view.MotionEvent;
23 import android.view.VelocityTracker;
24 import android.view.View;
25 import android.view.ViewConfiguration;
26 
27 import androidx.annotation.NonNull;
28 import androidx.recyclerview.widget.RecyclerView;
29 
30 import java.util.Optional;
31 
32 /**
33  * Controls the all touch events of the accessibility target features view{@link RecyclerView} in
34  * the {@link MenuView}. And then compute the gestures' velocity for fling and spring
35  * animations.
36  */
37 class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
38     private static final int VELOCITY_UNIT_SECONDS = 1000;
39     private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
40     private final MenuAnimationController mMenuAnimationController;
41     private final PointF mDown = new PointF();
42     private final PointF mMenuTranslationDown = new PointF();
43     private boolean mIsDragging = false;
44     private float mTouchSlop;
45     private final DragToInteractAnimationController mDragToInteractAnimationController;
46     private Optional<Runnable> mOnActionDownEnd = Optional.empty();
47 
MenuListViewTouchHandler(MenuAnimationController menuAnimationController, DragToInteractAnimationController dragToInteractAnimationController)48     MenuListViewTouchHandler(MenuAnimationController menuAnimationController,
49             DragToInteractAnimationController dragToInteractAnimationController) {
50         mMenuAnimationController = menuAnimationController;
51         mDragToInteractAnimationController = dragToInteractAnimationController;
52     }
53 
54     @Override
onInterceptTouchEvent(@onNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent)55     public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
56             @NonNull MotionEvent motionEvent) {
57 
58         final View menuView = (View) recyclerView.getParent();
59         addMovement(motionEvent);
60 
61         final float dx = motionEvent.getRawX() - mDown.x;
62         final float dy = motionEvent.getRawY() - mDown.y;
63 
64         switch (motionEvent.getAction()) {
65             case MotionEvent.ACTION_DOWN:
66                 mMenuAnimationController.fadeInNowIfEnabled();
67                 mTouchSlop = ViewConfiguration.get(recyclerView.getContext()).getScaledTouchSlop();
68                 mDown.set(motionEvent.getRawX(), motionEvent.getRawY());
69                 mMenuTranslationDown.set(menuView.getTranslationX(), menuView.getTranslationY());
70 
71                 mMenuAnimationController.cancelAnimations();
72                 mDragToInteractAnimationController.maybeConsumeDownMotionEvent(motionEvent);
73 
74                 mOnActionDownEnd.ifPresent(Runnable::run);
75                 break;
76             case MotionEvent.ACTION_MOVE:
77                 if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) {
78                     if (!mIsDragging) {
79                         mIsDragging = true;
80                         mMenuAnimationController.onDraggingStart();
81                     }
82 
83                     mDragToInteractAnimationController.showInteractView(/* show= */ true);
84                     if (mDragToInteractAnimationController.maybeConsumeMoveMotionEvent(motionEvent)
85                             == empty) {
86                         mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx);
87                         mMenuAnimationController.moveToPositionYIfNeeded(
88                                 mMenuTranslationDown.y + dy);
89                     }
90                 }
91                 break;
92             case MotionEvent.ACTION_UP:
93             case MotionEvent.ACTION_CANCEL:
94                 if (mIsDragging) {
95                     final float endX = mMenuTranslationDown.x + dx;
96                     mIsDragging = false;
97 
98                     mDragToInteractAnimationController.showInteractView(/* show= */ false);
99 
100                     if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) {
101                         mMenuAnimationController.fadeOutIfEnabled();
102                         return true;
103                     }
104 
105                     if (mDragToInteractAnimationController.maybeConsumeUpMotionEvent(motionEvent)
106                             == empty) {
107                         mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS);
108                         mMenuAnimationController.flingMenuThenSpringToEdge(endX,
109                                 mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
110                         mMenuAnimationController.fadeOutIfEnabled();
111                     }
112                     // Avoid triggering the listener of the item.
113                     return true;
114                 }
115 
116                 mMenuAnimationController.fadeOutIfEnabled();
117                 break;
118             default: // Do nothing
119         }
120 
121         // not consume all the events here because keeping the scroll behavior of list view.
122         return false;
123     }
124 
125     @Override
onTouchEvent(@onNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent)126     public void onTouchEvent(@NonNull RecyclerView recyclerView,
127             @NonNull MotionEvent motionEvent) {
128         // Do nothing
129     }
130 
131     @Override
onRequestDisallowInterceptTouchEvent(boolean b)132     public void onRequestDisallowInterceptTouchEvent(boolean b) {
133         // Do nothing
134     }
135 
setOnActionDownEndListener(Runnable onActionDownEndListener)136     void setOnActionDownEndListener(Runnable onActionDownEndListener) {
137         mOnActionDownEnd = Optional.ofNullable(onActionDownEndListener);
138     }
139 
140     /**
141      * Adds a movement to the velocity tracker using raw screen coordinates.
142      */
addMovement(MotionEvent motionEvent)143     private void addMovement(MotionEvent motionEvent) {
144         final float deltaX = motionEvent.getRawX() - motionEvent.getX();
145         final float deltaY = motionEvent.getRawY() - motionEvent.getY();
146         motionEvent.offsetLocation(deltaX, deltaY);
147         mVelocityTracker.addMovement(motionEvent);
148         motionEvent.offsetLocation(-deltaX, -deltaY);
149     }
150 }
151