| 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;
|
| + }
|
| +}
|
|
|