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.
5 package org.chromium.chrome.browser.appmenu;
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.GestureDetector;
13 import android.view.GestureDetector.SimpleOnGestureListener;
14 import android.view.MotionEvent;
15 import android.view.View;
16 import android.widget.ImageButton;
17 import android.widget.LinearLayout;
18 import android.widget.ListPopupWindow;
19 import android.widget.ListView;
21 import org.chromium.chrome.R;
22 import org.chromium.chrome.browser.UmaBridge;
24 import java.util.ArrayList;
27 * Handles the drag touch events on AppMenu that start from the menu button.
29 * Lint suppression for NewApi is added because we are using TimeAnimator class that was marked
32 @SuppressLint("NewApi")
33 class AppMenuDragHelper {
34 private final Activity mActivity;
35 private final AppMenu mAppMenu;
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;
42 private static final float AUTO_SCROLL_AREA_MAX_RATIO = 0.25f;
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;
56 // These are used in a function locally, but defined here to avoid heap allocation on every
58 private final Rect mScreenVisibleRect = new Rect();
59 private final int[] mScreenVisiblePoint = new int[2];
61 AppMenuDragHelper(Activity activity, AppMenu appMenu, int itemRowHeight) {
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() {
72 public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
73 ListPopupWindow popup = mAppMenu.getPopup();
74 if (popup == null || popup.getListView() == null) return;
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);
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);
91 mGestureSingleTapDetector = new GestureDetector(activity, new SimpleOnGestureListener() {
93 public boolean onSingleTapUp(MotionEvent e) {
94 mIsSingleTapUpHappened = true;
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
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;
115 if (startDragging) mDragScrolling.start();
119 * Dragging mode will be stopped by calling this function. Note that it will fall back to normal
122 void finishDragging() {
123 menuItemAction(0, 0, ITEM_ACTION_CLEAR_HIGHLIGHT_ALL);
124 mDragScrolling.cancel();
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.
133 * @param event Touch event to be processed.
134 * @return Whether the event is handled.
136 boolean handleDragging(MotionEvent event) {
137 if (!mAppMenu.isShowing() || !mDragScrolling.isRunning()) return false;
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
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();
153 if (eventActionMasked == MotionEvent.ACTION_CANCEL) {
158 if (!mIsSingleTapUpHappened) {
159 mGestureSingleTapDetector.onTouchEvent(event);
160 if (mIsSingleTapUpHappened) {
161 UmaBridge.usingMenu(false, false);
166 // After this line, drag scrolling is happening.
167 if (!mDragScrolling.isRunning()) return false;
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;
176 case MotionEvent.ACTION_UP:
177 itemAction = ITEM_ACTION_PERFORM;
182 didPerformClick = menuItemAction(roundedRawX, roundedRawY, itemAction);
184 if (eventActionMasked == MotionEvent.ACTION_UP && !didPerformClick) {
185 UmaBridge.usingMenu(false, true);
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());
193 (rawY - getScreenVisibleRect(listView).top) / listView.getHeight();
194 if (normalizedY < autoScrollAreaRatio) {
196 mDragScrollingVelocity = (normalizedY / autoScrollAreaRatio - 1.0f)
197 * mAutoScrollFullVelocity;
198 } else if (normalizedY > 1.0f - autoScrollAreaRatio) {
200 mDragScrollingVelocity = ((normalizedY - 1.0f) / autoScrollAreaRatio + 1.0f)
201 * mAutoScrollFullVelocity;
203 // Middle or not scrollable.
204 mDragScrollingVelocity = 0.0f;
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).
219 private boolean menuItemAction(int screenX, int screenY, int action) {
220 ListView listView = mAppMenu.getPopup().getListView();
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;
232 if (!hasImageButtons) itemViews.add(listView.getChildAt(i));
235 boolean didPerformClick = false;
236 for (int i = 0; i < itemViews.size(); ++i) {
237 View itemView = itemViews.get(i);
239 boolean shouldPerform = itemView.isEnabled() && itemView.isShown() &&
240 getScreenVisibleRect(itemView).contains(screenX, screenY);
243 case ITEM_ACTION_HIGHLIGHT:
244 itemView.setPressed(shouldPerform);
246 case ITEM_ACTION_PERFORM:
248 UmaBridge.usingMenu(false, true);
249 itemView.performClick();
250 didPerformClick = true;
253 case ITEM_ACTION_CLEAR_HIGHLIGHT_ALL:
254 itemView.setPressed(false);
261 return didPerformClick;
265 * @return Visible rect in screen coordinates for the given View.
267 private Rect getScreenVisibleRect(View view) {
268 view.getLocalVisibleRect(mScreenVisibleRect);
269 view.getLocationOnScreen(mScreenVisiblePoint);
270 mScreenVisibleRect.offset(mScreenVisiblePoint[0], mScreenVisiblePoint[1]);
271 return mScreenVisibleRect;