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