已由任玉剛老師公衆號「玉剛說」發表,謝絕其餘轉載。git
傳送門es6
但願各位朋友能多提意見,共同進步,祝各位同窗,前程似錦,萬事如意。github
github地址canvas
/**
* 圖片展現方式
* 0 -- 圖片頂部開始展現,鋪滿,若是Y軸鋪滿時,X軸大,則圖片水平居中
* 1 -- 圖片中心點與指定區域中心重合
* 2 -- 圖片底部開始展現,鋪滿,若是Y軸鋪滿時,X軸大,則圖片水平居中
* 3 -- 圖片徹底展現
*/
public static final int TOP = 0;
public static final int CENTER = 1;
public static final int BOTTOM = 2;
public static final int FITXY = 3;
複製代碼
private void initRadius() {
// 該處便於代碼編寫 如XML設置 radius = 20,topLeftRadius = 10,最終結果是 10 20 20 20
if (radius != 0) {
topLeftRadius = topLeftRadius == 0 ? radius : topLeftRadius;
...
}
// 若是設置了 radius = 20,topLeftRadius = 10,topLeftRadius_x = 30,
// 最終結果,topLeftRadius_x = 30,topLeftRadius_y = 10,其他 20
topLeftRadius_x = topLeftRadius_x == 0 ? topLeftRadius : topLeftRadius_x;
topLeftRadius_y = topLeftRadius_y == 0 ? topLeftRadius : topLeftRadius_y;
...
}
複製代碼
// 判斷是否須要調用繪製函數
circle = borderWidth != 0 || borderSpace != 0 ||
topLeftRadius_x != 0 || topLeftRadius_y != 0 ||
topRightRadius_x != 0 || topRightRadius_y != 0 ||
bottomLeftRadius_x != 0 || bottomLeftRadius_y != 0 ||
bottomRightRadius_x != 0 || bottomRightRadius_y != 0;
複製代碼
if (circle) {
// 爲何設置這一條,由於Glide中,在into 源碼內
// 不一樣的 ScaleType 會對drawable進行壓縮,一旦壓縮了,咱們在onDraw裏面獲取圖片的大小就沒有意義了
setScaleType(ScaleType.MATRIX);
}
複製代碼
在繪製邊框時候,線寬是以線爲中心,兩邊擴大,因此會有一半的線寬繪製不出來 因此,咱們在繪製描邊時候,調用 RectF.inset,調整矩形大小bash
繪製圖片(2.3 與 2.5 是最重要的部分,在下面有詳細解釋)ide
2.一、在Canvans中爲圖片指定繪製區域 RectF,RectF 須要進行 inset() 調整,調整值爲 描邊寬度 + 內間距寬度函數
2.二、調用 canvas.saveLayer,獲得 layerIDui
2.三、繪製圓角矩形,看函數spa
drawPath(canvas, rectF, borderPaint, i);
複製代碼
2.四、根據 rectF,在BitMap中找到 與 rectF的類似矩形 src,而後返回3d
Rect src = getSrc(bitmap, (int) rectF.width(), (int) rectF.height());
複製代碼
2.五、設置挖洞模式
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
複製代碼
2.六、將 BitMap 指定大小的 src 區域的像素,繪製到 rectF,使用畫筆 paint,而畫筆已經設置了挖洞模式
canvas.drawBitmap(bitmap, src, rectF, paint);
複製代碼
2.七、paint還原,將 layerID 的圖層繪製到 Canvas
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
Drawable drawable = getDrawable();
// 使用局部變量,下降函數調用次數
int vw = getMeasuredWidth();
int vh = getMeasuredHeight();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
// 繪製描邊
if (borderWidth != 0) {
RectF rectF = new RectF(paddingLeft, paddingTop, vw - paddingRight, vh - paddingBottom);
// 描邊會有一半處於框體以外
float i = borderWidth / 2;
// 移動矩形,以便於描邊都處於view內
rectF.inset(i, i);
// 繪製描邊,半徑須要進行偏移 i
drawPath(canvas, rectF, borderPaint, i);
}
if ((null != drawable && circle)) {
RectF rectF = new RectF(paddingLeft, paddingTop, vw - paddingRight, vh - paddingBottom);
// 矩形須要縮小的值
float i = borderWidth + borderSpace;
// 這裏解釋一下,爲何要減去一個像素,由於像素融合時,因爲鋸齒的存在和圖片像素不高,會致使圖片和邊框出現1像素的間隙
// 你們能夠試一下,去掉這一句,而後用高清圖就不會出問題,用非高清圖就會出現
i = i > 1 ? i - 1 : 0;
// 矩形偏移
rectF.inset(i, i);
int layerId = canvas.saveLayer(rectF, null, Canvas.ALL_SAVE_FLAG);
// 多邊形
drawPath(canvas, rectF, paint, i);
// 設置像素融合模式
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
// drawable轉爲 bitmap
Bitmap bitmap = drawableToBitmap(drawable);
// 根據圖片的大小,控件的大小,圖片的展現形式,而後來計算圖片的src取值範圍
Rect src = getSrc(bitmap, (int) rectF.width(), (int) rectF.height());
// dst取整個控件,也就是表示,咱們的圖片要佔滿整個控件
canvas.drawBitmap(bitmap, src, rectF, paint);
paint.setXfermode(null);
canvas.restoreToCount(layerId);
} else {
super.onDraw(canvas);
}
}
複製代碼
使用 path繪製圓角矩形,並且提供了 四個角,每一個角均可以單獨設置 X Y的半徑,CW是順時針的意思
複製代碼
/**
* 繪製多邊形
*
* @param canvas 畫布
* @param rectF 矩形
* @param paint 畫筆
* @param offset 半徑偏移量
*/
private void drawPath(Canvas canvas, RectF rectF, Paint paint, float offset) {
Path path = new Path();
path.addRoundRect(rectF,
new float[]{
offsetRadius(topLeftRadius_x, offset), offsetRadius(topLeftRadius_y, offset),
offsetRadius(topRightRadius_x, offset), offsetRadius(topRightRadius_y, offset),
offsetRadius(bottomRightRadius_x, offset), offsetRadius(bottomRightRadius_y, offset),
offsetRadius(bottomLeftRadius_x, offset), offsetRadius(bottomLeftRadius_y, offset)}, Path.Direction.CW);
path.close();
canvas.drawPath(path, paint);
}
/**
* 計算半徑偏移值
* 在SDK < 18 時,若是半徑小於0會出現變形。
*
* @param radius 半徑
* @param offset 偏移量
* @return 偏移半徑
*/
private float offsetRadius(float radius, float offset) {
return Math.max(radius - offset, 0);
}
複製代碼
這部分代碼較長,好好看下,先看下示例圖
複製代碼
根據展現類型,修改圖片截取區域。這部分就不解釋了,很簡單。
複製代碼
/**
* 這裏詳細說一下,咱們的目標就是在 bitmap 中找到一個 和 view 寬高比例相等的 一塊矩形
* tempRect,而後截取出來 放到整個view中
* tempRect 老是會存在
*
* @param bitmap bitmap
* @param rw 繪製區域的寬度
* @param rh 繪製區域的高度
* @return 矩形
*/
private Rect getSrc(@NonNull Bitmap bitmap, int rw, int rh) {
// bw bh,bitmap 的寬高
// vw vh,view 的寬高
int bw = bitmap.getWidth();
int bh = bitmap.getHeight();
int left = 0, top = 0, right = 0, bottom = 0;
// 判斷 bw/bh 與 vw/vh
int temp1 = bw * rh;
int temp2 = rw * bh;
// 類似矩形的寬高
int[] tempRect = {bw, bh};
if (temp1 == temp2) {
return new Rect(0, 0, bw, bh);
}
// tempRect 的寬度比 bw 小
else if (temp1 > temp2) {
int tempBw = temp2 / rh;
tempRect[0] = tempBw;
}
// tempRect 的寬度比 bw 大
else if (temp1 < temp2) {
int tempBh = temp1 / rw;
tempRect[1] = tempBh;
}
// tempRect 的寬度與 bw 的比值
Boolean compare = bw > tempRect[0];
switch (styleType) {
case TOP:
// 從上往下展現,咱們這裏的效果是不止從上往下,compare = true,還要居中
left = compare ? (bw - tempRect[0]) / 2 : 0;
top = 0;
right = compare ? (bw + tempRect[0]) / 2 : tempRect[0];
bottom = tempRect[1];
break;
case CENTER:
// 居中
left = compare ? (bw - tempRect[0]) / 2 : 0;
top = compare ? 0 : (bh - tempRect[1]) / 2;
right = compare ? (bw + tempRect[0]) / 2 : tempRect[0];
bottom = compare ? tempRect[1] : (bh + tempRect[1]) / 2;
break;
case BOTTOM:
left = compare ? (bw - tempRect[0]) / 2 : 0;
top = compare ? 0 : bh - tempRect[1];
right = compare ? (bw + tempRect[0]) / 2 : tempRect[0];
bottom = compare ? tempRect[1] : bh;
break;
case FITXY:
left = 0;
top = 0;
right = bw;
bottom = bh;
break;
default:
}
return new Rect(left, top, right, bottom);
}
複製代碼
通過上面的步驟,咱們在圖片中找到了一塊內容,這塊內容所在的矩形大小 和 將要繪製到的區域矩形 是類似的。
好比咱們找到的區域是 0,0,300,300,目標矩形是 150 X 150,那麼就至關於把圖片壓縮2倍。
這個和 圖片大小 300 X 300,ImageView 寬高 150 X 150 ,是一個意思。
複製代碼