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