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 17 package android.widget; 18 19 20 import android.annotation.NonNull; 21 import android.content.Context; 22 import android.view.MotionEvent; 23 import android.view.View; 24 25 import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller; 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 */ 35 public 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 * Runnable posted when we are awaiting hover event resolution. When set, 79 * drawable state changes are postponed. 80 */ 81 private ResolveHoverRunnable mResolveHoverRunnable; 82 83 /** 84 * Creates a new list view wrapper. 85 * 86 * @param context this view's context 87 */ DropDownListView(@onNull Context context, boolean hijackFocus)88 public DropDownListView(@NonNull Context context, boolean hijackFocus) { 89 this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle); 90 } 91 92 /** 93 * Creates a new list view wrapper. 94 * 95 * @param context this view's context 96 */ DropDownListView(@onNull Context context, boolean hijackFocus, int defStyleAttr)97 public DropDownListView(@NonNull Context context, boolean hijackFocus, int defStyleAttr) { 98 super(context, null, defStyleAttr); 99 mHijackFocus = hijackFocus; 100 // TODO: Add an API to control this 101 setCacheColorHint(0); // Transparent, since the background drawable could be anything. 102 } 103 104 @Override shouldShowSelector()105 boolean shouldShowSelector() { 106 return isHovered() || super.shouldShowSelector(); 107 } 108 109 @Override onTouchEvent(MotionEvent ev)110 public boolean onTouchEvent(MotionEvent ev) { 111 if (mResolveHoverRunnable != null) { 112 // Resolved hover event as hover => touch transition. 113 mResolveHoverRunnable.cancel(); 114 } 115 116 return super.onTouchEvent(ev); 117 } 118 119 @Override onHoverEvent(@onNull MotionEvent ev)120 public boolean onHoverEvent(@NonNull MotionEvent ev) { 121 final int action = ev.getActionMasked(); 122 if (action == MotionEvent.ACTION_HOVER_EXIT && mResolveHoverRunnable == null) { 123 // This may be transitioning to TOUCH_DOWN. Postpone drawable state 124 // updates until either the next frame or the next touch event. 125 mResolveHoverRunnable = new ResolveHoverRunnable(); 126 mResolveHoverRunnable.post(); 127 } 128 129 // Allow the super class to handle hover state management first. 130 final boolean handled = super.onHoverEvent(ev); 131 132 if (action == MotionEvent.ACTION_HOVER_ENTER 133 || action == MotionEvent.ACTION_HOVER_MOVE) { 134 final int position = pointToPosition((int) ev.getX(), (int) ev.getY()); 135 if (position != INVALID_POSITION && position != mSelectedPosition) { 136 final View hoveredItem = getChildAt(position - getFirstVisiblePosition()); 137 if (hoveredItem.isEnabled()) { 138 // Force a focus so that the proper selector state gets 139 // used when we update. 140 requestFocus(); 141 142 positionSelector(position, hoveredItem); 143 setSelectedPositionInt(position); 144 setNextSelectedPositionInt(position); 145 } 146 updateSelectorState(); 147 } 148 } else { 149 // Do not cancel the selected position if the selection is visible 150 // by other means. 151 if (!super.shouldShowSelector()) { 152 setSelectedPositionInt(INVALID_POSITION); 153 setNextSelectedPositionInt(INVALID_POSITION); 154 } 155 } 156 157 return handled; 158 } 159 160 @Override drawableStateChanged()161 protected void drawableStateChanged() { 162 if (mResolveHoverRunnable == null) { 163 super.drawableStateChanged(); 164 } 165 } 166 167 /** 168 * Handles forwarded events. 169 * 170 * @param activePointerId id of the pointer that activated forwarding 171 * @return whether the event was handled 172 */ onForwardedEvent(@onNull MotionEvent event, int activePointerId)173 public boolean onForwardedEvent(@NonNull MotionEvent event, int activePointerId) { 174 boolean handledEvent = true; 175 boolean clearPressedItem = false; 176 177 final int actionMasked = event.getActionMasked(); 178 switch (actionMasked) { 179 case MotionEvent.ACTION_CANCEL: 180 handledEvent = false; 181 break; 182 case MotionEvent.ACTION_UP: 183 handledEvent = false; 184 // $FALL-THROUGH$ 185 case MotionEvent.ACTION_MOVE: 186 final int activeIndex = event.findPointerIndex(activePointerId); 187 if (activeIndex < 0) { 188 handledEvent = false; 189 break; 190 } 191 192 final int x = (int) event.getX(activeIndex); 193 final int y = (int) event.getY(activeIndex); 194 final int position = pointToPosition(x, y); 195 if (position == INVALID_POSITION) { 196 clearPressedItem = true; 197 break; 198 } 199 200 final View child = getChildAt(position - getFirstVisiblePosition()); 201 setPressedItem(child, position, x, y); 202 handledEvent = true; 203 204 if (actionMasked == MotionEvent.ACTION_UP) { 205 final long id = getItemIdAtPosition(position); 206 performItemClick(child, position, id); 207 } 208 break; 209 } 210 211 // Failure to handle the event cancels forwarding. 212 if (!handledEvent || clearPressedItem) { 213 clearPressedItem(); 214 } 215 216 // Manage automatic scrolling. 217 if (handledEvent) { 218 if (mScrollHelper == null) { 219 mScrollHelper = new AbsListViewAutoScroller(this); 220 } 221 mScrollHelper.setEnabled(true); 222 mScrollHelper.onTouch(this, event); 223 } else if (mScrollHelper != null) { 224 mScrollHelper.setEnabled(false); 225 } 226 227 return handledEvent; 228 } 229 230 /** 231 * Sets whether the list selection is hidden, as part of a workaround for a 232 * touch mode issue (see the declaration for mListSelectionHidden). 233 * 234 * @param hideListSelection {@code true} to hide list selection, 235 * {@code false} to show 236 */ setListSelectionHidden(boolean hideListSelection)237 public void setListSelectionHidden(boolean hideListSelection) { 238 mListSelectionHidden = hideListSelection; 239 } 240 clearPressedItem()241 private void clearPressedItem() { 242 mDrawsInPressedState = false; 243 setPressed(false); 244 updateSelectorState(); 245 246 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 247 if (motionView != null) { 248 motionView.setPressed(false); 249 } 250 } 251 setPressedItem(@onNull View child, int position, float x, float y)252 private void setPressedItem(@NonNull View child, int position, float x, float y) { 253 mDrawsInPressedState = true; 254 255 // Ordering is essential. First, update the container's pressed state. 256 drawableHotspotChanged(x, y); 257 if (!isPressed()) { 258 setPressed(true); 259 } 260 261 // Next, run layout if we need to stabilize child positions. 262 if (mDataChanged) { 263 layoutChildren(); 264 } 265 266 // Manage the pressed view based on motion position. This allows us to 267 // play nicely with actual touch and scroll events. 268 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 269 if (motionView != null && motionView != child && motionView.isPressed()) { 270 motionView.setPressed(false); 271 } 272 mMotionPosition = position; 273 274 // Offset for child coordinates. 275 final float childX = x - child.getLeft(); 276 final float childY = y - child.getTop(); 277 child.drawableHotspotChanged(childX, childY); 278 if (!child.isPressed()) { 279 child.setPressed(true); 280 } 281 282 // Ensure that keyboard focus starts from the last touched position. 283 setSelectedPositionInt(position); 284 positionSelectorLikeTouch(position, child, x, y); 285 286 // Refresh the drawable state to reflect the new pressed state, 287 // which will also update the selector state. 288 refreshDrawableState(); 289 } 290 291 @Override touchModeDrawsInPressedState()292 boolean touchModeDrawsInPressedState() { 293 return mDrawsInPressedState || super.touchModeDrawsInPressedState(); 294 } 295 296 /** 297 * Avoids jarring scrolling effect by ensuring that list elements 298 * made of a text view fit on a single line. 299 * 300 * @param position the item index in the list to get a view for 301 * @return the view for the specified item 302 */ 303 @Override obtainView(int position, boolean[] isScrap)304 View obtainView(int position, boolean[] isScrap) { 305 View view = super.obtainView(position, isScrap); 306 307 if (view instanceof TextView) { 308 ((TextView) view).setHorizontallyScrolling(true); 309 } 310 311 return view; 312 } 313 314 @Override isInTouchMode()315 public boolean isInTouchMode() { 316 // WARNING: Please read the comment where mListSelectionHidden is declared 317 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); 318 } 319 320 /** 321 * Returns the focus state in the drop down. 322 * 323 * @return true always if hijacking focus 324 */ 325 @Override hasWindowFocus()326 public boolean hasWindowFocus() { 327 return mHijackFocus || super.hasWindowFocus(); 328 } 329 330 /** 331 * Returns the focus state in the drop down. 332 * 333 * @return true always if hijacking focus 334 */ 335 @Override isFocused()336 public boolean isFocused() { 337 return mHijackFocus || super.isFocused(); 338 } 339 340 /** 341 * Returns the focus state in the drop down. 342 * 343 * @return true always if hijacking focus 344 */ 345 @Override hasFocus()346 public boolean hasFocus() { 347 return mHijackFocus || super.hasFocus(); 348 } 349 350 /** 351 * Runnable that forces hover event resolution and updates drawable state. 352 */ 353 private class ResolveHoverRunnable implements Runnable { 354 @Override run()355 public void run() { 356 // Resolved hover event as standard hover exit. 357 mResolveHoverRunnable = null; 358 drawableStateChanged(); 359 } 360 cancel()361 public void cancel() { 362 mResolveHoverRunnable = null; 363 removeCallbacks(this); 364 } 365 post()366 public void post() { 367 DropDownListView.this.post(this); 368 } 369 } 370 }