一、 特點
- 基於AppCompatImageView擴充套件
- 支援圓角、圓形顯示
- 可繪製邊框,圓形時可繪製內外兩層邊框
- 支援邊框不覆蓋圖片
- 可繪製遮罩
- ......
二、基本原理
我們要實現的圖片控制元件繼承自AppCompatImageView
,它是ImageView
的子類,但提供了更好的相容性,我們在此基礎上新增了若干自定義的屬性和方法以實現最終的 NiceImageView:
public class NiceImageView extends AppCompatImageView {
......
}
複製程式碼
要實圓角或者圓形的顯示效果,就是對圖片顯示的內容區域進行“裁剪”,只顯示指定的區域即可。如何做呢?
一種比較直接的辦法是這樣的,由於圖片是被繪製在畫布上的,所以用canvas
的 clipPath()
方法先將畫布裁剪成指定形狀,這樣就能讓圖片按指定形狀顯示了,重新draw()
方法即可:
@Override
public void draw(Canvas canvas) {
canvas.save();
canvas.clipPath(path);
super.draw(canvas);
canvas.restore();
}
複製程式碼
這樣使用src
、background
屬性給ImageView設定顯示的圖片都能達到預期的顯示效果。但是由於clipPath()
方法不支援抗鋸齒,圖片邊緣會有明顯的毛糙感,體驗並不理想,所以需要尋找其它方法。
另一種方法是使用影象的 Alpha 合成模式,即 PorterDuff 來實現,官方文件。這裡我們使用其中的DST_IN模式。整個過程就是先繪製目標影象,也就是圖片;再繪製原影象,即一個圓角矩形或者圓形,這樣最終目標影象只顯示和原影象重合的區域。
xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
複製程式碼
@Override
protected void onDraw(Canvas canvas) {
// 使用離屏快取,新建一個srcRectF區域大小的圖層
canvas.saveLayer(srcRectF, null, Canvas.ALL_SAVE_FLAG);
// ImageView自身的繪製流程,即繪製圖片
super.onDraw(canvas);
// 給path新增一個圓角矩形或者圓形
if (isCircle) {
path.addCircle(width / 2.0f, height / 2.0f, radius, Path.Direction.CCW);
} else {
path.addRoundRect(srcRectF, srcRadii, Path.Direction.CCW);
}
paint.setAntiAlias(true);
// 畫筆為填充模式
paint.setStyle(Paint.Style.FILL);
// 設定混合模式
paint.setXfermode(xfermode);
// 繪製path
canvas.drawPath(path, paint);
// 清除Xfermode
paint.setXfermode(null);
// 恢復畫布狀態
canvas.restore();
}
複製程式碼
到這裡就實現了顯示為圓角或者圓形了。但是需要通過src
屬性或者對應的方法來設定圖片,否則不能達到預期效果。
三、繪製邊框
繪製邊框就相對容易理解了,只需要繪製一個指定樣式的圓角矩形或者圓形即可:
private void drawBorders(Canvas canvas) {
if (isCircle) {
if (borderWidth > 0) {
drawCircleBorder(canvas, borderWidth, borderColor, radius - borderWidth / 2.0f);
}
if (innerBorderWidth > 0) {
drawCircleBorder(canvas, innerBorderWidth, innerBorderColor, radius - borderWidth - innerBorderWidth / 2.0f);
}
} else {
if (borderWidth > 0) {
drawRectFBorder(canvas, borderWidth, borderColor, borderRectF, borderRadii);
}
}
}
private void drawCircleBorder(Canvas canvas, int borderWidth, int borderColor, float radius) {
initBorderPaint(borderWidth, borderColor);
path.addCircle(width / 2.0f, height / 2.0f, radius, Path.Direction.CCW);
canvas.drawPath(path, paint);
}
private void drawRectFBorder(Canvas canvas, int borderWidth, int borderColor, RectF rectF, float[] radii) {
initBorderPaint(borderWidth, borderColor);
path.addRoundRect(rectF, radii, Path.Direction.CCW);
canvas.drawPath(path, paint);
}
private void initBorderPaint(int borderWidth, int borderColor) {
path.reset();
// 設定畫筆為描邊模式
paint.setStyle(Paint.Style.STROKE);
// 描邊寬度
paint.setStrokeWidth(borderWidth);
// 描邊顏色
paint.setColor(borderColor);
}
複製程式碼
當圖片顯示為圓形時,還可以繪製一個內邊框,但圓角矩形的話由於圓角大小的問題,目前只能設定一個邊框咯。
但是有個問題,繪製的邊框會覆蓋在圖片上,如果邊框太寬會導致圖片的可見區域變小了,影像顯示效果,像這樣,左下角的花盆不見了:
那麼如何讓邊框不覆蓋在圖片上呢?可以在 Alpha 合成繪製前先將畫布縮小一定比例,最後再繪製邊框,這樣問題就解決了。 @Override
protected void onDraw(Canvas canvas) {
canvas.saveLayer(srcRectF, null, Canvas.ALL_SAVE_FLAG);
// 縮小畫布
if (!isCoverSrc) {
float sx = 1.0f * (width - 2 * borderWidth - 2 * innerBorderWidth) / width;
float sy = 1.0f * (height - 2 * borderWidth - 2 * innerBorderWidth) / height;
// 縮小畫布,使圖片內容不被border、padding覆蓋
canvas.scale(sx, sy, width / 2.0f, height / 2.0f);
}
......
canvas.restore();
// 繪製邊框
drawBorders(canvas);
}
複製程式碼
縮放後的ImageView顯示區域的寬高就是原寬、高分別減去2倍的邊框寬度,這樣縮小的比例也就顯而易見了。效果如下,左下角的花盆出來了:
四、繪製遮罩
遮罩可以理解為一層帶透明度的顏色,遮罩預設不繪製,當制定了遮罩顏色時才會繪製,實現很簡單:
@Override
protected void onDraw(Canvas canvas) {
......
// 繪製遮罩
if (maskColor != 0) {
paint.setColor(maskColor);
canvas.drawPath(path, paint);
}
canvas.restore();
drawBorders(canvas);
}
複製程式碼
例如加一個透明度30%的紅色遮罩後的效果:
核心的實現邏輯就這些了,剩下的就是自定義屬性和方法了,有興趣的可以看原始碼,都很簡單,希望對你有所幫助吧!
更多細節及用法見GitHub:github.com/Othershe/Ni…
五、其它
如果你需要實現類似釘釘的圓形組合頭像,例如:
可以先生成對應的Bitmap,並用圓形的 NiceImageView 顯示即可。如何生成組合Bitmap可以參考這裡:CombineBitmap。