Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f44d90b5 authored by Oren Blasberg's avatar Oren Blasberg
Browse files

Pull out widget helpers into their own classes.

This is in service of desktop-style submenus. Follow up CLs will make
use of the newly independent classes. MOST of the changes in this CL
are simply moving code from one place to another. Below are
descriptions of the new files added:

DropDownListView:
Decouple this from ListPopupWindow, so other "menu popup"
implementations [forthcoming] may make use of it.
This class is UNCHANGED except for the addition of the
setListSelectionHidden method.

ForwardingListener:
Decouple this from ListPopupWindow, so that other clients of this
listener can expect a more general ShowableListMenu rather than a
concrete ListPopupWindow.  This will be useful later when we want to
use something other than ListPopupWindow to show a menu.
This class is UNCHANGED, except for (1) using the getListView() on
popup rather than accessing its mDropDownList member, and (2)
replacing ListPopupWindow return values with ShowableListMenu.

MenuAdapter:
Decouple from MenuPopupHelper so it may be used by forthcoming "menu
popup" implementations rather than just by MenuPopupHelper itself.
This class is UNCHANGED.

ShowableListMenu:
A new interface to encapsulate a menu that can be shown or hidden.
This will be implemented by both ListPopupWindow and a forthcoming
"menu popup" class.

Bug: 20127825
Change-Id: I565b444d25e966ff8b8c6ceac7be8de56a9116df
parent 6bfab4e4
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -37,7 +37,6 @@ import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ListPopupWindow.ForwardingListener;
import com.android.internal.view.ActionBarPolicy;
import com.android.internal.view.menu.ActionMenuItemView;
import com.android.internal.view.menu.BaseMenuPresenter;
@@ -45,6 +44,7 @@ import com.android.internal.view.menu.MenuBuilder;
import com.android.internal.view.menu.MenuItemImpl;
import com.android.internal.view.menu.MenuPopupHelper;
import com.android.internal.view.menu.MenuView;
import com.android.internal.view.menu.ShowableListMenu;
import com.android.internal.view.menu.SubMenuBuilder;

import java.util.ArrayList;
@@ -828,7 +828,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter

            setOnTouchListener(new ForwardingListener(this) {
                @Override
                public ListPopupWindow getPopup() {
                public ShowableListMenu getPopup() {
                    if (mOverflowPopup == null) {
                        return null;
                    }
@@ -1003,7 +1003,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter

    private class ActionMenuPopupCallback extends ActionMenuItemView.PopupCallback {
        @Override
        public ListPopupWindow getPopup() {
        public ShowableListMenu getPopup() {
            return mActionButtonPopup != null ? mActionButtonPopup.getPopup() : null;
        }
    }
+2 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package android.widget;

import com.android.internal.R;
import com.android.internal.view.menu.ShowableListMenu;

import android.annotation.StringRes;
import android.content.Context;
@@ -37,7 +38,6 @@ import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ActivityChooserModel.ActivityChooserModelClient;
import android.widget.ListPopupWindow.ForwardingListener;

/**
 * This class is a view for choosing an activity for handling a given {@link Intent}.
@@ -263,7 +263,7 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod
        });
        expandButton.setOnTouchListener(new ForwardingListener(expandButton) {
            @Override
            public ListPopupWindow getPopup() {
            public ShowableListMenu getPopup() {
                return getListPopupWindow();
            }

+344 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 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 android.widget;


import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.IntProperty;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.TextView;
import android.widget.ListView;


/**
 * Wrapper class for a ListView. This wrapper can hijack the focus to
 * make sure the list uses the appropriate drawables and states when
 * displayed on screen within a drop down. The focus is never actually
 * passed to the drop down in this mode; the list only looks focused.
 *
 * @hide
 */
public class DropDownListView extends ListView {
    /** 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.
     *
     * Touch mode is propagated lazily to windows. This causes problems in
     * the following scenario:
     * - Type something in the AutoCompleteTextView and get some results
     * - Move down with the d-pad to select an item in the list
     * - Move up with the d-pad until the selection disappears
     * - Type more text in the AutoCompleteTextView *using the soft keyboard*
     *   and get new results; you are now in touch mode
     * - The selection comes back on the first item in the list, even though
     *   the list is supposed to be in touch mode
     *
     * Using the soft keyboard triggers the touch mode change but that change
     * is propagated to our window only after the first list layout, therefore
     * after the list attempts to resurrect the selection.
     *
     * The trick to work around this issue is to pretend the list is in touch
     * mode when we know that the selection should not appear, that is when
     * we know the user moved the selection away from the list.
     *
     * This boolean is set to true whenever we explicitly hide the list's
     * selection and reset to false whenever we know the user moved the
     * selection back to the list.
     *
     * When this boolean is true, isInTouchMode() returns true, otherwise it
     * returns super.isInTouchMode().
     */
    private boolean mListSelectionHidden;

    /**
     * True if this wrapper should fake focus.
     */
    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;

    /** Helper for drag-to-open auto scrolling. */
    private AbsListViewAutoScroller mScrollHelper;

    /**
     * Creates a new list view wrapper.
     *
     * @param context this view's context
     */
    public DropDownListView(Context context, boolean hijackFocus) {
        this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle);
    }

    /**
     * Creates a new list view wrapper.
     *
     * @param context this view's context
     */
    public DropDownListView(Context context, boolean hijackFocus, int defStyleAttr) {
        super(context, null, defStyleAttr);
        mHijackFocus = hijackFocus;
        // TODO: Add an API to control this
        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, x, y);
                handledEvent = true;

                if (actionMasked == MotionEvent.ACTION_UP) {
                    clickPressedItem(child, position);
                }
                break;
        }

        // Failure to handle the event cancels forwarding.
        if (!handledEvent || clearPressedItem) {
            clearPressedItem();
        }

        // Manage automatic scrolling.
        if (handledEvent) {
            if (mScrollHelper == null) {
                mScrollHelper = new AbsListViewAutoScroller(this);
            }
            mScrollHelper.setEnabled(true);
            mScrollHelper.onTouch(this, event);
        } else if (mScrollHelper != null) {
            mScrollHelper.setEnabled(false);
        }

        return handledEvent;
    }

    /**
     * Sets whether the list selection is hidden, as part of a workaround for a touch mode issue
     * (see the declaration for mListSelectionHidden).
     * @param listSelectionHidden
     */
    public void setListSelectionHidden(boolean listSelectionHidden) {
        this.mListSelectionHidden = listSelectionHidden;
    }

    /**
     * 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();

        final View motionView = getChildAt(mMotionPosition - mFirstPosition);
        if (motionView != null) {
            motionView.setPressed(false);
        }

        if (mClickAnimation != null) {
            mClickAnimation.cancel();
            mClickAnimation = null;
        }
    }

    private void setPressedItem(View child, int position, float x, float y) {
        mDrawsInPressedState = true;

        // Ordering is essential. First, update the container's pressed state.
        drawableHotspotChanged(x, y);
        if (!isPressed()) {
            setPressed(true);
        }

        // Next, run layout if we need to stabilize child positions.
        if (mDataChanged) {
            layoutChildren();
        }

        // Manage the pressed view based on motion position. This allows us to
        // play nicely with actual touch and scroll events.
        final View motionView = getChildAt(mMotionPosition - mFirstPosition);
        if (motionView != null && motionView != child && motionView.isPressed()) {
            motionView.setPressed(false);
        }
        mMotionPosition = position;

        // Offset for child coordinates.
        final float childX = x - child.getLeft();
        final float childY = y - child.getTop();
        child.drawableHotspotChanged(childX, childY);
        if (!child.isPressed()) {
            child.setPressed(true);
        }

        // Ensure that keyboard focus starts from the last touched position.
        setSelectedPositionInt(position);
        positionSelectorLikeTouch(position, child, x, y);

        // 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.
     *
     * @param position the item index in the list to get a view for
     * @return the view for the specified item
     */
    @Override
    View obtainView(int position, boolean[] isScrap) {
        View view = super.obtainView(position, isScrap);

        if (view instanceof TextView) {
            ((TextView) view).setHorizontallyScrolling(true);
        }

        return view;
    }

    @Override
    public boolean isInTouchMode() {
        // WARNING: Please read the comment where mListSelectionHidden is declared
        return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
    }

    /**
     * Returns the focus state in the drop down.
     *
     * @return true always if hijacking focus
     */
    @Override
    public boolean hasWindowFocus() {
        return mHijackFocus || super.hasWindowFocus();
    }

    /**
     * Returns the focus state in the drop down.
     *
     * @return true always if hijacking focus
     */
    @Override
    public boolean isFocused() {
        return mHijackFocus || super.isFocused();
    }

    /**
     * Returns the focus state in the drop down.
     *
     * @return true always if hijacking focus
     */
    @Override
    public boolean hasFocus() {
        return mHijackFocus || super.hasFocus();
    }
}
 No newline at end of file
+299 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 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 android.widget;

import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewParent;

import com.android.internal.view.menu.ShowableListMenu;

/**
 * Abstract class that forwards touch events to a {@link ListPopupWindow}.
 *
 * @hide
 */
public abstract class ForwardingListener
        implements View.OnTouchListener, View.OnAttachStateChangeListener {

    /** Scaled touch slop, used for detecting movement outside bounds. */
    private final float mScaledTouchSlop;

    /** Timeout before disallowing intercept on the source's parent. */
    private final int mTapTimeout;

    /** Timeout before accepting a long-press to start forwarding. */
    private final int mLongPressTimeout;

    /** Source view from which events are forwarded. */
    private final View mSrc;

    /** Runnable used to prevent conflicts with scrolling parents. */
    private Runnable mDisallowIntercept;

    /** Runnable used to trigger forwarding on long-press. */
    private Runnable mTriggerLongPress;

    /** Whether this listener is currently forwarding touch events. */
    private boolean mForwarding;

    /**
     * Whether forwarding was initiated by a long-press. If so, we won't
     * force the window to dismiss when the touch stream ends.
     */
    private boolean mWasLongPress;

    /** The id of the first pointer down in the current event stream. */
    private int mActivePointerId;

    public ForwardingListener(View src) {
        mSrc = src;
        mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop();
        mTapTimeout = ViewConfiguration.getTapTimeout();

        // Use a medium-press timeout. Halfway between tap and long-press.
        mLongPressTimeout = (mTapTimeout + ViewConfiguration.getLongPressTimeout()) / 2;

        src.addOnAttachStateChangeListener(this);
    }

    /**
     * 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 cancellation of
     * forwarding.
     *
     * @return the popup to which this listener is forwarding events
     */
    public abstract ShowableListMenu getPopup();

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        final boolean wasForwarding = mForwarding;
        final boolean forwarding;
        if (wasForwarding) {
            forwarding = onTouchForwarded(event) || !onForwardingStopped();
        } else {
            forwarding = onTouchObserved(event) && onForwardingStarted();

            if (forwarding) {
                // Make sure we cancel any ongoing source event stream.
                final long now = SystemClock.uptimeMillis();
                final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL,
                        0.0f, 0.0f, 0);
                mSrc.onTouchEvent(e);
                e.recycle();
            }
        }

        mForwarding = forwarding;
        return forwarding || wasForwarding;
    }

    @Override
    public void onViewAttachedToWindow(View v) {
    }

    @Override
    public void onViewDetachedFromWindow(View v) {
        mForwarding = false;
        mActivePointerId = MotionEvent.INVALID_POINTER_ID;

        if (mDisallowIntercept != null) {
            mSrc.removeCallbacks(mDisallowIntercept);
        }
    }

    /**
     * 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
     */
    protected boolean onForwardingStarted() {
        final ShowableListMenu 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
     */
    protected boolean onForwardingStopped() {
        final ShowableListMenu popup = getPopup();
        if (popup != null && popup.isShowing()) {
            popup.dismiss();
        }
        return true;
    }

    /**
     * Observes motion events and determines when to start forwarding.
     *
     * @param srcEvent motion event in source view coordinates
     * @return true to start forwarding motion events, false otherwise
     */
    private boolean onTouchObserved(MotionEvent srcEvent) {
        final View src = mSrc;
        if (!src.isEnabled()) {
            return false;
        }

        final int actionMasked = srcEvent.getActionMasked();
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN:
                mActivePointerId = srcEvent.getPointerId(0);
                mWasLongPress = false;

                if (mDisallowIntercept == null) {
                    mDisallowIntercept = new DisallowIntercept();
                }
                src.postDelayed(mDisallowIntercept, mTapTimeout);

                if (mTriggerLongPress == null) {
                    mTriggerLongPress = new TriggerLongPress();
                }
                src.postDelayed(mTriggerLongPress, mLongPressTimeout);
                break;
            case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
                if (activePointerIndex >= 0) {
                    final float x = srcEvent.getX(activePointerIndex);
                    final float y = srcEvent.getY(activePointerIndex);

                    // Has the pointer moved outside of the view?
                    if (!src.pointInView(x, y, mScaledTouchSlop)) {
                        clearCallbacks();

                        // Don't let the parent intercept our events.
                        src.getParent().requestDisallowInterceptTouchEvent(true);
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                clearCallbacks();
                break;
        }

        return false;
    }

    private void clearCallbacks() {
        if (mTriggerLongPress != null) {
            mSrc.removeCallbacks(mTriggerLongPress);
        }

        if (mDisallowIntercept != null) {
            mSrc.removeCallbacks(mDisallowIntercept);
        }
    }

    private void onLongPress() {
        clearCallbacks();

        final View src = mSrc;
        if (!src.isEnabled() || src.isLongClickable()) {
            // Ignore long-press if the view is disabled or has its own
            // handler.
            return;
        }

        if (!onForwardingStarted()) {
            return;
        }

        // Don't let the parent intercept our events.
        src.getParent().requestDisallowInterceptTouchEvent(true);

        // Make sure we cancel any ongoing source event stream.
        final long now = SystemClock.uptimeMillis();
        final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
        src.onTouchEvent(e);
        e.recycle();

        mForwarding = true;
        mWasLongPress = true;
    }

    /**
     * Handles forwarded motion events and determines when to stop
     * forwarding.
     *
     * @param srcEvent motion event in source view coordinates
     * @return true to continue forwarding motion events, false to cancel
     */
    private boolean onTouchForwarded(MotionEvent srcEvent) {
        final View src = mSrc;
        final ShowableListMenu popup = getPopup();
        if (popup == null || !popup.isShowing()) {
            return false;
        }

        final DropDownListView dst = (DropDownListView) popup.getListView();
        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();

        // Always cancel forwarding when the touch stream ends.
        final int action = srcEvent.getActionMasked();
        final boolean keepForwarding = action != MotionEvent.ACTION_UP
                && action != MotionEvent.ACTION_CANCEL;

        return handled && keepForwarding;
    }

    private class DisallowIntercept implements Runnable {
        @Override
        public void run() {
            final ViewParent parent = mSrc.getParent();
            parent.requestDisallowInterceptTouchEvent(true);
        }
    }

    private class TriggerLongPress implements Runnable {
        @Override
        public void run() {
            onLongPress();
        }
    }
}
 No newline at end of file
+11 −570

File changed.

Preview size limit exceeded, changes collapsed.

Loading