1/* 2 * Copyright (C) 2016 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.support.v7.widget; 18 19import android.content.Context; 20import android.os.Build; 21import android.support.v4.view.ViewPropertyAnimatorCompat; 22import android.support.v4.widget.ListViewAutoScrollHelper; 23import android.support.v7.appcompat.R; 24import android.view.MotionEvent; 25import android.view.View; 26 27/** 28 * <p>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.</p> 32 */ 33class DropDownListView extends ListViewCompat { 34 35 /* 36 * WARNING: This is a workaround for a touch mode issue. 37 * 38 * Touch mode is propagated lazily to windows. This causes problems in 39 * the following scenario: 40 * - Type something in the AutoCompleteTextView and get some results 41 * - Move down with the d-pad to select an item in the list 42 * - Move up with the d-pad until the selection disappears 43 * - Type more text in the AutoCompleteTextView *using the soft keyboard* 44 * and get new results; you are now in touch mode 45 * - The selection comes back on the first item in the list, even though 46 * the list is supposed to be in touch mode 47 * 48 * Using the soft keyboard triggers the touch mode change but that change 49 * is propagated to our window only after the first list layout, therefore 50 * after the list attempts to resurrect the selection. 51 * 52 * The trick to work around this issue is to pretend the list is in touch 53 * mode when we know that the selection should not appear, that is when 54 * we know the user moved the selection away from the list. 55 * 56 * This boolean is set to true whenever we explicitly hide the list's 57 * selection and reset to false whenever we know the user moved the 58 * selection back to the list. 59 * 60 * When this boolean is true, isInTouchMode() returns true, otherwise it 61 * returns super.isInTouchMode(). 62 */ 63 private boolean mListSelectionHidden; 64 65 /** 66 * True if this wrapper should fake focus. 67 */ 68 private boolean mHijackFocus; 69 70 /** Whether to force drawing of the pressed state selector. */ 71 private boolean mDrawsInPressedState; 72 73 /** Current drag-to-open click animation, if any. */ 74 private ViewPropertyAnimatorCompat mClickAnimation; 75 76 /** Helper for drag-to-open auto scrolling. */ 77 private ListViewAutoScrollHelper mScrollHelper; 78 79 /** 80 * <p>Creates a new list view wrapper.</p> 81 * 82 * @param context this view's context 83 */ 84 public DropDownListView(Context context, boolean hijackFocus) { 85 super(context, null, R.attr.dropDownListViewStyle); 86 mHijackFocus = hijackFocus; 87 setCacheColorHint(0); // Transparent, since the background drawable could be anything. 88 } 89 90 /** 91 * Handles forwarded events. 92 * 93 * @param activePointerId id of the pointer that activated forwarding 94 * @return whether the event was handled 95 */ 96 public boolean onForwardedEvent(MotionEvent event, int activePointerId) { 97 boolean handledEvent = true; 98 boolean clearPressedItem = false; 99 100 final int actionMasked = event.getActionMasked(); 101 switch (actionMasked) { 102 case MotionEvent.ACTION_CANCEL: 103 handledEvent = false; 104 break; 105 case MotionEvent.ACTION_UP: 106 handledEvent = false; 107 // $FALL-THROUGH$ 108 case MotionEvent.ACTION_MOVE: 109 final int activeIndex = event.findPointerIndex(activePointerId); 110 if (activeIndex < 0) { 111 handledEvent = false; 112 break; 113 } 114 115 final int x = (int) event.getX(activeIndex); 116 final int y = (int) event.getY(activeIndex); 117 final int position = pointToPosition(x, y); 118 if (position == INVALID_POSITION) { 119 clearPressedItem = true; 120 break; 121 } 122 123 final View child = getChildAt(position - getFirstVisiblePosition()); 124 setPressedItem(child, position, x, y); 125 handledEvent = true; 126 127 if (actionMasked == MotionEvent.ACTION_UP) { 128 clickPressedItem(child, position); 129 } 130 break; 131 } 132 133 // Failure to handle the event cancels forwarding. 134 if (!handledEvent || clearPressedItem) { 135 clearPressedItem(); 136 } 137 138 // Manage automatic scrolling. 139 if (handledEvent) { 140 if (mScrollHelper == null) { 141 mScrollHelper = new ListViewAutoScrollHelper(this); 142 } 143 mScrollHelper.setEnabled(true); 144 mScrollHelper.onTouch(this, event); 145 } else if (mScrollHelper != null) { 146 mScrollHelper.setEnabled(false); 147 } 148 149 return handledEvent; 150 } 151 152 /** 153 * Starts an alpha animation on the selector. When the animation ends, 154 * the list performs a click on the item. 155 */ 156 private void clickPressedItem(final View child, final int position) { 157 final long id = getItemIdAtPosition(position); 158 performItemClick(child, position, id); 159 } 160 161 /** 162 * Sets whether the list selection is hidden, as part of a workaround for a 163 * touch mode issue (see the declaration for mListSelectionHidden). 164 * 165 * @param hideListSelection {@code true} to hide list selection, 166 * {@code false} to show 167 */ 168 void setListSelectionHidden(boolean hideListSelection) { 169 mListSelectionHidden = hideListSelection; 170 } 171 172 private void clearPressedItem() { 173 mDrawsInPressedState = false; 174 setPressed(false); 175 // This will call through to updateSelectorState() 176 drawableStateChanged(); 177 178 final View motionView = getChildAt(mMotionPosition - getFirstVisiblePosition()); 179 if (motionView != null) { 180 motionView.setPressed(false); 181 } 182 183 if (mClickAnimation != null) { 184 mClickAnimation.cancel(); 185 mClickAnimation = null; 186 } 187 } 188 189 private void setPressedItem(View child, int position, float x, float y) { 190 mDrawsInPressedState = true; 191 192 // Ordering is essential. First, update the container's pressed state. 193 if (Build.VERSION.SDK_INT >= 21) { 194 drawableHotspotChanged(x, y); 195 } 196 if (!isPressed()) { 197 setPressed(true); 198 } 199 200 // Next, run layout to stabilize child positions. 201 layoutChildren(); 202 203 // Manage the pressed view based on motion position. This allows us to 204 // play nicely with actual touch and scroll events. 205 if (mMotionPosition != INVALID_POSITION) { 206 final View motionView = getChildAt(mMotionPosition - getFirstVisiblePosition()); 207 if (motionView != null && motionView != child && motionView.isPressed()) { 208 motionView.setPressed(false); 209 } 210 } 211 mMotionPosition = position; 212 213 // Offset for child coordinates. 214 final float childX = x - child.getLeft(); 215 final float childY = y - child.getTop(); 216 if (Build.VERSION.SDK_INT >= 21) { 217 child.drawableHotspotChanged(childX, childY); 218 } 219 if (!child.isPressed()) { 220 child.setPressed(true); 221 } 222 223 // Ensure that keyboard focus starts from the last touched position. 224 positionSelectorLikeTouchCompat(position, child, x, y); 225 226 // This needs some explanation. We need to disable the selector for this next call 227 // due to the way that ListViewCompat works. Otherwise both ListView and ListViewCompat 228 // will draw the selector and bad things happen. 229 setSelectorEnabled(false); 230 231 // Refresh the drawable state to reflect the new pressed state, 232 // which will also update the selector state. 233 refreshDrawableState(); 234 } 235 236 @Override 237 protected boolean touchModeDrawsInPressedStateCompat() { 238 return mDrawsInPressedState || super.touchModeDrawsInPressedStateCompat(); 239 } 240 241 @Override 242 public boolean isInTouchMode() { 243 // WARNING: Please read the comment where mListSelectionHidden is declared 244 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); 245 } 246 247 /** 248 * <p>Returns the focus state in the drop down.</p> 249 * 250 * @return true always if hijacking focus 251 */ 252 @Override 253 public boolean hasWindowFocus() { 254 return mHijackFocus || super.hasWindowFocus(); 255 } 256 257 /** 258 * <p>Returns the focus state in the drop down.</p> 259 * 260 * @return true always if hijacking focus 261 */ 262 @Override 263 public boolean isFocused() { 264 return mHijackFocus || super.isFocused(); 265 } 266 267 /** 268 * <p>Returns the focus state in the drop down.</p> 269 * 270 * @return true always if hijacking focus 271 */ 272 @Override 273 public boolean hasFocus() { 274 return mHijackFocus || super.hasFocus(); 275 } 276} 277