• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.appmenu;
6 
7 import android.animation.TimeAnimator;
8 import android.annotation.SuppressLint;
9 import android.app.Activity;
10 import android.content.res.Resources;
11 import android.graphics.Rect;
12 import android.view.MotionEvent;
13 import android.view.View;
14 import android.widget.ImageButton;
15 import android.widget.LinearLayout;
16 import android.widget.ListPopupWindow;
17 import android.widget.ListView;
18 
19 import org.chromium.chrome.R;
20 
21 import java.util.ArrayList;
22 
23 /**
24  * Handles the drag touch events on AppMenu that start from the menu button.
25  *
26  * Lint suppression for NewApi is added because we are using TimeAnimator class that was marked
27  * hidden in API 16.
28  */
29 @SuppressLint("NewApi")
30 class AppMenuDragHelper {
31     private final Activity mActivity;
32     private final AppMenu mAppMenu;
33 
34     // Internally used action constants for dragging.
35     private static final int ITEM_ACTION_HIGHLIGHT = 0;
36     private static final int ITEM_ACTION_PERFORM = 1;
37     private static final int ITEM_ACTION_CLEAR_HIGHLIGHT_ALL = 2;
38 
39     private static final float AUTO_SCROLL_AREA_MAX_RATIO = 0.25f;
40 
41     // Dragging related variables, i.e., menu showing initiated by touch down and drag to navigate.
42     private final float mAutoScrollFullVelocity;
43     private final TimeAnimator mDragScrolling = new TimeAnimator();
44     private float mDragScrollOffset;
45     private int mDragScrollOffsetRounded;
46     private volatile float mDragScrollingVelocity;
47     private volatile float mLastTouchX;
48     private volatile float mLastTouchY;
49     private final int mItemRowHeight;
50 
51     // These are used in a function locally, but defined here to avoid heap allocation on every
52     // touch event.
53     private final Rect mScreenVisibleRect = new Rect();
54     private final int[] mScreenVisiblePoint = new int[2];
55 
AppMenuDragHelper(Activity activity, AppMenu appMenu, int itemRowHeight)56     AppMenuDragHelper(Activity activity, AppMenu appMenu, int itemRowHeight) {
57         mActivity = activity;
58         mAppMenu = appMenu;
59         mItemRowHeight = itemRowHeight;
60         Resources res = mActivity.getResources();
61         mAutoScrollFullVelocity = res.getDimensionPixelSize(R.dimen.auto_scroll_full_velocity);
62         // If user is dragging and the popup ListView is too big to display at once,
63         // mDragScrolling animator scrolls mPopup.getListView() automatically depending on
64         // the user's touch position.
65         mDragScrolling.setTimeListener(new TimeAnimator.TimeListener() {
66             @Override
67             public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
68                 ListPopupWindow popup = mAppMenu.getPopup();
69                 if (popup == null || popup.getListView() == null) return;
70 
71                 // We keep both mDragScrollOffset and mDragScrollOffsetRounded because
72                 // the actual scrolling is by the rounded value but at the same time we also
73                 // want to keep the precise scroll value in float.
74                 mDragScrollOffset += (deltaTime * 0.001f) * mDragScrollingVelocity;
75                 int diff = Math.round(mDragScrollOffset - mDragScrollOffsetRounded);
76                 mDragScrollOffsetRounded += diff;
77                 popup.getListView().smoothScrollBy(diff, 0);
78 
79                 // Force touch move event to highlight items correctly for the scrolled position.
80                 if (!Float.isNaN(mLastTouchX) && !Float.isNaN(mLastTouchY)) {
81                     menuItemAction(Math.round(mLastTouchX), Math.round(mLastTouchY),
82                             ITEM_ACTION_HIGHLIGHT);
83                 }
84             }
85         });
86     }
87 
88     /**
89      * Sets up all the internal state to prepare for menu dragging.
90      * @param startDragging      Whether dragging is started. For example, if the app menu
91      *                           is showed by tapping on a button, this should be false. If it is
92      *                           showed by start dragging down on the menu button, this should be
93      *                           true.
94      */
onShow(boolean startDragging)95     void onShow(boolean startDragging) {
96         mLastTouchX = Float.NaN;
97         mLastTouchY = Float.NaN;
98         mDragScrollOffset = 0.0f;
99         mDragScrollOffsetRounded = 0;
100         mDragScrollingVelocity = 0.0f;
101 
102         if (startDragging) mDragScrolling.start();
103     }
104 
onDismiss()105     void onDismiss() {
106         mDragScrolling.cancel();
107     }
108 
109     /**
110      * Gets all the touch events and updates dragging related logic. Note that if this app menu
111      * is initiated by software UI control, then the control should set onTouchListener and forward
112      * all the events to this method because the initial UI control that processed ACTION_DOWN will
113      * continue to get all the subsequent events.
114      *
115      * @param event Touch event to be processed.
116      * @return Whether the event is handled.
117      */
handleDragging(MotionEvent event)118     boolean handleDragging(MotionEvent event) {
119         if (!mAppMenu.isShowing() || !mDragScrolling.isRunning()) return false;
120 
121         // We will only use the screen space coordinate (rawX, rawY) to reduce confusion.
122         // This code works across many different controls, so using local coordinates will be
123         // a disaster.
124 
125         final float rawX = event.getRawX();
126         final float rawY = event.getRawY();
127         final int roundedRawX = Math.round(rawX);
128         final int roundedRawY = Math.round(rawY);
129         final int eventActionMasked = event.getActionMasked();
130         final ListView listView = mAppMenu.getPopup().getListView();
131 
132         mLastTouchX = rawX;
133         mLastTouchY = rawY;
134 
135         if (eventActionMasked == MotionEvent.ACTION_CANCEL) {
136             mAppMenu.dismiss();
137             return true;
138         }
139 
140         // After this line, drag scrolling is happening.
141         if (!mDragScrolling.isRunning()) return false;
142 
143         boolean didPerformClick = false;
144         int itemAction = ITEM_ACTION_CLEAR_HIGHLIGHT_ALL;
145         switch (eventActionMasked) {
146             case MotionEvent.ACTION_DOWN:
147             case MotionEvent.ACTION_MOVE:
148                 itemAction = ITEM_ACTION_HIGHLIGHT;
149                 break;
150             case MotionEvent.ACTION_UP:
151                 itemAction = ITEM_ACTION_PERFORM;
152                 break;
153             default:
154                 break;
155         }
156         didPerformClick = menuItemAction(roundedRawX, roundedRawY, itemAction);
157 
158         if (eventActionMasked == MotionEvent.ACTION_UP && !didPerformClick) {
159             mAppMenu.dismiss();
160         } else if (eventActionMasked == MotionEvent.ACTION_MOVE) {
161             // Auto scrolling on the top or the bottom of the listView.
162             if (listView.getHeight() > 0) {
163                 float autoScrollAreaRatio = Math.min(AUTO_SCROLL_AREA_MAX_RATIO,
164                         mItemRowHeight * 1.2f / listView.getHeight());
165                 float normalizedY =
166                         (rawY - getScreenVisibleRect(listView).top) / listView.getHeight();
167                 if (normalizedY < autoScrollAreaRatio) {
168                     // Top
169                     mDragScrollingVelocity = (normalizedY / autoScrollAreaRatio - 1.0f)
170                             * mAutoScrollFullVelocity;
171                 } else if (normalizedY > 1.0f - autoScrollAreaRatio) {
172                     // Bottom
173                     mDragScrollingVelocity = ((normalizedY - 1.0f) / autoScrollAreaRatio + 1.0f)
174                             * mAutoScrollFullVelocity;
175                 } else {
176                     // Middle or not scrollable.
177                     mDragScrollingVelocity = 0.0f;
178                 }
179             }
180         }
181 
182         return true;
183     }
184 
185     /**
186      * Performs the specified action on the menu item specified by the screen coordinate position.
187      * @param screenX X in screen space coordinate.
188      * @param screenY Y in screen space coordinate.
189      * @param action  Action type to perform, it should be one of ITEM_ACTION_* constants.
190      * @return true whether or not a menu item is performed (executed).
191      */
menuItemAction(int screenX, int screenY, int action)192     private boolean menuItemAction(int screenX, int screenY, int action) {
193         ListView listView = mAppMenu.getPopup().getListView();
194 
195         ArrayList<View> itemViews = new ArrayList<View>();
196         for (int i = 0; i < listView.getChildCount(); ++i) {
197             boolean hasImageButtons = false;
198             if (listView.getChildAt(i) instanceof LinearLayout) {
199                 LinearLayout layout = (LinearLayout) listView.getChildAt(i);
200                 for (int j = 0; j < layout.getChildCount(); ++j) {
201                     itemViews.add(layout.getChildAt(j));
202                     if (layout.getChildAt(j) instanceof ImageButton) hasImageButtons = true;
203                 }
204             }
205             if (!hasImageButtons) itemViews.add(listView.getChildAt(i));
206         }
207 
208         boolean didPerformClick = false;
209         for (int i = 0; i < itemViews.size(); ++i) {
210             View itemView = itemViews.get(i);
211 
212             boolean shouldPerform = itemView.isEnabled() && itemView.isShown() &&
213                     getScreenVisibleRect(itemView).contains(screenX, screenY);
214 
215             switch (action) {
216                 case ITEM_ACTION_HIGHLIGHT:
217                     itemView.setPressed(shouldPerform);
218                     break;
219                 case ITEM_ACTION_PERFORM:
220                     if (shouldPerform) {
221                         itemView.performClick();
222                         didPerformClick = true;
223                     }
224                     break;
225                 case ITEM_ACTION_CLEAR_HIGHLIGHT_ALL:
226                     itemView.setPressed(false);
227                     break;
228                 default:
229                     assert false;
230                     break;
231             }
232         }
233         return didPerformClick;
234     }
235 
236     /**
237      * @return Visible rect in screen coordinates for the given View.
238      */
getScreenVisibleRect(View view)239     private Rect getScreenVisibleRect(View view) {
240         view.getLocalVisibleRect(mScreenVisibleRect);
241         view.getLocationOnScreen(mScreenVisiblePoint);
242         mScreenVisibleRect.offset(mScreenVisiblePoint[0], mScreenVisiblePoint[1]);
243         return mScreenVisibleRect;
244     }
245 }
246