一次搞懂怎麼設定圓角圖片,ImageView的各種圓角設定

語言: CN / TW / HK

theme: smartblue highlight: agate


我報名參加金石計劃1期挑戰——瓜分10萬獎池,這是我的第2篇文章,點選檢視活動詳情

Android ImageView到底怎麼設定圓角圖片?背景圓角?控制元件圓角?圖片圓角?佔位圖圓角?

前言

在我們實際開發應用的過程中,我想大家或多或少都遇到過需要載入圓角圖片的場景,還有一些圖片是四周圓角不對稱,異性圓角等等情況。

我們可能會去網上或Github上搜索一些RoundImageView之類的第三方自定義View,有效果就行,如果沒有效果就換一個庫,總有能行的庫是吧。

我早幾年前做專案就遇到這樣的場景,設定背景之後,在上面載入圓角的圖片,但是背景會漏出來,好不容易讓UI切圖解決之後,佔位圖又出問題,佔位圖不是圓角的就把整個佈局蓋住了,不符合UI的美學,哎,我要吐了。

我們需要了解他們的大致原理,載入圓角的圖片有幾種思路,每一種思路又又幾種實現的方案?各種方案如何實現的?有什麼差異?

其實圓角圖片的載入有兩種思路,一種是載入的過程中對Bitmap做裁剪,另一種是Bitmap沒有裁剪,但是對ImageView顯示的時候做裁剪。

例如第一種思路,我們使用Glide圖片載入庫來處理圓角。

例如第二種思路,我們常用RoundImageView之類的自定義View來實現。

而第二種思路又有不同的方案實現,相容性和目標性也不一致,如果你找到的第三庫不能達到全部的效果,那應該怎麼辦?

所以現在我就要想一個通用的解決方案,我想要背景圓角,佔位圖圓角,圖片內容圓角,全部的功能,我全都想要!

話不多說,先看看自定義ImageView的幾種方案的簡單示例。

一、幾種圓角ImageView的定義方式

通常來說,我們定義一個控制元件的圓角裁剪有三種方案。 ClipPath Xfermode Shader

(PS.其實還有一種ViewOutlineProvider 的方案,由於要求21以上才能用,所以這裡沒有統計)

下面我們看看三者分別如何定義:

ClipPath 直接剪輯路線: ``` java public class ClipPathRoundImageView extends AppCompatImageView {

float width, height;
private int mRoundRadius = CommUtils.dip2px(15);

public ClipPathRoundImageView(Context context) {
    this(context, null);
}

public ClipPathRoundImageView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public ClipPathRoundImageView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //在api11到api18之間設定禁用硬體加速
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
            && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    width = getWidth();
    height = getHeight();
}

@Override
protected void onDraw(Canvas canvas) {
    if (width > mRoundRadius && height > mRoundRadius) {
        Path path = new Path();
        path.moveTo(mRoundRadius, 0);

        path.lineTo(width - mRoundRadius, 0);  //上側的橫線

        path.quadTo(width, 0, width, mRoundRadius);  //右上的圓弧曲線

        path.lineTo(width, height - mRoundRadius);  //右側的直線

        path.quadTo(width, height, width - mRoundRadius, height);  //右下的圓弧曲線

        path.lineTo(mRoundRadius, height);  //下側的橫線

        path.quadTo(0, height, 0, height - mRoundRadius);  //左下的圓弧曲線

        path.lineTo(0, mRoundRadius);       //左側的直線

        path.quadTo(0, 0, mRoundRadius, 0);   //左上的圓弧曲線

        canvas.clipPath(path);
    }

    super.onDraw(canvas);
}

}

```

Xfermode 設定混合模式: ``` java public class XfermodeRoundImageView extends AppCompatImageView {

private Paint mPaint;
private Xfermode mXfermode;
private int mRoundRadius = CommUtils.dip2px(15);

public XfermodeRoundImageView(Context context) {
    this(context, null);
}

public XfermodeRoundImageView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public XfermodeRoundImageView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //在api11到api18之間設定禁用硬體加速
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
            && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }

    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
}

@Override
protected void onDraw(Canvas canvas) {

    if (getDrawable() == null) {
        return;
    }

    int sc = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
    //畫源影象,為一個圓角矩形
    canvas.drawRoundRect(new RectF(0, 0, getWidth(), getHeight()), mRoundRadius, mRoundRadius, mPaint);
    //設定混合模式
    mPaint.setXfermode(mXfermode);
    //畫目標影象
    canvas.drawBitmap(drawableToBitamp(exChangeSize(getDrawable())), 0, 0, mPaint);
    // 還原混合模式
    mPaint.setXfermode(null);
    canvas.restoreToCount(sc);

}

/**
 * 圖片拉昇
 */
private Drawable exChangeSize(Drawable drawable) {
    float scale = 1.0f;
    scale = Math.max(getWidth() * 1.0f / drawable.getIntrinsicWidth(), getHeight()
            * 1.0f / drawable.getIntrinsicHeight());
    drawable.setBounds(0, 0, (int) (scale * drawable.getIntrinsicWidth()),
            (int) (scale * drawable.getIntrinsicHeight()));
    return drawable;
}


private Bitmap drawableToBitamp(Drawable drawable) {
    if (drawable instanceof BitmapDrawable) {
        BitmapDrawable bd = (BitmapDrawable) drawable;
        return bd.getBitmap();
    }
    int w = drawable.getIntrinsicWidth() <= 0 ? getWidth() : drawable.getIntrinsicWidth();
    int h = drawable.getIntrinsicHeight() <= 0 ? getHeight() : drawable.getIntrinsicHeight();
    Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    drawable.setBounds(0, 0, w, h);
    drawable.draw(canvas);
    return bitmap;
}

} ```

BitmapShader 指定範圍渲染 ``` java public class ShaderRoundImageView extends AppCompatImageView {

//圓角大小,預設為10
private int mRoundRadius = CommUtils.dip2px(15);

private Paint mPaint;

// 3x3 矩陣,主要用於縮小放大
private Matrix mMatrix;

//渲染影象,使用影象為繪製圖形著色
private BitmapShader mBitmapShader;

public ShaderRoundImageView(Context context) {
    this(context, null);
}

public ShaderRoundImageView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public ShaderRoundImageView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    mMatrix = new Matrix();
    mPaint = new Paint();
    mPaint.setAntiAlias(true);

}

@Override
protected void onDraw(Canvas canvas) {
    if (getDrawable() == null) {
        return;
    }
    Bitmap bitmap = drawableToBitamp(getDrawable());
    mBitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    float scale = 1.0f;
    if (!(bitmap.getWidth() == getWidth() && bitmap.getHeight() == getHeight())) {
        // 如果圖片的寬或者高與view的寬高不匹配,計算出需要縮放的比例;縮放後的圖片的寬高,一定要大於我們view的寬高;所以我們這裡取大值;
        scale = Math.max(getWidth() * 1.0f / bitmap.getWidth(),
                getHeight() * 1.0f / bitmap.getHeight());
    }
    // shader的變換矩陣,我們這裡主要用於放大或者縮小
    mMatrix.setScale(scale, scale);
    // 設定變換矩陣
    mBitmapShader.setLocalMatrix(mMatrix);
    // 設定shader
    mPaint.setShader(mBitmapShader);
    canvas.drawRoundRect(new RectF(0, 0, getWidth(), getHeight()), mRoundRadius, mRoundRadius, mPaint);
}


private Bitmap drawableToBitamp(Drawable drawable) {
    if (drawable instanceof BitmapDrawable) {
        BitmapDrawable bd = (BitmapDrawable) drawable;
        return bd.getBitmap();
    }
    int w = drawable.getIntrinsicWidth() <= 0 ? getWidth() : drawable.getIntrinsicWidth();
    int h = drawable.getIntrinsicHeight() <= 0 ? getHeight() : drawable.getIntrinsicHeight();
    Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    drawable.setBounds(0, 0, w, h);
    drawable.draw(canvas);
    return bitmap;
}

} ```

前兩種方案在一些版本中需要配合關閉硬體加速,BitmapShared的方案相容性更好。

在xml中的順序如下

效果:

此時我們載入網路圖片,並且不設定裁剪模式,我們加上圓形的圖片做對比,他們的展示如下:

加上裁剪模式為居中裁剪之後,他們的效果如下:

簡單的使用確實是可以用了。

如果加上backgroundColor之後呢?

醜,背景無法裁剪!這個問題後面單獨再說。

那如果我們不使用自定義View,就是預設的ImageView,我們還能不能實現圖片圓角的展示?

二、內容圓角之Glide的transforms

當然可以,我們可以直接對Bitmap操作,裁剪Bitmap得到一個圓角的Bitmap。

例如我們常用的Glide圖片載入框架,我們可以通過transforms的轉換得到對應的圓角圖片

java transforms(CenterCrop(), RoundedCorners(roundRadius))

具體執行的原始碼如下 ```java public static Bitmap roundedCorners( @NonNull BitmapPool pool, @NonNull Bitmap inBitmap, final int roundingRadius) { Preconditions.checkArgument(roundingRadius > 0, "roundingRadius must be greater than 0.");

return roundedCorners(
    pool,
    inBitmap,
    new DrawRoundedCornerFn() {
      @Override
      public void drawRoundedCorners(Canvas canvas, Paint paint, RectF rect) {
        canvas.drawRoundRect(rect, roundingRadius, roundingRadius, paint);
      }
    });

}

public static Bitmap roundedCorners( @NonNull BitmapPool pool, @NonNull Bitmap inBitmap, final float topLeft, final float topRight, final float bottomRight, final float bottomLeft) { return roundedCorners( pool, inBitmap, new DrawRoundedCornerFn() { @Override public void drawRoundedCorners(Canvas canvas, Paint paint, RectF rect) { Path path = new Path(); path.addRoundRect( rect, new float[] { topLeft, topLeft, topRight, topRight, bottomRight, bottomRight, bottomLeft, bottomLeft }, Path.Direction.CW); canvas.drawPath(path, paint); } }); } ```

一種是四角固定圓角,一種是分別設定圓角。我們對比一下效果:

效果:

確實是可以達到效果,背景一樣無法裁剪。可以想象,但是這種情況有一種問題,就是佔位圖的問題,由於ImageView就是一個標準的正方形,所以如果我們使用統一的長方形佔位圖,那麼就會出現問題。

明明我想要的是圓角20的圖片,可是佔位圖確實方的,如果是錯誤圖片,就會覺得很突兀。

可以看到只有第一種方案的圓角ImageView,它的佔位圖是符合想象的。

三、如何設定圓角背景與佔位圖

如何設定圓角的背景與佔位圖呢?

  1. 最簡單的,找UI去切圖,每一個比例,每一個圓角,都可以要對應的佔位圖和背景圖。也是我們最常用的方式。
  2. 使用一個圓角的父容器包裹一層,由於父容器裁剪了展示的範圍,所以子ImageView作為一個正常的非自定義View就可以方便的實現各種圓角。但是這種方案嵌套了一層,並且在需求複雜的情況下,比如各個圓角不同的方案下使用相對麻煩。
  3. 自定義RoundImageView,做一個自己的背景,不使用View的setBackground,或者重寫View的setBackground,去呼叫自己的背景方法,自己的背景方法中我們自己draw一個背景的圖層。

這裡演示的是第三種方案,我們自己先 draw 一個rect當背景,然後再 draw 一個指定的Bitmap當內容,即可實現效果。部分核心程式碼如下:

```java @Override public void setBackgroundColor(int color) { setRoundBackgroundColor(color); }

public int getRoundBackgroundColor() {
    return mRoundBackgroundColor;
}

public void setRoundBackgroundColor(@ColorInt int roundBackgroundColor) {
    if (roundBackgroundColor == mRoundBackgroundColor) {
        return;
    }

    mRoundBackgroundColor = roundBackgroundColor;
    mRoundBackgroundPaint.setColor(roundBackgroundColor);
    invalidate();
}

public void setRoundBackgroundColorResource(@ColorRes int circleBackgroundRes) {
    setRoundBackgroundColor(getContext().getResources().getColor(circleBackgroundRes));
}

private void init(){
    mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

    mBitmapPaint.setAntiAlias(true);
    mBitmapPaint.setShader(mBitmapShader);

    mRoundBackgroundPaint.setStyle(Paint.Style.FILL);
    mRoundBackgroundPaint.setAntiAlias(true);
    mRoundBackgroundPaint.setColor(mRoundBackgroundColor);
}

@Override
protected void onDraw(Canvas canvas) {

    if (mBitmap == null) {
        return;
    }

    if (mRoundBackgroundColor != Color.TRANSPARENT) {
        canvas.drawRoundRect(mDrawableRect, mRoundRadius, mRoundRadius, mRoundBackgroundPaint);
    }

    canvas.drawRoundRect(mDrawableRect, mRoundRadius, mRoundRadius, mBitmapPaint);

}

```

這裡我只做了背景color的用法,如果想要drawable背景的話,也是一樣的道理那我們drawBackground的時候就是draw一個背景的bitmap即可,和內容Bitmap的繪製類似。

效果:

佔位圖:

四、如何設定四個角不同圓角

看到之前我們的三種方案,大部分都是在 drawRoundRect 既然是這樣那麼就只能保證一種圓角角度,如果想四個角分別是不同的圓角,那麼我們就只能用其中 clipPath 的方案,它是Path的路徑繪製,可以很方便的按照路徑裁剪指定的圓角。

我們自定義View如下: ```java /* * 可以自定義四個角的不同的圓角值 / public class CustomRadiusImageView extends AppCompatImageView { private float width, height; private int defaultRadius = 0; private int radius; private int leftTopRadius; private int rightTopRadius; private int rightBottomRadius; private int leftBottomRadius;

public CustomRadiusImageView(Context context) {
    this(context, null);
    init(context, null);
}

public CustomRadiusImageView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    init(context, attrs);
}

public CustomRadiusImageView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context, attrs);
}

private void init(Context context, AttributeSet attrs) {
    //在api11到api18之間設定禁用硬體加速
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
            && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }

    //讀取配置屬性
    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomRadiusImageView);
    radius = array.getDimensionPixelOffset(R.styleable.CustomRadiusImageView_radius, defaultRadius);
    leftTopRadius = array.getDimensionPixelOffset(R.styleable.CustomRadiusImageView_left_top_radius, defaultRadius);
    rightTopRadius = array.getDimensionPixelOffset(R.styleable.CustomRadiusImageView_right_top_radius, defaultRadius);
    rightBottomRadius = array.getDimensionPixelOffset(R.styleable.CustomRadiusImageView_right_bottom_radius, defaultRadius);
    leftBottomRadius = array.getDimensionPixelOffset(R.styleable.CustomRadiusImageView_left_bottom_radius, defaultRadius);

    //設定四個角的預設圓角值
    if (defaultRadius == leftTopRadius) {
        leftTopRadius = radius;
    }
    if (defaultRadius == rightTopRadius) {
        rightTopRadius = radius;
    }
    if (defaultRadius == rightBottomRadius) {
        rightBottomRadius = radius;
    }
    if (defaultRadius == leftBottomRadius) {
        leftBottomRadius = radius;
    }

    array.recycle();
}


@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    width = getWidth();
    height = getHeight();
}

@Override
protected void onDraw(Canvas canvas) {

    int maxLeft = Math.max(leftTopRadius, leftBottomRadius);
    int maxRight = Math.max(rightTopRadius, rightBottomRadius);
    int minWidth = maxLeft + maxRight;
    int maxTop = Math.max(leftTopRadius, rightTopRadius);
    int maxBottom = Math.max(leftBottomRadius, rightBottomRadius);
    int minHeight = maxTop + maxBottom;
    if (width >= minWidth && height > minHeight) {

        Path path = new Path();
        path.moveTo(leftTopRadius, 0);
        path.lineTo(width - rightTopRadius, 0);
        path.quadTo(width, 0, width, rightTopRadius);

        path.lineTo(width, height - rightBottomRadius);
        path.quadTo(width, height, width - rightBottomRadius, height);

        path.lineTo(leftBottomRadius, height);
        path.quadTo(0, height, 0, height - leftBottomRadius);

        path.lineTo(0, leftTopRadius);
        path.quadTo(0, 0, leftTopRadius, 0);

        canvas.clipPath(path);
    }
    super.onDraw(canvas);
}

} ```

使用:

效果:

可以看到除了背景無法圓角之外,可以完美的滿足我們的需求了?為什麼背景無法設定,因為背景無法通過 drawRoundRect 繪製出來,Bitmap也不能通過 drawRoundRect 繪製不同的圓角。

之前我們講過,其實三種方案 Shader 的相容性是相對較好的,那我們就想通過Shader的方案來繪製不同的圓角Bitmap,順便繪製我們的自定義背景不就完美解決問題了嗎?

問題就來到如何繪製不同圓角上來了,我們都知道drawCircle drawRect drawRoundRect來繪製一些簡單的規則的圖形,如果是不規則的圖形,我們就需要用到DrawPath來完成,我們把Path中指定Rect的時候指定不同的圓角不就行了嗎,然後再通過drawPath繪製出來。

核心程式碼如下:

java Path path = new Path(); path.addRoundRect( mDrawableRect, new float[]{mTopLeft, mTopLeft, mTopRight, mTopRight, mBottomRight, mBottomRight, mBottomLeft, mBottomLeft}, Path.Direction.CW); canvas.drawPath(path, mBitmapPaint);

我們就能使用 Shader 的方案來實現自定義圓角View,這是最完美的方案,我們可以設定背景圓角,控制元件圓角,還能指定各自的圓角角度。

使用:

上面的載入圖片,下面的載入佔位圖,兩者都已經設定了背景 ```kotlin findViewById(R.id.iv_custom_round).setRoundBackgroundColorResource(R.color.picture_color_blue) findViewById(R.id.iv_custom_round).extLoad(imgUrl, R.drawable.test_img_placeholder)

    findViewById<RoundCircleImageView>(R.id.iv_custom_round2).setRoundBackgroundColorResource(R.color.picture_color_blue)
    findViewById<RoundCircleImageView>(R.id.iv_custom_round2).extLoad("123", R.drawable.test_img_placeholder)

```

效果:

五、推薦的最佳方案方案完整程式碼

在 Shader 方案的基礎上,我們實現了一些簡單的實現,現在我們進一步封裝。

我們順便加上圓形圖片的繪製,圓角的圖片繪製,各自圓角的繪製,和對應的背景的圓角,圖片背景與顏色背景設定等一系列功能。

```java /* * 只能支援四周固定的圓角,或者圓形的圖片 * 支援設定自定義圓角或圓形背景顏色,(需要使用當前類提供的背景顏色方法) *

* 支援四個角各自定義角度 / public class RoundCircleImageView extends AppCompatImageView {

private int mRoundRadius = 0;
private boolean isCircleType;
private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;

private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
private static final int COLORDRAWABLE_DIMENSION = 2;

private final RectF mDrawableRect = new RectF();
private final Matrix mShaderMatrix = new Matrix();

private final Paint mBitmapPaint = new Paint();
private final Paint mRoundBackgroundPaint = new Paint();

private Drawable mRoundBackgroundDrawable;

private Bitmap mBitmap;
private BitmapShader mBitmapShader;
private int mBitmapWidth;
private int mBitmapHeight;

private Bitmap mBackgroundBitmap;
private BitmapShader mBackgroundBitmapShader;

private ColorFilter mColorFilter;
private float mDrawableRadius;
private boolean mReady;
private boolean mSetupPending;
private float mTopLeft;
private float mTopRight;
private float mBottomLeft;
private float mBottomRight;


public RoundCircleImageView(Context context) {
    super(context);
    init();
}

public RoundCircleImageView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public RoundCircleImageView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);

    //讀取配置屬性
    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RoundCircleImageView);
    mRoundRadius = array.getDimensionPixelOffset(R.styleable.RoundCircleImageView_round_radius, 0);
    mTopLeft = array.getDimensionPixelOffset(R.styleable.RoundCircleImageView_topLeft, 0);
    mTopRight = array.getDimensionPixelOffset(R.styleable.RoundCircleImageView_topRight, 0);
    mBottomLeft = array.getDimensionPixelOffset(R.styleable.RoundCircleImageView_bottomLeft, 0);
    mBottomRight = array.getDimensionPixelOffset(R.styleable.RoundCircleImageView_bottomRight, 0);

    if (array.hasValue(R.styleable.RoundCircleImageView_round_background_color)) {
        int roundBackgroundColor = array.getColor(R.styleable.RoundCircleImageView_round_background_color, Color.TRANSPARENT);
        mRoundBackgroundDrawable = new ColorDrawable(roundBackgroundColor);
        mBackgroundBitmap = getBitmapFromDrawable(mRoundBackgroundDrawable);
    }

    if (array.hasValue(R.styleable.RoundCircleImageView_round_background_drawable)) {
        mRoundBackgroundDrawable = array.getDrawable(R.styleable.RoundCircleImageView_round_background_drawable);
        mBackgroundBitmap = getBitmapFromDrawable(mRoundBackgroundDrawable);
    }

    isCircleType = array.getBoolean(R.styleable.RoundCircleImageView_isCircle, false);

    array.recycle();

    init();
}

private void init() {
    super.setScaleType(SCALE_TYPE);
    mReady = true;

    if (mSetupPending) {
        setup();
        mSetupPending = false;
    }
}

@Override
public ScaleType getScaleType() {
    return SCALE_TYPE;
}

@Override
public void setScaleType(ScaleType scaleType) {
    if (scaleType != SCALE_TYPE) {
        throw new IllegalArgumentException("已經自帶設定了,你無需再設定了");
    }
}

@Override
public void setAdjustViewBounds(boolean adjustViewBounds) {
    if (adjustViewBounds) {
        throw new IllegalArgumentException("已經自帶設定了,你無需再設定了");
    }
}

@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {

    if (mBitmap == null && mBackgroundBitmap == null) {
        return;
    }

    if (isCircleType) {

        if (mRoundBackgroundDrawable != null && mBackgroundBitmap != null) {
            canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mRoundBackgroundPaint);
        }

        if (mBitmap != null) {
            canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
        }

    } else {

        if (mTopLeft > 0 || mTopRight > 0 || mBottomLeft > 0 || mBottomRight > 0) {
            //使用單獨的圓角
            if (mRoundBackgroundDrawable != null && mBackgroundBitmap != null) {
                Path path = new Path();
                path.addRoundRect(
                        mDrawableRect,
                        new float[]{mTopLeft, mTopLeft, mTopRight, mTopRight, mBottomRight, mBottomRight, mBottomLeft, mBottomLeft},
                        Path.Direction.CW);
                canvas.drawPath(path, mRoundBackgroundPaint);
            }

            if (mBitmap != null) {
                Path path = new Path();
                path.addRoundRect(
                        mDrawableRect,
                        new float[]{mTopLeft, mTopLeft, mTopRight, mTopRight, mBottomRight, mBottomRight, mBottomLeft, mBottomLeft},
                        Path.Direction.CW);
                canvas.drawPath(path, mBitmapPaint);
            }


        } else {
            //使用統一的圓角
            if (mRoundBackgroundDrawable != null && mBackgroundBitmap != null) {
                canvas.drawRoundRect(mDrawableRect, mRoundRadius, mRoundRadius, mRoundBackgroundPaint);
            }

            if (mBitmap != null) {
                canvas.drawRoundRect(mDrawableRect, mRoundRadius, mRoundRadius, mBitmapPaint);
            }
        }

    }

}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    setup();
}

@Override
public void setPadding(int left, int top, int right, int bottom) {
    super.setPadding(left, top, right, bottom);
    setup();
}

@Override
public void setPaddingRelative(int start, int top, int end, int bottom) {
    super.setPaddingRelative(start, top, end, bottom);
    setup();
}

@Override
public void setBackgroundColor(int color) {
    setRoundBackgroundColor(color);
}

@Override
public void setBackground(Drawable background) {
    setRoundBackgroundDrawable(background);
}

@Override
public void setBackgroundDrawable(@Nullable Drawable background) {
    setRoundBackgroundDrawable(background);
}

@Override
public void setBackgroundResource(int resId) {
    @SuppressLint("UseCompatLoadingForDrawables")
    Drawable drawable = getContext().getResources().getDrawable(resId);
    setRoundBackgroundDrawable(drawable);
}

@Override
public Drawable getBackground() {
    return getRoundBackgroundDrawable();
}

public void setRoundBackgroundColor(@ColorInt int roundBackgroundColor) {
    ColorDrawable drawable = new ColorDrawable(roundBackgroundColor);
    setRoundBackgroundDrawable(drawable);
}

public void setRoundBackgroundColorResource(@ColorRes int circleBackgroundRes) {
    setRoundBackgroundColor(getContext().getResources().getColor(circleBackgroundRes));
}

public Drawable getRoundBackgroundDrawable() {
    return mRoundBackgroundDrawable;
}

public void setRoundBackgroundDrawable(Drawable drawable) {
    mRoundBackgroundDrawable = drawable;
    initializeBitmap();
}

@Override
public void setImageBitmap(Bitmap bm) {
    super.setImageBitmap(bm);
    initializeBitmap();
}

@Override
public void setImageDrawable(Drawable drawable) {
    super.setImageDrawable(drawable);
    initializeBitmap();
}

@Override
public void setImageResource(@DrawableRes int resId) {
    super.setImageResource(resId);
    initializeBitmap();
}

@Override
public void setImageURI(Uri uri) {
    super.setImageURI(uri);
    initializeBitmap();
}

@Override
public void setColorFilter(ColorFilter cf) {
    if (cf == mColorFilter) {
        return;
    }

    mColorFilter = cf;
    applyColorFilter();
    invalidate();
}

@Override
public ColorFilter getColorFilter() {
    return mColorFilter;
}

private void applyColorFilter() {
    if (mBitmapPaint != null) {
        mBitmapPaint.setColorFilter(mColorFilter);
    }
}

private Bitmap getBitmapFromDrawable(Drawable drawable) {
    if (drawable == null) {
        return null;
    }

    if (drawable instanceof BitmapDrawable) {
        return ((BitmapDrawable) drawable).getBitmap();
    }

    try {
        Bitmap bitmap;

        if (drawable instanceof ColorDrawable) {
            bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
        } else {
            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
        }

        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

private void initializeBitmap() {

    mBitmap = getBitmapFromDrawable(getDrawable());

    if (mRoundBackgroundDrawable != null) {
        mBackgroundBitmap = getBitmapFromDrawable(mRoundBackgroundDrawable);
    }

    setup();
}

private void setup() {
    if (!mReady) {
        mSetupPending = true;
        return;
    }

    if (getWidth() == 0 && getHeight() == 0) {
        return;
    }

    if (mBitmap == null && mBackgroundBitmap == null) {
        invalidate();
        return;
    }

    if (mBitmap != null) {
        mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        mBitmapPaint.setAntiAlias(true);
        mBitmapPaint.setShader(mBitmapShader);
    }

    if (mRoundBackgroundDrawable != null && mBackgroundBitmap != null) {
        mBackgroundBitmapShader = new BitmapShader(mBackgroundBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        mRoundBackgroundPaint.setAntiAlias(true);
        mRoundBackgroundPaint.setShader(mBackgroundBitmapShader);
    }

    Bitmap bitmap = mBitmap != null ? mBitmap : mBackgroundBitmap;
    mBitmapHeight = bitmap.getHeight();
    mBitmapWidth = bitmap.getWidth();

    mDrawableRect.set(calculateBounds());
    mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);

    applyColorFilter();
    updateShaderMatrix();
    //重繪
    invalidate();
}

private RectF calculateBounds() {
    int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
    int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();

    int sideLength = Math.min(availableWidth, availableHeight);

    float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
    float top = getPaddingTop() + (availableHeight - sideLength) / 2f;

    return new RectF(left, top, left + sideLength, top + sideLength);
}

private void updateShaderMatrix() {
    float scale;
    float dx = 0;
    float dy = 0;

    mShaderMatrix.set(null);

    if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
        scale = mDrawableRect.height() / (float) mBitmapHeight;
        dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
    } else {
        scale = mDrawableRect.width() / (float) mBitmapWidth;
        dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
    }

    mShaderMatrix.setScale(scale, scale);
    mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);

    if (mBitmapShader != null) {
        mBitmapShader.setLocalMatrix(mShaderMatrix);
    }
    if (mBackgroundBitmapShader != null) {
        mBackgroundBitmapShader.setLocalMatrix(mShaderMatrix);
    }
}

} ```

自定義屬性: xml <!-- 自定義圓角佈局 ,可以設定圓角,圓形 --> <declare-styleable name="RoundCircleImageView"> <attr name="isCircle" format="boolean" /> <attr name="round_background_color" format="color" /> <attr name="round_background_drawable" format="reference" /> <attr name="round_radius" format="dimension" /> <attr name="topLeft" format="dimension" /> <attr name="topRight" format="dimension" /> <attr name="bottomRight" format="dimension" /> <attr name="bottomLeft" format="dimension" /> </declare-styleable>

由於設定圖片與佔位圖上面都已經展示過了,這裡只演示背景設定的效果:

支援xml設定

效果:

分別是設定ColorDrawable、Drawable、Color 三種設定方式。

同時也支援程式碼中設定:

```kotlin findViewById(R.id.iv_custom_round).background = drawable(R.drawable.shape_blue)

findViewById(R.id.iv_custom_round2).background = drawable(R.drawable.chengxiao) ```

有需要可以直接拿走,這應該是非常好用的實戰可用的RoundImageView了。如果覺得複製都嫌麻煩,可以去下面檢視原始碼獲取。

總結

目前來說想要展示圓角的圖片,我們就是兩種思路。

  1. 在圖片載入的過程中,直接對Bitmap做處理,得到一個圓角的Bitmap。載入到對應的ImageView中。
  2. 圖片載入的是標準的圖片,通過自定義View的方式,裁剪顯示的區域,從而到達看起來圓角圖片的效果。

總的來說這兩種思路都可以達到目標,但是又有一點差異,大家注意一下兩種方案都可用,自己選擇即可。

第一種思路:對背景和佔位圖有需求,最好是找UI切圖,對應的比例的圓角背景圖和圓角佔位圖,如果一個專案有多個不同的比例的,不同圓角的設計圖,那麼可以想象專案中需要匯入多少圖片,Apk會變大。

第二種思路:我們直接使用預設的一個背景,和一個預設的佔位圖,讓自定義View去居中裁剪展示背景與佔位圖,相對來說簡單一點,但是需要注意的是這樣的自定義View網上太多方案,不知道哪一種好,所以本篇文章就是圍繞這一點展開。

自定義View終極方案是 BitmapShader 的方案,圍繞這一點又封裝了一套自定義View。支援設定圓形圖片、背景、佔位圖。支援圓角圖片、背景、佔位圖。還支援了四周不同圓角的設定。

所以大家的專案中都是用的哪一種思路,又是哪一種方案呢?

本文全部程式碼均以開源,原始碼在此。大家可以點個Star關注一波。

單獨的 RoundCircleImageView 的原始碼在此。如果不想Copy程式碼,我已經把程式碼推到 MavenCentral ,大家也可以使用遠端依賴。

implementation "io.github.liukai2530533:round_circle_imageview:1.0.1"

具體的使用本文上面已經說明了,大家也可以去專案裡看看。

如果感覺本文對你有一點點的啟發,還望你能點贊支援一下,你的支援是我最大的動力。

Ok,這一期就此完結。

「其他文章」