1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.chrome.browser.appmenu; 6 7import android.animation.TimeAnimator; 8import android.annotation.SuppressLint; 9import android.app.Activity; 10import android.content.res.Resources; 11import android.graphics.Rect; 12import android.view.GestureDetector; 13import android.view.GestureDetector.SimpleOnGestureListener; 14import android.view.MotionEvent; 15import android.view.View; 16import android.widget.ImageButton; 17import android.widget.LinearLayout; 18import android.widget.ListPopupWindow; 19import android.widget.ListView; 20 21import org.chromium.chrome.R; 22import org.chromium.chrome.browser.UmaBridge; 23 24import java.util.ArrayList; 25 26/** 27 * Handles the drag touch events on AppMenu that start from the menu button. 28 * 29 * Lint suppression for NewApi is added because we are using TimeAnimator class that was marked 30 * hidden in API 16. 31 */ 32@SuppressLint("NewApi") 33class AppMenuDragHelper { 34 private final Activity mActivity; 35 private final AppMenu mAppMenu; 36 37 // Internally used action constants for dragging. 38 private static final int ITEM_ACTION_HIGHLIGHT = 0; 39 private static final int ITEM_ACTION_PERFORM = 1; 40 private static final int ITEM_ACTION_CLEAR_HIGHLIGHT_ALL = 2; 41 42 private static final float AUTO_SCROLL_AREA_MAX_RATIO = 0.25f; 43 44 // Dragging related variables, i.e., menu showing initiated by touch down and drag to navigate. 45 private final float mAutoScrollFullVelocity; 46 private final TimeAnimator mDragScrolling = new TimeAnimator(); 47 private float mDragScrollOffset; 48 private int mDragScrollOffsetRounded; 49 private volatile float mDragScrollingVelocity; 50 private volatile float mLastTouchX; 51 private volatile float mLastTouchY; 52 private final int mItemRowHeight; 53 private boolean mIsSingleTapUpHappened; 54 GestureDetector mGestureSingleTapDetector; 55 56 // These are used in a function locally, but defined here to avoid heap allocation on every 57 // touch event. 58 private final Rect mScreenVisibleRect = new Rect(); 59 private final int[] mScreenVisiblePoint = new int[2]; 60 61 AppMenuDragHelper(Activity activity, AppMenu appMenu, int itemRowHeight) { 62 mActivity = activity; 63 mAppMenu = appMenu; 64 mItemRowHeight = itemRowHeight; 65 Resources res = mActivity.getResources(); 66 mAutoScrollFullVelocity = res.getDimensionPixelSize(R.dimen.auto_scroll_full_velocity); 67 // If user is dragging and the popup ListView is too big to display at once, 68 // mDragScrolling animator scrolls mPopup.getListView() automatically depending on 69 // the user's touch position. 70 mDragScrolling.setTimeListener(new TimeAnimator.TimeListener() { 71 @Override 72 public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) { 73 ListPopupWindow popup = mAppMenu.getPopup(); 74 if (popup == null || popup.getListView() == null) return; 75 76 // We keep both mDragScrollOffset and mDragScrollOffsetRounded because 77 // the actual scrolling is by the rounded value but at the same time we also 78 // want to keep the precise scroll value in float. 79 mDragScrollOffset += (deltaTime * 0.001f) * mDragScrollingVelocity; 80 int diff = Math.round(mDragScrollOffset - mDragScrollOffsetRounded); 81 mDragScrollOffsetRounded += diff; 82 popup.getListView().smoothScrollBy(diff, 0); 83 84 // Force touch move event to highlight items correctly for the scrolled position. 85 if (!Float.isNaN(mLastTouchX) && !Float.isNaN(mLastTouchY)) { 86 menuItemAction(Math.round(mLastTouchX), Math.round(mLastTouchY), 87 ITEM_ACTION_HIGHLIGHT); 88 } 89 } 90 }); 91 mGestureSingleTapDetector = new GestureDetector(activity, new SimpleOnGestureListener() { 92 @Override 93 public boolean onSingleTapUp(MotionEvent e) { 94 mIsSingleTapUpHappened = true; 95 return true; 96 } 97 }); 98 } 99 100 /** 101 * Sets up all the internal state to prepare for menu dragging. 102 * @param startDragging Whether dragging is started. For example, if the app menu 103 * is showed by tapping on a button, this should be false. If it is 104 * showed by start dragging down on the menu button, this should be 105 * true. 106 */ 107 void onShow(boolean startDragging) { 108 mLastTouchX = Float.NaN; 109 mLastTouchY = Float.NaN; 110 mDragScrollOffset = 0.0f; 111 mDragScrollOffsetRounded = 0; 112 mDragScrollingVelocity = 0.0f; 113 mIsSingleTapUpHappened = false; 114 115 if (startDragging) mDragScrolling.start(); 116 } 117 118 /** 119 * Dragging mode will be stopped by calling this function. Note that it will fall back to normal 120 * non-dragging mode. 121 */ 122 void finishDragging() { 123 menuItemAction(0, 0, ITEM_ACTION_CLEAR_HIGHLIGHT_ALL); 124 mDragScrolling.cancel(); 125 } 126 127 /** 128 * Gets all the touch events and updates dragging related logic. Note that if this app menu 129 * is initiated by software UI control, then the control should set onTouchListener and forward 130 * all the events to this method because the initial UI control that processed ACTION_DOWN will 131 * continue to get all the subsequent events. 132 * 133 * @param event Touch event to be processed. 134 * @return Whether the event is handled. 135 */ 136 boolean handleDragging(MotionEvent event) { 137 if (!mAppMenu.isShowing() || !mDragScrolling.isRunning()) return false; 138 139 // We will only use the screen space coordinate (rawX, rawY) to reduce confusion. 140 // This code works across many different controls, so using local coordinates will be 141 // a disaster. 142 143 final float rawX = event.getRawX(); 144 final float rawY = event.getRawY(); 145 final int roundedRawX = Math.round(rawX); 146 final int roundedRawY = Math.round(rawY); 147 final int eventActionMasked = event.getActionMasked(); 148 final ListView listView = mAppMenu.getPopup().getListView(); 149 150 mLastTouchX = rawX; 151 mLastTouchY = rawY; 152 153 if (eventActionMasked == MotionEvent.ACTION_CANCEL) { 154 mAppMenu.dismiss(); 155 return true; 156 } 157 158 if (!mIsSingleTapUpHappened) { 159 mGestureSingleTapDetector.onTouchEvent(event); 160 if (mIsSingleTapUpHappened) { 161 UmaBridge.usingMenu(false, false); 162 finishDragging(); 163 } 164 } 165 166 // After this line, drag scrolling is happening. 167 if (!mDragScrolling.isRunning()) return false; 168 169 boolean didPerformClick = false; 170 int itemAction = ITEM_ACTION_CLEAR_HIGHLIGHT_ALL; 171 switch (eventActionMasked) { 172 case MotionEvent.ACTION_DOWN: 173 case MotionEvent.ACTION_MOVE: 174 itemAction = ITEM_ACTION_HIGHLIGHT; 175 break; 176 case MotionEvent.ACTION_UP: 177 itemAction = ITEM_ACTION_PERFORM; 178 break; 179 default: 180 break; 181 } 182 didPerformClick = menuItemAction(roundedRawX, roundedRawY, itemAction); 183 184 if (eventActionMasked == MotionEvent.ACTION_UP && !didPerformClick) { 185 UmaBridge.usingMenu(false, true); 186 mAppMenu.dismiss(); 187 } else if (eventActionMasked == MotionEvent.ACTION_MOVE) { 188 // Auto scrolling on the top or the bottom of the listView. 189 if (listView.getHeight() > 0) { 190 float autoScrollAreaRatio = Math.min(AUTO_SCROLL_AREA_MAX_RATIO, 191 mItemRowHeight * 1.2f / listView.getHeight()); 192 float normalizedY = 193 (rawY - getScreenVisibleRect(listView).top) / listView.getHeight(); 194 if (normalizedY < autoScrollAreaRatio) { 195 // Top 196 mDragScrollingVelocity = (normalizedY / autoScrollAreaRatio - 1.0f) 197 * mAutoScrollFullVelocity; 198 } else if (normalizedY > 1.0f - autoScrollAreaRatio) { 199 // Bottom 200 mDragScrollingVelocity = ((normalizedY - 1.0f) / autoScrollAreaRatio + 1.0f) 201 * mAutoScrollFullVelocity; 202 } else { 203 // Middle or not scrollable. 204 mDragScrollingVelocity = 0.0f; 205 } 206 } 207 } 208 209 return true; 210 } 211 212 /** 213 * Performs the specified action on the menu item specified by the screen coordinate position. 214 * @param screenX X in screen space coordinate. 215 * @param screenY Y in screen space coordinate. 216 * @param action Action type to perform, it should be one of ITEM_ACTION_* constants. 217 * @return true whether or not a menu item is performed (executed). 218 */ 219 private boolean menuItemAction(int screenX, int screenY, int action) { 220 ListView listView = mAppMenu.getPopup().getListView(); 221 222 ArrayList<View> itemViews = new ArrayList<View>(); 223 for (int i = 0; i < listView.getChildCount(); ++i) { 224 boolean hasImageButtons = false; 225 if (listView.getChildAt(i) instanceof LinearLayout) { 226 LinearLayout layout = (LinearLayout) listView.getChildAt(i); 227 for (int j = 0; j < layout.getChildCount(); ++j) { 228 itemViews.add(layout.getChildAt(j)); 229 if (layout.getChildAt(j) instanceof ImageButton) hasImageButtons = true; 230 } 231 } 232 if (!hasImageButtons) itemViews.add(listView.getChildAt(i)); 233 } 234 235 boolean didPerformClick = false; 236 for (int i = 0; i < itemViews.size(); ++i) { 237 View itemView = itemViews.get(i); 238 239 boolean shouldPerform = itemView.isEnabled() && itemView.isShown() && 240 getScreenVisibleRect(itemView).contains(screenX, screenY); 241 242 switch (action) { 243 case ITEM_ACTION_HIGHLIGHT: 244 itemView.setPressed(shouldPerform); 245 break; 246 case ITEM_ACTION_PERFORM: 247 if (shouldPerform) { 248 UmaBridge.usingMenu(false, true); 249 itemView.performClick(); 250 didPerformClick = true; 251 } 252 break; 253 case ITEM_ACTION_CLEAR_HIGHLIGHT_ALL: 254 itemView.setPressed(false); 255 break; 256 default: 257 assert false; 258 break; 259 } 260 } 261 return didPerformClick; 262 } 263 264 /** 265 * @return Visible rect in screen coordinates for the given View. 266 */ 267 private Rect getScreenVisibleRect(View view) { 268 view.getLocalVisibleRect(mScreenVisibleRect); 269 view.getLocationOnScreen(mScreenVisiblePoint); 270 mScreenVisibleRect.offset(mScreenVisiblePoint[0], mScreenVisiblePoint[1]); 271 return mScreenVisibleRect; 272 } 273} 274