/* * Copyright (C) 2016 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.support.v7.widget; import android.content.Context; import android.os.Build; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewPropertyAnimatorCompat; import android.support.v4.widget.ListViewAutoScrollHelper; import android.support.v7.appcompat.R; import android.view.MotionEvent; import android.view.View; /** * <p>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.</p> */ class DropDownListView extends ListViewCompat { /* * 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 ViewPropertyAnimatorCompat mClickAnimation; /** Helper for drag-to-open auto scrolling. */ private ListViewAutoScrollHelper mScrollHelper; /** * <p>Creates a new list view wrapper.</p> * * @param context this view's context */ public DropDownListView(Context context, boolean hijackFocus) { super(context, null, R.attr.dropDownListViewStyle); mHijackFocus = hijackFocus; 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 = MotionEventCompat.getActionMasked(event); 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 ListViewAutoScrollHelper(this); } mScrollHelper.setEnabled(true); mScrollHelper.onTouch(this, event); } else if (mScrollHelper != null) { mScrollHelper.setEnabled(false); } 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); performItemClick(child, position, id); } /** * Sets whether the list selection is hidden, as part of a workaround for a * touch mode issue (see the declaration for mListSelectionHidden). * * @param hideListSelection {@code true} to hide list selection, * {@code false} to show */ void setListSelectionHidden(boolean hideListSelection) { mListSelectionHidden = hideListSelection; } private void clearPressedItem() { mDrawsInPressedState = false; setPressed(false); // This will call through to updateSelectorState() drawableStateChanged(); final View motionView = getChildAt(mMotionPosition - getFirstVisiblePosition()); 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. if (Build.VERSION.SDK_INT >= 21) { drawableHotspotChanged(x, y); } if (!isPressed()) { setPressed(true); } // Next, run layout to stabilize child positions. layoutChildren(); // Manage the pressed view based on motion position. This allows us to // play nicely with actual touch and scroll events. if (mMotionPosition != INVALID_POSITION) { final View motionView = getChildAt(mMotionPosition - getFirstVisiblePosition()); 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(); if (Build.VERSION.SDK_INT >= 21) { child.drawableHotspotChanged(childX, childY); } if (!child.isPressed()) { child.setPressed(true); } // Ensure that keyboard focus starts from the last touched position. positionSelectorLikeTouchCompat(position, child, x, y); // This needs some explanation. We need to disable the selector for this next call // due to the way that ListViewCompat works. Otherwise both ListView and ListViewCompat // will draw the selector and bad things happen. setSelectorEnabled(false); // Refresh the drawable state to reflect the new pressed state, // which will also update the selector state. refreshDrawableState(); } @Override protected boolean touchModeDrawsInPressedStateCompat() { return mDrawsInPressedState || super.touchModeDrawsInPressedStateCompat(); } @Override public boolean isInTouchMode() { // WARNING: Please read the comment where mListSelectionHidden is declared return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); } /** * <p>Returns the focus state in the drop down.</p> * * @return true always if hijacking focus */ @Override public boolean hasWindowFocus() { return mHijackFocus || super.hasWindowFocus(); } /** * <p>Returns the focus state in the drop down.</p> * * @return true always if hijacking focus */ @Override public boolean isFocused() { return mHijackFocus || super.isFocused(); } /** * <p>Returns the focus state in the drop down.</p> * * @return true always if hijacking focus */ @Override public boolean hasFocus() { return mHijackFocus || super.hasFocus(); } }