1. 程式人生 > >Android自定義圓角圓形圖片

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,感興趣的可以參考我之前的一篇文章:

http://blog.csdn.net/binbinqq86/article/details/78329238,裡面講述了另外一種實現圓形圖片的方案。

下面來看一下獲取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就講解完了,老規矩,有疑問的同學可以在下方留言,最後放出原始碼:

原始碼下載

參考: