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