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.inputmethod.EditorInfo; 28 import android.view.inputmethod.InputConnection; 29 import android.widget.MultiAutoCompleteTextView; 30 import android.widget.TextView; 31 32 import androidx.annotation.DrawableRes; 33 import androidx.annotation.RestrictTo; 34 import androidx.appcompat.R; 35 import androidx.appcompat.content.res.AppCompatResources; 36 import androidx.core.view.TintableBackgroundView; 37 import androidx.core.widget.TintableCompoundDrawablesView; 38 import androidx.resourceinspection.annotation.AppCompatShadowedAttributes; 39 40 import org.jspecify.annotations.NonNull; 41 import org.jspecify.annotations.Nullable; 42 43 /** 44 * A {@link MultiAutoCompleteTextView} which supports compatible features on older version of the 45 * platform, including: 46 * <ul> 47 * <li>Supports {@link R.attr#textAllCaps} style attribute which works back to 48 * {@link android.os.Build.VERSION_CODES#GINGERBREAD Gingerbread}.</li> 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 MultiAutoCompleteTextView} in your layouts. 56 * You should only need to manually use this class when writing custom views.</p> 57 */ 58 @AppCompatShadowedAttributes 59 public class AppCompatMultiAutoCompleteTextView extends MultiAutoCompleteTextView 60 implements TintableBackgroundView, EmojiCompatConfigurationView, 61 TintableCompoundDrawablesView { 62 63 private static final int[] TINT_ATTRS = { 64 android.R.attr.popupBackground 65 }; 66 67 private final AppCompatBackgroundHelper mBackgroundTintHelper; 68 private final AppCompatTextHelper mTextHelper; 69 private final @NonNull AppCompatEmojiEditTextHelper mAppCompatEmojiEditTextHelper; 70 AppCompatMultiAutoCompleteTextView(@onNull Context context)71 public AppCompatMultiAutoCompleteTextView(@NonNull Context context) { 72 this(context, null); 73 } 74 AppCompatMultiAutoCompleteTextView( @onNull Context context, @Nullable AttributeSet attrs)75 public AppCompatMultiAutoCompleteTextView( 76 @NonNull Context context, @Nullable AttributeSet attrs) { 77 this(context, attrs, R.attr.autoCompleteTextViewStyle); 78 } 79 AppCompatMultiAutoCompleteTextView( @onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)80 public AppCompatMultiAutoCompleteTextView( 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 * Adds EmojiCompat KeyListener to correctly edit multi-codepoint emoji when they've been 244 * converted to spans. 245 * 246 * {@inheritDoc} 247 */ 248 @Override setKeyListener(@ullable KeyListener keyListener)249 public void setKeyListener(@Nullable KeyListener keyListener) { 250 super.setKeyListener(mAppCompatEmojiEditTextHelper.getKeyListener(keyListener)); 251 } 252 253 @Override setEmojiCompatEnabled(boolean enabled)254 public void setEmojiCompatEnabled(boolean enabled) { 255 mAppCompatEmojiEditTextHelper.setEnabled(enabled); 256 } 257 258 @Override isEmojiCompatEnabled()259 public boolean isEmojiCompatEnabled() { 260 return mAppCompatEmojiEditTextHelper.isEnabled(); 261 } 262 263 @Override setCompoundDrawables(@ullable Drawable left, @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom)264 public void setCompoundDrawables(@Nullable Drawable left, @Nullable Drawable top, 265 @Nullable Drawable right, @Nullable Drawable bottom) { 266 super.setCompoundDrawables(left, top, right, bottom); 267 if (mTextHelper != null) { 268 mTextHelper.onSetCompoundDrawables(); 269 } 270 } 271 272 @Override setCompoundDrawablesRelative(@ullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom)273 public void setCompoundDrawablesRelative(@Nullable Drawable start, @Nullable Drawable top, 274 @Nullable Drawable end, @Nullable Drawable bottom) { 275 super.setCompoundDrawablesRelative(start, top, end, bottom); 276 if (mTextHelper != null) { 277 mTextHelper.onSetCompoundDrawables(); 278 } 279 } 280 281 /** 282 * This should be accessed via 283 * {@link androidx.core.widget.TextViewCompat#getCompoundDrawableTintList(TextView)} 284 * 285 * @return the tint applied to the compound drawables 286 * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTint 287 * @see #setSupportCompoundDrawablesTintList(ColorStateList) 288 * 289 */ 290 @Override 291 @RestrictTo(LIBRARY_GROUP_PREFIX) getSupportCompoundDrawablesTintList()292 public @Nullable ColorStateList getSupportCompoundDrawablesTintList() { 293 return mTextHelper.getCompoundDrawableTintList(); 294 } 295 296 /** 297 * This should be accessed via {@link 298 * androidx.core.widget.TextViewCompat#setCompoundDrawableTintList(TextView, ColorStateList)} 299 * 300 * Applies a tint to the compound drawables. Does not modify the current tint mode, which is 301 * {@link PorterDuff.Mode#SRC_IN} by default. 302 * <p> 303 * Subsequent calls to {@link #setCompoundDrawables(Drawable, Drawable, Drawable, Drawable)} and 304 * related methods will automatically mutate the drawables and apply the specified tint and tint 305 * mode using {@link Drawable#setTintList(ColorStateList)}. 306 * 307 * @param tintList the tint to apply, may be {@code null} to clear tint 308 * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTint 309 * @see #getSupportCompoundDrawablesTintList() 310 * 311 */ 312 @Override 313 @RestrictTo(LIBRARY_GROUP_PREFIX) setSupportCompoundDrawablesTintList(@ullable ColorStateList tintList)314 public void setSupportCompoundDrawablesTintList(@Nullable ColorStateList tintList) { 315 mTextHelper.setCompoundDrawableTintList(tintList); 316 mTextHelper.applyCompoundDrawablesTints(); 317 } 318 319 /** 320 * This should be accessed via 321 * {@link androidx.core.widget.TextViewCompat#getCompoundDrawableTintMode(TextView)} 322 * 323 * Returns the blending mode used to apply the tint to the compound drawables, if specified. 324 * 325 * @return the blending mode used to apply the tint to the compound drawables 326 * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTintMode 327 * @see #setSupportCompoundDrawablesTintMode(PorterDuff.Mode) 328 * 329 */ 330 @Override 331 @RestrictTo(LIBRARY_GROUP_PREFIX) getSupportCompoundDrawablesTintMode()332 public PorterDuff.@Nullable Mode getSupportCompoundDrawablesTintMode() { 333 return mTextHelper.getCompoundDrawableTintMode(); 334 } 335 336 /** 337 * This should be accessed via {@link 338 * androidx.core.widget.TextViewCompat#setCompoundDrawableTintMode(TextView, PorterDuff.Mode)} 339 * 340 * Specifies the blending mode used to apply the tint specified by 341 * {@link #setSupportCompoundDrawablesTintList(ColorStateList)} to the compound drawables. The 342 * default mode is {@link PorterDuff.Mode#SRC_IN}. 343 * 344 * @param tintMode the blending mode used to apply the tint, may be {@code null} to clear tint 345 * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTintMode 346 * @see #setSupportCompoundDrawablesTintList(ColorStateList) 347 * 348 */ 349 @Override 350 @RestrictTo(LIBRARY_GROUP_PREFIX) setSupportCompoundDrawablesTintMode(PorterDuff.@ullable Mode tintMode)351 public void setSupportCompoundDrawablesTintMode(PorterDuff.@Nullable Mode tintMode) { 352 mTextHelper.setCompoundDrawableTintMode(tintMode); 353 mTextHelper.applyCompoundDrawablesTints(); 354 } 355 } 356