原始碼地址為 https://github.com/hdodenhof/CircleImageView
實際上就是一個圓形的imageview 的自定義控制元件。程式碼寫的很優雅,實現效果也很好,
特此分析。原始碼其實不難 主要就是一個類,可以把我的這個加了註釋的原始碼放到你自己的工程裡直接替換
然後run,這樣效果更佳。
1 package de.hdodenhof.circleimageview; 2 3 import android.content.Context; 4 import android.content.res.TypedArray; 5 import android.graphics.Bitmap; 6 import android.graphics.BitmapShader; 7 import android.graphics.Canvas; 8 import android.graphics.Color; 9 import android.graphics.ColorFilter; 10 import android.graphics.Matrix; 11 import android.graphics.Paint; 12 import android.graphics.RectF; 13 import android.graphics.Shader; 14 import android.graphics.drawable.BitmapDrawable; 15 import android.graphics.drawable.ColorDrawable; 16 import android.graphics.drawable.Drawable; 17 import android.net.Uri; 18 import android.support.annotation.ColorRes; 19 import android.support.annotation.DrawableRes; 20 import android.util.AttributeSet; 21 import android.util.Log; 22 import android.widget.ImageView; 23 24 /** 25 * 實際上整體思路還是比較簡單的,利用BitmapShader 來把imageview裡的圖片分割成圓形 26 * 畫出圓形來以後 再畫描邊。 27 * 這個開源控制元件做的比較出色的地方是updateShaderMatrix 函式會幫忙做圖片修正,使切割出來的圖片損失度最小. 28 * 此外就是各種情況考慮的比較多,流程控制的比較嚴謹,其中主要是多次呼叫setup函式 來完成imageview的及時重新整理 29 */ 30 public class CircleImageView extends ImageView { 31 32 private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP; 33 34 private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; 35 private static final int COLORDRAWABLE_DIMENSION = 2; 36 37 private static final int DEFAULT_BORDER_WIDTH = 0; 38 private static final int DEFAULT_BORDER_COLOR = Color.BLACK; 39 private static final boolean DEFAULT_BORDER_OVERLAY = false; 40 41 private final RectF mDrawableRect = new RectF(); 42 private final RectF mBorderRect = new RectF(); 43 44 private final Matrix mShaderMatrix = new Matrix(); 45 //這個畫筆最重要的是關聯了mBitmapShader 使canvas在執行的時候可以切割原圖片(mBitmapShader是關聯了原圖的bitmap的) 46 private final Paint mBitmapPaint = new Paint(); 47 //這個描邊,則與本身的原圖bitmap沒有任何關聯, 48 private final Paint mBorderPaint = new Paint(); 49 50 //這裡定義了 圓形邊緣的預設寬度和顏色 51 private int mBorderColor = DEFAULT_BORDER_COLOR; 52 private int mBorderWidth = DEFAULT_BORDER_WIDTH; 53 54 private Bitmap mBitmap; 55 private BitmapShader mBitmapShader; 56 private int mBitmapWidth; 57 private int mBitmapHeight; 58 59 private float mDrawableRadius; 60 private float mBorderRadius; 61 62 private ColorFilter mColorFilter; 63 64 /** 65 * 初始值都為false 66 */ 67 private boolean mReady; 68 private boolean mSetupPending; 69 private boolean mBorderOverlay; 70 71 public CircleImageView(Context context) { 72 super(context); 73 74 init(); 75 } 76 77 public CircleImageView(Context context, AttributeSet attrs) { 78 this(context, attrs, 0); 79 } 80 81 public CircleImageView(Context context, AttributeSet attrs, int defStyle) { 82 super(context, attrs, defStyle); 83 Log.v("CircleImageView", "gou zao han shu"); 84 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0); 85 86 //取得我們在xml裡定義的引數值 87 mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_border_width, DEFAULT_BORDER_WIDTH); 88 mBorderColor = a.getColor(R.styleable.CircleImageView_border_color, DEFAULT_BORDER_COLOR); 89 mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_border_overlay, DEFAULT_BORDER_OVERLAY); 90 91 a.recycle(); 92 93 init(); 94 } 95 96 /** 97 * 這個函式 只在建構函式裡面呼叫 他的作用就是 保證setup函式裡的流程一定要在 98 * 建構函式執行完畢的時候去呼叫 mReady為true setup函式裡的程式碼才能向下執行 99 */ 100 private void init() { 101 Log.v("CircleImageView", "init()"); 102 super.setScaleType(SCALE_TYPE); 103 mReady = true; 104 105 if (mSetupPending) { 106 setup(); 107 mSetupPending = false; 108 } 109 } 110 111 @Override 112 public ScaleType getScaleType() { 113 return SCALE_TYPE; 114 } 115 116 /** 117 * 這裡明確指出 此種imageview 只支援CENTER_CROP 這一種屬性 118 * 119 * @param scaleType 120 */ 121 @Override 122 public void setScaleType(ScaleType scaleType) { 123 if (scaleType != SCALE_TYPE) { 124 throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType)); 125 } 126 } 127 128 @Override 129 public void setAdjustViewBounds(boolean adjustViewBounds) { 130 if (adjustViewBounds) { 131 throw new IllegalArgumentException("adjustViewBounds not supported."); 132 } 133 } 134 135 @Override 136 protected void onDraw(Canvas canvas) { 137 Log.v("CircleImageView", "onDraw"); 138 if (getDrawable() == null) { 139 return; 140 } 141 142 //這行程式碼就是把imageview 切割成最終的圓形 143 canvas.drawCircle(getWidth() / 2, getHeight() / 2, mDrawableRadius, mBitmapPaint); 144 //如果圓形邊緣的寬度不為0 我們還要繼續畫這個描邊 145 if (mBorderWidth != 0) { 146 canvas.drawCircle(getWidth() / 2, getHeight() / 2, mBorderRadius, mBorderPaint); 147 } 148 } 149 150 @Override 151 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 152 super.onSizeChanged(w, h, oldw, oldh); 153 setup(); 154 } 155 156 public int getBorderColor() { 157 return mBorderColor; 158 } 159 160 public void setBorderColor(int borderColor) { 161 if (borderColor == mBorderColor) { 162 return; 163 } 164 165 mBorderColor = borderColor; 166 mBorderPaint.setColor(mBorderColor); 167 invalidate(); 168 } 169 170 public void setBorderColorResource(@ColorRes int borderColorRes) { 171 setBorderColor(getContext().getResources().getColor(borderColorRes)); 172 } 173 174 public int getBorderWidth() { 175 return mBorderWidth; 176 } 177 178 public void setBorderWidth(int borderWidth) { 179 if (borderWidth == mBorderWidth) { 180 return; 181 } 182 183 mBorderWidth = borderWidth; 184 setup(); 185 } 186 187 public boolean isBorderOverlay() { 188 return mBorderOverlay; 189 } 190 191 public void setBorderOverlay(boolean borderOverlay) { 192 if (borderOverlay == mBorderOverlay) { 193 return; 194 } 195 196 mBorderOverlay = borderOverlay; 197 setup(); 198 } 199 200 @Override 201 public void setImageBitmap(Bitmap bm) { 202 super.setImageBitmap(bm); 203 mBitmap = bm; 204 setup(); 205 } 206 207 208 /** 209 * 注意這個函式 是在我們的建構函式呼叫之前就呼叫了 210 * 211 * @param drawable 212 */ 213 @Override 214 public void setImageDrawable(Drawable drawable) { 215 Log.v("CircleImageView", "setImageDrawable Drawable"); 216 super.setImageDrawable(drawable); 217 mBitmap = getBitmapFromDrawable(drawable); 218 setup(); 219 } 220 221 @Override 222 public void setImageResource(@DrawableRes int resId) { 223 super.setImageResource(resId); 224 mBitmap = getBitmapFromDrawable(getDrawable()); 225 setup(); 226 } 227 228 @Override 229 public void setImageURI(Uri uri) { 230 super.setImageURI(uri); 231 mBitmap = getBitmapFromDrawable(getDrawable()); 232 setup(); 233 } 234 235 @Override 236 public void setColorFilter(ColorFilter cf) { 237 if (cf == mColorFilter) { 238 return; 239 } 240 241 mColorFilter = cf; 242 mBitmapPaint.setColorFilter(mColorFilter); 243 invalidate(); 244 } 245 246 private Bitmap getBitmapFromDrawable(Drawable drawable) { 247 Log.v("CircleImageView", "getBitmapFromDrawable"); 248 if (drawable == null) { 249 Log.v("CircleImageView", "drawable==null"); 250 //這種情況一般不會發生 251 return null; 252 } 253 254 if (drawable instanceof BitmapDrawable) { 255 Log.v("CircleImageView", "drawable instanceof BitmapDrawable"); 256 //通常來說 我們的程式碼就是執行到這裡就返回了。返回的就是我們最原始的bitmap 257 return ((BitmapDrawable) drawable).getBitmap(); 258 } 259 Log.v("CircleImageView", "drawable is not instanceof BitmapDrawable"); 260 261 try { 262 Bitmap bitmap; 263 264 if (drawable instanceof ColorDrawable) { 265 Log.v("CircleImageView", "drawable instanceof ColorDrawable"); 266 bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG); 267 } else { 268 Log.v("CircleImageView", "drawable is not instanceof ColorDrawable"); 269 bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG); 270 } 271 272 Canvas canvas = new Canvas(bitmap); 273 drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 274 drawable.draw(canvas); 275 return bitmap; 276 } catch (OutOfMemoryError e) { 277 return null; 278 } 279 } 280 281 /** 282 * 這個函式比較關鍵,就是在進行一些重繪引數的初始化 283 */ 284 private void setup() { 285 Log.v("CircleImageView", "setup()"); 286 Log.v("CircleImageView", "mReady==" + mReady + " mSetupPending==" + mSetupPending); 287 //這個地方要注意mReady的預設值為false,也就是說第一次進這個函式的時候 因為為false 所以直接進入括號 288 //體內然後返回,後面的程式碼並沒有執行。 同時也能知道,mReady的值更改成true 是在init函式裡面做的 289 if (!mReady) { 290 mSetupPending = true; 291 return; 292 } 293 294 //防止空指標異常 295 if (mBitmap == null) { 296 return; 297 } 298 299 //引數值就代表 如果圖片太小的話 就直接拉伸,repeat引數代表 圖片大小的話就重複放圖片 mirror就是映象對著放圖片的意思 跟大家設定pc 屏保時候其實是一樣的 300 mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 301 302 mBitmapPaint.setAntiAlias(true); 303 mBitmapPaint.setShader(mBitmapShader); 304 305 306 mBorderPaint.setStyle(Paint.Style.STROKE); 307 mBorderPaint.setAntiAlias(true); 308 mBorderPaint.setColor(mBorderColor); 309 mBorderPaint.setStrokeWidth(mBorderWidth); 310 311 //這個地方是取的原圖片的大小 312 mBitmapHeight = mBitmap.getHeight(); 313 mBitmapWidth = mBitmap.getWidth(); 314 315 //注意這個地方取的是imageview的實際大小,也就是說這個地方畫了一個和imageview實際大小一致的方形圖 316 mBorderRect.set(0, 0, getWidth(), getHeight()); 317 Log.v("CircleImageView", "mBitmapHeight==" + mBitmapHeight + " mBitmapWidth==" + mBitmapWidth); 318 Log.v("CircleImageView", "getWidth()" + getWidth() + " getHeight()==" + getHeight()); 319 //這個地方就是算最小半徑的,注意是要減去邊緣寬度的 因為這裡計算的是 圓形邊緣部分的最小半徑 320 mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2, (mBorderRect.width() - mBorderWidth) / 2); 321 322 mDrawableRect.set(mBorderRect); 323 if (!mBorderOverlay) { 324 mDrawableRect.inset(mBorderWidth, mBorderWidth); 325 } 326 //這裡計算的是圓形內部的最小半徑,其實很好理解,因為這個自定義控制元件提供了設定圓形邊緣寬度的屬性方法,所以在這裡對於一個圓形邊緣有寬度的圖形來說 327 //半徑就是有2個,一個是外部半徑,一個內部半徑,上面的mBorderRadius就是內部半徑 而這裡是外部半徑 一般來說 mDrawableRadius>=mBorderRadius 328 mDrawableRadius = Math.min(mDrawableRect.height() / 2, mDrawableRect.width() / 2); 329 updateShaderMatrix(); 330 //手動觸發ondraw()函式 完成最終的繪製 331 invalidate(); 332 } 333 334 /** 335 * 這個函式很好理解,就是做平移變換 放大或者縮小圖片 所使用的,儘量保證 我們切割出來的圖片 損失度最小。‘ 336 * <p/> 337 * 這裡面的演算法可以好好研讀一下 此方法能保證你每次切割出來的圖片都是 原始圖片正中央的那一部分 338 */ 339 private void updateShaderMatrix() { 340 Log.v("CircleImageView", "updateShaderMatrix()"); 341 float scale; 342 float dx = 0; 343 float dy = 0; 344 345 mShaderMatrix.set(null); 346 347 if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { 348 349 //此縮放策略就是y軸縮放 x軸平移 350 scale = mDrawableRect.height() / (float) mBitmapHeight; 351 dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f; 352 } else { 353 //此縮放策略是 x軸縮放 y軸平移 354 scale = mDrawableRect.width() / (float) mBitmapWidth; 355 dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f; 356 } 357 358 mShaderMatrix.setScale(scale, scale); 359 mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top); 360 361 mBitmapShader.setLocalMatrix(mShaderMatrix); 362 } 363 364 }