From c05027214f1f4dda67296a072dfc9af9176dc590 Mon Sep 17 00:00:00 2001 From: Alan Viverette Date: Thu, 15 Aug 2013 18:05:52 -0700 Subject: [PATCH] Forward events to ListPopupWindow, highlight touched items Moves most of the drag-to-open behavior into ListPopupWindow's particular implementation of ListView. Uses hidden View API for forwarding events between different windows. Overflow menu opens on first touch, closes on touch end outside the overflow button. Clicks that occur during drag-to-open mode result in alpha animation of the selector drawable. BUG: 9437139 Change-Id: I70f540555a03450638a27880b3ae3b031ca6e2ed --- core/java/android/widget/ListPopupWindow.java | 172 +++++++++++++++++++++ .../internal/view/menu/ActionMenuPresenter.java | 45 +++--- .../internal/view/menu/MenuPopupHelper.java | 70 ++------- 3 files changed, 209 insertions(+), 78 deletions(-) diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java index 414c3187519c..2b4e5206aa47 100644 --- a/core/java/android/widget/ListPopupWindow.java +++ b/core/java/android/widget/ListPopupWindow.java @@ -16,6 +16,9 @@ package android.widget; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; import android.content.Context; import android.database.DataSetObserver; import android.graphics.Rect; @@ -23,6 +26,7 @@ import android.graphics.drawable.Drawable; import android.os.Handler; import android.text.TextUtils; import android.util.AttributeSet; +import android.util.IntProperty; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; @@ -31,6 +35,7 @@ import android.view.View.MeasureSpec; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.animation.AccelerateDecelerateInterpolator; import java.util.Locale; @@ -956,6 +961,33 @@ public class ListPopupWindow { } /** + * Receives motion events forwarded from a source view. This is used + * internally to implement support for drag-to-open. + * + * @param src view from which the event was forwarded + * @param srcEvent forwarded motion event in source-local coordinates + * @param activePointerId id of the pointer that activated forwarding + * @return whether the event was handled + * @hide + */ + public boolean onForwardedEvent(View src, MotionEvent srcEvent, int activePointerId) { + final DropDownListView dst = mDropDownList; + if (dst == null || !dst.isShown()) { + return false; + } + + // Convert event to local coordinates. + final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent); + src.toGlobalMotionEvent(dstEvent); + dst.toLocalMotionEvent(dstEvent); + + // Forward converted event, then recycle it. + final boolean handled = dst.onForwardedEvent(dstEvent, activePointerId); + dstEvent.recycle(); + return handled; + } + + /** *

Builds the popup window's content and returns the height the popup * should have. Returns -1 when the content already exists.

* @@ -1130,6 +1162,27 @@ public class ListPopupWindow { */ private static class DropDownListView extends ListView { private static final String TAG = ListPopupWindow.TAG + ".DropDownListView"; + + /** Duration in milliseconds of the drag-to-open click animation. */ + private static final long CLICK_ANIM_DURATION = 150; + + /** Target alpha value for drag-to-open click animation. */ + private static final int CLICK_ANIM_ALPHA = 0x80; + + /** Wrapper around Drawable's alpha property. */ + private static final IntProperty DRAWABLE_ALPHA = + new IntProperty("alpha") { + @Override + public void setValue(Drawable object, int value) { + object.setAlpha(value); + } + + @Override + public Integer get(Drawable object) { + return object.getAlpha(); + } + }; + /* * WARNING: This is a workaround for a touch mode issue. * @@ -1165,6 +1218,12 @@ public class ListPopupWindow { */ private boolean mHijackFocus; + /** Whether to force drawing of the pressed state selector. */ + private boolean mDrawsInPressedState; + + /** Current drag-to-open click animation, if any. */ + private Animator mClickAnimation; + /** *

Creates a new list view wrapper.

* @@ -1178,6 +1237,119 @@ public class ListPopupWindow { } /** + * Handles forwarded events. + * + * @param activePointerId id of the pointer that activated forwarding + * @return whether the event was handled + */ + public boolean onForwardedEvent(MotionEvent event, int activePointerId) { + boolean handledEvent = true; + boolean clearPressedItem = false; + + final int actionMasked = event.getActionMasked(); + switch (actionMasked) { + case MotionEvent.ACTION_CANCEL: + handledEvent = false; + break; + case MotionEvent.ACTION_UP: + handledEvent = false; + // $FALL-THROUGH$ + case MotionEvent.ACTION_MOVE: + final int activeIndex = event.findPointerIndex(activePointerId); + if (activeIndex < 0) { + handledEvent = false; + break; + } + + final int x = (int) event.getX(activeIndex); + final int y = (int) event.getY(activeIndex); + final int position = pointToPosition(x, y); + if (position == INVALID_POSITION) { + clearPressedItem = true; + break; + } + + final View child = getChildAt(position - getFirstVisiblePosition()); + setPressedItem(child, position); + handledEvent = true; + + if (actionMasked == MotionEvent.ACTION_UP) { + clickPressedItem(child, position); + } + break; + } + + // Failure to handle the event cancels forwarding. + if (!handledEvent || clearPressedItem) { + clearPressedItem(); + } + + return handledEvent; + } + + /** + * Starts an alpha animation on the selector. When the animation ends, + * the list performs a click on the item. + */ + private void clickPressedItem(final View child, final int position) { + final long id = getItemIdAtPosition(position); + final Animator anim = ObjectAnimator.ofInt( + mSelector, DRAWABLE_ALPHA, 0xFF, CLICK_ANIM_ALPHA, 0xFF); + anim.setDuration(CLICK_ANIM_DURATION); + anim.setInterpolator(new AccelerateDecelerateInterpolator()); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + performItemClick(child, position, id); + } + }); + anim.start(); + + if (mClickAnimation != null) { + mClickAnimation.cancel(); + } + mClickAnimation = anim; + } + + private void clearPressedItem() { + mDrawsInPressedState = false; + setPressed(false); + updateSelectorState(); + + if (mClickAnimation != null) { + mClickAnimation.cancel(); + mClickAnimation = null; + } + } + + private void setPressedItem(View child, int position) { + mDrawsInPressedState = true; + + // Ordering is essential. First update the pressed state and layout + // the children. This will ensure the selector actually gets drawn. + setPressed(true); + layoutChildren(); + + // Ensure that keyboard focus starts from the last touched position. + setSelectedPositionInt(position); + positionSelector(position, child); + + // Refresh the drawable state to reflect the new pressed state, + // which will also update the selector state. + refreshDrawableState(); + + if (mClickAnimation != null) { + mClickAnimation.cancel(); + mClickAnimation = null; + } + } + + @Override + boolean touchModeDrawsInPressedState() { + return mDrawsInPressedState || super.touchModeDrawsInPressedState(); + } + + /** *

Avoids jarring scrolling effect by ensuring that list elements * made of a text view fit on a single line.

* diff --git a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java index ff9678cf03bd..863d8ccffe12 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java +++ b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java @@ -30,9 +30,7 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.View.MeasureSpec; import android.view.ViewGroup; -import android.widget.AbsListView; import android.widget.ImageButton; -import android.widget.ListPopupWindow; import com.android.internal.view.ActionBarPolicy; import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView; @@ -694,32 +692,43 @@ public class ActionMenuPresenter extends BaseMenuPresenter } @Override - public boolean onTouchObserved(View v, MotionEvent ev) { - if (ev.getActionMasked() == MotionEvent.ACTION_MOVE && v.isEnabled() - && !v.pointInView(ev.getX(), ev.getY(), mScaledTouchSlop)) { - mActivePointerId = ev.getPointerId(0); - v.performClick(); - return true; + public boolean onTouchObserved(View src, MotionEvent srcEvent) { + if (!src.isEnabled()) { + return false; } - return false; + // Always start forwarding events when the source view is touched. + mActivePointerId = srcEvent.getPointerId(0); + src.performClick(); + return true; } @Override - public boolean onTouchForwarded(View v, MotionEvent ev) { - if (!v.isEnabled() || mOverflowPopup == null || !mOverflowPopup.isShowing()) { - return false; - } - - if (mActivePointerId != MotionEvent.INVALID_POINTER_ID) { - if (mOverflowPopup.forwardMotionEvent(v, ev, mActivePointerId)) { + public boolean onTouchForwarded(View src, MotionEvent srcEvent) { + final OverflowPopup popup = mOverflowPopup; + if (popup != null && popup.isShowing()) { + final int activePointerId = mActivePointerId; + if (activePointerId != MotionEvent.INVALID_POINTER_ID && src.isEnabled() + && popup.forwardMotionEvent(src, srcEvent, activePointerId)) { + // Handled the motion event, continue forwarding. return true; } - mActivePointerId = MotionEvent.INVALID_POINTER_ID; + final int activePointerIndex = srcEvent.findPointerIndex(activePointerId); + if (activePointerIndex >= 0) { + final float x = srcEvent.getX(activePointerIndex); + final float y = srcEvent.getY(activePointerIndex); + if (src.pointInView(x, y, mScaledTouchSlop)) { + // The user is touching the source view. Cancel + // forwarding, but don't dismiss the popup. + return false; + } + } + + popup.dismiss(); } - mOverflowPopup.dismiss(); + // Cancel forwarding. return false; } } diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java index 945f42b9612e..9b266dfb3ca7 100644 --- a/core/java/com/android/internal/view/menu/MenuPopupHelper.java +++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java @@ -27,7 +27,6 @@ import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.view.ViewTreeObserver; -import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.FrameLayout; @@ -48,8 +47,6 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On static final int ITEM_LAYOUT = com.android.internal.R.layout.popup_menu_item_layout; - private final int[] mTempLocation = new int[2]; - private final Context mContext; private final LayoutInflater mInflater; private final MenuBuilder mMenu; @@ -162,67 +159,20 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On return mPopup != null && mPopup.isShowing(); } - public boolean forwardMotionEvent(View v, MotionEvent ev, int activePointerId) { + /** + * Forwards motion events from a source view to the popup window. + * + * @param src view from which the event was forwarded + * @param event forwarded motion event in source-local coordinates + * @param activePointerId id of the pointer that activated forwarding + * @return whether the event was handled + */ + public boolean forwardMotionEvent(View src, MotionEvent event, int activePointerId) { if (mPopup == null || !mPopup.isShowing()) { return false; } - final AbsListView dstView = mPopup.getListView(); - if (dstView == null || !dstView.isShown()) { - return false; - } - - boolean cancelForwarding = false; - final int actionMasked = ev.getActionMasked(); - switch (actionMasked) { - case MotionEvent.ACTION_CANCEL: - cancelForwarding = true; - break; - case MotionEvent.ACTION_UP: - cancelForwarding = true; - // $FALL-THROUGH$ - case MotionEvent.ACTION_MOVE: - final int activeIndex = ev.findPointerIndex(activePointerId); - if (activeIndex < 0) { - return false; - } - - final int[] location = mTempLocation; - int x = (int) ev.getX(activeIndex); - int y = (int) ev.getY(activeIndex); - - // Convert to global coordinates. - v.getLocationOnScreen(location); - x += location[0]; - y += location[1]; - - // Convert to local coordinates. - dstView.getLocationOnScreen(location); - x -= location[0]; - y -= location[1]; - - final int position = dstView.pointToPosition(x, y); - if (position >= 0) { - final int childCount = dstView.getChildCount(); - final int firstVisiblePosition = dstView.getFirstVisiblePosition(); - final int index = position - firstVisiblePosition; - if (index < childCount) { - final View child = dstView.getChildAt(index); - if (actionMasked == MotionEvent.ACTION_UP) { - // Touch ended, click highlighted item. - final long id = dstView.getItemIdAtPosition(position); - dstView.performItemClick(child, position, id); - } else if (actionMasked == MotionEvent.ACTION_MOVE) { - // TODO: Highlight touched item, activate after - // long-hover. Consider forwarding events as HOVER and - // letting ListView handle this. - } - } - } - break; - } - - return true; + return mPopup.onForwardedEvent(src, event, activePointerId); } @Override -- 2.11.0