在非 Material Design 主题(如 Theme.AppCompat.Light.NoActionBar)中使用 com.google.android.material.imageview.ShapeableImageView,如果不需要 strokeColor ,或者在外部 style 中统一定义 <item name="strokeColor">@null</item> 或<item name="strokeColor">@android:color/transparent</item>,在类内部获取描边颜色时会输出一堆找不到颜色的日志:
Image decoding logging dropped!
Failed to inflate ColorStateList, leaving it to the framework
java.lang.UnsupportedOperationException: Failed to resolve attribute at index 0: TypedValue{t=0x2/d=0x7f04013c a=-1}, theme={InheritanceMap=[id=0x7f13024ecom.a.b.c.d:style/Theme.Common, id=0x7f13024ccom.a.b.c.d:style/Theme.AppCompat.Light.NoActionBar, id=0x7f130246com.a.b.c.d:style/Theme.AppCompat.Light, id=0x7f130053com.a.b.c.d:style/Base.Theme.AppCompat.Light, id=0x7f1300bbcom.a.b.c.d:style/Base.V28.Theme.AppCompat.Light, id=0x7f1300b8com.a.b.c.d:style/Base.V26.Theme.AppCompat.Light, id=0x7f1300b2com.a.b.c.d:style/Base.V23.Theme.AppCompat.Light, id=0x7f1300b0com.a.b.c.d:style/Base.V22.Theme.AppCompat.Light, id=0x7f1300a5com.a.b.c.d:style/Base.V21.Theme.AppCompat.Light, id=0x7f1300becom.a.b.c.d:style/Base.V7.Theme.AppCompat.Light, id=0x7f13015acom.a.b.c.d:style/Platform.AppCompat.Light, id=0x7f130165com.a.b.c.d:style/Platform.V25.AppCompat.Light, id=0x1030241android:style/Theme.Material.Light.NoActionBar, id=0x1030237android:style/Theme.Material.Light, id=0x103000candroid:style/Theme.Light, id=0x1030005android:style/Theme], Themes=[com.a.b.c.d:style/Theme.Common, forced, com.a.b.c.d:style/Theme.AppCompat.Empty, forced, android:style/Theme.DeviceDefault.Light.DarkActionBar, forced]}
at android.content.res.TypedArray.getColor(TypedArray.java:536)
at androidx.core.content.res.ColorStateListInflaterCompat.inflate(ColorStateListInflaterCompat.java:157)
at androidx.core.content.res.ColorStateListInflaterCompat.createFromXmlInner(ColorStateListInflaterCompat.java:122)
at androidx.core.content.res.ColorStateListInflaterCompat.createFromXml(ColorStateListInflaterCompat.java:102)
at androidx.core.content.res.ResourcesCompat.inflateColorStateList(ResourcesCompat.java:259)
at androidx.core.content.res.ResourcesCompat.getColorStateList(ResourcesCompat.java:234)
at androidx.core.content.ContextCompat.getColorStateList(ContextCompat.java:516)
at androidx.appcompat.content.res.AppCompatResources.getColorStateList(AppCompatResources.java:46)
搜索了一些文章,所说的将 strokeColor 设置为 @null 其实是无效的,本地改为透明的也无效,依然输出一堆日志。
为解决不输出上诉日志,有两种方案,第一是用 Material Design 相关的主题,比如 Theme.MaterialComponents.Light.NoActionBar 等 Theme.MaterialComponents 的子主题;第二种方案是自己搞个类似的视图。
本文解决方法为直接把该类拷出来,把获取 strokeColor 的地方改一下即可。
下面贴出相关类,直接拿去用:
package com.a.b.c.d;
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import androidx.annotation.ColorRes;
import androidx.annotation.DimenRes;
import androidx.annotation.Dimension;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatImageView;
import com.google.android.material.R;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
import com.google.android.material.shape.ShapeAppearancePathProvider;
import com.google.android.material.shape.Shapeable;
import java.lang.reflect.InvocationTargetException;
/**
* 本类为实现在非 Material Design 主题中使用 ShapeableImageView ,基于 ShapeableImageView 修改获取描边颜色,避免输出一堆找不到颜色的日志
*
* @see com.google.android.material.imageview.ShapeableImageView 是AppCompatImageView 的一个扩展,专门用于应用自定义形状(如圆角、切角)来显示图像,该类需要配合支持 Material Design 的主题使用,否则 {@code com.google.android.material.resources.MaterialResources.getColorStateList()} 会输出找不到颜色的日志
* <p>
* An ImageView that draws the bitmap with the provided Shape.
*/
public class ShapeableImageView2 extends AppCompatImageView implements Shapeable {
private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_ShapeableImageView;
private static final int UNDEFINED_PADDING = Integer.MIN_VALUE;
// 改动 一
// private final ShapeAppearancePathProvider pathProvider = ShapeAppearancePathProvider.getInstance();
private final ShapeAppearancePathProvider pathProvider =
(ShapeAppearancePathProvider) Class.forName("com.google.android.material.shape.ShapeAppearancePathProvider").getMethod("getInstance").invoke(null);
private final RectF destination;
private final RectF maskRect;
private final Paint borderPaint;
private final Paint clearPaint;
private final Path path = new Path();
@Nullable
private ColorStateList strokeColor;
@Nullable
private MaterialShapeDrawable shadowDrawable;
private ShapeAppearanceModel shapeAppearanceModel;
@Dimension
private float strokeWidth;
private Path maskPath;
@Dimension
private int leftContentPadding;
@Dimension
private int topContentPadding;
@Dimension
private int rightContentPadding;
@Dimension
private int bottomContentPadding;
@Dimension
private int startContentPadding;
@Dimension
private int endContentPadding;
private boolean hasAdjustedPaddingAfterLayoutDirectionResolved = false;
public ShapeableImageView2(Context context) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
this(context, null, 0);
}
public ShapeableImageView2(Context context, @Nullable AttributeSet attrs) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
this(context, attrs, 0);
}
public ShapeableImageView2(Context context, @Nullable AttributeSet attrs, int defStyle) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
super(wrap(context, attrs, defStyle, DEF_STYLE_RES), attrs, defStyle);
// Ensure we are using the correctly themed context rather than the context that was passed in.
context = getContext();
clearPaint = new Paint();
clearPaint.setAntiAlias(true);
clearPaint.setColor(Color.WHITE);
clearPaint.setXfermode(new PorterDuffXfermode(Mode.DST_OUT));
destination = new RectF();
maskRect = new RectF();
maskPath = new Path();
TypedArray attributes =
context.obtainStyledAttributes(
attrs, R.styleable.ShapeableImageView, defStyle, DEF_STYLE_RES);
setLayerType(LAYER_TYPE_HARDWARE, null);
// 改动 二
// MaterialResources.getColorStateList(
// context, attributes, R.styleable.ShapeableImageView_strokeColor);
strokeColor = attributes.getColorStateList(R.styleable.ShapeableImageView_strokeColor);
strokeWidth = attributes.getDimensionPixelSize(R.styleable.ShapeableImageView_strokeWidth, 0);
// First set all 4 contentPadding values from the `app:contentPadding` attribute:
int contentPadding = attributes
.getDimensionPixelSize(R.styleable.ShapeableImageView_contentPadding, 0);
leftContentPadding = contentPadding;
topContentPadding = contentPadding;
rightContentPadding = contentPadding;
bottomContentPadding = contentPadding;
// Update each contentPadding value individually from the `app:contentPadding<Side>`
leftContentPadding = attributes.getDimensionPixelSize(
R.styleable.ShapeableImageView_contentPaddingLeft, contentPadding);
topContentPadding = attributes.getDimensionPixelSize(
R.styleable.ShapeableImageView_contentPaddingTop, contentPadding);
rightContentPadding = attributes.getDimensionPixelSize(
R.styleable.ShapeableImageView_contentPaddingRight, contentPadding);
bottomContentPadding = attributes.getDimensionPixelSize(
R.styleable.ShapeableImageView_contentPaddingBottom, contentPadding);
// Update the relative start and end contentPadding values from those attributes:
startContentPadding = attributes.getDimensionPixelSize(
R.styleable.ShapeableImageView_contentPaddingStart, UNDEFINED_PADDING);
endContentPadding = attributes.getDimensionPixelSize(
R.styleable.ShapeableImageView_contentPaddingEnd, UNDEFINED_PADDING);
attributes.recycle();
borderPaint = new Paint();
borderPaint.setStyle(Style.STROKE);
borderPaint.setAntiAlias(true);
shapeAppearanceModel =
ShapeAppearanceModel.builder(context, attrs, defStyle, DEF_STYLE_RES).build();
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
setOutlineProvider(new ShapeableImageView2.OutlineProvider());
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (hasAdjustedPaddingAfterLayoutDirectionResolved) {
return;
}
if (VERSION.SDK_INT > 19 && !isLayoutDirectionResolved()) {
return;
}
hasAdjustedPaddingAfterLayoutDirectionResolved = true;
// Update the super padding to be the combined `android:padding` and
// `app:contentPadding`, keeping with ShapeableImageView2's internal padding contract:
if (VERSION.SDK_INT >= 21 && (isPaddingRelative() || isContentPaddingRelative())) {
setPaddingRelative(
super.getPaddingStart(),
super.getPaddingTop(),
super.getPaddingEnd(),
super.getPaddingBottom());
return;
}
setPadding(
super.getPaddingLeft(),
super.getPaddingTop(),
super.getPaddingRight(),
super.getPaddingBottom());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(maskPath, clearPaint);
drawStroke(canvas);
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
updateShapeMask(width, height);
}
/**
* Set additional padding on the image that is not applied to the background.
*
* @param left the padding on the left of the image in pixels
* @param top the padding on the top of the image in pixels
* @param right the padding on the right of the image in pixels
* @param bottom the padding on the bottom of the image in pixels
*/
public void setContentPadding(
@Dimension int left, @Dimension int top, @Dimension int right, @Dimension int bottom) {
startContentPadding = UNDEFINED_PADDING;
endContentPadding = UNDEFINED_PADDING;
// Super padding is equal to background padding + content padding. Adjust the content padding
// portion of the super padding here:
super.setPadding(
super.getPaddingLeft() - leftContentPadding + left,
super.getPaddingTop() - topContentPadding + top,
super.getPaddingRight() - rightContentPadding + right,
super.getPaddingBottom() - bottomContentPadding + bottom);
leftContentPadding = left;
topContentPadding = top;
rightContentPadding = right;
bottomContentPadding = bottom;
}
/**
* Set additional relative padding on the image that is not applied to the background.
*
* @param start the padding on the start of the image in pixels
* @param top the padding on the top of the image in pixels
* @param end the padding on the end of the image in pixels
* @param bottom the padding on the bottom of the image in pixels
*/
@RequiresApi(17)
public void setContentPaddingRelative(
@Dimension int start, @Dimension int top, @Dimension int end, @Dimension int bottom) {
// Super padding is equal to background padding + content padding. Adjust the content padding
// portion of the super padding here:
super.setPaddingRelative(
super.getPaddingStart() - getContentPaddingStart() + start,
super.getPaddingTop() - topContentPadding + top,
super.getPaddingEnd() - getContentPaddingEnd() + end,
super.getPaddingBottom() - bottomContentPadding + bottom);
leftContentPadding = isRtl() ? end : start;
topContentPadding = top;
rightContentPadding = isRtl() ? start : end;
bottomContentPadding = bottom;
}
private boolean isContentPaddingRelative() {
return startContentPadding != UNDEFINED_PADDING || endContentPadding != UNDEFINED_PADDING;
}
/**
* The additional padding on the bottom of the image, which is not applied to the background.
*
* @return the bottom padding on the image
*/
@Dimension
public int getContentPaddingBottom() {
return bottomContentPadding;
}
/**
* The additional relative padding on the end of the image, which is not applied to the
* background.
*
* @return the end padding on the image
*/
@Dimension
public final int getContentPaddingEnd() {
if (endContentPadding != UNDEFINED_PADDING) {
return endContentPadding;
} else {
return isRtl() ? leftContentPadding : rightContentPadding;
}
}
/**
* The additional padding on the left of the image, which is not applied to the background.
*
* @return the left padding on the image
*/
@Dimension
public int getContentPaddingLeft() {
if (isContentPaddingRelative()) {
if (isRtl() && endContentPadding != UNDEFINED_PADDING) {
return endContentPadding;
} else if (!isRtl() && startContentPadding != UNDEFINED_PADDING) {
return startContentPadding;
}
}
return leftContentPadding;
}
/**
* The additional padding on the right of the image, which is not applied to the background.
*
* @return the right padding on the image
*/
@Dimension
public int getContentPaddingRight() {
if (isContentPaddingRelative()) {
if (isRtl() && startContentPadding != UNDEFINED_PADDING) {
return startContentPadding;
} else if (!isRtl() && endContentPadding != UNDEFINED_PADDING) {
return endContentPadding;
}
}
return rightContentPadding;
}
/**
* The additional relative padding on the start of the image, which is not applied to the
* background.
*
* @return the start padding on the image
*/
@Dimension
public final int getContentPaddingStart() {
if (startContentPadding != UNDEFINED_PADDING) {
return startContentPadding;
} else {
return isRtl() ? rightContentPadding : leftContentPadding;
}
}
/**
* The additional padding on the top of the image, which is not applied to the background.
*
* @return the top padding on the image
*/
@Dimension
public int getContentPaddingTop() {
return topContentPadding;
}
private boolean isRtl() {
return VERSION.SDK_INT >= 17 && getLayoutDirection() == LAYOUT_DIRECTION_RTL;
}
/**
* Set the padding. This is applied to both the background and the image, and does not affect the
* content padding differentiating the image from the background.
*
* @param left the left padding in pixels
* @param top the top padding in pixels
* @param right the right padding in pixels
* @param bottom the bottom padding in pixels
*/
@Override
public void setPadding(
@Dimension int left, @Dimension int top, @Dimension int right, @Dimension int bottom) {
super.setPadding(
left + getContentPaddingLeft(),
top + getContentPaddingTop(),
right + getContentPaddingRight(),
bottom + getContentPaddingBottom());
}
/**
* Set the relative padding. This is applied to both the background and the image, and does not
* affect the content padding differentiating the image from the background.
*
* @param start the start padding in pixels
* @param top the top padding in pixels
* @param end the end padding in pixels
* @param bottom the bottom padding in pixels
*/
@Override
public void setPaddingRelative(
@Dimension int start, @Dimension int top, @Dimension int end, @Dimension int bottom) {
super.setPaddingRelative(
start + getContentPaddingStart(),
top + getContentPaddingTop(),
end + getContentPaddingEnd(),
bottom + getContentPaddingBottom());
}
/**
* The padding on the bottom of the View, applied to both the image and the background.
*
* @return the bottom padding
*/
@Override
@Dimension
public int getPaddingBottom() {
return super.getPaddingBottom() - getContentPaddingBottom();
}
/**
* The relative padding on the end of the View, applied to both the image and the background.
*
* @return the end padding
*/
@Override
@Dimension
public int getPaddingEnd() {
return super.getPaddingEnd() - getContentPaddingEnd();
}
/**
* The padding on the left of the View, applied to both the image and the background.
*
* @return the left padding
*/
@Override
@Dimension
public int getPaddingLeft() {
return super.getPaddingLeft() - getContentPaddingLeft();
}
/**
* The padding on the right of the View, applied to both the image and the background.
*
* @return the right padding
*/
@Override
@Dimension
public int getPaddingRight() {
return super.getPaddingRight() - getContentPaddingRight();
}
/**
* The relative padding on the start of the View, applied to both the image and the background.
*
* @return the start padding
*/
@Override
@Dimension
public int getPaddingStart() {
return super.getPaddingStart() - getContentPaddingStart();
}
/**
* The padding on the top of the View, applied to both the image and the background.
*
* @return the top padding
*/
@Override
@Dimension
public int getPaddingTop() {
return super.getPaddingTop() - getContentPaddingTop();
}
@Override
public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanceModel) {
this.shapeAppearanceModel = shapeAppearanceModel;
if (shadowDrawable != null) {
shadowDrawable.setShapeAppearanceModel(shapeAppearanceModel);
}
updateShapeMask(getWidth(), getHeight());
invalidate();
if (VERSION.SDK_INT >= 21) {
invalidateOutline();
}
}
@NonNull
@Override
public ShapeAppearanceModel getShapeAppearanceModel() {
return shapeAppearanceModel;
}
private void updateShapeMask(int width, int height) {
destination.set(
getPaddingLeft(), getPaddingTop(), width - getPaddingRight(), height - getPaddingBottom());
pathProvider.calculatePath(shapeAppearanceModel, 1f /*interpolation*/, destination, path);
// Remove path from rect to draw with clear paint.
maskPath.rewind();
maskPath.addPath(path);
// Do not include padding to clip the background too.
maskRect.set(0, 0, width, height);
maskPath.addRect(maskRect, Direction.CCW);
}
private void drawStroke(Canvas canvas) {
if (strokeColor == null) {
return;
}
borderPaint.setStrokeWidth(strokeWidth);
int colorForState =
strokeColor.getColorForState(getDrawableState(), strokeColor.getDefaultColor());
if (strokeWidth > 0 && colorForState != Color.TRANSPARENT) {
borderPaint.setColor(colorForState);
canvas.drawPath(path, borderPaint);
}
}
/**
* Sets the stroke color resource for this ImageView. Both stroke color and stroke width must be
* set for a stroke to be drawn.
*
* @param strokeColorResourceId Color resource to use for the stroke.
* @attr ref com.google.android.material.R.styleable#ShapeableImageView_strokeColor
* @see #setStrokeColor(ColorStateList)
* @see #getStrokeColor()
*/
public void setStrokeColorResource(@ColorRes int strokeColorResourceId) {
setStrokeColor(AppCompatResources.getColorStateList(getContext(), strokeColorResourceId));
}
/**
* Returns the stroke color for this ImageView.
*
* @attr ref com.google.android.material.R.styleable#ShapeableImageView_strokeColor
* @see #setStrokeColor(ColorStateList)
* @see #setStrokeColorResource(int)
*/
@Nullable
public ColorStateList getStrokeColor() {
return strokeColor;
}
/**
* Sets the stroke width for this ImageView. Both stroke color and stroke width must be set for a
* stroke to be drawn.
*
* @param strokeWidth Stroke width for this ImageView.
* @attr ref com.google.android.material.R.styleable#ShapeableImageView_strokeWidth
* @see #setStrokeWidthResource(int)
* @see #getStrokeWidth()
*/
public void setStrokeWidth(@Dimension float strokeWidth) {
if (this.strokeWidth != strokeWidth) {
this.strokeWidth = strokeWidth;
invalidate();
}
}
/**
* Sets the stroke width dimension resource for this ImageView. Both stroke color and stroke width
* must be set for a stroke to be drawn.
*
* @param strokeWidthResourceId Stroke width dimension resource for this ImageView.
* @attr ref com.google.android.material.R.styleable#ShapeableImageView_strokeWidth
* @see #setStrokeWidth(float)
* @see #getStrokeWidth()
*/
public void setStrokeWidthResource(@DimenRes int strokeWidthResourceId) {
setStrokeWidth(getResources().getDimensionPixelSize(strokeWidthResourceId));
}
/**
* Gets the stroke width for this ImageView.
*
* @return Stroke width for this ImageView.
* @attr ref com.google.android.material.R.styleable#ShapeableImageView_strokeWidth
* @see #setStrokeWidth(float)
* @see #setStrokeWidthResource(int)
*/
@Dimension
public float getStrokeWidth() {
return strokeWidth;
}
public void setStrokeColor(@Nullable ColorStateList strokeColor) {
this.strokeColor = strokeColor;
invalidate();
}
@TargetApi(VERSION_CODES.LOLLIPOP)
class OutlineProvider extends ViewOutlineProvider {
private final Rect rect = new Rect();
@Override
public void getOutline(View view, Outline outline) {
if (shapeAppearanceModel == null) {
return;
}
if (shadowDrawable == null) {
shadowDrawable = new MaterialShapeDrawable(shapeAppearanceModel);
}
destination.round(rect);
shadowDrawable.setBounds(rect);
shadowDrawable.getOutline(outline);
}
}
}