/*
 * 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.annotation.NonNull;
import android.content.Context;
import android.view.MotionEvent;
import android.view.View;

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

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
/** * 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 { /* * 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.
/** * True if this wrapper should fake focus. */
private boolean mHijackFocus;
Whether to force drawing of the pressed state selector.
/** Whether to force drawing of the pressed state selector. */
private boolean mDrawsInPressedState;
Helper for drag-to-open auto scrolling.
/** Helper for drag-to-open auto scrolling. */
private AbsListViewAutoScroller mScrollHelper;
Runnable posted when we are awaiting hover event resolution. When set, drawable state changes are postponed.
/** * Runnable posted when we are awaiting hover event resolution. When set, * drawable state changes are postponed. */
private ResolveHoverRunnable mResolveHoverRunnable;
Creates a new list view wrapper.
Params:
  • context – this view's context
/** * Creates a new list view wrapper. * * @param context this view's context */
public DropDownListView(@NonNull Context context, boolean hijackFocus) { this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle); }
Creates a new list view wrapper.
Params:
  • context – this view's context
/** * Creates a new list view wrapper. * * @param context this view's context */
public DropDownListView(@NonNull 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. } @Override boolean shouldShowSelector() { return isHovered() || super.shouldShowSelector(); } @Override public boolean onTouchEvent(MotionEvent ev) { if (mResolveHoverRunnable != null) { // Resolved hover event as hover => touch transition. mResolveHoverRunnable.cancel(); } return super.onTouchEvent(ev); } @Override public boolean onHoverEvent(@NonNull MotionEvent ev) { final int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_HOVER_EXIT && mResolveHoverRunnable == null) { // This may be transitioning to TOUCH_DOWN. Postpone drawable state // updates until either the next frame or the next touch event. mResolveHoverRunnable = new ResolveHoverRunnable(); mResolveHoverRunnable.post(); } // Allow the super class to handle hover state management first. final boolean handled = super.onHoverEvent(ev); if (action == MotionEvent.ACTION_HOVER_ENTER || action == MotionEvent.ACTION_HOVER_MOVE) { final int position = pointToPosition((int) ev.getX(), (int) ev.getY()); if (position != INVALID_POSITION && position != mSelectedPosition) { final View hoveredItem = getChildAt(position - getFirstVisiblePosition()); if (hoveredItem.isEnabled()) { // Force a focus so that the proper selector state gets // used when we update. requestFocus(); positionSelector(position, hoveredItem); setSelectedPositionInt(position); setNextSelectedPositionInt(position); } updateSelectorState(); } } else { // Do not cancel the selected position if the selection is visible // by other means. if (!super.shouldShowSelector()) { setSelectedPositionInt(INVALID_POSITION); setNextSelectedPositionInt(INVALID_POSITION); } } return handled; } @Override protected void drawableStateChanged() { if (mResolveHoverRunnable == null) { super.drawableStateChanged(); } }
Handles forwarded events.
Params:
  • activePointerId – id of the pointer that activated forwarding
Returns:whether the event was handled
/** * Handles forwarded events. * * @param activePointerId id of the pointer that activated forwarding * @return whether the event was handled */
public boolean onForwardedEvent(@NonNull 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) { final long id = getItemIdAtPosition(position); performItemClick(child, position, id); } 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).
Params:
  • hideListSelection – true to hide list selection, false to show
/** * 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 */
public void setListSelectionHidden(boolean hideListSelection) { mListSelectionHidden = hideListSelection; } private void clearPressedItem() { mDrawsInPressedState = false; setPressed(false); updateSelectorState(); final View motionView = getChildAt(mMotionPosition - mFirstPosition); if (motionView != null) { motionView.setPressed(false); } } private void setPressedItem(@NonNull 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(); } @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.
Params:
  • position – the item index in the list to get a view for
Returns:the view for the specified item
/** * 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.
Returns:true always if hijacking focus
/** * 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.
Returns:true always if hijacking focus
/** * 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.
Returns:true always if hijacking focus
/** * Returns the focus state in the drop down. * * @return true always if hijacking focus */
@Override public boolean hasFocus() { return mHijackFocus || super.hasFocus(); }
Runnable that forces hover event resolution and updates drawable state.
/** * Runnable that forces hover event resolution and updates drawable state. */
private class ResolveHoverRunnable implements Runnable { @Override public void run() { // Resolved hover event as standard hover exit. mResolveHoverRunnable = null; drawableStateChanged(); } public void cancel() { mResolveHoverRunnable = null; removeCallbacks(this); } public void post() { DropDownListView.this.post(this); } } }