1 /* 2 * Copyright (C) 2014 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 androidx.appcompat.widget; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; 20 21 import android.content.Context; 22 import android.content.res.ColorStateList; 23 import android.graphics.PorterDuff; 24 import android.graphics.drawable.Drawable; 25 import android.text.method.KeyListener; 26 import android.util.AttributeSet; 27 import android.view.ActionMode; 28 import android.view.inputmethod.EditorInfo; 29 import android.view.inputmethod.InputConnection; 30 import android.widget.AutoCompleteTextView; 31 import android.widget.TextView; 32 33 import androidx.annotation.DrawableRes; 34 import androidx.annotation.RestrictTo; 35 import androidx.appcompat.R; 36 import androidx.appcompat.content.res.AppCompatResources; 37 import androidx.core.view.TintableBackgroundView; 38 import androidx.core.widget.TextViewCompat; 39 import androidx.core.widget.TintableCompoundDrawablesView; 40 import androidx.resourceinspection.annotation.AppCompatShadowedAttributes; 41 42 import org.jspecify.annotations.NonNull; 43 import org.jspecify.annotations.Nullable; 44 45 /** 46 * A {@link AutoCompleteTextView} which supports compatible features on older versions of the 47 * platform, including: 48 * <ul> 49 * <li>Allows dynamic tint of its background via the background tint methods in 50 * {@link androidx.core.view.ViewCompat}.</li> 51 * <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and 52 * {@link R.attr#backgroundTintMode}.</li> 53 * </ul> 54 * 55 * <p>This will automatically be used when you use {@link AutoCompleteTextView} in your layouts 56 * and the top-level activity / dialog is provided by 57 * <a href="{@docRoot}topic/libraries/support-library/packages.html#v7-appcompat">appcompat</a>. 58 * You should only need to manually use this class when writing custom views.</p> 59 */ 60 @AppCompatShadowedAttributes 61 public class AppCompatAutoCompleteTextView extends AutoCompleteTextView implements 62 TintableBackgroundView, EmojiCompatConfigurationView, TintableCompoundDrawablesView { 63 64 private static final int[] TINT_ATTRS = { 65 android.R.attr.popupBackground 66 }; 67 68 private final AppCompatBackgroundHelper mBackgroundTintHelper; 69 private final AppCompatTextHelper mTextHelper; 70 private final @NonNull AppCompatEmojiEditTextHelper mAppCompatEmojiEditTextHelper; 71 AppCompatAutoCompleteTextView(@onNull Context context)72 public AppCompatAutoCompleteTextView(@NonNull Context context) { 73 this(context, null); 74 } 75 AppCompatAutoCompleteTextView(@onNull Context context, @Nullable AttributeSet attrs)76 public AppCompatAutoCompleteTextView(@NonNull Context context, @Nullable AttributeSet attrs) { 77 this(context, attrs, R.attr.autoCompleteTextViewStyle); 78 } 79 AppCompatAutoCompleteTextView( @onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)80 public AppCompatAutoCompleteTextView( 81 @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 82 super(TintContextWrapper.wrap(context), attrs, defStyleAttr); 83 84 ThemeUtils.checkAppCompatTheme(this, getContext()); 85 86 TintTypedArray a = TintTypedArray.obtainStyledAttributes(getContext(), attrs, 87 TINT_ATTRS, defStyleAttr, 0); 88 if (a.hasValue(0)) { 89 setDropDownBackgroundDrawable(a.getDrawable(0)); 90 } 91 a.recycle(); 92 93 mBackgroundTintHelper = new AppCompatBackgroundHelper(this); 94 mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr); 95 96 mTextHelper = new AppCompatTextHelper(this); 97 mTextHelper.loadFromAttributes(attrs, defStyleAttr); 98 mTextHelper.applyCompoundDrawablesTints(); 99 100 mAppCompatEmojiEditTextHelper = new AppCompatEmojiEditTextHelper(this); 101 mAppCompatEmojiEditTextHelper.loadFromAttributes(attrs, defStyleAttr); 102 initEmojiKeyListener(mAppCompatEmojiEditTextHelper); 103 } 104 105 /** 106 * Call from the constructor to safely add KeyListener for emoji2. 107 * 108 * This will always call super methods to avoid leaking a partially constructed this to 109 * overrides of non-final methods. 110 * 111 * @param appCompatEmojiEditTextHelper emojicompat helper 112 */ initEmojiKeyListener(AppCompatEmojiEditTextHelper appCompatEmojiEditTextHelper)113 void initEmojiKeyListener(AppCompatEmojiEditTextHelper appCompatEmojiEditTextHelper) { 114 // setKeyListener will cause a reset both focusable and the inputType to the most basic 115 // style for the key listener. Since we're calling this from the View constructor, this 116 // will cause both focusable and inputType to reset from the XML attributes. 117 // See: b/191061070 and b/188049943 for details 118 // 119 // We will only reset this during ctor invocation, and default to the platform behavior 120 // for later calls to setKeyListener, to emulate the exact behavior that a regular 121 // EditText would provide. 122 // 123 // Since we're calling non-final methods from a ctor (setKeyListener, setRawInputType, 124 // setFocusable) move this out of AppCompatEmojiEditTextHelper and into the respective 125 // views to ensure we only call the super methods during construction (b/208480173). 126 KeyListener currentKeyListener = getKeyListener(); 127 if (appCompatEmojiEditTextHelper.isEmojiCapableKeyListener(currentKeyListener)) { 128 boolean wasFocusable = super.isFocusable(); 129 boolean wasClickable = super.isClickable(); 130 boolean wasLongClickable = super.isLongClickable(); 131 int inputType = super.getInputType(); 132 KeyListener wrappedKeyListener = appCompatEmojiEditTextHelper.getKeyListener( 133 currentKeyListener); 134 // don't call parent setKeyListener if it's not wrapped 135 if (wrappedKeyListener == currentKeyListener) return; 136 super.setKeyListener(wrappedKeyListener); 137 // reset the input type and focusable attributes after calling setKeyListener 138 super.setRawInputType(inputType); 139 super.setFocusable(wasFocusable); 140 super.setClickable(wasClickable); 141 super.setLongClickable(wasLongClickable); 142 } 143 } 144 145 @Override setDropDownBackgroundResource(@rawableRes int resId)146 public void setDropDownBackgroundResource(@DrawableRes int resId) { 147 setDropDownBackgroundDrawable(AppCompatResources.getDrawable(getContext(), resId)); 148 } 149 150 @Override setBackgroundResource(@rawableRes int resId)151 public void setBackgroundResource(@DrawableRes int resId) { 152 super.setBackgroundResource(resId); 153 if (mBackgroundTintHelper != null) { 154 mBackgroundTintHelper.onSetBackgroundResource(resId); 155 } 156 } 157 158 @Override setBackgroundDrawable(@ullable Drawable background)159 public void setBackgroundDrawable(@Nullable Drawable background) { 160 super.setBackgroundDrawable(background); 161 if (mBackgroundTintHelper != null) { 162 mBackgroundTintHelper.onSetBackgroundDrawable(background); 163 } 164 } 165 166 /** 167 * This should be accessed via 168 * {@link androidx.core.view.ViewCompat#setBackgroundTintList(android.view.View, ColorStateList)} 169 * 170 */ 171 @RestrictTo(LIBRARY_GROUP_PREFIX) 172 @Override setSupportBackgroundTintList(@ullable ColorStateList tint)173 public void setSupportBackgroundTintList(@Nullable ColorStateList tint) { 174 if (mBackgroundTintHelper != null) { 175 mBackgroundTintHelper.setSupportBackgroundTintList(tint); 176 } 177 } 178 179 /** 180 * This should be accessed via 181 * {@link androidx.core.view.ViewCompat#getBackgroundTintList(android.view.View)} 182 * 183 */ 184 @RestrictTo(LIBRARY_GROUP_PREFIX) 185 @Override getSupportBackgroundTintList()186 public @Nullable ColorStateList getSupportBackgroundTintList() { 187 return mBackgroundTintHelper != null 188 ? mBackgroundTintHelper.getSupportBackgroundTintList() : null; 189 } 190 191 /** 192 * This should be accessed via 193 * {@link androidx.core.view.ViewCompat#setBackgroundTintMode(android.view.View, PorterDuff.Mode)} 194 * 195 */ 196 @RestrictTo(LIBRARY_GROUP_PREFIX) 197 @Override setSupportBackgroundTintMode(PorterDuff.@ullable Mode tintMode)198 public void setSupportBackgroundTintMode(PorterDuff.@Nullable Mode tintMode) { 199 if (mBackgroundTintHelper != null) { 200 mBackgroundTintHelper.setSupportBackgroundTintMode(tintMode); 201 } 202 } 203 204 /** 205 * This should be accessed via 206 * {@link androidx.core.view.ViewCompat#getBackgroundTintMode(android.view.View)} 207 * 208 */ 209 @RestrictTo(LIBRARY_GROUP_PREFIX) 210 @Override getSupportBackgroundTintMode()211 public PorterDuff.@Nullable Mode getSupportBackgroundTintMode() { 212 return mBackgroundTintHelper != null 213 ? mBackgroundTintHelper.getSupportBackgroundTintMode() : null; 214 } 215 216 @Override drawableStateChanged()217 protected void drawableStateChanged() { 218 super.drawableStateChanged(); 219 if (mBackgroundTintHelper != null) { 220 mBackgroundTintHelper.applySupportBackgroundTint(); 221 } 222 if (mTextHelper != null) { 223 mTextHelper.applyCompoundDrawablesTints(); 224 } 225 } 226 227 @Override setTextAppearance(Context context, int resId)228 public void setTextAppearance(Context context, int resId) { 229 super.setTextAppearance(context, resId); 230 if (mTextHelper != null) { 231 mTextHelper.onSetTextAppearance(context, resId); 232 } 233 } 234 235 @Override onCreateInputConnection(EditorInfo outAttrs)236 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 237 InputConnection inputConnection = AppCompatHintHelper.onCreateInputConnection( 238 super.onCreateInputConnection(outAttrs), outAttrs, this); 239 return mAppCompatEmojiEditTextHelper.onCreateInputConnection(inputConnection, outAttrs); 240 } 241 242 /** 243 * See 244 * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)} 245 */ 246 @Override setCustomSelectionActionModeCallback( ActionMode.@ullable Callback actionModeCallback)247 public void setCustomSelectionActionModeCallback( 248 ActionMode.@Nullable Callback actionModeCallback) { 249 super.setCustomSelectionActionModeCallback( 250 TextViewCompat.wrapCustomSelectionActionModeCallback(this, actionModeCallback)); 251 } 252 253 @Override getCustomSelectionActionModeCallback()254 public ActionMode.@Nullable Callback getCustomSelectionActionModeCallback() { 255 return TextViewCompat.unwrapCustomSelectionActionModeCallback( 256 super.getCustomSelectionActionModeCallback()); 257 } 258 259 /** 260 * Adds EmojiCompat KeyListener to correctly edit multi-codepoint emoji when they've been 261 * converted to spans. 262 * 263 * {@inheritDoc} 264 */ 265 @Override setKeyListener(@ullable KeyListener keyListener)266 public void setKeyListener(@Nullable KeyListener keyListener) { 267 super.setKeyListener(mAppCompatEmojiEditTextHelper.getKeyListener(keyListener)); 268 } 269 270 @Override setEmojiCompatEnabled(boolean enabled)271 public void setEmojiCompatEnabled(boolean enabled) { 272 mAppCompatEmojiEditTextHelper.setEnabled(enabled); 273 } 274 275 @Override isEmojiCompatEnabled()276 public boolean isEmojiCompatEnabled() { 277 return mAppCompatEmojiEditTextHelper.isEnabled(); 278 } 279 280 @Override setCompoundDrawables(@ullable Drawable left, @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom)281 public void setCompoundDrawables(@Nullable Drawable left, @Nullable Drawable top, 282 @Nullable Drawable right, @Nullable Drawable bottom) { 283 super.setCompoundDrawables(left, top, right, bottom); 284 if (mTextHelper != null) { 285 mTextHelper.onSetCompoundDrawables(); 286 } 287 } 288 289 @Override setCompoundDrawablesRelative(@ullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom)290 public void setCompoundDrawablesRelative(@Nullable Drawable start, @Nullable Drawable top, 291 @Nullable Drawable end, @Nullable Drawable bottom) { 292 super.setCompoundDrawablesRelative(start, top, end, bottom); 293 if (mTextHelper != null) { 294 mTextHelper.onSetCompoundDrawables(); 295 } 296 } 297 298 /** 299 * This should be accessed via 300 * {@link androidx.core.widget.TextViewCompat#getCompoundDrawableTintList(TextView)} 301 * 302 * @return the tint applied to the compound drawables 303 * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTint 304 * @see #setSupportCompoundDrawablesTintList(ColorStateList) 305 * 306 */ 307 @Override 308 @RestrictTo(LIBRARY_GROUP_PREFIX) getSupportCompoundDrawablesTintList()309 public @Nullable ColorStateList getSupportCompoundDrawablesTintList() { 310 return mTextHelper.getCompoundDrawableTintList(); 311 } 312 313 /** 314 * This should be accessed via {@link 315 * androidx.core.widget.TextViewCompat#setCompoundDrawableTintList(TextView, ColorStateList)} 316 * 317 * Applies a tint to the compound drawables. Does not modify the current tint mode, which is 318 * {@link PorterDuff.Mode#SRC_IN} by default. 319 * <p> 320 * Subsequent calls to {@link #setCompoundDrawables(Drawable, Drawable, Drawable, Drawable)} and 321 * related methods will automatically mutate the drawables and apply the specified tint and tint 322 * mode using {@link Drawable#setTintList(ColorStateList)}. 323 * 324 * @param tintList the tint to apply, may be {@code null} to clear tint 325 * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTint 326 * @see #getSupportCompoundDrawablesTintList() 327 * 328 */ 329 @Override 330 @RestrictTo(LIBRARY_GROUP_PREFIX) setSupportCompoundDrawablesTintList(@ullable ColorStateList tintList)331 public void setSupportCompoundDrawablesTintList(@Nullable ColorStateList tintList) { 332 mTextHelper.setCompoundDrawableTintList(tintList); 333 mTextHelper.applyCompoundDrawablesTints(); 334 } 335 336 /** 337 * This should be accessed via 338 * {@link androidx.core.widget.TextViewCompat#getCompoundDrawableTintMode(TextView)} 339 * 340 * Returns the blending mode used to apply the tint to the compound drawables, if specified. 341 * 342 * @return the blending mode used to apply the tint to the compound drawables 343 * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTintMode 344 * @see #setSupportCompoundDrawablesTintMode(PorterDuff.Mode) 345 * 346 */ 347 @Override 348 @RestrictTo(LIBRARY_GROUP_PREFIX) getSupportCompoundDrawablesTintMode()349 public PorterDuff.@Nullable Mode getSupportCompoundDrawablesTintMode() { 350 return mTextHelper.getCompoundDrawableTintMode(); 351 } 352 353 /** 354 * This should be accessed via {@link 355 * androidx.core.widget.TextViewCompat#setCompoundDrawableTintMode(TextView, PorterDuff.Mode)} 356 * 357 * Specifies the blending mode used to apply the tint specified by 358 * {@link #setSupportCompoundDrawablesTintList(ColorStateList)} to the compound drawables. The 359 * default mode is {@link PorterDuff.Mode#SRC_IN}. 360 * 361 * @param tintMode the blending mode used to apply the tint, may be {@code null} to clear tint 362 * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTintMode 363 * @see #setSupportCompoundDrawablesTintList(ColorStateList) 364 * 365 */ 366 @Override 367 @RestrictTo(LIBRARY_GROUP_PREFIX) setSupportCompoundDrawablesTintMode(PorterDuff.@ullable Mode tintMode)368 public void setSupportCompoundDrawablesTintMode(PorterDuff.@Nullable Mode tintMode) { 369 mTextHelper.setCompoundDrawableTintMode(tintMode); 370 mTextHelper.applyCompoundDrawablesTints(); 371 } 372 } 373