Merge "Move forwarding code to ListPopupWindow, add drag-to-open in Spinner" into klp-dev

This commit is contained in:
Alan Viverette
2013-08-21 17:56:00 +00:00
committed by Android (Google) Code Review
5 changed files with 208 additions and 170 deletions

View File

@ -33,6 +33,7 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnTouchListener;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.AccelerateDecelerateInterpolator;
@ -960,33 +961,6 @@ 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>
@ -1154,6 +1128,147 @@ public class ListPopupWindow {
return listContent + otherHeights;
}
/**
* Abstract class that forwards touch events to a {@link ListPopupWindow}.
*
* @hide
*/
public static abstract class ForwardingListener implements View.OnTouchListener {
/** Scaled touch slop, used for detecting movement outside bounds. */
private final float mScaledTouchSlop;
/** Whether this listener is currently forwarding touch events. */
private boolean mForwarding;
/** The id of the first pointer down in the current event stream. */
private int mActivePointerId;
public ForwardingListener(Context context) {
mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
/**
* Returns the popup to which this listener is forwarding events.
* <p>
* Override this to return the correct popup. If the popup is displayed
* asynchronously, you may also need to override
* {@link #onForwardingStopped} to prevent premature cancelation of
* forwarding.
*
* @return the popup to which this listener is forwarding events
*/
public abstract ListPopupWindow getPopup();
@Override
public boolean onTouch(View v, MotionEvent event) {
final boolean wasForwarding = mForwarding;
final boolean forwarding;
if (wasForwarding) {
forwarding = onTouchForwarded(v, event) || !onForwardingStopped();
} else {
forwarding = onTouchObserved(v, event) && onForwardingStarted();
}
mForwarding = forwarding;
return forwarding || wasForwarding;
}
/**
* Called when forwarding would like to start.
* <p>
* By default, this will show the popup returned by {@link #getPopup()}.
* It may be overridden to perform another action, like clicking the
* source view or preparing the popup before showing it.
*
* @return true to start forwarding, false otherwise
*/
public boolean onForwardingStarted() {
final ListPopupWindow popup = getPopup();
if (popup != null && !popup.isShowing()) {
popup.show();
}
return true;
}
/**
* Called when forwarding would like to stop.
* <p>
* By default, this will dismiss the popup returned by
* {@link #getPopup()}. It may be overridden to perform some other
* action.
*
* @return true to stop forwarding, false otherwise
*/
public boolean onForwardingStopped() {
final ListPopupWindow popup = getPopup();
if (popup != null && popup.isShowing()) {
popup.dismiss();
}
return true;
}
/**
* Observes motion events and determines when to start forwarding.
*
* @param src view from which the event originated
* @param srcEvent motion event in source view coordinates
* @return true to start forwarding motion events, false otherwise
*/
private boolean onTouchObserved(View src, MotionEvent srcEvent) {
if (!src.isEnabled()) {
return false;
}
// The first pointer down is always the active pointer.
final int actionMasked = srcEvent.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mActivePointerId = srcEvent.getPointerId(0);
}
final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
if (activePointerIndex >= 0) {
final float x = srcEvent.getX(activePointerIndex);
final float y = srcEvent.getY(activePointerIndex);
if (!src.pointInView(x, y, mScaledTouchSlop)) {
// The pointer has moved outside of the view.
return true;
}
}
return false;
}
/**
* Handled forwarded motion events and determines when to stop
* forwarding.
*
* @param src view from which the event originated
* @param srcEvent motion event in source view coordinates
* @return true to continue forwarding motion events, false to cancel
*/
private boolean onTouchForwarded(View src, MotionEvent srcEvent) {
final ListPopupWindow popup = getPopup();
if (popup == null || !popup.isShowing()) {
return false;
}
final DropDownListView dst = popup.mDropDownList;
if (dst == null || !dst.isShown()) {
return false;
}
// Convert event to destination-local coordinates.
final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
src.toGlobalMotionEvent(dstEvent);
dst.toLocalMotionEvent(dstEvent);
// Forward converted event to destination view, then recycle it.
final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId);
dstEvent.recycle();
return handled;
}
}
/**
* <p>Wrapper class for a ListView. This wrapper can hijack the focus to
* make sure the list uses the appropriate drawables and states when

View File

@ -30,12 +30,14 @@ import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ListPopupWindow.ForwardingListener;
import android.widget.PopupWindow.OnDismissListener;
@ -76,7 +78,10 @@ public class Spinner extends AbsSpinner implements OnClickListener {
* Use the theme-supplied value to select the dropdown mode.
*/
private static final int MODE_THEME = -1;
/** Forwarding listener used to implement drag-to-open. */
private ForwardingListener mForwardingListener;
private SpinnerPopup mPopup;
private DropDownAdapter mTempAdapter;
int mDropDownWidth;
@ -173,7 +178,7 @@ public class Spinner extends AbsSpinner implements OnClickListener {
}
case MODE_DROPDOWN: {
DropdownPopup popup = new DropdownPopup(context, attrs, defStyle);
final DropdownPopup popup = new DropdownPopup(context, attrs, defStyle);
mDropDownWidth = a.getLayoutDimension(
com.android.internal.R.styleable.Spinner_dropDownWidth,
@ -193,6 +198,20 @@ public class Spinner extends AbsSpinner implements OnClickListener {
}
mPopup = popup;
mForwardingListener = new ForwardingListener(context) {
@Override
public ListPopupWindow getPopup() {
return popup;
}
@Override
public boolean onForwardingStarted() {
if (!mPopup.isShowing()) {
mPopup.show(getTextDirection(), getTextAlignment());
}
return true;
}
};
break;
}
}
@ -448,6 +467,15 @@ public class Spinner extends AbsSpinner implements OnClickListener {
super.setOnItemClickListener(l);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mForwardingListener != null && mForwardingListener.onTouch(this, event)) {
return true;
}
return super.onTouchEvent(event);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

View File

@ -32,6 +32,8 @@ import android.view.View.MeasureSpec;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ListPopupWindow;
import android.widget.ListPopupWindow.ForwardingListener;
import com.android.internal.view.ActionBarPolicy;
import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView;
@ -562,7 +564,36 @@ public class ActionMenuPresenter extends BaseMenuPresenter
setFocusable(true);
setVisibility(VISIBLE);
setEnabled(true);
setOnTouchListener(new OverflowForwardListener(context));
setOnTouchListener(new ForwardingListener(context) {
@Override
public ListPopupWindow getPopup() {
if (mOverflowPopup == null) {
return null;
}
return mOverflowPopup.getPopup();
}
@Override
public boolean onForwardingStarted() {
showOverflowMenu();
return true;
}
@Override
public boolean onForwardingStopped() {
// Displaying the popup occurs asynchronously, so wait for
// the runnable to finish before deciding whether to stop
// forwarding.
if (mPostedOpenRunnable != null) {
return false;
}
hideOverflowMenu();
return true;
}
});
}
@Override
@ -687,56 +718,4 @@ public class ActionMenuPresenter extends BaseMenuPresenter
mPostedOpenRunnable = null;
}
}
private class OverflowForwardListener extends TouchForwardingListener {
/** Scaled touch slop, used for detecting movement outside bounds. */
private final float mScaledTouchSlop;
private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
public OverflowForwardListener(Context context) {
mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onTouchObserved(View src, MotionEvent srcEvent) {
if (!src.isEnabled()) {
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 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;
}
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();
}
// Cancel forwarding.
return false;
}
}
}

View File

@ -108,6 +108,10 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On
}
}
public ListPopupWindow getPopup() {
return mPopup;
}
public boolean tryShow() {
mPopup = new ListPopupWindow(mContext, null, com.android.internal.R.attr.popupMenuStyle);
mPopup.setOnDismissListener(this);
@ -159,22 +163,6 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On
return mPopup != null && mPopup.isShowing();
}
/**
* 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;
}
return mPopup.onForwardedEvent(src, event, activePointerId);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
MenuAdapter adapter = mAdapter;

View File

@ -1,72 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.view.menu;
import android.view.MotionEvent;
import android.view.View;
/**
* Touch listener used to intercept touches and forward them out of a view.
*/
abstract class TouchForwardingListener implements View.OnTouchListener {
/** Whether this listener is currently forwarding touch events. */
private boolean mForwarding;
@Override
public boolean onTouch(View v, MotionEvent ev) {
final int actionMasked = ev.getActionMasked();
if (mForwarding) {
// Rejecting the event or ending the stream stops forwarding.
if (!onTouchForwarded(v, ev) || actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_CANCEL) {
stopForwarding();
}
} else {
if (onTouchObserved(v, ev)) {
startForwarding();
}
}
return mForwarding;
}
public void startForwarding() {
mForwarding = true;
}
public void stopForwarding() {
mForwarding = false;
}
/**
* Attempts to start forwarding motion events.
*
* @param v The view that triggered forwarding.
* @return True to start forwarding motion events, or false to cancel.
*/
public abstract boolean onTouchObserved(View v, MotionEvent ev);
/**
* Handles forwarded motion events.
*
* @param v The view from which the event was forwarded.
* @param ev The forwarded motion event.
* @return True to continue forwarding motion events, or false to cancel.
*/
public abstract boolean onTouchForwarded(View v, MotionEvent ev);
}