Merge "Forward events to ListPopupWindow, highlight touched items" into klp-dev

This commit is contained in:
Alan Viverette
2013-08-16 17:56:42 +00:00
committed by Android (Google) Code Review
3 changed files with 211 additions and 80 deletions

View File

@ -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;
@ -955,6 +960,33 @@ public class ListPopupWindow {
return false;
}
/**
* 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;
}
/**
* <p>Builds the popup window's content and returns the height the popup
* should have. Returns -1 when the content already exists.</p>
@ -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 <code>alpha</code> property. */
private static final IntProperty<Drawable> DRAWABLE_ALPHA =
new IntProperty<Drawable>("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;
/**
* <p>Creates a new list view wrapper.</p>
*
@ -1177,6 +1236,119 @@ public class ListPopupWindow {
setCacheColorHint(0); // Transparent, since the background drawable could be anything.
}
/**
* 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();
}
/**
* <p>Avoids jarring scrolling effect by ensuring that list elements
* made of a text view fit on a single line.</p>

View File

@ -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;
}
return false;
}
@Override
public boolean onTouchForwarded(View v, MotionEvent ev) {
if (!v.isEnabled() || mOverflowPopup == null || !mOverflowPopup.isShowing()) {
public boolean onTouchObserved(View src, MotionEvent srcEvent) {
if (!src.isEnabled()) {
return false;
}
if (mActivePointerId != MotionEvent.INVALID_POINTER_ID) {
if (mOverflowPopup.forwardMotionEvent(v, ev, mActivePointerId)) {
// Always start forwarding events when the source view is touched.
mActivePointerId = srcEvent.getPointerId(0);
src.performClick();
return true;
}
@Override
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;
}
}

View File

@ -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