// 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 com.android.browser.appmenu; import android.animation.TimeAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.content.res.Resources; import android.graphics.Rect; import android.view.GestureDetector; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.MotionEvent; import android.view.View; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.ListPopupWindow; import android.widget.ListView; import com.android.browser.R; 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 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; // Dragging related variables, i.e., menu showing initiated by touch down and drag to navigate. private final float mAutoScrollFullVelocity; 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 final int mItemRowHeight; private boolean mIsSingleTapUpHappened; GestureDetector mGestureSingleTapDetector; // 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]; AppMenuDragHelper(Activity activity, AppMenu appMenu, int itemRowHeight) { mActivity = activity; mAppMenu = appMenu; mItemRowHeight = itemRowHeight; Resources res = mActivity.getResources(); mAutoScrollFullVelocity = res.getDimensionPixelSize(R.dimen.auto_scroll_full_velocity); // 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)) { menuItemAction(Math.round(mLastTouchX), Math.round(mLastTouchY), ITEM_ACTION_HIGHLIGHT); } } }); mGestureSingleTapDetector = new GestureDetector(activity, new SimpleOnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent e) { mIsSingleTapUpHappened = true; return true; } }); } /** * Sets up all the internal state to prepare for menu dragging. * @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. */ void onShow(boolean startDragging) { mLastTouchX = Float.NaN; mLastTouchY = Float.NaN; mDragScrollOffset = 0.0f; mDragScrollOffsetRounded = 0; mDragScrollingVelocity = 0.0f; mIsSingleTapUpHappened = false; if (startDragging) mDragScrolling.start(); } /** * Dragging mode will be stopped by calling this function. Note that it will fall back to normal * non-dragging mode. */ void finishDragging() { menuItemAction(0, 0, ITEM_ACTION_CLEAR_HIGHLIGHT_ALL); mDragScrolling.cancel(); } /** * 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() || !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; if (eventActionMasked == MotionEvent.ACTION_CANCEL) { mAppMenu.dismiss(); return true; } if (!mIsSingleTapUpHappened) { mGestureSingleTapDetector.onTouchEvent(event); if (mIsSingleTapUpHappened) { finishDragging(); } } // After this line, drag scrolling is happening. if (!mDragScrolling.isRunning()) return false; boolean didPerformClick = false; int itemAction = ITEM_ACTION_CLEAR_HIGHLIGHT_ALL; 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, mItemRowHeight * 1.2f / listView.getHeight()); float normalizedY = (rawY - getScreenVisibleRect(listView).top) / listView.getHeight(); if (normalizedY < autoScrollAreaRatio) { // Top mDragScrollingVelocity = (normalizedY / autoScrollAreaRatio - 1.0f) * mAutoScrollFullVelocity; } else if (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 = mAppMenu.getPopup().getListView(); ArrayList itemViews = new ArrayList(); for (int i = 0; i < listView.getChildCount(); ++i) { boolean hasImageButtons = false; if (listView.getChildAt(i) instanceof LinearLayout) { LinearLayout layout = (LinearLayout) listView.getChildAt(i); for (int j = 0; j < layout.getChildCount(); ++j) { itemViews.add(layout.getChildAt(j)); if (layout.getChildAt(j) instanceof ImageButton) hasImageButtons = true; } } if (!hasImageButtons) itemViews.add(listView.getChildAt(i)); } boolean didPerformClick = false; for (int i = 0; i < itemViews.size(); ++i) { View itemView = itemViews.get(i); 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) { itemView.performClick(); didPerformClick = true; } break; case ITEM_ACTION_CLEAR_HIGHLIGHT_ALL: itemView.setPressed(false); break; default: assert false; break; } } return didPerformClick; } /** * @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; } }