一次搞懂怎麼設定圓角圖片,ImageView的各種圓角設定
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,它的佔位圖是符合想象的。
三、如何設定圓角背景與佔位圖
如何設定圓角的背景與佔位圖呢?
- 最簡單的,找UI去切圖,每一個比例,每一個圓角,都可以要對應的佔位圖和背景圖。也是我們最常用的方式。
- 使用一個圓角的父容器包裹一層,由於父容器裁剪了展示的範圍,所以子ImageView作為一個正常的非自定義View就可以方便的實現各種圓角。但是這種方案嵌套了一層,並且在需求複雜的情況下,比如各個圓角不同的方案下使用相對麻煩。
- 自定義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
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
findViewById
有需要可以直接拿走,這應該是非常好用的實戰可用的RoundImageView了。如果覺得複製都嫌麻煩,可以去下面檢視原始碼獲取。
總結
目前來說想要展示圓角的圖片,我們就是兩種思路。
- 在圖片載入的過程中,直接對Bitmap做處理,得到一個圓角的Bitmap。載入到對應的ImageView中。
- 圖片載入的是標準的圖片,通過自定義View的方式,裁剪顯示的區域,從而到達看起來圓角圖片的效果。
總的來說這兩種思路都可以達到目標,但是又有一點差異,大家注意一下兩種方案都可用,自己選擇即可。
第一種思路:對背景和佔位圖有需求,最好是找UI切圖,對應的比例的圓角背景圖和圓角佔位圖,如果一個專案有多個不同的比例的,不同圓角的設計圖,那麼可以想象專案中需要匯入多少圖片,Apk會變大。
第二種思路:我們直接使用預設的一個背景,和一個預設的佔位圖,讓自定義View去居中裁剪展示背景與佔位圖,相對來說簡單一點,但是需要注意的是這樣的自定義View網上太多方案,不知道哪一種好,所以本篇文章就是圍繞這一點展開。
自定義View終極方案是 BitmapShader 的方案,圍繞這一點又封裝了一套自定義View。支援設定圓形圖片、背景、佔位圖。支援圓角圖片、背景、佔位圖。還支援了四周不同圓角的設定。
所以大家的專案中都是用的哪一種思路,又是哪一種方案呢?
本文全部程式碼均以開源,原始碼在此。大家可以點個Star關注一波。
單獨的 RoundCircleImageView 的原始碼在此。如果不想Copy程式碼,我已經把程式碼推到 MavenCentral ,大家也可以使用遠端依賴。
implementation "io.github.liukai2530533:round_circle_imageview:1.0.1"
具體的使用本文上面已經說明了,大家也可以去專案裡看看。
如果感覺本文對你有一點點的啟發,還望你能點贊
支援一下,你的支援是我最大的動力。
Ok,這一期就此完結。
- Android操作檔案也太難了趴,File vs DocumentFile 以及 DocumentsProvider vs FileProvider 的異同
- findViewById不香嗎?為什麼要把簡單的問題複雜化?為什麼要用DataBinding?
- Android自定義View繪製進階-水波浪溫度刻度表
- Android自定義ViewGroup佈局進階,完整的九宮格實現
- 記錄仿抖音的視訊播放並快取預載入視訊的效果實現
- Kotlin物件的懶載入方式?by lazy 與 lateinit 的異同
- 定位都得整合第三方?Android原生定位服務LocationManager不行嗎?
- 還用第三方庫管理狀態列嗎?Android關於狀態列管理的幾種方案實現!
- 下載需要整合第三方?Android原生下載服務DownloadManager不行嗎?
- Android陰影實現的幾種方案-自定義圓角ViewGroup加入陰影效果
- 操作Android視窗的幾種方式?WindowInsets與其相容庫的使用與踩坑
- Android軟鍵盤與佈局的協調-不同的效果與實現方案的探討
- ViewPager2:ViewPager都能自動巢狀滾動了,我不行?我麻了!該怎麼做?
- Android軟鍵盤的監聽與高度控制的幾種方案及常用效果
- 圓角升級啦,來手把手一起實現自定義ViewGroup的各種圓角與背景
- Android導航欄的處理-HostStatusLayout加入底部的導航欄適配
- 一次搞懂怎麼設定圓角圖片,ImageView的各種圓角設定
- 一看就會 Android框架DataBinding的使用與封裝
- 別濫用FileProvider了,Android中FileProvider的各種場景應用
- Android登入攔截的場景-基於攔截器模式實現