自定義圓形圖片---開源庫CircleImageView

CircleImageView

A fast circular ImageView perfect for profile images. This is based on RoundedImageView from Vince Mi which itself is based on techniques recommended by Romain Guy.

CircleImageView
CircleImageView

It uses a BitmapShader and does not:

  • create a copy of the original bitmap
  • use a clipPath (which is neither hardware accelerated nor anti-aliased)
  • use setXfermode to clip the bitmap (which means drawing twice to the canvas)

As this is just a custom ImageView and not a custom Drawable or a combination of both, it can be used with all kinds of drawables, i.e. a PicassoDrawable from Picasso or other non-standard drawables (needs some testing though).

Gradle

dependencies {
    ...
    compile 'de.hdodenhof:circleimageview:2.1.0'
}

Usage

<de.hdodenhof.circleimageview.CircleImageView
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/profile_image"
    android:layout_width="96dp"
    android:layout_height="96dp"
    android:src="@drawable/profile"
    app:civ_border_width="2dp"
    app:civ_border_color="#FF000000"/>

Demo

/**
 * 流程控制的比較嚴(yán)謹(jǐn)鞍帝,比如setup函數(shù)的使用
 * updateShaderMatrix保證圖片損失度最小和始終繪制圖片正中央的那部分
 * 作者思路是畫圓用渲染器位圖填充沐旨,而不是把Bitmap重繪切割成一個圓形圖片闷串。
 */
public class CircleImageView extends android.support.v7.widget.AppCompatImageView {
    //縮放類型
    private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;
    private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
    private static final int COLORDRAWABLE_DIMENSION = 2;
    // 默認(rèn)邊界寬度
    private static final int DEFAULT_BORDER_WIDTH = 0;
    // 默認(rèn)邊界顏色
    private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
    private static final boolean DEFAULT_BORDER_OVERLAY = false;
    //覆蓋矩形
    private final RectF mDrawableRect = new RectF();
    //邊界矩形
    private final RectF mBorderRect = new RectF();
    //位圖變換
    private final Matrix mShaderMatrix = new Matrix();

    //這個畫筆最重要的是關(guān)聯(lián)了mBitmapShader
    // 使canvas在執(zhí)行的時候可以切割原圖片(mBitmapShader是關(guān)聯(lián)了原圖的bitmap的)
    private final Paint mBitmapPaint = new Paint();
    //這個描邊,則與本身的原圖bitmap沒有任何關(guān)聯(lián)畏纲,
    private final Paint mBorderPaint = new Paint();
    //這里定義了 圓形邊緣的默認(rèn)寬度和顏色
    private int mBorderColor = DEFAULT_BORDER_COLOR;
    private int mBorderWidth = DEFAULT_BORDER_WIDTH;

    private Bitmap mBitmap;
    private BitmapShader mBitmapShader; // 位圖渲染
    private int mBitmapWidth;   // 位圖寬度
    private int mBitmapHeight;  // 位圖高度

    private float mDrawableRadius;// 圖片半徑
    private float mBorderRadius;// 帶邊框的的圖片半徑

    private ColorFilter mColorFilter;
    //初始false
    private boolean mReady;
    private boolean mSetupPending;
    private boolean mBorderOverlay;
    //構(gòu)造函數(shù)
    public CircleImageView(Context context) {
        super(context);
        init();
    }
    //構(gòu)造函數(shù)
    public CircleImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        init();
    }
    /**
     * 構(gòu)造函數(shù)
     */
    public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        //通過obtainStyledAttributes 獲得一組值賦給 TypedArray(數(shù)組) , 這一組值來自于res/values/attrs.xml中的name="CircleImageView"的declare-styleable中。
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);
        //通過TypedArray提供的一系列方法getXXXX取得我們在xml里定義的參數(shù)值裹赴;
        // 獲取邊界的寬度
        mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView2_border_width, DEFAULT_BORDER_WIDTH);
        // 獲取邊界的顏色
        mBorderColor = a.getColor(R.styleable.CircleImageView2_border_color, DEFAULT_BORDER_COLOR);
        mBorderOverlay = a.getBoolean(R.styleable.CircleImageView2_border_overlay, DEFAULT_BORDER_OVERLAY);
        //調(diào)用 recycle() 回收TypedArray
        a.recycle();
        System.out.println("CircleImageView -- 構(gòu)造函數(shù)");
        init();
    }
    /**
     * 作用就是保證第一次執(zhí)行setup函數(shù)里下面代碼要在構(gòu)造函數(shù)執(zhí)行完畢時調(diào)用
     */
    private void init() {
        //在這里ScaleType被強(qiáng)制設(shè)定為CENTER_CROP羡忘,就是將圖片水平垂直居中,進(jìn)行縮放案狠。
        super.setScaleType(SCALE_TYPE);
        //準(zhǔn)備完畢
        mReady = true;
        //是否開始設(shè)置
        if (mSetupPending) {
            setup();
            mSetupPending = false;
        }
    }

    /**
     * 返回縮放類型
     * @return
     */
    @Override
    public ScaleType getScaleType() {
        return SCALE_TYPE;
    }
    /**
     * 這里明確指出 此種imageview 只支持CENTER_CROP 這一種屬性
     *
     * @param scaleType
     */
    @Override
    public void setScaleType(ScaleType scaleType) {
        if (scaleType != SCALE_TYPE) {
            throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
        }
    }

    /**
     * 明確指出不支持調(diào)整邊界
     * @param adjustViewBounds
     */

    @Override
    public void setAdjustViewBounds(boolean adjustViewBounds) {
        if (adjustViewBounds) {
            throw new IllegalArgumentException("adjustViewBounds not supported.");
        }
    }

    /**
     * 最定義關(guān)鍵
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        //如果圖片不存在就不畫
        if (getDrawable() == null) {
            return;
        }
        //繪制內(nèi)圓形  畫筆為mBitmapPaint
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, mDrawableRadius, mBitmapPaint);
        //如果圓形邊緣的寬度不為0 我們還要繪制帶邊界的外圓形 邊界畫筆為mBorderPaint
        if (mBorderWidth != 0) {
            canvas.drawCircle(getWidth() / 2, getHeight() / 2, mBorderRadius, mBorderPaint);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        setup();
    }

    public int getBorderColor() {
        return mBorderColor;
    }

    public void setBorderColor(int borderColor) {
        if (borderColor == mBorderColor) {
            return;
        }

        mBorderColor = borderColor;
        mBorderPaint.setColor(mBorderColor);
        invalidate();
    }

    public void setBorderColorResource(@ColorRes int borderColorRes) {
        setBorderColor(getContext().getResources().getColor(borderColorRes));
    }

    public int getBorderWidth() {
        return mBorderWidth;
    }

    public void setBorderWidth(int borderWidth) {
        if (borderWidth == mBorderWidth) {
            return;
        }

        mBorderWidth = borderWidth;
        setup();
    }

    public boolean isBorderOverlay() {
        return mBorderOverlay;
    }

    public void setBorderOverlay(boolean borderOverlay) {
        if (borderOverlay == mBorderOverlay) {
            return;
        }
        mBorderOverlay = borderOverlay;
        setup();
    }

    /**
     * 以下四個函數(shù)都是
     * 復(fù)寫ImageView的setImageXxx()方法
     * 注意這個函數(shù)先于構(gòu)造函數(shù)調(diào)用之前調(diào)用
     * @param bm
     */
    @Override
    public void setImageBitmap(Bitmap bm) {
        super.setImageBitmap(bm);
        mBitmap = bm;
        setup();
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        mBitmap = getBitmapFromDrawable(drawable);
        System.out.println("setImageDrawable -- setup");
        setup();
    }

    @Override
    public void setImageResource(@DrawableRes int resId) {
        super.setImageResource(resId);
        mBitmap = getBitmapFromDrawable(getDrawable());
        setup();
    }

    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);
        mBitmap = getBitmapFromDrawable(getDrawable());
        setup();
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        if (cf == mColorFilter) {
            return;
        }

        mColorFilter = cf;
        mBitmapPaint.setColorFilter(mColorFilter);
        invalidate();
    }
    /**
     * Drawable轉(zhuǎn)Bitmap
     * @param drawable
     * @return
     */
    private Bitmap getBitmapFromDrawable(Drawable drawable) {
        if (drawable == null) {
            return null;
        }

        if (drawable instanceof BitmapDrawable) {
            //通常來說 我們的代碼就是執(zhí)行到這里就返回了服傍。返回的就是我們最原始的bitmap
            return ((BitmapDrawable) drawable).getBitmap();
        }

        try {
            Bitmap bitmap;

            if (drawable instanceof ColorDrawable) {
                bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION,
                        COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
            } else {
                bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
                        drawable.getIntrinsicHeight(), BITMAP_CONFIG);
            }

            Canvas canvas = new Canvas(bitmap);
            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);
            return bitmap;
        } catch (OutOfMemoryError e) {
            return null;
        }
    }
    /**
     * 這個函數(shù)很關(guān)鍵,進(jìn)行圖片畫筆邊界畫筆(Paint)一些重繪參數(shù)初始化:
     * 構(gòu)建渲染器BitmapShader用Bitmap來填充繪制區(qū)域,設(shè)置樣式以及內(nèi)外圓半徑計算等骂铁,
     * 以及調(diào)用updateShaderMatrix()函數(shù)和 invalidate()函數(shù)吹零;
     */
    private void setup() {
        //因為mReady默認(rèn)值為false,所以第一次進(jìn)這個函數(shù)的時候if語句為真進(jìn)入括號體內(nèi)
        //設(shè)置mSetupPending為true然后直接返回,后面的代碼并沒有執(zhí)行拉庵。
        if (!mReady) {
            mSetupPending = true;
            return;
        }
        //防止空指針異常
        if (mBitmap == null) {
            return;
        }
        // 構(gòu)建渲染器灿椅,用mBitmap位圖來填充繪制區(qū)域 ,參數(shù)值代表如果圖片太小的話 邊緣處拉伸
        mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        // 設(shè)置圖片畫筆反鋸齒
        mBitmapPaint.setAntiAlias(true);
        // 設(shè)置圖片畫筆渲染器
        mBitmapPaint.setShader(mBitmapShader);
        // 設(shè)置邊界畫筆樣式
        mBorderPaint.setStyle(Paint.Style.STROKE);//設(shè)畫筆為空心
        mBorderPaint.setAntiAlias(true);
        mBorderPaint.setColor(mBorderColor);    //畫筆顏色
        mBorderPaint.setStrokeWidth(mBorderWidth);//畫筆邊界寬度
        //這個地方是取的原圖片的寬高
        mBitmapHeight = mBitmap.getHeight();
        mBitmapWidth = mBitmap.getWidth();
        // 設(shè)置含邊界顯示區(qū)域,取的是CircleImageView的布局實際大小茫蛹,為方形操刀,查看xml也就是160dp(240px)
        // getWidth得到是某個view的實際尺寸
        mBorderRect.set(0, 0, getWidth(), getHeight());
        //計算 圓形帶邊界部分(外圓)的最小半徑,
        // 取mBorderRect的寬高減去一個邊緣大小的一半的較小值(這個地方我比較納悶為什么求外圓半徑需要先減去一個邊緣大杏ね荨)
        mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2,
                (mBorderRect.width() - mBorderWidth) / 2);

        // 初始圖片顯示區(qū)域為mBorderRect(CircleImageView的布局實際大泄强印)
        mDrawableRect.set(mBorderRect);
        if (!mBorderOverlay) {
            //demo里始終執(zhí)行
            //通過inset方法  使得圖片顯示的區(qū)域從mBorderRect大小上下左右內(nèi)移邊界的寬度形成區(qū)域
            // ,查看xml邊界寬度為2dp(3px),所以方形邊長為就是160-4=156dp(234px)
            mDrawableRect.inset(mBorderWidth, mBorderWidth);
        }
        //這里計算的是內(nèi)圓的最小半徑窃蹋,也即去除邊界寬度的半徑
        mDrawableRadius = Math.min(mDrawableRect.height() / 2, mDrawableRect.width() / 2);
        //設(shè)置渲染器的變換矩陣也即是mBitmap用何種縮放形式填充
        updateShaderMatrix();
        //手動觸發(fā)ondraw()函數(shù) 完成最終的繪制
        invalidate();
    }
    /**
     * 這個函數(shù)為設(shè)置BitmapShader的Matrix參數(shù)卡啰,設(shè)置最小縮放比例,平移參數(shù)警没。
     * 作用:保證圖片損失度最小和始終繪制圖片正中央的那部分
     */
    private void updateShaderMatrix() {
        float scale;
        float dx = 0;
        float dy = 0;

        mShaderMatrix.set(null);
        //取最小的縮放比例
        /*     bW       bH
              ----  =  -----
               mDW     mDH
         */
        if (mBitmapWidth * mDrawableRect.height() >  mBitmapHeight*mDrawableRect.width() ) {
            //y軸縮放 x軸平移 使得圖片的y軸方向的邊的尺寸縮放到圖片顯示區(qū)域(mDrawableRect)一樣)
            scale = mDrawableRect.height() / (float) mBitmapHeight;
            dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
        } else {
            //x軸縮放 y軸平移 使得圖片的x軸方向的邊的尺寸縮放到圖片顯示區(qū)域(mDrawableRect)一樣)
            scale = mDrawableRect.width() / (float) mBitmapWidth;
            dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
        }
        // shaeder的變換矩陣匈辱,我們這里主要用于放大或者縮小。
        mShaderMatrix.setScale(scale, scale);
        // 平移
        mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
        // 設(shè)置變換矩陣
        mBitmapShader.setLocalMatrix(mShaderMatrix);
    }

}

Limitations

  • The ScaleType is always CENTER_CROP and you'll get an exception if you try to change it. This is (currently) by design as it's perfectly fine for profile images.
  • Enabling adjustViewBounds is not supported as this requires an unsupported ScaleType
  • If you use an image loading library like Picasso or Glide, you need to disable their fade animations to avoid messed up images. For Picasso use the noFade() option, for Glide use dontAnimate(). If you want to keep the fadeIn animation, you have to fetch the image into a Target and apply a custom animation yourself when receiving the Bitmap.
  • Using a TransitionDrawable with CircleImageView doesn't work properly and leads to messed up images.

FAQ

How can I use a VectorDrawable with CircleImageView?

Short answer: you shouldn't. Using a VectorDrawable with CircleImageView is very inefficient. You should modify your vectors to be in a circular shape and use them with a regular ImageView instead.

Why doesn't CircleImageView extend AppCompatImageView?

Extending AppCompatImageView would require adding a runtime dependency for the support library without any real benefit.

How can I add a selector (e.g. ripple effect) bound to a circle?

There's currently no direct support for a circle bound selector but you can follow these steps to implement it yourself.

How can I add a gap between image and border?

Adding a gap is also not support directly but there's a workaround.

Changelog

  • next
    • Add support for elevation
    • Add circle background color attribute to replace fill color
  • 2.1.0
    • Add support for padding
    • Add option to disable circular transformation
    • Fix hairline gap being drawn between image and border under some conditions
    • Fix NPE when using tint attribute (which is not supported)
    • Deprecate fill color as it seems to cause quite some confusion
  • 2.0.0
    • BREAKING: Custom xml attributes are now prefixed with "civ_"
    • Graceful handling of incompatible drawables
    • Add support for a fill color shown behind images with transparent areas
    • Fix dimension calculation issues with small images
    • Fix bitmap not being cleared when set to null
  • 1.3.0
    • Add setBorderColorResource(int resId)
    • Add resource type annotations
    • Add border_overlay attribute to allow drawing border on top of the base image
  • 1.2.2
    • Add ColorFilter support
  • 1.2.1
    • Fix ColorDrawables not being rendered properly on Lollipop
  • 1.2.0
    • Add support for setImageURI(Uri uri)
    • Fix view not being initialized when using CircleImageView(Context context)
  • 1.1.1
    • Fix border being shown although border width is set to 0
  • 1.1.0
    • Add support for ColorDrawables
    • Add getters and setters for border color and border width
  • 1.0.1
    • Prevent crash due to OutOfMemoryError
  • 1.0.0
    • Initial release
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末杀迹,一起剝皮案震驚了整個濱河市亡脸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌树酪,老刑警劉巖浅碾,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異续语,居然都是意外死亡垂谢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門疮茄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滥朱,“玉大人,你說我怎么就攤上這事力试♂懔冢” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵畸裳,是天一觀的道長缰犁。 經(jīng)常有香客問我,道長怖糊,這世上最難降的妖魔是什么帅容? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮伍伤,結(jié)果婚禮上丰嘉,老公的妹妹穿的比我還像新娘。我一直安慰自己嚷缭,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著阅爽,像睡著了一般路幸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上付翁,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天简肴,我揣著相機(jī)與錄音,去河邊找鬼百侧。 笑死砰识,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的佣渴。 我是一名探鬼主播辫狼,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼辛润!你這毒婦竟也來了膨处?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤砂竖,失蹤者是張志新(化名)和其女友劉穎真椿,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乎澄,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡突硝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了置济。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片解恰。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖舟肉,靈堂內(nèi)的尸體忽然破棺而出修噪,到底是詐尸還是另有隱情,我是刑警寧澤路媚,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布黄琼,位于F島的核電站,受9級特大地震影響整慎,放射性物質(zhì)發(fā)生泄漏脏款。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一裤园、第九天 我趴在偏房一處隱蔽的房頂上張望撤师。 院中可真熱鬧,春花似錦拧揽、人聲如沸剃盾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痒谴。三九已至衰伯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間积蔚,已是汗流浹背意鲸。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留尽爆,地道東北人怎顾。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像漱贱,于是被迫代替她去往敵國和親槐雾。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容