Android自定義圓角圓形圖片
說起Android裡面的自定義圓角圓形圖片,已經算是老生常談的話題了,之前一直使用別人的,最近使用的時候發現自己居然沒有一個這樣屬於自己的工具庫,實在遺憾,畢竟還是自己的東西用起來最順手,所以就打造了一個,先來看看效果:
怎麼樣,還不錯吧~支援各種圖案,邊框,各種圓角。其中原理也是很簡單的,無非就是canvas知識的應用。接下來我們一個一個形狀來看,首先是圓形,這個應該是最簡單的了,直接使用canvas的drawCircle來繪製一個圓形就搞定了,看程式碼:
float r = hasBorder ? width / 2f - borderWidth : width / 2f;
canvas.drawCircle(width / 2f , height / 2f, r, mPaintDrawable);
其中的r就是圓的半徑,這裡我們用一個變數hasBorder 來區分是否繪製邊框(邊框後面細說),drawCircle的前兩個引數就是圓心座標,最後一個引數則是畫筆,我們的圖片呢???確定這樣就能畫出來圓形圖片???其實圖片被我們封裝在筆刷裡面了:BitmapShader。這個類的使用很簡單,官方文件是這樣解釋的:
Shader used to draw a bitmap as a texture.
就是使用特定的圖片來作為紋理使用。這裡就不再詳細解釋這個類的使用了,不懂的同學可以參考文章最後的連結。其實除了使用BitmapShader還有另外一種方案,就是PorterDuffXfermode,感興趣的可以參考我之前的一篇文章:
下面來看一下獲取Bitmap的方法:
/**
* 獲取imageview設定的圖片(針對大圖,此時必須已經處理過了,否則會造成記憶體溢位)
* <p>
* 獲取bitmap:
* 1、如果設定了src為圖片則返回該圖片,
* 2、如果設定了src為顏色值則返回顏色值,
* 3、如果沒有設定src,則返回預設顏色值(未設定則為透明)
*
* @param drawable
* @return
*/
private Bitmap getBitmap(Drawable drawable) {
Bitmap bitmap = null;
if (drawable instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) drawable).getBitmap();
} else if (drawable instanceof ColorDrawable) {
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bitmap);
int color = ((ColorDrawable) drawable).getColor();
c.drawARGB(Color.alpha(color), Color.red(color), Color.green(color), Color.blue(color));
} else {
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bitmap);
c.drawARGB(Color.alpha(defaultColor), Color.red(defaultColor), Color.green(defaultColor), Color.blue(defaultColor));
}
if (isBlur) {
//高斯模糊
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
try {
bitmap = RSBlur.blur(getContext(), bitmap, (int) blurRadius);
} catch (Exception e) {
bitmap = FastBlur.blur(bitmap, (int) blurRadius, true);
}
} else {
bitmap = FastBlur.blur(bitmap, (int) blurRadius, true);
}
}
return bitmap;
}
這裡就是獲取imageview設定的圖片,然後就可以隨意繪製我們想要的效果了,這裡我們取圖片的中間區域進行繪製,可以防止圖片跟檢視大小不一樣(前提是我們已經根據imageView的寬高去縮放過圖片了,這裡處理的只是繪製部分,類似imageView的centerCrop),看程式碼:
/**
* 處理圖片大於或者小於控制元件的情況
* (不是針對大圖記憶體溢位的處理,此處的處理只是為了讓圖片居中繪製——centerCrop:參照imageView的處理)
*
* @param bitmap
*/
private void setUpShader(Bitmap bitmap) {
mBitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
int dWidth = bitmap.getWidth();
int dHeight = bitmap.getHeight();
int vWidth = width;
int vHeight = height;
if (hasBorder) {
vWidth -= 2 * borderWidth;
vHeight -= 2 * borderWidth;
}
if (dWidth == vWidth && dHeight == vHeight) {
} else {
float scale = 1.0f;
float dx = 0, dy = 0;
if (dWidth * vHeight > vWidth * dHeight) {
scale = (float) vHeight / (float) dHeight;
dx = (vWidth - dWidth * scale) * 0.5f;
} else {
scale = (float) vWidth / (float) dWidth;
dy = (vHeight - dHeight * scale) * 0.5f;
}
mMatrix.setScale(scale, scale);
if (hasBorder) {//有邊框的情況,view檢視縮小了,所以需要對圖片移動一個邊框寬度的處理
dx += borderWidth;
dy += borderWidth;
}
//上一個縮放操作完成之後,進行移動(把圖片中心與檢視中心對應,這樣保證圖片居中,而原來圖片是左上角對應檢視左上角),與pre對應
mMatrix.postTranslate(dx, dy);
mBitmapShader.setLocalMatrix(mMatrix);
}
mPaintDrawable.setShader(mBitmapShader);
}
裡面的主要邏輯就是用matrix去設定BitmapShader的縮放效果,這裡我們參照的是imageView的原始碼:
private void configureBounds() {
if (mDrawable == null || !mHaveFrame) {
return;
}
final int dwidth = mDrawableWidth;
final int dheight = mDrawableHeight;
final int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
final int vheight = getHeight() - mPaddingTop - mPaddingBottom;
final boolean fits = (dwidth < 0 || vwidth == dwidth)
&& (dheight < 0 || vheight == dheight);
if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
/* If the drawable has no intrinsic size, or we're told to
scaletofit, then we just fill our entire view.
*/
mDrawable.setBounds(0, 0, vwidth, vheight);
mDrawMatrix = null;
} else {
// We need to do the scaling ourself, so have the drawable
// use its native size.
mDrawable.setBounds(0, 0, dwidth, dheight);
if (ScaleType.MATRIX == mScaleType) {
// Use the specified matrix as-is.
//省略...
} else if (fits) {
// The bitmap fits exactly, no transform needed.
mDrawMatrix = null;
} else if (ScaleType.CENTER == mScaleType) {
// Center bitmap in view, no scaling.
//省略...
} else if (ScaleType.CENTER_CROP == mScaleType) {
mDrawMatrix = mMatrix;
float scale;
float dx = 0, dy = 0;
if (dwidth * vheight > vwidth * dheight) {
scale = (float) vheight / (float) dheight;
dx = (vwidth - dwidth * scale) * 0.5f;
} else {
scale = (float) vwidth / (float) dwidth;
dy = (vheight - dheight * scale) * 0.5f;
}
mDrawMatrix.setScale(scale, scale);
mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));
} else if (ScaleType.CENTER_INSIDE == mScaleType) {
//省略...
} else {
// Generate the required transform.
//省略...scaleTypeToScaleToFit(mScaleType));
}
}
}
矩陣的變換可以參考文章最後的連結,矩陣post,pre,set我在上面的註釋也寫了,基本上就是set為直接變換,pre和post分別是變換之前和變換之後再進行後續的變換。
至此,bitmap的準備工作就完成了,我們就可以呼叫canvas的drawCircle來繪製圓形圖片了。上面提到了邊框的繪製,這裡我們就來說一下,其實原理是一樣的,無非就是把畫筆paint設定為STOKE模式,並且在計算Rect的四個點的座標的時候邊框和內容要注意一下,可以把邊框設定為半透明模式,看看圖片是否繪製到邊框下面了,如果兩者邊緣正好貼合,則說明半徑或者矩形計算的正確,否則就是計算有誤。另外一點就是圖片的畫筆和邊框的最好區分開來,這樣各司其職,否則就比較混亂。邊框的繪製還有一點需要注意的就是,它的半徑是圓心到邊框寬度的中間,而不是邊緣,因為是空心的。
下面就是圓角圖片了,這個則是呼叫canvas的drawRoundRect方法,首先第一個引數就是一個矩形,他是圓角的外接矩形,然後就是圓角的x,y半徑了,最後一個引數是畫筆。圓角的情況一共可以分為15種,四個角全部是圓的,另外就是單個圓角的組合,這裡我才用了列舉型別:
/**
* 圓角的型別
* 順時針1234四個角,四個角可分為1,2,3,4,12,13,14,23,24,34,123,124,134,234,1234十五種情況
*/
public enum CornerType {
ALL,
TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT,
TOP_LEFT_TOP_RIGHT, TOP_LEFT_BOTTOM_RIGHT, TOP_LEFT_BOTTOM_LEFT,
TOP_RIGHT_BOTTOM_RIGHT, TOP_RIGHT_BOTTOM_LEFT, BOTTOM_RIGHT_BOTTOM_LEFT,
TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT, TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT, TOP_LEFT_BOTTOM_RIGHT_BOTTOM_LEFT, TOP_RIGHT_BOTTOM_RIGHT_BOTTOM_LEFT
}
而這些引數都可以在xml屬性中配置,也可以通過程式碼去set具體的type。下面是具體繪製程式碼:
case ALL:
if (hasBorder) {
//Math.ceil進位來保證不留白,擴大一點繪製區域
RectF rf = new RectF(borderWidth, borderWidth, (float) Math.ceil(width - borderWidth), (float) Math.ceil(height - borderWidth));
canvas.drawRoundRect(rf, cr, cr, mPaintDrawable);
} else {
RectF rf = new RectF(0, 0, width, height);
//corner為圓心到邊緣的距離
canvas.drawRoundRect(rf, cornerRadius, cornerRadius, mPaintDrawable);
}
break;
case TOP_LEFT:
//分塊繪製,也可以採用path來繪製
if (hasBorder) {
// path.reset();
// path.moveTo(borderWidth,borderWidth+cr+borderWidth);
// path.addArc(new RectF(borderWidth, borderWidth, cr * 2f + borderWidth, cr * 2f + borderWidth), 180, 90);
// path.lineTo(width-borderWidth,borderWidth);
// path.lineTo(width-borderWidth,height-borderWidth);
// path.lineTo(borderWidth,height-borderWidth);
// path.close();
// canvas.drawPath(path,mPaintDrawable);
// canvas.drawRoundRect(new RectF(borderWidth, borderWidth, cr * 2f+borderWidth, cr * 2f+borderWidth),cr,cr,mPaintDrawable);
canvas.drawArc(new RectF(borderWidth, borderWidth, cornerRadius * 2f - borderWidth, cr * 2f + borderWidth), 180, 90, true, mPaintDrawable);
canvas.drawRect(new RectF(borderWidth, cornerRadius, cornerRadius, height - borderWidth), mPaintDrawable);
canvas.drawRect(new RectF(cornerRadius, borderWidth, width - borderWidth, height - borderWidth), mPaintDrawable);
} else {
//drawRoundRect也可以,不過多繪製了一部分圓
canvas.drawRoundRect(new RectF(0, 0, cornerRadius * 2f, cornerRadius * 2f), cornerRadius, cornerRadius, mPaintDrawable);
canvas.drawRect(new RectF(0, cornerRadius, cornerRadius, height), mPaintDrawable);
canvas.drawRect(new RectF(cornerRadius, 0, width, height), mPaintDrawable);
}
break;
這裡我只列舉了兩個情況:全部圓角和左上角圓角,可以看到我們繪製的方案可以多種,單個圓角組合的情況其實就是把圖片拆分為不同區塊,然後連線起來去繪製,原理就是這麼簡單!既然可以用path去繪製各種各樣的圖形,那麼我們是不是可以繪製一些特殊形狀呢,比如五角星,小熊,三角形,五邊形,六邊形。。。等等,自己可以隨意去擴充套件了,這裡我用了一個OtherType來表示特殊形狀:
/**
* 其他型別,如五角星,小熊,六邊形等等不規則的
*/
public enum OtherType {
STAR, BEAR, HEXAGON
}
具體使用的時候可以自己根據具體情況去擴充套件。這裡我只列出了六邊形:
path.reset();
path.moveTo(width * 0.25f, 0);
path.lineTo(width * 0.75f, 0);
path.lineTo(width, height * 0.5f);
path.lineTo(width * 0.75f, height);
path.lineTo(width * 0.25f, height);
path.lineTo(0, height * 0.5f);
path.close();
canvas.drawPath(path, mPaintDrawable);
可以看到用到了一些數學知識,尤其是當你畫五角星的時候,這些特殊形狀基本上都是數學知識的運用和path的api的基本使用。
最後就是狀態的儲存與恢復了,防止我們的自定義view出現異常,核心思想就是儲存我們設定的屬性,然後在恢復的時候去重新繪製就可以恢復銷燬之前的原狀了。
@Nullable
@Override
protected Parcelable onSaveInstanceState() {
super.onSaveInstanceState();
//狀態儲存
Bundle bundle = new Bundle();
bundle.putBoolean("hasBorder", hasBorder);
bundle.putBoolean("isCircle", isCircle);
bundle.putBoolean("isBlur", isBlur);
bundle.putBoolean("isOval", isOval);
bundle.putFloat("cornerRadius", cornerRadius);
bundle.putFloat("borderWidth", borderWidth);
bundle.putFloat("blurRadius", blurRadius);
bundle.putInt("borderColor", borderColor);
bundle.putInt("defaultColor", defaultColor);
bundle.putSerializable("otherType", otherType);
bundle.putSerializable("cornerType", cornerType);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
super.onRestoreInstanceState(state);
//狀態恢復
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
setHasBorder(bundle.getBoolean("hasBorder"));
setCircle(bundle.getBoolean("isCircle"));
setBlur(bundle.getBoolean("isBlur"));
setOval(bundle.getBoolean("isOval"));
setCornerRadius(bundle.getFloat("cornerRadius"));
setBorderWidth(bundle.getFloat("borderWidth"));
setBlurRadius(bundle.getFloat("blurRadius"));
setBorderColor(bundle.getInt("borderColor"));
setDefaultColor(bundle.getInt("defaultColor"));
setOtherType((OtherType) bundle.getSerializable("otherType"));
setCornerType((CornerType) bundle.getSerializable("cornerType"));
}
reDraw();
}
最後就是自定義屬性了:
<declare-styleable name="baselib_BaseImageView">
<!--其他一切不規則圖案-->
<attr name="baselib_other_type">
<enum name="STAR" value="1"/>
<enum name="BEAR" value="2"/>
<enum name="HEXAGON" value="3"/>
</attr>
<!--順時針1234四個角,四個角可分為1,2,3,4,12,13,14,23,24,34,123,124,134,234,1234十五種情況-->
<attr name="baselib_corner_type">
<enum name="ALL" value="1234"/>
<enum name="TOP_LEFT" value="1"/>
<enum name="TOP_RIGHT" value="2"/>
<enum name="BOTTOM_RIGHT" value="3"/>
<enum name="BOTTOM_LEFT" value="4"/>
<enum name="TOP_LEFT_TOP_RIGHT" value="12"/>
<enum name="TOP_LEFT_BOTTOM_RIGHT" value="13"/>
<enum name="TOP_LEFT_BOTTOM_LEFT" value="14"/>
<enum name="TOP_RIGHT_BOTTOM_RIGHT" value="23"/>
<enum name="TOP_RIGHT_BOTTOM_LEFT" value="24"/>
<enum name="BOTTOM_RIGHT_BOTTOM_LEFT" value="34"/>
<enum name="TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT" value="123"/>
<enum name="TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT" value="124"/>
<enum name="TOP_LEFT_BOTTOM_RIGHT_BOTTOM_LEFT" value="134"/>
<enum name="TOP_RIGHT_BOTTOM_RIGHT_BOTTOM_LEFT" value="234"/>
</attr>
<!--橢圓-->
<attr name="baselib_is_oval" format="boolean"/>
<!--是否是圓形圖片-->
<attr name="baselib_is_circle" format="boolean"/>
<!--=====================上面是互斥的屬性,下面的可以與上面的共存=====================-->
<!--是否帶邊框-->
<attr name="baselib_has_border" format="boolean"/>
<!--邊框顏色-->
<attr name="baselib_border_color" format="color|reference"/>
<!--邊框寬度-->
<attr name="baselib_border_width" format="dimension"/>
<!--圓角的度數(代表所有角,不再單獨針對部分圓角的情況提供某一個角的度數)-->
<attr name="baselib_corner_radius" format="dimension"/>
<!--是否高斯模糊-->
<attr name="baselib_is_blur" format="boolean"/>
<!--高斯模糊半徑-->
<attr name="baselib_blur_radius" format="float"/>
<!--當圖片為空的時候,預設顏色-->
<attr name="baselib_default_color" format="color|reference"/>
</declare-styleable>
所有屬性都可以通過程式碼set和get,而且可以自己擴充套件,相信這個imageview基本上滿足日常開發了,最後如果使用glide載入圖片的話,在getBitmap方法裡面需要加上一種情況:
else if (drawable instanceof GlideBitmapDrawable) {
bitmap = ((GlideBitmapDrawable) drawable).getBitmap();
}
到此整個imageview就講解完了,老規矩,有疑問的同學可以在下方留言,最後放出原始碼:
原始碼下載
參考: