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