1/* 2 * Copyright (C) 2018 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 */ 16package com.android.launcher3.touch; 17 18import static android.view.MotionEvent.ACTION_CANCEL; 19import static android.view.MotionEvent.ACTION_DOWN; 20import static android.view.MotionEvent.ACTION_POINTER_UP; 21import static android.view.MotionEvent.ACTION_UP; 22import static android.view.ViewConfiguration.getLongPressTimeout; 23 24import static com.android.launcher3.LauncherState.NORMAL; 25 26import android.graphics.PointF; 27import android.graphics.Rect; 28import android.view.HapticFeedbackConstants; 29import android.view.MotionEvent; 30import android.view.View; 31import android.view.View.OnTouchListener; 32 33import com.android.launcher3.AbstractFloatingView; 34import com.android.launcher3.CellLayout; 35import com.android.launcher3.DeviceProfile; 36import com.android.launcher3.Launcher; 37import com.android.launcher3.Workspace; 38import com.android.launcher3.dragndrop.DragLayer; 39import com.android.launcher3.views.OptionsPopupView; 40import com.android.launcher3.userevent.nano.LauncherLogProto.Action; 41import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 42 43/** 44 * Helper class to handle touch on empty space in workspace and show options popup on long press 45 */ 46public class WorkspaceTouchListener implements OnTouchListener, Runnable { 47 48 /** 49 * STATE_PENDING_PARENT_INFORM is the state between longPress performed & the next motionEvent. 50 * This next event is used to send an ACTION_CANCEL to Workspace, to that it clears any 51 * temporary scroll state. After that, the state is set to COMPLETED, and we just eat up all 52 * subsequent motion events. 53 */ 54 private static final int STATE_CANCELLED = 0; 55 private static final int STATE_REQUESTED = 1; 56 private static final int STATE_PENDING_PARENT_INFORM = 2; 57 private static final int STATE_COMPLETED = 3; 58 59 private final Rect mTempRect = new Rect(); 60 private final Launcher mLauncher; 61 private final Workspace mWorkspace; 62 private final PointF mTouchDownPoint = new PointF(); 63 64 private int mLongPressState = STATE_CANCELLED; 65 66 public WorkspaceTouchListener(Launcher launcher, Workspace workspace) { 67 mLauncher = launcher; 68 mWorkspace = workspace; 69 } 70 71 @Override 72 public boolean onTouch(View view, MotionEvent ev) { 73 int action = ev.getActionMasked(); 74 if (action == ACTION_DOWN) { 75 // Check if we can handle long press. 76 boolean handleLongPress = canHandleLongPress(); 77 78 if (handleLongPress) { 79 // Check if the event is not near the edges 80 DeviceProfile dp = mLauncher.getDeviceProfile(); 81 DragLayer dl = mLauncher.getDragLayer(); 82 Rect insets = dp.getInsets(); 83 84 mTempRect.set(insets.left, insets.top, dl.getWidth() - insets.right, 85 dl.getHeight() - insets.bottom); 86 mTempRect.inset(dp.edgeMarginPx, dp.edgeMarginPx); 87 handleLongPress = mTempRect.contains((int) ev.getX(), (int) ev.getY()); 88 } 89 90 cancelLongPress(); 91 if (handleLongPress) { 92 mLongPressState = STATE_REQUESTED; 93 mTouchDownPoint.set(ev.getX(), ev.getY()); 94 mWorkspace.postDelayed(this, getLongPressTimeout()); 95 } 96 97 mWorkspace.onTouchEvent(ev); 98 // Return true to keep receiving touch events 99 return true; 100 } 101 102 if (mLongPressState == STATE_PENDING_PARENT_INFORM) { 103 // Inform the workspace to cancel touch handling 104 ev.setAction(ACTION_CANCEL); 105 mWorkspace.onTouchEvent(ev); 106 107 ev.setAction(action); 108 mLongPressState = STATE_COMPLETED; 109 } 110 111 final boolean result; 112 if (mLongPressState == STATE_COMPLETED) { 113 // We have handled the touch, so workspace does not need to know anything anymore. 114 result = true; 115 } else if (mLongPressState == STATE_REQUESTED) { 116 mWorkspace.onTouchEvent(ev); 117 if (mWorkspace.isHandlingTouch()) { 118 cancelLongPress(); 119 } 120 121 result = true; 122 } else { 123 // We don't want to handle touch, let workspace handle it as usual. 124 result = false; 125 } 126 127 if (action == ACTION_UP || action == ACTION_POINTER_UP) { 128 if (!mWorkspace.isTouchActive()) { 129 final CellLayout currentPage = 130 (CellLayout) mWorkspace.getChildAt(mWorkspace.getCurrentPage()); 131 if (currentPage != null) { 132 mWorkspace.onWallpaperTap(ev); 133 } 134 } 135 } 136 137 if (action == ACTION_UP || action == ACTION_CANCEL) { 138 cancelLongPress(); 139 } 140 return result; 141 } 142 143 private boolean canHandleLongPress() { 144 return AbstractFloatingView.getTopOpenView(mLauncher) == null 145 && mLauncher.isInState(NORMAL); 146 } 147 148 private void cancelLongPress() { 149 mWorkspace.removeCallbacks(this); 150 mLongPressState = STATE_CANCELLED; 151 } 152 153 @Override 154 public void run() { 155 if (mLongPressState == STATE_REQUESTED) { 156 if (canHandleLongPress()) { 157 mLongPressState = STATE_PENDING_PARENT_INFORM; 158 mWorkspace.getParent().requestDisallowInterceptTouchEvent(true); 159 160 mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, 161 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); 162 mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS, 163 Action.Direction.NONE, ContainerType.WORKSPACE, 164 mWorkspace.getCurrentPage()); 165 OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y); 166 } else { 167 cancelLongPress(); 168 } 169 } 170 } 171} 172