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