【YLCircleImageView】圖片處理

汐丶諾發表於2019-03-04

已由任玉剛老師公眾號“玉剛說”發表,謝絕其他轉載。

傳送門

希望各位朋友能多提意見,共同進步,祝各位同學,前程似錦,萬事如意。

github地址

功能

  1. 描邊功能
  2. 圖片和描邊間距功能
  3. 圖片四個角,每個角均可單獨設定 X Y 軸方向的圓弧半徑

效果圖

【YLCircleImageView】圖片處理

【YLCircleImageView】圖片處理

【YLCircleImageView】圖片處理

可用屬性

【YLCircleImageView】圖片處理

    /**
     * 圖片展示方式
     * 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;
複製程式碼

主要邏輯

  1. 使用Path建立圖形路徑
  2. 根據繪製所需佔據的矩形RectF,在BitMap中找到相似矩形 Src,然後將BitMap指定區域內容繪製到 RectF中,配合PorterDuffXfermode進行挖洞

程式碼解讀

  1. 構造器裡面獲取自定義屬性的值,並提供預設值
  2. initRadius,設定半徑,看註釋
 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;
        ...  
}
複製程式碼
  1. 判斷是否需要繪製多邊形,也就是說,如果使用者不設定如下的屬性,那麼將等同於普通的 imageView
        //  判斷是否需要呼叫繪製函式
        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;
複製程式碼
  1. 針對Glide設定
        if (circle) {
            //  為什麼設定這一條,因為Glide中,在into 原始碼內
            //  不同的 ScaleType 會對drawable進行壓縮,一旦壓縮了,我們在onDraw裡面獲取圖片的大小就沒有意義了
            setScaleType(ScaleType.MATRIX);
        }
複製程式碼

onDraw

知識點

  1. 繪製邊框

在繪製邊框時候,線寬是以線為中心,兩邊擴大,所以會有一半的線寬繪製不出來 所以,我們在繪製描邊時候,呼叫 RectF.inset,調整矩形大小

  1. 繪製圖片(2.3 與 2.5 是最重要的部分,在下面有詳細解釋)

    2.1、在Canvans中為圖片指定繪製區域 RectF,RectF 需要進行 inset() 調整,調整值為 描邊寬度 + 內間距寬度

    2.2、呼叫 canvas.saveLayer,得到 layerID

    2.3、繪製圓角矩形,看函式

     drawPath(canvas, rectF, borderPaint, i);
    複製程式碼

    2.4、根據 rectF,在BitMap中找到 與 rectF的相似矩形 src,然後返回

     Rect src = getSrc(bitmap, (int) rectF.width(), (int) rectF.height());
    複製程式碼

    2.5、設定挖洞模式

     paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    複製程式碼

    2.6、將 BitMap 指定大小的 src 區域的畫素,繪製到 rectF,使用畫筆 paint,而畫筆已經設定了挖洞模式

     canvas.drawBitmap(bitmap, src, rectF, paint);
    複製程式碼

    2.7、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);
        }
    }
複製程式碼

2.3 繪製圓角矩形

使用 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);
    }
複製程式碼

2.5、根據 rectF,在BitMap中找到 與 rectF的相似矩形 src

這部分程式碼較長,好好看下,先看下示例圖
複製程式碼

【YLCircleImageView】圖片處理

【YLCircleImageView】圖片處理

最後一步

根據展示型別,修改圖片擷取區域。這部分就不解釋了,很簡單。
複製程式碼
   /**
     * 這裡詳細說一下,我們的目標就是在 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 ,是一個意思。
複製程式碼

相關文章