CircleImageView詳解與源碼分析

一、前言

這篇文章要分析的是來自hdodenhof的CircleImageView,其目標是實現一個圓形圖片的展示,並且它還支持設置一個可愛的邊框。下面是其 demo 展示的效果。帥氣的大叔。

CircleImageView詳解與源碼分析


二、庫簡介

主要是項目主頁文檔的解讀,基礎好或者英語還不錯的同學可以直接去過一遍原文,畢竟自己的理解才是真正的理解。

原理

(1) 繼承自 ImageView,即自定義了 ImageView。

(2) 使用 BitmapShader 來實現圓形繪製,但出於內存優化以及效率的考慮。

a.不會對圖片進行復制,即不會有圖片的副本。

b.不使用 clipPath,因為其既不支持硬件加速也不支持抗鋸齒。

c.不使用setXfermode通過裁剪圖片的方式實現,因為那將要繪製 2 次。

限制

(1) 不支持除 CENTER_CROP 之外的 ScaleType,也就是說只支持圖片中心裁剪的縮放方式。

(2) 不支持 adjustViewBounds(是否保持原圖的長寬比),當然不支持了,因為其不支持其他的 ScaleType。

(3) 不支持 Picasso or Glide 的動畫。不過,話說這 2 個庫本身就可以自己實現支持圓形,圓角或者任意形狀的圖片。

(4) 不支持 TransitionDrawable(漸變),可能會導致圖片混亂。

使用

(1) 依賴

<code>dependencies {
...
implementation 'de.hdodenhof:circleimageview:2.2.0'
}
/<code>

(2) 在 xml 中使用

<code><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:class="lazy" src="//p2.ttnews.xyz/loading.gif" data-original="@drawable/profile"
app:civ_border_width="2dp"
app:civ_border_color="#FF000000"/>
/<de.hdodenhof.circleimageview.circleimageview>/<code>

三、源碼分析

構造函數

<code>public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);

TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);

mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH);
mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR);
mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY);

// Look for deprecated civ_fill_color if civ_circle_background_color is not set
if (a.hasValue(R.styleable.CircleImageView_civ_circle_background_color)) {
mCircleBackgroundColor = a.getColor(R.styleable.CircleImageView_civ_circle_background_color,
DEFAULT_CIRCLE_BACKGROUND_COLOR);
} else if (a.hasValue(R.styleable.CircleImageView_civ_fill_color)) {
mCircleBackgroundColor = a.getColor(R.styleable.CircleImageView_civ_fill_color,
DEFAULT_CIRCLE_BACKGROUND_COLOR);
}

a.recycle();

init();
}
/<code>

構造函數首先是屬性的初始化,這裡主要是 mBorderWidth(邊框粗細),mBorderColor(邊框顏色),mBorderOverlay(邊框是否覆蓋圖片)。如果在 xml 中在配置了這 3 個屬性,則會用 xml 中的,否則就是默認配置。接著是 init() 方法的調用,對整個繪製環境進行初始化。

init()

<code>private void init() {
super.setScaleType(SCALE_TYPE);
mReady = true;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setOutlineProvider(new OutlineProvider());
}

if (mSetupPending) {
setup();
mSetupPending = false;
}
}
/<code>

初始化時限定了 ScaleType 為默認 ScaleType.CENTER_CROP,對於 LOLLIPOP 及以上的平臺,還可以通過設置輪廓線來約束View的形狀。接下來就是最重要的 setup() 調用。

setup()

<code>private void setup() {
// 狀態、數據有效性的檢查
if (!mReady) {
mSetupPending = true;
return;
}

if (getWidth() == 0 && getHeight() == 0) {
return;
}

if (mBitmap == null) {
invalidate();
return;
}

// 以需要渲染的 bitmap 來構造 BitmapShader
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

// 設置搞鋸齒
mBitmapPaint.setAntiAlias(true);
// 設置畫筆的 Shader(著色器) 為 BitmapShader
mBitmapPaint.setShader(mBitmapShader);

// 設置邊框畫筆的樣式為 stroke,並設置抗鋸齒,畫筆顏色,畫筆寬度
mBorderPaint.setStyle(Paint.Style.STROKE);

mBorderPaint.setAntiAlias(true);
mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStrokeWidth(mBorderWidth);

// 設置背景畫筆的樣式為 fill ,並設置抗鋸齒,顏色等
mCircleBackgroundPaint.setStyle(Paint.Style.FILL);
mCircleBackgroundPaint.setAntiAlias(true);
mCircleBackgroundPaint.setColor(mCircleBackgroundColor);

mBitmapHeight = mBitmap.getHeight();
mBitmapWidth = mBitmap.getWidth();

// 計算邊框的半徑
mBorderRect.set(calculateBounds());
// 以較小的那一條邊減去畫筆寬度為直徑,那麼除 2 就是半徑了
mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f);

mDrawableRect.set(mBorderRect);
if (!mBorderOverlay && mBorderWidth > 0) {
// 邊框不覆蓋並且邊框的寬度大於 0 的情況下,留 1 個像素的邊框
mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f);
}
// 以較小的邊為直徑
mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);
// 通過 ColorFilter 來設置圖片畫刷的顏色
applyColorFilter();
// 更新 BitmapShader 的 matrix ,主要作用是作縮放變換
updateShaderMatrix();
// 最後,刷新 View
invalidate();
}
/<code>

代碼中的註釋幾乎是一行一行的給出瞭解釋,當然如果不熟悉 Shader,ColorFilter,Matrix 這些概念的,可能還是不明白。沒關係,這些概念我們適當的展開一下。這裡就瞭解一下Shader,後面會再講解 ColorFilter 與 Matrix 的相關概念。

Shader 有渲染器,著色器的意思,在圖形圖像中是一個非常基礎的概念。用白話講,就是我們所期望的某個區域該如何填充,可以填充顏色,也可以填充圖片。這就類似於我們在 PPT 或者 Word 中畫一個矩形可以填充純顏色,漸變色,圖片等是一個原理。在Android中,系統為我們定義如下 5 個Shader:BitmapShader:這個簡單,也是這裡要用到的,就是用圖片填充

CircleImageView詳解與源碼分析

image.png


ComposeShader:組合 Shader,就是將 2 個 Shader 的效果放在一起進行渲染。當然,這裡的組合只能也且是 2 個。LinearGradient:漸變Shader,是最普通的線性漸變

CircleImageView詳解與源碼分析

image.png


RadialGradient:漸變Shader,從中心向四周發散的輻射漸變效果

CircleImageView詳解與源碼分析

image.png


SweepGradient:漸變Shader,創建360度顏色旋轉漸變效果

CircleImageView詳解與源碼分析

image.png


關於 Shader 上面的截圖,都是來自於Android中Canvas繪圖之Shader使用圖文詳解,有興趣的同學可以再進去詳細瞭解。同時也感謝作者的分享。

關於 shader 就瞭解這麼多,最主要是我們要有這麼一個認知,那麼,再回到 setup() 中來。從 setup() 的實現來看,其主要定義了 3 個畫刷,圖片畫刷,邊框畫刷以及背景畫刷。3 個畫刷就確定了它的 3 個功能,圓角圖片,邊框,背景。我們先進一步分析其內部另外調用的 3 個方法:calculateBounds(),applyColorFilter(),updateShaderMatrix()

calculateBounds()

<code>private RectF calculateBounds() {
int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();

int sideLength = Math.min(availableWidth, availableHeight);

float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
float top = getPaddingTop() + (availableHeight - sideLength) / 2f;

return new RectF(left, top, left + sideLength, top + sideLength);
}
/<code>

上面 calculateBounds() 的計算代碼,可以以下圖的方式來表示。


CircleImageView詳解與源碼分析

image.png


假設 padding 都為 0,而又假設置寬高為 8 和 10,那麼最後就會得到藍色區域這個框,也就是矩形的中心區域。

applyColorFilter()

<code>private void applyColorFilter() {
if (mBitmapPaint != null) {
mBitmapPaint.setColorFilter(mColorFilter);
}
}
/<code>

這個方法非常簡單,就是將這個 ColorFilter 設置圖片畫刷。那麼何為 ColorFilter 呢?字面直譯就是顏色過濾,其應該理解為一個顏色濾鏡。ColorFilter 會與畫刷所設置的顏色進行特定的算法運算,從而來改變當前繪製出來的效果。而這種改變不僅僅限於單純的顏色的改變,還包括飽和度,亮度等。Android 中為我們定義了 3 個非常有用的 ColorFilter,分別是:ColorMatrixColorFilter:矩陣過濾器,通過一個 4 * 5 的顏色矩陣可以達到提高或者減小原色彩的飽和度等LightingColorFilter:亮度過濾器,通過對 RGB 3 個分量分別提高或者減少其分量值,從而達到調整亮度的目的。PorterDuffColorFilter:PorterDuff 過濾器。所謂的 PorterDuff 是一種用定義圖像/顏色的組合模式。其名字來由是 Thomas Porter and Tom Duff。他們最初將其發表在論文《Compositing Digital Images》裡面,有興趣的同學可以去拜讀一下。

updateShaderMatrix()

<code>private void updateShaderMatrix() {
float scale;
float dx = 0;
float dy = 0;
// 矩陣重置
mShaderMatrix.set(null);
// mDrawableRect 的寬與高是一樣的。所以下方是以長邊為基準進行縮放比例的計算
if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
scale = mDrawableRect.height() / (float) mBitmapHeight;
dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
} else {
scale = mDrawableRect.width() / (float) mBitmapWidth;
dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
}
// 設置寬高的縮放係數
mShaderMatrix.setScale(scale, scale);
// 設置圖片的位移,以使它能定位在通過 calculateBounds() 計算出來的區域裡面
mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);
// 最後送給著色器的局部轉換矩陣
mBitmapShader.setLocalMatrix(mShaderMatrix);
}
/<code>

updateShaderMatrix() 的作用就是構造出一個變換矩陣,以使圖片可以按照預期的大小進行縮放,並且展示在指定的位置上。

矩陣在圖形圖像編程中非常重要,不管是2D還是3D都有著不可或缺的作用。在Android 最基礎的 2D 圖像中,有前面所提及的 ColorMatrixColorFilter,還有這裡的 Matrix ,以及動畫變換的內部實現都是用的 Matrix 的變換。

到這裡 setup() 就分析完了,其主要的作用就是構造好繪製的環境與所需要的材料,圖片畫刷,邊框畫刷,背景畫刷。圖片的半徑,變換矩陣,邊框寬度,邊框半徑等。準備好了這些材料,接下來就是等待系統刷新來調用我們的 onDraw() 了。

onDraw()

<code> @Override
protected void onDraw(Canvas canvas) {
if (mDisableCircularTransformation) {
super.onDraw(canvas);
return;
}

if (mBitmap == null) {
return;
}
// 繪製背景
if (mCircleBackgroundColor != Color.TRANSPARENT) {
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint);
}
// 繪製圖片
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
// 繪製邊框
if (mBorderWidth > 0) {
canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
}
}
/<code>

onDraw()的實現是非常簡單的,先繪製背景,再繪製圖片,最後再繪製邊框,然後就完成了整個繪製了。其實可以注意到了,這裡實現的原理是圖片的畫刷可以畫出一個圖片,而 canvas.drawCircle() 可以確定該畫出什麼形狀。那麼類似的,只要改變 canvas.drawXXX() 就可以達到繪製出其他形狀的圖片了。是不是很簡單呢。

至此,整個 CircleImageView 都分析完了。裡面所涉及到的知識點也做了適當的展開與分析,以幫助我們能更深入的理解其實現的原理。

四、總結

最後,感謝你能讀到此文章並讀完此文章。希望我的分享能夠幫助到你,如果分析的過程中存在錯誤或者疑問都歡迎留言討論。最後再簡單小結下 CircleImageView 的原理:

(1) 通過 BitmapShader 構造一個可以繪製出圖片的畫刷

(2) 通過 View 的大小,Padding,邊框等計算出圖片需要進行變換的變換矩陣

(3) 通過 canvas.drawCircle() 方法繪製出最終需要的圓形圖片


分享到:


相關文章: