DropDownListView.java revision 8e12f8df076d38853e0fedde7ed79e2e8689d59e
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.animation.Animator; 23import android.animation.AnimatorListenerAdapter; 24import android.animation.ObjectAnimator; 25import android.content.Context; 26import android.graphics.drawable.Drawable; 27import android.util.IntProperty; 28import android.view.MotionEvent; 29import android.view.View; 30import android.view.animation.AccelerateDecelerateInterpolator; 31import android.widget.TextView; 32import android.widget.ListView; 33 34 35/** 36 * Wrapper class for a ListView. This wrapper can hijack the focus to 37 * make sure the list uses the appropriate drawables and states when 38 * displayed on screen within a drop down. The focus is never actually 39 * passed to the drop down in this mode; the list only looks focused. 40 * 41 * @hide 42 */ 43public class DropDownListView extends ListView { 44 /** Duration in milliseconds of the drag-to-open click animation. */ 45 private static final long CLICK_ANIM_DURATION = 150; 46 47 /** Target alpha value for drag-to-open click animation. */ 48 private static final int CLICK_ANIM_ALPHA = 0x80; 49 50 /** Wrapper around Drawable's <code>alpha</code> property. */ 51 private static final IntProperty<Drawable> DRAWABLE_ALPHA = 52 new IntProperty<Drawable>("alpha") { 53 @Override 54 public void setValue(Drawable object, int value) { 55 object.setAlpha(value); 56 } 57 58 @Override 59 public Integer get(Drawable object) { 60 return object.getAlpha(); 61 } 62 }; 63 64 /* 65 * WARNING: This is a workaround for a touch mode issue. 66 * 67 * Touch mode is propagated lazily to windows. This causes problems in 68 * the following scenario: 69 * - Type something in the AutoCompleteTextView and get some results 70 * - Move down with the d-pad to select an item in the list 71 * - Move up with the d-pad until the selection disappears 72 * - Type more text in the AutoCompleteTextView *using the soft keyboard* 73 * and get new results; you are now in touch mode 74 * - The selection comes back on the first item in the list, even though 75 * the list is supposed to be in touch mode 76 * 77 * Using the soft keyboard triggers the touch mode change but that change 78 * is propagated to our window only after the first list layout, therefore 79 * after the list attempts to resurrect the selection. 80 * 81 * The trick to work around this issue is to pretend the list is in touch 82 * mode when we know that the selection should not appear, that is when 83 * we know the user moved the selection away from the list. 84 * 85 * This boolean is set to true whenever we explicitly hide the list's 86 * selection and reset to false whenever we know the user moved the 87 * selection back to the list. 88 * 89 * When this boolean is true, isInTouchMode() returns true, otherwise it 90 * returns super.isInTouchMode(). 91 */ 92 private boolean mListSelectionHidden; 93 94 /** 95 * True if this wrapper should fake focus. 96 */ 97 private boolean mHijackFocus; 98 99 /** Whether to force drawing of the pressed state selector. */ 100 private boolean mDrawsInPressedState; 101 102 /** Current drag-to-open click animation, if any. */ 103 private Animator mClickAnimation; 104 105 /** Helper for drag-to-open auto scrolling. */ 106 private AbsListViewAutoScroller mScrollHelper; 107 108 /** 109 * Creates a new list view wrapper. 110 * 111 * @param context this view's context 112 */ 113 public DropDownListView(Context context, boolean hijackFocus) { 114 this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle); 115 } 116 117 /** 118 * Creates a new list view wrapper. 119 * 120 * @param context this view's context 121 */ 122 public DropDownListView(Context context, boolean hijackFocus, int defStyleAttr) { 123 super(context, null, defStyleAttr); 124 mHijackFocus = hijackFocus; 125 // TODO: Add an API to control this 126 setCacheColorHint(0); // Transparent, since the background drawable could be anything. 127 } 128 129 @Override 130 protected boolean shouldShowSelector() { 131 View selectedView = getSelectedView(); 132 return selectedView != null && selectedView.isEnabled() || super.shouldShowSelector(); 133 } 134 135 protected void clearSelection() { 136 setSelectedPositionInt(-1); 137 setNextSelectedPositionInt(-1); 138 } 139 140 @Override 141 public boolean onHoverEvent(MotionEvent ev) { 142 final int action = ev.getActionMasked(); 143 if (action == MotionEvent.ACTION_HOVER_ENTER 144 || action == MotionEvent.ACTION_HOVER_MOVE) { 145 final int position = pointToPosition((int) ev.getX(), (int) ev.getY()); 146 if (position != INVALID_POSITION && position != mSelectedPosition) { 147 final View hoveredItem = getChildAt(position - getFirstVisiblePosition()); 148 if (hoveredItem.isEnabled()) { 149 // Force a focus so that the proper selector state gets used when we update. 150 requestFocus(); 151 152 positionSelector(position, hoveredItem); 153 setSelectedPositionInt(position); 154 setNextSelectedPositionInt(position); 155 } 156 updateSelectorState(); 157 } 158 } else { 159 // Do not cancel the selected position if the selection is visible by other reasons. 160 if (!super.shouldShowSelector()) { 161 setSelectedPositionInt(INVALID_POSITION); 162 } 163 } 164 return super.onHoverEvent(ev); 165 } 166 167 @Override 168 public boolean onTouchEvent(MotionEvent event) { 169 final int x = (int) event.getX(); 170 final int y = (int) event.getY(); 171 final int position = pointToPosition(x, y); 172 if (position == INVALID_POSITION) { 173 return super.onTouchEvent(event); 174 } 175 176 if (position != mSelectedPosition) { 177 setSelectedPositionInt(position); 178 setNextSelectedPositionInt(position); 179 } 180 181 return super.onTouchEvent(event); 182 } 183 184 /** 185 * Handles forwarded events. 186 * 187 * @param activePointerId id of the pointer that activated forwarding 188 * @return whether the event was handled 189 */ 190 public boolean onForwardedEvent(MotionEvent event, int activePointerId) { 191 boolean handledEvent = true; 192 boolean clearPressedItem = false; 193 194 final int actionMasked = event.getActionMasked(); 195 switch (actionMasked) { 196 case MotionEvent.ACTION_CANCEL: 197 handledEvent = false; 198 break; 199 case MotionEvent.ACTION_UP: 200 handledEvent = false; 201 // $FALL-THROUGH$ 202 case MotionEvent.ACTION_MOVE: 203 final int activeIndex = event.findPointerIndex(activePointerId); 204 if (activeIndex < 0) { 205 handledEvent = false; 206 break; 207 } 208 209 final int x = (int) event.getX(activeIndex); 210 final int y = (int) event.getY(activeIndex); 211 final int position = pointToPosition(x, y); 212 if (position == INVALID_POSITION) { 213 clearPressedItem = true; 214 break; 215 } 216 217 final View child = getChildAt(position - getFirstVisiblePosition()); 218 setPressedItem(child, position, x, y); 219 handledEvent = true; 220 221 if (actionMasked == MotionEvent.ACTION_UP) { 222 clickPressedItem(child, position); 223 } 224 break; 225 } 226 227 // Failure to handle the event cancels forwarding. 228 if (!handledEvent || clearPressedItem) { 229 clearPressedItem(); 230 } 231 232 // Manage automatic scrolling. 233 if (handledEvent) { 234 if (mScrollHelper == null) { 235 mScrollHelper = new AbsListViewAutoScroller(this); 236 } 237 mScrollHelper.setEnabled(true); 238 mScrollHelper.onTouch(this, event); 239 } else if (mScrollHelper != null) { 240 mScrollHelper.setEnabled(false); 241 } 242 243 return handledEvent; 244 } 245 246 /** 247 * Sets whether the list selection is hidden, as part of a workaround for a touch mode issue 248 * (see the declaration for mListSelectionHidden). 249 * @param listSelectionHidden 250 */ 251 public void setListSelectionHidden(boolean listSelectionHidden) { 252 this.mListSelectionHidden = listSelectionHidden; 253 } 254 255 /** 256 * Starts an alpha animation on the selector. When the animation ends, 257 * the list performs a click on the item. 258 */ 259 private void clickPressedItem(final View child, final int position) { 260 final long id = getItemIdAtPosition(position); 261 final Animator anim = ObjectAnimator.ofInt( 262 mSelector, DRAWABLE_ALPHA, 0xFF, CLICK_ANIM_ALPHA, 0xFF); 263 anim.setDuration(CLICK_ANIM_DURATION); 264 anim.setInterpolator(new AccelerateDecelerateInterpolator()); 265 anim.addListener(new AnimatorListenerAdapter() { 266 @Override 267 public void onAnimationEnd(Animator animation) { 268 performItemClick(child, position, id); 269 } 270 }); 271 anim.start(); 272 273 if (mClickAnimation != null) { 274 mClickAnimation.cancel(); 275 } 276 mClickAnimation = anim; 277 } 278 279 private void clearPressedItem() { 280 mDrawsInPressedState = false; 281 setPressed(false); 282 updateSelectorState(); 283 284 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 285 if (motionView != null) { 286 motionView.setPressed(false); 287 } 288 289 if (mClickAnimation != null) { 290 mClickAnimation.cancel(); 291 mClickAnimation = null; 292 } 293 } 294 295 private void setPressedItem(View child, int position, float x, float y) { 296 mDrawsInPressedState = true; 297 298 // Ordering is essential. First, update the container's pressed state. 299 drawableHotspotChanged(x, y); 300 if (!isPressed()) { 301 setPressed(true); 302 } 303 304 // Next, run layout if we need to stabilize child positions. 305 if (mDataChanged) { 306 layoutChildren(); 307 } 308 309 // Manage the pressed view based on motion position. This allows us to 310 // play nicely with actual touch and scroll events. 311 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 312 if (motionView != null && motionView != child && motionView.isPressed()) { 313 motionView.setPressed(false); 314 } 315 mMotionPosition = position; 316 317 // Offset for child coordinates. 318 final float childX = x - child.getLeft(); 319 final float childY = y - child.getTop(); 320 child.drawableHotspotChanged(childX, childY); 321 if (!child.isPressed()) { 322 child.setPressed(true); 323 } 324 325 // Ensure that keyboard focus starts from the last touched position. 326 setSelectedPositionInt(position); 327 positionSelectorLikeTouch(position, child, x, y); 328 329 // Refresh the drawable state to reflect the new pressed state, 330 // which will also update the selector state. 331 refreshDrawableState(); 332 333 if (mClickAnimation != null) { 334 mClickAnimation.cancel(); 335 mClickAnimation = null; 336 } 337 } 338 339 @Override 340 boolean touchModeDrawsInPressedState() { 341 return mDrawsInPressedState || super.touchModeDrawsInPressedState(); 342 } 343 344 /** 345 * Avoids jarring scrolling effect by ensuring that list elements 346 * made of a text view fit on a single line. 347 * 348 * @param position the item index in the list to get a view for 349 * @return the view for the specified item 350 */ 351 @Override 352 View obtainView(int position, boolean[] isScrap) { 353 View view = super.obtainView(position, isScrap); 354 355 if (view instanceof TextView) { 356 ((TextView) view).setHorizontallyScrolling(true); 357 } 358 359 return view; 360 } 361 362 @Override 363 public boolean isInTouchMode() { 364 // WARNING: Please read the comment where mListSelectionHidden is declared 365 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); 366 } 367 368 /** 369 * Returns the focus state in the drop down. 370 * 371 * @return true always if hijacking focus 372 */ 373 @Override 374 public boolean hasWindowFocus() { 375 return mHijackFocus || super.hasWindowFocus(); 376 } 377 378 /** 379 * Returns the focus state in the drop down. 380 * 381 * @return true always if hijacking focus 382 */ 383 @Override 384 public boolean isFocused() { 385 return mHijackFocus || super.isFocused(); 386 } 387 388 /** 389 * Returns the focus state in the drop down. 390 * 391 * @return true always if hijacking focus 392 */ 393 @Override 394 public boolean hasFocus() { 395 return mHijackFocus || super.hasFocus(); 396 } 397}