1/* 2 * Copyright (C) 2016 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 17package com.android.documentsui.dirlist; 18 19import static com.android.documentsui.base.Shared.VERBOSE; 20 21import android.annotation.Nullable; 22import android.content.Context; 23import android.os.Build; 24import android.support.v7.widget.RecyclerView; 25import android.support.v7.widget.RecyclerView.OnItemTouchListener; 26import android.util.Log; 27import android.view.GestureDetector; 28import android.view.MotionEvent; 29import android.view.ScaleGestureDetector; 30 31import com.android.documentsui.base.BooleanConsumer; 32import com.android.documentsui.base.EventHandler; 33import com.android.documentsui.base.Events; 34import com.android.documentsui.base.Events.InputEvent; 35import com.android.documentsui.base.Events.MotionInputEvent; 36import com.android.documentsui.base.Features; 37import com.android.documentsui.selection.BandController; 38import com.android.documentsui.selection.GestureSelector; 39 40import java.util.function.Consumer; 41 42//Receives event meant for both directory and empty view, and either pass them to 43//{@link UserInputHandler} for simple gestures (Single Tap, Long-Press), or intercept them for 44//other types of gestures (drag n' drop) 45final class ListeningGestureDetector extends GestureDetector implements OnItemTouchListener { 46 47 private static final String TAG = "ListeningGestureDetector"; 48 49 private final Features mFeatures; 50 private final GestureSelector mGestureSelector; 51 private final EventHandler<InputEvent> mMouseDragListener; 52 private final BooleanConsumer mRefreshLayoutEnabler; 53 private final BandController mBandController; 54 private final MouseDelegate mMouseDelegate = new MouseDelegate(); 55 private final TouchDelegate mTouchDelegate = new TouchDelegate(); 56 57 // Currently only initialized on IS_DEBUGGABLE builds. 58 private final @Nullable ScaleGestureDetector mScaleDetector; 59 60 public ListeningGestureDetector( 61 Features features, 62 Context context, 63 RecyclerView recView, 64 EventHandler<InputEvent> mouseDragListener, 65 BooleanConsumer refreshLayoutEnabler, 66 GestureSelector gestureSelector, 67 UserInputHandler<? extends InputEvent> handler, 68 @Nullable BandController bandController, 69 Consumer<Float> scaleHandler) { 70 71 super(context, handler); 72 73 mFeatures = features; 74 mMouseDragListener = mouseDragListener; 75 mRefreshLayoutEnabler = refreshLayoutEnabler; 76 mGestureSelector = gestureSelector; 77 mBandController = bandController; 78 recView.addOnItemTouchListener(this); 79 80 mScaleDetector = !Build.IS_DEBUGGABLE 81 ? null 82 : new ScaleGestureDetector( 83 context, 84 new ScaleGestureDetector.SimpleOnScaleGestureListener() { 85 @Override 86 public boolean onScale(ScaleGestureDetector detector) { 87 if (VERBOSE) Log.v(TAG, 88 "Received scale event: " + detector.getScaleFactor()); 89 scaleHandler.accept(detector.getScaleFactor()); 90 return true; 91 } 92 }); 93 } 94 95 @Override 96 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { 97 boolean handled = false; 98 99 // TODO: Re-wire event handling so that we're not dispatching 100 // events to to scaledetector's #onTouchEvent from this 101 // #onInterceptTouchEvent touch event. 102 if (mFeatures.isGestureScaleEnabled() 103 && mScaleDetector != null) { 104 mScaleDetector.onTouchEvent(e); 105 } 106 107 try (InputEvent event = MotionInputEvent.obtain(e, rv)) { 108 if (event.isMouseEvent()) { 109 if (event.isActionDown()) { 110 mRefreshLayoutEnabler.accept(false); 111 } 112 handled |= mMouseDelegate.onInterceptTouchEvent(event); 113 } else { 114 // If user has started some gesture while RecyclerView is not at the top, disable 115 // refresh 116 if (event.isActionDown() && rv.computeVerticalScrollOffset() != 0) { 117 mRefreshLayoutEnabler.accept(false); 118 } 119 handled |= mTouchDelegate.onInterceptTouchEvent(event); 120 } 121 122 123 if (event.isActionUp()) { 124 mRefreshLayoutEnabler.accept(true); 125 } 126 } 127 128 // Forward all events to UserInputHandler. 129 // This is necessary since UserInputHandler needs to always see the first DOWN event. Or 130 // else all future UP events will be tossed. 131 handled |= onTouchEvent(e); 132 133 return handled; 134 } 135 136 @Override 137 public void onTouchEvent(RecyclerView rv, MotionEvent e) { 138 try (InputEvent event = MotionInputEvent.obtain(e, rv)) { 139 if (Events.isMouseEvent(e)) { 140 mMouseDelegate.onTouchEvent(event); 141 } else { 142 mTouchDelegate.onTouchEvent(rv, event); 143 } 144 145 if (event.isActionUp()) { 146 mRefreshLayoutEnabler.accept(true); 147 } 148 } 149 150 // Note: even though this event is being handled as part of gestures such as drag and band, 151 // continue forwarding to the GestureDetector. The detector needs to see the entire cluster 152 // of events in order to properly interpret other gestures, such as long press. 153 onTouchEvent(e); 154 } 155 156 private class MouseDelegate { 157 boolean onInterceptTouchEvent(InputEvent e) { 158 if (Events.isMouseDragEvent(e)) { 159 return mMouseDragListener.accept(e); 160 } else if (mBandController != null && 161 (mBandController.shouldStart(e) || mBandController.shouldStop(e))) { 162 return mBandController.onInterceptTouchEvent(e); 163 } 164 return false; 165 } 166 167 void onTouchEvent(InputEvent e) { 168 if (mBandController != null) { 169 mBandController.onTouchEvent(e); 170 } 171 } 172 } 173 174 private class TouchDelegate { 175 boolean onInterceptTouchEvent(InputEvent e) { 176 // Gesture Selector needs to be constantly fed events, so that when a long press does 177 // happen, we would have the last DOWN event that occurred to keep track of our anchor 178 // point 179 return mGestureSelector.onInterceptTouchEvent(e); 180 } 181 182 // TODO: Make this take just an InputEvent, no RecyclerView 183 void onTouchEvent(RecyclerView rv, InputEvent e) { 184 mGestureSelector.onTouchEvent(rv, e); 185 } 186 } 187 188 @Override 189 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {} 190} 191