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

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

Issue 79573003: Upstreaming AppMenu. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Actually add tvdpi Created 7 years, 1 month 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/AppMenu.java
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenu.java b/chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenu.java
new file mode 100644
index 0000000000000000000000000000000000000000..e8fa56dbcaea0c5100706dc73328f51bf034b7d3
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenu.java
@@ -0,0 +1,1005 @@
+// Copyright 2011 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.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Display;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnKeyListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.HeaderViewListAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListPopupWindow;
+import android.widget.ListView;
+import android.widget.ListView.FixedViewInfo;
+import android.widget.PopupWindow;
+import android.widget.PopupWindow.OnDismissListener;
+import android.widget.TextView;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.BookmarksBridge;
+import org.chromium.chrome.browser.UmaBridge;
+import org.chromium.chrome.browser.util.KeyNavigationUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Shows a popup of menuitems anchored to a host view. When a item is selected we call
+ * Activity.onOptionsItemSelected with the appropriate MenuItem.
+ * - Only visible MenuItems are shown.
+ * - Disabled items are grayed out.
+ */
+public class AppMenu implements AdapterView.OnItemClickListener, OnKeyListener {
+ private static final String TAG = "AppMenu";
+
+ private static final float LAST_ITEM_SHOW_FRACTION = 0.5f;
+ private static final int DIVIDER_HEIGHT_DP = 1;
+
+ private static final float AUTO_SCROLL_AREA_MAX_RATIO = 0.25f;
+ private static final int EDGE_SWIPE_IN_ADDITIONAL_SLOP_TIME_MS = 500;
+
+ // 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 final Menu mMenu;
+ private final Activity mActivity;
+ private final int mItemRowHeight;
+ private final int mItemDividerHeight;
+ private final int mVerticalFadeDistance;
+ private final int mAdditionalVerticalOffset;
+ private ListPopupWindow mPopup;
+ private LayoutInflater mInflater;
+ private boolean mShowIconRow;
+ private MenuAdapter mAdapter;
+ private ImageButton mBookmarkButton;
+ private View mIconRowView;
+ private AppMenuHandler mHandler;
+
+ // 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 mIsByHardwareButton;
+ 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 int mCurrentScreenRotation = -1;
+ private float mTopTouchMovedBound;
+ private float mBottomTouchMovedBound;
+ private boolean mIsDownScrollable;
+ private boolean mIsUpScrollable;
+
+ // 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 AppMenu.this.handleDragging(event);
+ }
+ };
+
+ // 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];
+
+ /**
+ * Creates and sets up the App Menu.
+ * @param activity Activity that will handle app menu callbacks.
+ * @param menu Original menu created by the framework.
+ * @param itemRowHeight Desired height for each app menu row.
+ */
+ AppMenu(Activity activity, Menu menu, int itemRowHeight, AppMenuHandler handler) {
+ mActivity = activity;
+ mMenu = menu;
+ mScaledTouchSlop =
+ ViewConfiguration.get(mActivity.getApplicationContext()).getScaledTouchSlop();
+ mItemRowHeight = itemRowHeight;
+ assert mItemRowHeight > 0;
+
+ mHandler = handler;
+ Resources res = mActivity.getResources();
+
+ final float dpToPx = res.getDisplayMetrics().density;
+ mItemDividerHeight = (int) (DIVIDER_HEIGHT_DP * dpToPx);
+
+ mAdditionalVerticalOffset = res.getDimensionPixelSize(R.dimen.menu_vertical_offset);
+ mVerticalFadeDistance = res.getDimensionPixelSize(R.dimen.menu_vertical_fade_distance);
+ 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) {
+ if (mPopup == null || mPopup.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;
+ mPopup.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);
+ }
+ }
+ });
+ }
+
+ private void updateBookmarkButton() {
+ final MenuItem bookmarkMenuItem = mMenu.findItem(R.id.bookmark_this_page_id);
+ if (mBookmarkButton == null || bookmarkMenuItem == null) return;
+ if (bookmarkMenuItem.isEnabled()) {
+ mBookmarkButton.setImageResource(R.drawable.star);
+ mBookmarkButton.setContentDescription(mBookmarkButton.getContext().getString(
+ R.string.accessibility_menu_bookmark));
+ } else {
+ mBookmarkButton.setImageResource(R.drawable.star_lit);
+ mBookmarkButton.setContentDescription(mBookmarkButton.getContext().getString(
+ R.string.accessibility_menu_edit_bookmark));
+ }
+ }
+
+ /**
+ * Creates and shows the app menu anchored to the specified view.
+ *
+ * @param context The context of the app popup (ensure the proper theme is set on
+ * this context).
+ * @param anchorView The anchor {@link View} of the {@link ListPopupWindow}.
+ * @param showIconRow Whether or not the icon row should be shown,
+ * @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 show(Context context, View anchorView, boolean showIconRow,
+ boolean isByHardwareButton, boolean startDragging) {
+ mPopup = new ListPopupWindow(context, null, android.R.attr.popupMenuStyle);
+ mInflater = LayoutInflater.from(context);
+ mPopup.setModal(true);
+ mPopup.setAnchorView(anchorView);
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ mPopup.setOnDismissListener(new OnDismissListener() {
+ @Override
+ public void onDismiss() {
+ if (mPopup.getAnchorView() instanceof ImageButton) {
+ ((ImageButton) mPopup.getAnchorView()).setSelected(false);
+ }
+ mHandler.onMenuVisibilityChanged(false, ListView.INVALID_POSITION);
+ }
+ });
+ mPopup.setWidth(context.getResources().getDimensionPixelSize(R.dimen.menu_width));
+
+ mShowIconRow = showIconRow;
+ mCurrentScreenRotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
+ mIsByHardwareButton = isByHardwareButton;
+
+ // Extract visible items from the Menu.
+ int numItems = mMenu.size();
+ List<MenuItem> menuItems = new ArrayList<MenuItem>();
+ for (int i = 0; i < numItems; ++i) {
+ MenuItem item = mMenu.getItem(i);
+ if (item.isVisible()) {
+ menuItems.add(item);
+ }
+ }
+
+ // A List adapter for visible items in the Menu. The first row is added as a header to the
+ // list view.
+ mAdapter = new MenuAdapter(menuItems, mInflater);
+ if (mShowIconRow) {
+ mIconRowView = mInflater.inflate(R.layout.menu_icon_row, null);
+ // Add click handlers for the header icons.
+ setIconRowEventHandlers(mIconRowView, mActivity);
+ updateBookmarkButton();
+
+ // Icon row goes into header of List view.
+ ArrayList<FixedViewInfo> headerInfoList =
+ populateHeaderViewInfo(mActivity.getApplicationContext(), mIconRowView);
+
+ HeaderViewListAdapter headerViewListAdapter = new HeaderViewListAdapter(
+ headerInfoList, null, mAdapter);
+ mPopup.setAdapter(headerViewListAdapter);
+ } else {
+ mPopup.setAdapter(mAdapter);
+ }
+
+ // Get the height and width of the display.
+ Rect appRect = new Rect();
+ mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(appRect);
+
+ setMenuHeight(menuItems.size() + (mShowIconRow ? 1 : 0), appRect);
+ setPopupOffset(mPopup, mCurrentScreenRotation, appRect);
+ mPopup.setOnItemClickListener(this);
+ mPopup.show();
+ mPopup.getListView().setDividerHeight(mItemDividerHeight);
+
+ mHandler.onMenuVisibilityChanged(true, getCurrentFocusedPosition());
+
+ if (mVerticalFadeDistance > 0) {
+ mPopup.getListView().setVerticalFadingEdgeEnabled(true);
+ mPopup.getListView().setFadingEdgeLength(mVerticalFadeDistance);
+ }
+
+ mPopup.getListView().setOnKeyListener(this);
+
+ // Initiate drag related variables and listeners.
+ mLastTouchX = Float.NaN;
+ mLastTouchY = Float.NaN;
+ mDragScrollOffset = 0.0f;
+ mDragScrollOffsetRounded = 0;
+ mDragScrollingVelocity = 0.0f;
+
+ mDragPending = isByHardwareButton;
+ mIsDownScrollable = !isByHardwareButton;
+ mIsUpScrollable = !isByHardwareButton;
+
+ mTopTouchMovedBound = Float.POSITIVE_INFINITY;
+ mBottomTouchMovedBound = Float.NEGATIVE_INFINITY;
+ mHardwareMenuButtonUpTime = -1;
+
+ // Handles dragging related logic.
+ mPopup.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 = mPopup.getListView().getParent();
+ if (listViewParent instanceof View) {
+ ((View) listViewParent).setOnTouchListener(mDragScrollTouchEventForwarder);
+ } else {
+ assert false;
+ }
+
+ if (!isByHardwareButton && startDragging) mDragScrolling.start();
+ }
+
+ private void setPopupOffset(ListPopupWindow popup, int screenRotation, Rect appRect) {
+ Rect paddingRect = new Rect();
+ popup.getBackground().getPadding(paddingRect);
+ int[] anchorLocation = new int[2];
+ popup.getAnchorView().getLocationInWindow(anchorLocation);
+
+ // If we have a hardware menu button, locate the app menu closer to the estimated
+ // hardware menu button location.
+ if (mIsByHardwareButton) {
+ int horizontalOffset = -anchorLocation[0];
+ switch (screenRotation) {
+ case Surface.ROTATION_0:
+ case Surface.ROTATION_180:
+ horizontalOffset += (appRect.width() - mPopup.getWidth()) / 2;
+ break;
+ case Surface.ROTATION_90:
+ horizontalOffset += appRect.width() - mPopup.getWidth();
+ break;
+ case Surface.ROTATION_270:
+ break;
+ default:
+ assert false;
+ break;
+ }
+ popup.setHorizontalOffset(horizontalOffset);
+ // The menu is displayed above the anchored view, so shift the menu up by the top
+ // padding of the background.
+ popup.setVerticalOffset(mAdditionalVerticalOffset - paddingRect.bottom);
+ } else {
+ // The menu is displayed below the anchored view, so shift the menu up by the top
+ // padding of the background.
+ popup.setVerticalOffset(mAdditionalVerticalOffset - paddingRect.top);
+ }
+ }
+
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (mPopup == null || mPopup.getListView() == null) return false;
+
+ ListView listView = mPopup.getListView();
+ if (KeyNavigationUtil.isGoUp(event)) {
+ int previousPosition = listView.getSelectedItemPosition();
+ boolean handled = mPopup.onKeyDown(keyCode, event);
+ if (listView.getSelectedItemPosition() == ListView.INVALID_POSITION) {
+ listView.setSelection(0);
+ }
+
+ if (mShowIconRow && previousPosition == 1) {
+ // Clearing the selection is required to move into the icon row.
+ mPopup.clearListSelection();
+ requestFocusToEnabledIconRowButton();
+ }
+
+ mHandler.onKeyboardFocusChanged(getCurrentFocusedPosition());
+ return handled;
+ } else if (KeyNavigationUtil.isGoDown(event)) {
+ boolean handled = mPopup.onKeyDown(keyCode, event);
+ if (listView.getSelectedItemPosition() == ListView.INVALID_POSITION) {
+ listView.setSelection(listView.getCount() - 1);
+ }
+ mHandler.onKeyboardFocusChanged(getCurrentFocusedPosition());
+ return handled;
+ } else if (KeyNavigationUtil.isEnter(event)) {
+ int position = getCurrentFocusedPosition();
+ if (mShowIconRow && position == 0) return false;
+
+ int adjustedPosition = mShowIconRow ? position - 1 : position;
+ MenuItem clickedItem = mAdapter.getItem(adjustedPosition);
+ dismiss();
+ mActivity.onOptionsItemSelected(clickedItem);
+ mHandler.onKeyboardActivatedItem(position);
+ return true;
+ } else if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+ event.startTracking();
+ v.getKeyDispatcherState().startTracking(event, this);
+ return true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ v.getKeyDispatcherState().handleUpEvent(event);
+ if (event.isTracking() && !event.isCanceled()) {
+ dismiss();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ return false;
+ }
+
+ /**
+ * @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;
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * @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 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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 (!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 = mPopup.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) {
+ 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)) {
+ 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) {
+ 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,
+ mItemRowHeight * 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;
+ }
+
+ /**
+ * 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 = mPopup.getListView();
+
+ ArrayList<View> itemViews = new ArrayList<View>();
+ for (int i = 0; i < listView.getChildCount(); ++i) {
+ itemViews.add(listView.getChildAt(i));
+ }
+
+ if (mIconRowView != null && mShowIconRow) {
+ itemViews.add(mIconRowView.findViewById(R.id.menu_item_back));
+ itemViews.add(mIconRowView.findViewById(R.id.menu_item_forward));
+ itemViews.add(mIconRowView.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 (mShowIconRow && 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;
+ }
+
+ /**
+ * Requests focus whichever button in icon row is enabled from the left.
+ */
+ private void requestFocusToEnabledIconRowButton() {
+ View backView = mIconRowView.findViewById(R.id.menu_item_back);
+ View forwardView = mIconRowView.findViewById(R.id.menu_item_forward);
+ View bookmarkView = mIconRowView.findViewById(R.id.menu_item_bookmark);
+ if (backView.isFocusable()) {
+ backView.requestFocus();
+ } else if (forwardView.isFocusable()) {
+ forwardView.requestFocus();
+ } else {
+ bookmarkView.requestFocus();
+ }
+ }
+
+ private void setMenuHeight(int numMenuItems, Rect appDimensions) {
+ assert mPopup.getAnchorView() != null;
+ View anchorView = mPopup.getAnchorView();
+ int[] anchorViewLocation = new int[2];
+ anchorView.getLocationOnScreen(anchorViewLocation);
+ anchorViewLocation[1] -= appDimensions.top;
+
+ int availableScreenSpace = Math.max(anchorViewLocation[1],
+ appDimensions.height() - anchorViewLocation[1] - anchorView.getHeight());
+
+ Rect padding = new Rect();
+ mPopup.getBackground().getPadding(padding);
+
+ if (mIsByHardwareButton) {
+ availableScreenSpace -= padding.top;
+ } else {
+ availableScreenSpace -= padding.bottom;
+ }
+
+ int numCanFit = availableScreenSpace / (mItemRowHeight + mItemDividerHeight);
+
+ // Fade out the last item if we cannot fit all items.
+ if (numCanFit < numMenuItems) {
+ int spaceForFullItems = numCanFit * (mItemRowHeight + mItemDividerHeight);
+ int spaceForPartialItem = (int) (LAST_ITEM_SHOW_FRACTION * mItemRowHeight);
+ // Determine which item needs hiding.
+ if (spaceForFullItems + spaceForPartialItem < availableScreenSpace) {
+ mPopup.setHeight(spaceForFullItems + spaceForPartialItem +
+ padding.top + padding.bottom);
+ } else {
+ mPopup.setHeight(spaceForFullItems - mItemRowHeight + spaceForPartialItem +
+ padding.top + padding.bottom);
+ }
+ } else {
+ mPopup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+ }
+
+ private ArrayList<FixedViewInfo> populateHeaderViewInfo(Context context, View headerView) {
+ ArrayList<FixedViewInfo> headerInfoList = new ArrayList<FixedViewInfo>();
+ ListView lv = new ListView(context);
+ FixedViewInfo viewInfo = lv.new FixedViewInfo();
+ viewInfo.view = headerView;
+ // Make header not selectable, we handle the clicks on our own.
+ viewInfo.isSelectable = false;
+ headerInfoList.add(viewInfo);
+ return headerInfoList;
+ }
+
+ /**
+ * Adds click handlers for items in the icon row.
+ * Also disable/enable the view based on the menu item.
+ * We assume that we have Back, Forward and Bookmark-star icons in this view.
+ */
+ private void setIconRowEventHandlers(View iconRowView, final Activity activity) {
+ final MenuItem backMenuItem = mMenu.findItem(R.id.back_menu_id);
+ final MenuItem forwardMenuItem = mMenu.findItem(R.id.forward_menu_id);
+ final MenuItem bookmarkMenuItem = mMenu.findItem(R.id.bookmark_this_page_id);
+
+ View.OnFocusChangeListener focusListener = new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ mHandler.onKeyboardFocusChanged(getCurrentFocusedPosition());
+ }
+ };
+
+ OnKeyListener keyListener = new OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (isShowing() && v.isFocused()) {
+ if (keyCode == KeyEvent.KEYCODE_MENU) {
+ v.setSelected(false);
+ dismiss();
+ return true;
+ } else if (KeyNavigationUtil.isGoUp(event)) {
+ // Catch attempts to move out of bounds.
+ mHandler.onKeyboardFocusChanged(getCurrentFocusedPosition());
+ return true;
+ } else if (KeyNavigationUtil.isGoDown(event)) {
+ // Requesting focus on the mPopup.getListView().getChildAt(0) does not work.
+ // Requesting the focus on the ListView focuses the first non-header item.
+ mPopup.getListView().requestFocus();
+ mHandler.onKeyboardFocusChanged(getCurrentFocusedPosition());
+ return true;
+ } else if (KeyNavigationUtil.isEnter(event)) {
+ mHandler.onKeyboardActivatedItem(getCurrentFocusedPosition());
+ }
+ }
+ return false;
+ }
+ };
+
+ View backIcon = iconRowView.findViewById(R.id.menu_item_back);
+ backIcon.setEnabled(backMenuItem.isEnabled());
+ backIcon.setFocusable(backMenuItem.isEnabled());
+ backIcon.setOnKeyListener(keyListener);
+ backIcon.setOnFocusChangeListener(focusListener);
+ backIcon.setOnTouchListener(mDragScrollTouchEventForwarder);
+ backIcon.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dismiss();
+ activity.onOptionsItemSelected(backMenuItem);
+ }
+ });
+
+ View forwardIcon = iconRowView.findViewById(R.id.menu_item_forward);
+ forwardIcon.setEnabled(forwardMenuItem.isEnabled());
+ forwardIcon.setFocusable(forwardMenuItem.isEnabled());
+ forwardIcon.setOnKeyListener(keyListener);
+ forwardIcon.setOnFocusChangeListener(focusListener);
+ forwardIcon.setOnTouchListener(mDragScrollTouchEventForwarder);
+ forwardIcon.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dismiss();
+ activity.onOptionsItemSelected(forwardMenuItem);
+ }
+ });
+
+ // The bookmark button is assumed to be always enabled and focusable when navigating the
+ // menu using a keyboard.
+ mBookmarkButton = (ImageButton) iconRowView.findViewById(R.id.menu_item_bookmark);
+ mBookmarkButton.setEnabled(BookmarksBridge.isEditBookmarksEnabled());
+ mBookmarkButton.setOnKeyListener(keyListener);
+ mBookmarkButton.setOnFocusChangeListener(focusListener);
+ mBookmarkButton.setOnTouchListener(mDragScrollTouchEventForwarder);
+ mBookmarkButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dismiss();
+ activity.onOptionsItemSelected(bookmarkMenuItem);
+ }
+ });
+ }
+
+ /**
+ * Dismisses the app menu and cancels the drag-to-scroll if it is taking place.
+ */
+ void dismiss() {
+ mDragScrolling.cancel();
+ if (isShowing()) {
+ mPopup.dismiss();
+ }
+ }
+
+ /**
+ * @return Whether the app menu is currently showing.
+ */
+ public boolean isShowing() {
+ if (mPopup == null) {
+ return false;
+ }
+ return mPopup.isShowing();
+ }
+
+ private int getCurrentFocusedPosition() {
+ if (mPopup == null || mPopup.getListView() == null) return ListView.INVALID_POSITION;
+ ListView listView = mPopup.getListView();
+ int position = listView.getSelectedItemPosition();
+
+ // Check if any of the icon row icons are focused.
+ if (mShowIconRow) {
+ if (mIconRowView.findViewById(R.id.menu_item_back).isFocused() ||
+ mIconRowView.findViewById(R.id.menu_item_forward).isFocused() ||
+ mBookmarkButton.isFocused()) {
+ return 0;
+ }
+ }
+ return position;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ // Account for header. MenuAdapter does not know about header,
+ // but the 'position' includes the header.
+ int adjustedPosition = mShowIconRow ? position - 1 : position;
+ MenuItem clickedItem = mAdapter.getItem(adjustedPosition);
+ if (clickedItem.isEnabled()) {
+ dismiss();
+ mActivity.onOptionsItemSelected(clickedItem);
+ }
+ }
+
+ @VisibleForTesting
+ int getCount() {
+ if (mPopup == null || mPopup.getListView() == null) return 0;
+ return mPopup.getListView().getCount();
+ }
+
+ /**
+ * ListAdapter to customize the view of items in the list.
+ */
+ private static class MenuAdapter extends BaseAdapter {
+ private static final int VIEW_TYPE_MENUITEM = 0;
+ private static final int VIEW_TYPE_COUNT = 1;
+
+ private final LayoutInflater mInflater;
+ private final List<MenuItem> mMenuItems;
+ private final int mNumMenuItems;
+
+ public MenuAdapter(List<MenuItem> menuItems, LayoutInflater inflater) {
+ mMenuItems = menuItems;
+ mInflater = inflater;
+ mNumMenuItems = menuItems.size();
+ }
+
+ @Override
+ public int getCount() {
+ return mNumMenuItems;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return VIEW_TYPE_MENUITEM;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return getItem(position).getItemId();
+ }
+
+ @Override
+ public MenuItem getItem(int position) {
+ if (position == ListView.INVALID_POSITION) return null;
+ assert position >= 0;
+ assert position < mMenuItems.size();
+ return mMenuItems.get(position);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ // A ViewHolder keeps references to children views to avoid unneccessary calls
+ // to findViewById() on each row.
+ ViewHolder holder = null;
+
+ // When convertView is not null, we can reuse it directly, there is no need
+ // to reinflate it.
+ if (rowView == null) {
+ holder = new ViewHolder();
+ rowView = mInflater.inflate(R.layout.menu_item, null);
+ holder.text = (TextView) rowView.findViewById(R.id.menu_item_text);
+ holder.image = (MenuItemIcon) rowView.findViewById(R.id.menu_item_icon);
+ rowView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+ MenuItem item = getItem(position);
+
+ // Set up the icon.
+ Drawable icon = item.getIcon();
+ holder.image.setImageDrawable(icon);
+ holder.image.setVisibility(icon == null ? View.GONE : View.VISIBLE);
+ holder.image.setChecked(item.isChecked());
+
+ holder.text.setText(item.getTitle());
+ boolean isEnabled = item.isEnabled();
+ // Set the text color (using a color state list).
+ holder.text.setEnabled(isEnabled);
+ // This will ensure that the item is not highlighted when selected.
+ rowView.setEnabled(isEnabled);
+ return rowView;
+ }
+
+ static class ViewHolder {
+ TextView text;
+ MenuItemIcon image;
+ }
+ }
+
+ /**
+ * A menu icon that supports the checkable state.
+ */
+ static class MenuItemIcon extends ImageView {
+ private static final int[] CHECKED_STATE_SET = new int[] {android.R.attr.state_checked};
+ private boolean mCheckedState;
+
+ public MenuItemIcon(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Sets whether the item is checked and refreshes the View if necessary.
+ */
+ protected void setChecked(boolean state) {
+ if (state == mCheckedState) return;
+ mCheckedState = state;
+ refreshDrawableState();
+ }
+
+ @Override
+ public void setPressed(boolean state) {
+ // We don't want to highlight the checkbox icon since the parent item is already
+ // highlighted.
+ return;
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (mCheckedState) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698