DropDownListView.java revision be91ad5e21d917c3e9d5eff3bbd30d6df76c8213
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.widget; 18 19 20import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller; 21 22import android.annotation.NonNull; 23import android.content.Context; 24import android.view.MotionEvent; 25import android.view.View; 26 27/** 28 * Wrapper class for a ListView. This wrapper can hijack the focus to 29 * make sure the list uses the appropriate drawables and states when 30 * displayed on screen within a drop down. The focus is never actually 31 * passed to the drop down in this mode; the list only looks focused. 32 * 33 * @hide 34 */ 35public class DropDownListView extends ListView { 36 /* 37 * WARNING: This is a workaround for a touch mode issue. 38 * 39 * Touch mode is propagated lazily to windows. This causes problems in 40 * the following scenario: 41 * - Type something in the AutoCompleteTextView and get some results 42 * - Move down with the d-pad to select an item in the list 43 * - Move up with the d-pad until the selection disappears 44 * - Type more text in the AutoCompleteTextView *using the soft keyboard* 45 * and get new results; you are now in touch mode 46 * - The selection comes back on the first item in the list, even though 47 * the list is supposed to be in touch mode 48 * 49 * Using the soft keyboard triggers the touch mode change but that change 50 * is propagated to our window only after the first list layout, therefore 51 * after the list attempts to resurrect the selection. 52 * 53 * The trick to work around this issue is to pretend the list is in touch 54 * mode when we know that the selection should not appear, that is when 55 * we know the user moved the selection away from the list. 56 * 57 * This boolean is set to true whenever we explicitly hide the list's 58 * selection and reset to false whenever we know the user moved the 59 * selection back to the list. 60 * 61 * When this boolean is true, isInTouchMode() returns true, otherwise it 62 * returns super.isInTouchMode(). 63 */ 64 private boolean mListSelectionHidden; 65 66 /** 67 * True if this wrapper should fake focus. 68 */ 69 private boolean mHijackFocus; 70 71 /** Whether to force drawing of the pressed state selector. */ 72 private boolean mDrawsInPressedState; 73 74 /** Helper for drag-to-open auto scrolling. */ 75 private AbsListViewAutoScroller mScrollHelper; 76 77 /** 78 * Creates a new list view wrapper. 79 * 80 * @param context this view's context 81 */ 82 public DropDownListView(@NonNull Context context, boolean hijackFocus) { 83 this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle); 84 } 85 86 /** 87 * Creates a new list view wrapper. 88 * 89 * @param context this view's context 90 */ 91 public DropDownListView(@NonNull Context context, boolean hijackFocus, int defStyleAttr) { 92 super(context, null, defStyleAttr); 93 mHijackFocus = hijackFocus; 94 // TODO: Add an API to control this 95 setCacheColorHint(0); // Transparent, since the background drawable could be anything. 96 } 97 98 @Override 99 boolean shouldShowSelector() { 100 return isHovered() || super.shouldShowSelector(); 101 } 102 103 @Override 104 public boolean onHoverEvent(@NonNull MotionEvent ev) { 105 // Allow the super class to handle hover state management first. 106 final boolean handled = super.onHoverEvent(ev); 107 108 final int action = ev.getActionMasked(); 109 if (action == MotionEvent.ACTION_HOVER_ENTER 110 || action == MotionEvent.ACTION_HOVER_MOVE) { 111 final int position = pointToPosition((int) ev.getX(), (int) ev.getY()); 112 if (position != INVALID_POSITION && position != mSelectedPosition) { 113 final View hoveredItem = getChildAt(position - getFirstVisiblePosition()); 114 if (hoveredItem.isEnabled()) { 115 // Force a focus so that the proper selector state gets used when we update. 116 requestFocus(); 117 118 positionSelector(position, hoveredItem); 119 setSelectedPositionInt(position); 120 setNextSelectedPositionInt(position); 121 } 122 updateSelectorState(); 123 } 124 } else { 125 // Do not cancel the selected position if the selection is visible by other reasons. 126 if (!super.shouldShowSelector()) { 127 setSelectedPositionInt(INVALID_POSITION); 128 setNextSelectedPositionInt(INVALID_POSITION); 129 } 130 } 131 132 return handled; 133 } 134 135 /** 136 * Handles forwarded events. 137 * 138 * @param activePointerId id of the pointer that activated forwarding 139 * @return whether the event was handled 140 */ 141 public boolean onForwardedEvent(@NonNull MotionEvent event, int activePointerId) { 142 boolean handledEvent = true; 143 boolean clearPressedItem = false; 144 145 final int actionMasked = event.getActionMasked(); 146 switch (actionMasked) { 147 case MotionEvent.ACTION_CANCEL: 148 handledEvent = false; 149 break; 150 case MotionEvent.ACTION_UP: 151 handledEvent = false; 152 // $FALL-THROUGH$ 153 case MotionEvent.ACTION_MOVE: 154 final int activeIndex = event.findPointerIndex(activePointerId); 155 if (activeIndex < 0) { 156 handledEvent = false; 157 break; 158 } 159 160 final int x = (int) event.getX(activeIndex); 161 final int y = (int) event.getY(activeIndex); 162 final int position = pointToPosition(x, y); 163 if (position == INVALID_POSITION) { 164 clearPressedItem = true; 165 break; 166 } 167 168 final View child = getChildAt(position - getFirstVisiblePosition()); 169 setPressedItem(child, position, x, y); 170 handledEvent = true; 171 172 if (actionMasked == MotionEvent.ACTION_UP) { 173 final long id = getItemIdAtPosition(position); 174 performItemClick(child, position, id); 175 } 176 break; 177 } 178 179 // Failure to handle the event cancels forwarding. 180 if (!handledEvent || clearPressedItem) { 181 clearPressedItem(); 182 } 183 184 // Manage automatic scrolling. 185 if (handledEvent) { 186 if (mScrollHelper == null) { 187 mScrollHelper = new AbsListViewAutoScroller(this); 188 } 189 mScrollHelper.setEnabled(true); 190 mScrollHelper.onTouch(this, event); 191 } else if (mScrollHelper != null) { 192 mScrollHelper.setEnabled(false); 193 } 194 195 return handledEvent; 196 } 197 198 /** 199 * Sets whether the list selection is hidden, as part of a workaround for a touch mode issue 200 * (see the declaration for mListSelectionHidden). 201 * @param listSelectionHidden 202 */ 203 public void setListSelectionHidden(boolean listSelectionHidden) { 204 this.mListSelectionHidden = listSelectionHidden; 205 } 206 207 private void clearPressedItem() { 208 mDrawsInPressedState = false; 209 setPressed(false); 210 updateSelectorState(); 211 212 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 213 if (motionView != null) { 214 motionView.setPressed(false); 215 } 216 } 217 218 private void setPressedItem(@NonNull View child, int position, float x, float y) { 219 mDrawsInPressedState = true; 220 221 // Ordering is essential. First, update the container's pressed state. 222 drawableHotspotChanged(x, y); 223 if (!isPressed()) { 224 setPressed(true); 225 } 226 227 // Next, run layout if we need to stabilize child positions. 228 if (mDataChanged) { 229 layoutChildren(); 230 } 231 232 // Manage the pressed view based on motion position. This allows us to 233 // play nicely with actual touch and scroll events. 234 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 235 if (motionView != null && motionView != child && motionView.isPressed()) { 236 motionView.setPressed(false); 237 } 238 mMotionPosition = position; 239 240 // Offset for child coordinates. 241 final float childX = x - child.getLeft(); 242 final float childY = y - child.getTop(); 243 child.drawableHotspotChanged(childX, childY); 244 if (!child.isPressed()) { 245 child.setPressed(true); 246 } 247 248 // Ensure that keyboard focus starts from the last touched position. 249 setSelectedPositionInt(position); 250 positionSelectorLikeTouch(position, child, x, y); 251 252 // Refresh the drawable state to reflect the new pressed state, 253 // which will also update the selector state. 254 refreshDrawableState(); 255 } 256 257 @Override 258 boolean touchModeDrawsInPressedState() { 259 return mDrawsInPressedState || super.touchModeDrawsInPressedState(); 260 } 261 262 /** 263 * Avoids jarring scrolling effect by ensuring that list elements 264 * made of a text view fit on a single line. 265 * 266 * @param position the item index in the list to get a view for 267 * @return the view for the specified item 268 */ 269 @Override 270 View obtainView(int position, boolean[] isScrap) { 271 View view = super.obtainView(position, isScrap); 272 273 if (view instanceof TextView) { 274 ((TextView) view).setHorizontallyScrolling(true); 275 } 276 277 return view; 278 } 279 280 @Override 281 public boolean isInTouchMode() { 282 // WARNING: Please read the comment where mListSelectionHidden is declared 283 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); 284 } 285 286 /** 287 * Returns the focus state in the drop down. 288 * 289 * @return true always if hijacking focus 290 */ 291 @Override 292 public boolean hasWindowFocus() { 293 return mHijackFocus || super.hasWindowFocus(); 294 } 295 296 /** 297 * Returns the focus state in the drop down. 298 * 299 * @return true always if hijacking focus 300 */ 301 @Override 302 public boolean isFocused() { 303 return mHijackFocus || super.isFocused(); 304 } 305 306 /** 307 * Returns the focus state in the drop down. 308 * 309 * @return true always if hijacking focus 310 */ 311 @Override 312 public boolean hasFocus() { 313 return mHijackFocus || super.hasFocus(); 314 } 315}