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 17 package androidx.appcompat.widget; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; 20 21 import android.content.res.ColorStateList; 22 import android.graphics.PorterDuff; 23 import android.graphics.drawable.Drawable; 24 import android.os.Build; 25 import android.util.AttributeSet; 26 import android.widget.ImageView; 27 28 import androidx.annotation.RestrictTo; 29 import androidx.appcompat.R; 30 import androidx.appcompat.content.res.AppCompatResources; 31 import androidx.core.view.ViewCompat; 32 import androidx.core.widget.ImageViewCompat; 33 34 import org.jspecify.annotations.NonNull; 35 36 /** 37 */ 38 @RestrictTo(LIBRARY_GROUP_PREFIX) 39 public class AppCompatImageHelper { 40 private final @NonNull ImageView mView; 41 42 private TintInfo mInternalImageTint; 43 private TintInfo mImageTint; 44 private TintInfo mTmpInfo; 45 private int mLevel = 0; 46 AppCompatImageHelper(@onNull ImageView view)47 public AppCompatImageHelper(@NonNull ImageView view) { 48 mView = view; 49 } 50 loadFromAttributes(AttributeSet attrs, int defStyleAttr)51 public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) { 52 TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs, 53 R.styleable.AppCompatImageView, defStyleAttr, 0); 54 ViewCompat.saveAttributeDataForStyleable(mView, mView.getContext(), 55 R.styleable.AppCompatImageView, attrs, a.getWrappedTypeArray(), defStyleAttr, 0); 56 try { 57 Drawable drawable = mView.getDrawable(); 58 if (drawable == null) { 59 // If the view doesn't already have a drawable (from android:src), try loading 60 // it from srcCompat 61 final int id = a.getResourceId(R.styleable.AppCompatImageView_srcCompat, -1); 62 if (id != -1) { 63 drawable = AppCompatResources.getDrawable(mView.getContext(), id); 64 if (drawable != null) { 65 mView.setImageDrawable(drawable); 66 } 67 } 68 } 69 70 if (drawable != null) { 71 DrawableUtils.fixDrawable(drawable); 72 } 73 74 if (a.hasValue(R.styleable.AppCompatImageView_tint)) { 75 ImageViewCompat.setImageTintList(mView, 76 a.getColorStateList(R.styleable.AppCompatImageView_tint)); 77 } 78 if (a.hasValue(R.styleable.AppCompatImageView_tintMode)) { 79 ImageViewCompat.setImageTintMode(mView, 80 DrawableUtils.parseTintMode( 81 a.getInt(R.styleable.AppCompatImageView_tintMode, -1), null)); 82 } 83 } finally { 84 a.recycle(); 85 } 86 } 87 setImageResource(int resId)88 public void setImageResource(int resId) { 89 if (resId != 0) { 90 final Drawable d = AppCompatResources.getDrawable(mView.getContext(), resId); 91 if (d != null) { 92 DrawableUtils.fixDrawable(d); 93 } 94 mView.setImageDrawable(d); 95 } else { 96 mView.setImageDrawable(null); 97 } 98 99 applySupportImageTint(); 100 } 101 hasOverlappingRendering()102 boolean hasOverlappingRendering() { 103 final Drawable background = mView.getBackground(); 104 if (Build.VERSION.SDK_INT >= 21 105 && background instanceof android.graphics.drawable.RippleDrawable) { 106 // RippleDrawable has an issue on L+ when used with an alpha animation. 107 // This workaround should be disabled when the platform bug is fixed. See b/27715789 108 return false; 109 } 110 return true; 111 } 112 setSupportImageTintList(ColorStateList tint)113 void setSupportImageTintList(ColorStateList tint) { 114 if (mImageTint == null) { 115 mImageTint = new TintInfo(); 116 } 117 mImageTint.mTintList = tint; 118 mImageTint.mHasTintList = true; 119 applySupportImageTint(); 120 } 121 getSupportImageTintList()122 ColorStateList getSupportImageTintList() { 123 return mImageTint != null ? mImageTint.mTintList : null; 124 } 125 setSupportImageTintMode(PorterDuff.Mode tintMode)126 void setSupportImageTintMode(PorterDuff.Mode tintMode) { 127 if (mImageTint == null) { 128 mImageTint = new TintInfo(); 129 } 130 mImageTint.mTintMode = tintMode; 131 mImageTint.mHasTintMode = true; 132 133 applySupportImageTint(); 134 } 135 getSupportImageTintMode()136 PorterDuff.Mode getSupportImageTintMode() { 137 return mImageTint != null ? mImageTint.mTintMode : null; 138 } 139 applySupportImageTint()140 void applySupportImageTint() { 141 final Drawable imageViewDrawable = mView.getDrawable(); 142 if (imageViewDrawable != null) { 143 DrawableUtils.fixDrawable(imageViewDrawable); 144 } 145 146 if (imageViewDrawable != null) { 147 if (shouldApplyFrameworkTintUsingColorFilter() 148 && applyFrameworkTintUsingColorFilter(imageViewDrawable)) { 149 // This needs to be called before the internal tints below so it takes 150 // effect on any widgets using the compat tint on API 21 151 return; 152 } 153 154 if (mImageTint != null) { 155 AppCompatDrawableManager.tintDrawable(imageViewDrawable, mImageTint, 156 mView.getDrawableState()); 157 } else if (mInternalImageTint != null) { 158 AppCompatDrawableManager.tintDrawable(imageViewDrawable, mInternalImageTint, 159 mView.getDrawableState()); 160 } 161 } 162 } 163 setInternalImageTint(ColorStateList tint)164 void setInternalImageTint(ColorStateList tint) { 165 if (tint != null) { 166 if (mInternalImageTint == null) { 167 mInternalImageTint = new TintInfo(); 168 } 169 mInternalImageTint.mTintList = tint; 170 mInternalImageTint.mHasTintList = true; 171 } else { 172 mInternalImageTint = null; 173 } 174 applySupportImageTint(); 175 } 176 shouldApplyFrameworkTintUsingColorFilter()177 private boolean shouldApplyFrameworkTintUsingColorFilter() { 178 final int sdk = Build.VERSION.SDK_INT; 179 if (sdk > 21) { 180 // On API 22+, if we're using an internal compat image source tint, we're also 181 // responsible for applying any custom tint set via the framework impl 182 return mInternalImageTint != null; 183 } else if (sdk == 21) { 184 // GradientDrawable doesn't implement setTintList on API 21, and since there is 185 // no nice way to unwrap DrawableContainers we have to blanket apply this 186 // on API 21 187 return true; 188 } else { 189 // API 19 and below doesn't have framework tint 190 return false; 191 } 192 } 193 194 /** 195 * Applies the framework image source tint to a view, but using the compat method (ColorFilter) 196 * 197 * @return true if a tint was applied 198 */ applyFrameworkTintUsingColorFilter(@onNull Drawable imageSource)199 private boolean applyFrameworkTintUsingColorFilter(@NonNull Drawable imageSource) { 200 if (mTmpInfo == null) { 201 mTmpInfo = new TintInfo(); 202 } 203 final TintInfo info = mTmpInfo; 204 info.clear(); 205 206 final ColorStateList tintList = ImageViewCompat.getImageTintList(mView); 207 if (tintList != null) { 208 info.mHasTintList = true; 209 info.mTintList = tintList; 210 } 211 final PorterDuff.Mode mode = ImageViewCompat.getImageTintMode(mView); 212 if (mode != null) { 213 info.mHasTintMode = true; 214 info.mTintMode = mode; 215 } 216 217 if (info.mHasTintList || info.mHasTintMode) { 218 AppCompatDrawableManager.tintDrawable(imageSource, info, mView.getDrawableState()); 219 return true; 220 } 221 222 return false; 223 } 224 225 /** 226 * Extracts the level from the drawable parameter and save it for the future use in 227 * {@link #applyImageLevel()} 228 */ obtainLevelFromDrawable(@onNull Drawable drawable)229 void obtainLevelFromDrawable(@NonNull Drawable drawable) { 230 mLevel = drawable.getLevel(); 231 } 232 233 /** 234 * Applies the level previously set through {@link #obtainLevelFromDrawable} 235 */ applyImageLevel()236 void applyImageLevel() { 237 if (mView.getDrawable() != null) { 238 mView.getDrawable().setLevel(mLevel); 239 } 240 } 241 } 242