Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2672)

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenuDragHelper.java

Issue 199733002: Cleanup AppMenu code. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Move lint suppression to AppMenuDragHelper Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenuDragHelper.java
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenuDragHelper.java b/chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenuDragHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..dd795556c79058f89087339667fdfc9c53135bad
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenuDragHelper.java
@@ -0,0 +1,464 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.appmenu;
+
+import android.animation.TimeAnimator;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.ViewParent;
+import android.widget.ListPopupWindow;
+import android.widget.ListView;
+
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.UmaBridge;
+
+import java.util.ArrayList;
+
+/**
+ * Handles the drag touch events on AppMenu that start from the menu button.
+ *
+ * Lint suppression for NewApi is added because we are using TimeAnimator class that was marked
+ * hidden in API 16.
+ */
+@SuppressLint("NewApi")
+class AppMenuDragHelper {
+ private static final String TAG = "AppMenuDragHelper";
+
+ private final Activity mActivity;
+ private final AppMenu mAppMenu;
+
+ // Internally used action constants for dragging.
+ private static final int ITEM_ACTION_HIGHLIGHT = 0;
+ private static final int ITEM_ACTION_PERFORM = 1;
+ private static final int ITEM_ACTION_CLEAR_HIGHLIGHT_ALL = 2;
+
+ private static final float AUTO_SCROLL_AREA_MAX_RATIO = 0.25f;
+ private static final int EDGE_SWIPE_IN_ADDITIONAL_SLOP_TIME_MS = 500;
+
+ // Dragging related variables, i.e., menu showing initiated by touch down and drag to navigate.
+ private final float mAutoScrollFullVelocity;
+ private final int mEdgeSwipeInSlop;
+ private final int mEdgeSwipeInAdditionalSlop;
+ private final int mEdgeSwipeOutSlop;
+ private int mScaledTouchSlop;
+ private long mHardwareMenuButtonUpTime;
+ private boolean mDragPending;
+ private final TimeAnimator mDragScrolling = new TimeAnimator();
+ private float mDragScrollOffset;
+ private int mDragScrollOffsetRounded;
+ private volatile float mDragScrollingVelocity;
+ private volatile float mLastTouchX;
+ private volatile float mLastTouchY;
+ private float mTopTouchMovedBound;
+ private float mBottomTouchMovedBound;
+ private boolean mIsDownScrollable;
+ private boolean mIsUpScrollable;
+ private boolean mIsByHardwareButton;
+ private int mCurrentScreenRotation = -1;
+
+ // These are used in a function locally, but defined here to avoid heap allocation on every
+ // touch event.
+ private final Rect mScreenVisibleRect = new Rect();
+ private final int[] mScreenVisiblePoint = new int[2];
+
+ // Sub-UI-controls, backward, forward, bookmark and listView, are getting a touch event first
+ // if the app menu is initiated by hardware menu button. For those cases, we need to
+ // conditionally forward the touch event to our drag scrolling method.
+ private final OnTouchListener mDragScrollTouchEventForwarder = new OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ return handleDragging(event);
+ }
+ };
+
+ AppMenuDragHelper(Activity activity, AppMenu appMenu) {
+ mActivity = activity;
+ mAppMenu = appMenu;
+ mScaledTouchSlop = ViewConfiguration.get(
+ mActivity.getApplicationContext()).getScaledTouchSlop();
+ Resources res = mActivity.getResources();
+ mAutoScrollFullVelocity = res.getDimensionPixelSize(R.dimen.auto_scroll_full_velocity);
+ mEdgeSwipeInSlop = res.getDimensionPixelSize(R.dimen.edge_swipe_in_slop);
+ mEdgeSwipeInAdditionalSlop = res.getDimensionPixelSize(
+ R.dimen.edge_swipe_in_additional_slop);
+ mEdgeSwipeOutSlop = res.getDimensionPixelSize(R.dimen.edge_swipe_out_slop);
+ // If user is dragging and the popup ListView is too big to display at once,
+ // mDragScrolling animator scrolls mPopup.getListView() automatically depending on
+ // the user's touch position.
+ mDragScrolling.setTimeListener(new TimeAnimator.TimeListener() {
+ @Override
+ public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
+ ListPopupWindow popup = mAppMenu.getPopup();
+ if (popup == null || popup.getListView() == null) return;
+
+ // We keep both mDragScrollOffset and mDragScrollOffsetRounded because
+ // the actual scrolling is by the rounded value but at the same time we also
+ // want to keep the precise scroll value in float.
+ mDragScrollOffset += (deltaTime * 0.001f) * mDragScrollingVelocity;
+ int diff = Math.round(mDragScrollOffset - mDragScrollOffsetRounded);
+ mDragScrollOffsetRounded += diff;
+ popup.getListView().smoothScrollBy(diff, 0);
+
+ // Force touch move event to highlight items correctly for the scrolled position.
+ if (!Float.isNaN(mLastTouchX) && !Float.isNaN(mLastTouchY)) {
+ int actionToPerform = isInSwipeOutRegion(mLastTouchX, mLastTouchY) ?
+ ITEM_ACTION_CLEAR_HIGHLIGHT_ALL : ITEM_ACTION_HIGHLIGHT;
+ menuItemAction(Math.round(mLastTouchX), Math.round(mLastTouchY),
+ actionToPerform);
+ }
+ }
+ });
+ }
+
+ /**
+ * Sets up all the internal state to prepare for menu dragging.
+ *
+ * @param isByHardwareButton Whether or not hardware button triggered it. (oppose to software
+ * button)
+ * @param startDragging Whether dragging is started. For example, if the app menu
+ * is showed by tapping on a button, this should be false. If it is
+ * showed by start dragging down on the menu button, this should be
+ * true. Note that if isByHardwareButton is true, this is ignored.
+ */
+ void onShow(boolean isByHardwareButton, boolean startDragging) {
+ mCurrentScreenRotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
+ mLastTouchX = Float.NaN;
+ mLastTouchY = Float.NaN;
+ mDragScrollOffset = 0.0f;
+ mDragScrollOffsetRounded = 0;
+ mDragScrollingVelocity = 0.0f;
+
+ mIsByHardwareButton = isByHardwareButton;
+ mDragPending = isByHardwareButton;
+ mIsDownScrollable = !isByHardwareButton;
+ mIsUpScrollable = !isByHardwareButton;
+
+ mTopTouchMovedBound = Float.POSITIVE_INFINITY;
+ mBottomTouchMovedBound = Float.NEGATIVE_INFINITY;
+ mHardwareMenuButtonUpTime = -1;
+
+ ListPopupWindow popup = mAppMenu.getPopup();
+ popup.getListView().setOnTouchListener(mDragScrollTouchEventForwarder);
+
+ // We assume that the parent of popup ListView is an instance of View. Otherwise, dragging
+ // from a hardware menu button won't work.
+ ViewParent listViewParent = popup.getListView().getParent();
+ if (listViewParent instanceof View) {
+ ((View) listViewParent).setOnTouchListener(mDragScrollTouchEventForwarder);
+ } else {
+ assert false;
+ }
+
+ if (mAppMenu.isShowingIconRow()) {
+ View iconRowView = mAppMenu.getIconRowView();
+ iconRowView.findViewById(R.id.menu_item_back).setOnTouchListener(
+ mDragScrollTouchEventForwarder);
+ iconRowView.findViewById(R.id.menu_item_forward).setOnTouchListener(
+ mDragScrollTouchEventForwarder);
+ iconRowView.findViewById(R.id.menu_item_bookmark).setOnTouchListener(
+ mDragScrollTouchEventForwarder);
+
+ }
+
+ if (!isByHardwareButton && startDragging) mDragScrolling.start();
+ }
+
+ void onDismiss() {
+ mDragScrolling.cancel();
+ }
+
+ /**
+ * This is a hint for adjusting edgeSwipeInSlop. For example. If the touch event started
+ * immediately after hardware menu button up, then we use larger edgeSwipeInSlop because it
+ * implies user is swiping in fast.
+ */
+ public void hardwareMenuButtonUp() {
+ // There should be only one time hardware menu button up.
+ assert mHardwareMenuButtonUpTime == -1;
+ mHardwareMenuButtonUpTime = SystemClock.uptimeMillis();
+ }
+
+ /**
+ * Gets all the touch events and updates dragging related logic. Note that if this app menu
+ * is initiated by software UI control, then the control should set onTouchListener and forward
+ * all the events to this method because the initial UI control that processed ACTION_DOWN will
+ * continue to get all the subsequent events.
+ *
+ * @param event Touch event to be processed.
+ * @return Whether the event is handled.
+ */
+ boolean handleDragging(MotionEvent event) {
+ if (!mAppMenu.isShowing() || (!mDragPending && !mDragScrolling.isRunning())) return false;
+
+ // We will only use the screen space coordinate (rawX, rawY) to reduce confusion.
+ // This code works across many different controls, so using local coordinates will be
+ // a disaster.
+
+ final float rawX = event.getRawX();
+ final float rawY = event.getRawY();
+ final int roundedRawX = Math.round(rawX);
+ final int roundedRawY = Math.round(rawY);
+ final int eventActionMasked = event.getActionMasked();
+ final ListView listView = mAppMenu.getPopup().getListView();
+
+ mLastTouchX = rawX;
+ mLastTouchY = rawY;
+
+ // Because (hardware) menu button can be right or left side of the screen, if we just
+ // trigger auto scrolling based on Y inside the listView, it might be scrolled
+ // unintentionally. Therefore, we will require touch position to move up or down a certain
+ // amount of distance to trigger auto scrolling up or down.
+ mTopTouchMovedBound = Math.min(mTopTouchMovedBound, rawY);
+ mBottomTouchMovedBound = Math.max(mBottomTouchMovedBound, rawY);
+ if (rawY <= mBottomTouchMovedBound - mScaledTouchSlop) {
+ mIsUpScrollable = true;
+ }
+ if (rawY >= mTopTouchMovedBound + mScaledTouchSlop) {
+ mIsDownScrollable = true;
+ }
+
+ if (eventActionMasked == MotionEvent.ACTION_CANCEL) {
+ mAppMenu.dismiss();
+ return true;
+ }
+
+ if (eventActionMasked == MotionEvent.ACTION_DOWN) {
+ assert mIsByHardwareButton != mDragScrolling.isStarted();
+ if (mIsByHardwareButton) {
+ if (mDragPending && getDistanceFromHardwareMenuButtonSideEdge(rawX, rawY) <
+ getEdgeSwipeInSlop(event)) {
+ mDragScrolling.start();
+ mDragPending = false;
+ UmaBridge.usingMenu(true, true);
+ } else {
+ if (!getScreenVisibleRect(listView).contains(roundedRawX, roundedRawY)) {
+ mAppMenu.dismiss();
+ }
+ mDragPending = false;
+ UmaBridge.usingMenu(true, false);
+ return false;
+ }
+ }
+ }
+
+ // After this line, drag scrolling is happening.
+ if (!mDragScrolling.isRunning()) return false;
+
+ boolean didPerformClick = false;
+ int itemAction = ITEM_ACTION_CLEAR_HIGHLIGHT_ALL;
+ if (!isInSwipeOutRegion(rawX, rawY)) {
+ switch (eventActionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ itemAction = ITEM_ACTION_HIGHLIGHT;
+ break;
+ case MotionEvent.ACTION_UP:
+ itemAction = ITEM_ACTION_PERFORM;
+ break;
+ default:
+ break;
+ }
+ }
+ didPerformClick = menuItemAction(roundedRawX, roundedRawY, itemAction);
+
+ if (eventActionMasked == MotionEvent.ACTION_UP && !didPerformClick) {
+ mAppMenu.dismiss();
+ } else if (eventActionMasked == MotionEvent.ACTION_MOVE) {
+ // Auto scrolling on the top or the bottom of the listView.
+ if (listView.getHeight() > 0) {
+ float autoScrollAreaRatio = Math.min(AUTO_SCROLL_AREA_MAX_RATIO,
+ mAppMenu.getItemRowHeight() * 1.2f / listView.getHeight());
+ float normalizedY =
+ (rawY - getScreenVisibleRect(listView).top) / listView.getHeight();
+ if (mIsUpScrollable && normalizedY < autoScrollAreaRatio) {
+ // Top
+ mDragScrollingVelocity = (normalizedY / autoScrollAreaRatio - 1.0f)
+ * mAutoScrollFullVelocity;
+ } else if (mIsDownScrollable && normalizedY > 1.0f - autoScrollAreaRatio) {
+ // Bottom
+ mDragScrollingVelocity = ((normalizedY - 1.0f) / autoScrollAreaRatio + 1.0f)
+ * mAutoScrollFullVelocity;
+ } else {
+ // Middle or not scrollable.
+ mDragScrollingVelocity = 0.0f;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @return Whether or not the position should be considered swiping-out, if ACTION_UP happens
+ * at the position.
+ */
+ private boolean isInSwipeOutRegion(float rawX, float rawY) {
+ return getShortestDistanceFromEdge(rawX, rawY) < mEdgeSwipeOutSlop;
+ }
+
+ /**
+ * @return The shortest distance from the screen edges for the given position rawX, rawY
+ * in screen coordinates.
+ */
+ private float getShortestDistanceFromEdge(float rawX, float rawY) {
+ Display display = mActivity.getWindowManager().getDefaultDisplay();
+ Point displaySize = new Point();
+ display.getSize(displaySize);
+
+ float distance = Math.min(
+ Math.min(rawY, displaySize.y - rawY - 1),
+ Math.min(rawX, displaySize.x - rawX - 1));
+ if (distance < 0.0f) {
+ Log.d(TAG, "Received touch event out of the screen edge boundary. distance = " +
+ distance);
+ }
+ return Math.abs(distance);
+ }
+
+ /**
+ * Performs the specified action on the menu item specified by the screen coordinate position.
+ * @param screenX X in screen space coordinate.
+ * @param screenY Y in screen space coordinate.
+ * @param action Action type to perform, it should be one of ITEM_ACTION_* constants.
+ * @return true whether or not a menu item is performed (executed).
+ */
+ private boolean menuItemAction(int screenX, int screenY, int action) {
+ ListView listView = mAppMenu.getPopup().getListView();
+
+ ArrayList<View> itemViews = new ArrayList<View>();
+ for (int i = 0; i < listView.getChildCount(); ++i) {
+ itemViews.add(listView.getChildAt(i));
+ }
+
+ View iconRowView = mAppMenu.getIconRowView();
+ if (iconRowView != null && mAppMenu.isShowingIconRow()) {
+ itemViews.add(iconRowView.findViewById(R.id.menu_item_back));
+ itemViews.add(iconRowView.findViewById(R.id.menu_item_forward));
+ itemViews.add(iconRowView.findViewById(R.id.menu_item_bookmark));
+ }
+
+ boolean didPerformClick = false;
+ for (int i = 0; i < itemViews.size(); ++i) {
+ View itemView = itemViews.get(i);
+
+ // Skip the icon row that belongs to the listView because that doesn't really
+ // exist as an item.
+ int listViewPositionIndex = listView.getFirstVisiblePosition() + i;
+ if (mAppMenu.isShowingIconRow() && listViewPositionIndex == 0) continue;
+
+ boolean shouldPerform = itemView.isEnabled() && itemView.isShown() &&
+ getScreenVisibleRect(itemView).contains(screenX, screenY);
+
+ switch (action) {
+ case ITEM_ACTION_HIGHLIGHT:
+ itemView.setPressed(shouldPerform);
+ break;
+ case ITEM_ACTION_PERFORM:
+ if (shouldPerform) {
+ if (itemView.getParent() == listView) {
+ listView.performItemClick(itemView, listViewPositionIndex, 0);
+ } else {
+ itemView.performClick();
+ }
+ didPerformClick = true;
+ }
+ break;
+ case ITEM_ACTION_CLEAR_HIGHLIGHT_ALL:
+ itemView.setPressed(false);
+ break;
+ default:
+ assert false;
+ break;
+ }
+ }
+ return didPerformClick;
+ }
+
+ /**
+ * @return The distance from the screen edge that is likely where the hardware menu button is
+ * located at. We assume the hardware menu button is at the bottom in the default,
+ * ROTATION_0, rotation. Note that there is a bug filed for Android API to request
+ * hardware menu button position b/10007237.
+ */
+ private float getDistanceFromHardwareMenuButtonSideEdge(float rawX, float rawY) {
+ Display display = mActivity.getWindowManager().getDefaultDisplay();
+ Point displaySize = new Point();
+ display.getSize(displaySize);
+
+ float distance;
+ switch (mCurrentScreenRotation) {
+ case Surface.ROTATION_0:
+ distance = displaySize.y - rawY - 1;
+ break;
+ case Surface.ROTATION_180:
+ distance = rawY;
+ break;
+ case Surface.ROTATION_90:
+ distance = displaySize.x - rawX - 1;
+ break;
+ case Surface.ROTATION_270:
+ distance = rawX;
+ break;
+ default:
+ distance = 0.0f;
+ assert false;
+ break;
+ }
+ if (distance < 0.0f) {
+ Log.d(TAG, "Received touch event out of hardware menu button side edge boundary." +
+ " distance = " + distance);
+ }
+ return Math.abs(distance);
+ }
+
+ /**
+ * @return Visible rect in screen coordinates for the given View.
+ */
+ private Rect getScreenVisibleRect(View view) {
+ view.getLocalVisibleRect(mScreenVisibleRect);
+ view.getLocationOnScreen(mScreenVisiblePoint);
+ mScreenVisibleRect.offset(mScreenVisiblePoint[0], mScreenVisiblePoint[1]);
+ return mScreenVisibleRect;
+ }
+
+ /**
+ * Computes Edge-swipe-in-slop and returns it.
+ *
+ * When user swipes in from a hardware menu button, because the swiping-in touch event doesn't
+ * necessarily start form the exact edge, we should also consider slightly more inside touch
+ * event as swiping-in. This value, Edge-swipe-in-slop, is the threshold distance from the
+ * edge that separates swiping-in and normal touch.
+ *
+ * @param event Touch event that eventually made this call.
+ * @return Edge-swipe-in-slop.
+ */
+ private float getEdgeSwipeInSlop(MotionEvent event) {
+ float edgeSwipeInSlope = mEdgeSwipeInSlop;
+ if (mHardwareMenuButtonUpTime == -1) {
+ // Hardware menu hasn't even had UP event yet. That means, user is swiping in really
+ // really fast. So use large edgeSwipeInSlope.
+ edgeSwipeInSlope += mEdgeSwipeInAdditionalSlop;
+ } else {
+ // If it's right after we had hardware menu button UP event, use large edgeSwipeInSlop,
+ // Otherwise, use small edgeSwipeInSlop.
+ float additionalEdgeSwipeInSlop = ((mHardwareMenuButtonUpTime - event.getEventTime()
+ + EDGE_SWIPE_IN_ADDITIONAL_SLOP_TIME_MS) * 0.001f)
+ * mEdgeSwipeInAdditionalSlop;
+ edgeSwipeInSlope += Math.max(0.0f, additionalEdgeSwipeInSlop);
+ }
+ return edgeSwipeInSlope;
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698