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.MotionEventCompat; 22import android.support.v4.view.ViewPropertyAnimatorCompat; 23import android.support.v4.widget.ListViewAutoScrollHelper; 24import android.support.v7.appcompat.R; 25import android.view.MotionEvent; 26import android.view.View; 27 28/** 29 * <p>Wrapper class for a ListView. This wrapper can hijack the focus to 30 * make sure the list uses the appropriate drawables and states when 31 * displayed on screen within a drop down. The focus is never actually 32 * passed to the drop down in this mode; the list only looks focused.</p> 33 */ 34class DropDownListView extends ListViewCompat { 35 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 /** Current drag-to-open click animation, if any. */ 75 private ViewPropertyAnimatorCompat mClickAnimation; 76 77 /** Helper for drag-to-open auto scrolling. */ 78 private ListViewAutoScrollHelper mScrollHelper; 79 80 /** 81 * <p>Creates a new list view wrapper.</p> 82 * 83 * @param context this view's context 84 */ 85 public DropDownListView(Context context, boolean hijackFocus) { 86 super(context, null, R.attr.dropDownListViewStyle); 87 mHijackFocus = hijackFocus; 88 setCacheColorHint(0); // Transparent, since the background drawable could be anything. 89 } 90 91 /** 92 * Handles forwarded events. 93 * 94 * @param activePointerId id of the pointer that activated forwarding 95 * @return whether the event was handled 96 */ 97 public boolean onForwardedEvent(MotionEvent event, int activePointerId) { 98 boolean handledEvent = true; 99 boolean clearPressedItem = false; 100 101 final int actionMasked = MotionEventCompat.getActionMasked(event); 102 switch (actionMasked) { 103 case MotionEvent.ACTION_CANCEL: 104 handledEvent = false; 105 break; 106 case MotionEvent.ACTION_UP: 107 handledEvent = false; 108 // $FALL-THROUGH$ 109 case MotionEvent.ACTION_MOVE: 110 final int activeIndex = event.findPointerIndex(activePointerId); 111 if (activeIndex < 0) { 112 handledEvent = false; 113 break; 114 } 115 116 final int x = (int) event.getX(activeIndex); 117 final int y = (int) event.getY(activeIndex); 118 final int position = pointToPosition(x, y); 119 if (position == INVALID_POSITION) { 120 clearPressedItem = true; 121 break; 122 } 123 124 final View child = getChildAt(position - getFirstVisiblePosition()); 125 setPressedItem(child, position, x, y); 126 handledEvent = true; 127 128 if (actionMasked == MotionEvent.ACTION_UP) { 129 clickPressedItem(child, position); 130 } 131 break; 132 } 133 134 // Failure to handle the event cancels forwarding. 135 if (!handledEvent || clearPressedItem) { 136 clearPressedItem(); 137 } 138 139 // Manage automatic scrolling. 140 if (handledEvent) { 141 if (mScrollHelper == null) { 142 mScrollHelper = new ListViewAutoScrollHelper(this); 143 } 144 mScrollHelper.setEnabled(true); 145 mScrollHelper.onTouch(this, event); 146 } else if (mScrollHelper != null) { 147 mScrollHelper.setEnabled(false); 148 } 149 150 return handledEvent; 151 } 152 153 /** 154 * Starts an alpha animation on the selector. When the animation ends, 155 * the list performs a click on the item. 156 */ 157 private void clickPressedItem(final View child, final int position) { 158 final long id = getItemIdAtPosition(position); 159 performItemClick(child, position, id); 160 } 161 162 /** 163 * Sets whether the list selection is hidden, as part of a workaround for a 164 * touch mode issue (see the declaration for mListSelectionHidden). 165 * 166 * @param hideListSelection {@code true} to hide list selection, 167 * {@code false} to show 168 */ 169 void setListSelectionHidden(boolean hideListSelection) { 170 mListSelectionHidden = hideListSelection; 171 } 172 173 private void clearPressedItem() { 174 mDrawsInPressedState = false; 175 setPressed(false); 176 // This will call through to updateSelectorState() 177 drawableStateChanged(); 178 179 final View motionView = getChildAt(mMotionPosition - getFirstVisiblePosition()); 180 if (motionView != null) { 181 motionView.setPressed(false); 182 } 183 184 if (mClickAnimation != null) { 185 mClickAnimation.cancel(); 186 mClickAnimation = null; 187 } 188 } 189 190 private void setPressedItem(View child, int position, float x, float y) { 191 mDrawsInPressedState = true; 192 193 // Ordering is essential. First, update the container's pressed state. 194 if (Build.VERSION.SDK_INT >= 21) { 195 drawableHotspotChanged(x, y); 196 } 197 if (!isPressed()) { 198 setPressed(true); 199 } 200 201 // Next, run layout to stabilize child positions. 202 layoutChildren(); 203 204 // Manage the pressed view based on motion position. This allows us to 205 // play nicely with actual touch and scroll events. 206 if (mMotionPosition != INVALID_POSITION) { 207 final View motionView = getChildAt(mMotionPosition - getFirstVisiblePosition()); 208 if (motionView != null && motionView != child && motionView.isPressed()) { 209 motionView.setPressed(false); 210 } 211 } 212 mMotionPosition = position; 213 214 // Offset for child coordinates. 215 final float childX = x - child.getLeft(); 216 final float childY = y - child.getTop(); 217 if (Build.VERSION.SDK_INT >= 21) { 218 child.drawableHotspotChanged(childX, childY); 219 } 220 if (!child.isPressed()) { 221 child.setPressed(true); 222 } 223 224 // Ensure that keyboard focus starts from the last touched position. 225 positionSelectorLikeTouchCompat(position, child, x, y); 226 227 // This needs some explanation. We need to disable the selector for this next call 228 // due to the way that ListViewCompat works. Otherwise both ListView and ListViewCompat 229 // will draw the selector and bad things happen. 230 setSelectorEnabled(false); 231 232 // Refresh the drawable state to reflect the new pressed state, 233 // which will also update the selector state. 234 refreshDrawableState(); 235 } 236 237 @Override 238 protected boolean touchModeDrawsInPressedStateCompat() { 239 return mDrawsInPressedState || super.touchModeDrawsInPressedStateCompat(); 240 } 241 242 @Override 243 public boolean isInTouchMode() { 244 // WARNING: Please read the comment where mListSelectionHidden is declared 245 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); 246 } 247 248 /** 249 * <p>Returns the focus state in the drop down.</p> 250 * 251 * @return true always if hijacking focus 252 */ 253 @Override 254 public boolean hasWindowFocus() { 255 return mHijackFocus || super.hasWindowFocus(); 256 } 257 258 /** 259 * <p>Returns the focus state in the drop down.</p> 260 * 261 * @return true always if hijacking focus 262 */ 263 @Override 264 public boolean isFocused() { 265 return mHijackFocus || super.isFocused(); 266 } 267 268 /** 269 * <p>Returns the focus state in the drop down.</p> 270 * 271 * @return true always if hijacking focus 272 */ 273 @Override 274 public boolean hasFocus() { 275 return mHijackFocus || super.hasFocus(); 276 } 277} 278