在很多時候,原生控件不能滿足我們的需求,就需要我們自定義View,下面以ImageView圓角爲例講一下個人自定義View經驗。我們在自定義View的過程中,需要搞清楚下面幾點:
- 首先想想生活中畫圓的過程:
我們需要工具:圓規,一張紙,圓規裏面還必須要有筆,工具有了就可以畫圓了。
代碼開發出來的功能肯定是爲了服務生活,那就必須來源於生活。對應起來,就不那麼麻煩了。
-
然後就是將圖片圓角的思路:
(1)我們要畫出一個圓
(2)我們用一張圖片去填充這個圓就行了
(3)細節,我們需要控制圓心座標,計算圓的半徑等等問題
-
再然後接下來就是該清楚系統繪製過程,和系統繪製的機制
(1)用什麼去畫?
(2)怎麼畫?
(3)我們怎麼控制,讓他畫出我們想要的東西?
(4)在每個生命週期方法中都做了些什麼?
帶着思路上面的上面的四個問題,我們往下面看,先看完理論的講解,再去看源代碼,分析源代碼,分析源代碼的log,去慢慢搞懂這些問題就差不多了。
1.自定義View的過程或者說系統繪製的機制一般如下:
(1)有一支畫筆(Paint),對這個對象決定最終的效果,比如顏色(顏色就是用於填充我們畫出來的形狀,也可以用圖片去填充),風格等等渲染問題。
(2)有一張畫布(Canvas),在畫布上按照我們想要的效果繪製視圖,就是用canvas這個對象去畫畫,canvas對象決定了畫出來的圖形的形狀,座標等等,這些都需要我們調用API去進行
2.系統將視圖繪製到屏幕的整個過程中,ImageView的幾個關鍵生命週期方法調用過程:
(1)調用setImageDrawable:setImageResource.....這幾個設置圖片的方法,在這幾個方法中就能拿到ImageView中的bitmap或者resourceId,或者地址反正在這兒能得到你設置的圖片
(2)調用構造方法:我們需要自定義屬性,都需要在這兒先關聯attr.xml文件,並且拿到佈局文件中我們設置的屬性值,在後面繪製的時候用。需要注意的是在這兒系統還拿不到view的寬高,所以初始化畫筆等等的事兒,別在這兒做,有坑
(3)onSizeChanged方法:在這兒,我們就能拿到view的寬高了,所以在這兒初始化畫筆等等,時機正好
(4)onDraw方法:這個方法纔是真的讓視圖繪製出來,顯示在我們的眼前。但是這兒要注意需不需要調用父類的onDraw方法,有可能你不想有的視圖出現,完全是希望自己繪製,那就不要調用了,不然會出現雙層視圖
3.上源代碼,來點實際的
在values目錄下創建,自定義屬性,attr.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- 如果是用Android studio開發,注意styleable中的name需要跟自定義的View類名相同,否則,在佈局文件中無法引用下面的屬性 --> <declare-styleable name="CircleImage"> <!-- 是否有外邊框 --> <attr name="hasBorder" format="boolean" /> <!-- 有外邊框的情況下,外邊框的顏色 --> <attr name="borderColor" format="color" /> <!-- 有外邊框的情況下,外邊框的厚度 --> <attr name="borderWidth" format="dimension" /> </declare-styleable> </resources>
編寫xml佈局文件文件引用
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:dragon="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#0000FF"> <!-- xmlns:dragon="http://schemas.android.com/apk/res-auto" 注意看上面的這句話,是用於引用自定義的屬性,不可丟 --> <com.picovr.animatordemo.CircleImage android:id="@+id/iv_animator" android:layout_width="200dp" android:layout_height="200dp" android:layout_centerInParent="true" android:src="@mipmap/timgs" dragon:borderColor="#000000" dragon:borderWidth="3dp" dragon:hasBorder="true" /> </RelativeLayout>
創建類,繼承ImageView編寫代碼
package com.picovr.animatordemo; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.util.AttributeSet; import android.util.Log; import android.widget.ImageView; /** * Created by PICO-USER dragon on 2017/2/28. */ public class CircleImage extends ImageView { // 縮放類型 private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; private static final int COLORDRAWABLE_DIMENSION = 2; private float circleRadius; private Bitmap bitmap; private Paint bitmapPaint; private BitmapShader bitmapShader; private RectF drawableRect; private int bitmapWidth; private int bitmapHeight; private Matrix mShaderMatrix; private Paint borderPaint; private int borderColor; private int borderWidth; private RectF borderRect; private float borderRadius; private boolean hasBorder; public CircleImage(Context context) { super(context); } public CircleImage(Context context, AttributeSet attrs) { super(context, attrs); Log.i("CircleImage3", "CircleImage3 構造方法 :"); //自定義屬性,關聯attr.xml中<declare-styleable name="CircleImage">中的屬性 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleImage); //獲取佈局文件中的自定義屬性 hasBorder = typedArray.getBoolean(R.styleable.CircleImage_hasBorder, false); if (hasBorder) { borderWidth = typedArray.getDimensionPixelSize(R.styleable.CircleImage_borderWidth, 0); borderColor = typedArray.getColor(R.styleable.CircleImage_borderColor, Color.TRANSPARENT); } typedArray.recycle(); } /** * 構造方法中還不能拿到View的寬高,在這個方法中可以得到,所以在這兒實例化畫圓所需要的一切配置 * * @param w * @param h * @param oldw * @param oldh */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); initPaint(); } @Override protected void onDraw(Canvas canvas) { //這兒一定不要調用父類的onDraw方法,調用這個方法系統會直接將ImageView繪畫出來,這就會出現兩張圖片 // super.onDraw(canvas); Log.i("CircleImage3", "onDraw :"); canvas.drawCircle(getWidth() / 2, getHeight() / 2, circleRadius, bitmapPaint); if (hasBorder) { canvas.drawCircle(getWidth() / 2, getHeight() / 2, borderRadius, borderPaint); } } /** * Drawable轉Bitmap * * @param drawable * @return */ private Bitmap getBitmapFromDrawable(Drawable drawable) { if (drawable == null) { return null; } if (drawable instanceof BitmapDrawable) { // 通常來說 我們的代碼就是執行到這裏就返回了。返回的就是我們最原始的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; } } /** * 關鍵代碼就在這兒,畫筆初始化,圓半徑計算,圖片壓縮,構建Shader,用Bitmap填充圓 */ private void initPaint() { if (getDrawable() != null) { bitmap = getBitmapFromDrawable(getDrawable()); } if (bitmap == null) { throw new IllegalArgumentException("the bitmap of imageView is null !"); } if (hasBorder) { /** * 初始化外圓畫筆的屬性,外圓半徑的計算 */ borderPaint = new Paint(); //外圓所在矩形:這兒用一個矩形去控制外圓,Android中的View都是矩形,所以畫圓也是在一個矩形裏面畫內心圓 borderRect = new RectF(); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setAntiAlias(true); //外圓不需要用圖片去填充,而是用想要的顏色去填充 borderPaint.setColor(borderColor); borderPaint.setStrokeWidth(borderWidth); //RectF類中計算,矩形的寬 = right座標-left座標,高 = bottom座標-top座標, // 所以這兒只需要將本ImageView的寬值賦給矩形的right座標,高賦值給bottom座標即可 borderRect.set(0, 0, getWidth(), getHeight()); //外圓的半徑是算法(外圓所在矩形的寬和高這兩邊的短邊的一半減去外圓到內圓的距離) borderRadius = Math.min((borderRect.width() - borderWidth) / 2, (borderRect.height() - borderWidth) / 2); } //初始化內圓的畫筆屬性,半徑計算 bitmapPaint = new Paint(); //同外圓一樣,畫一個矩形的內心圓 drawableRect = new RectF(); //賦給shader的矩陣,用於壓縮圖片,讓圖片的中心部位去填充內圓 mShaderMatrix = new Matrix(); bitmapWidth = bitmap.getWidth(); bitmapHeight = bitmap.getHeight(); // 構建渲染器,用mBitmap位圖來填充繪製區域 ,參數值代表如果圖片太小的話 就直接拉伸 bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); bitmapPaint.setAntiAlias(true); // 設置圖片畫筆渲染器 bitmapPaint.setShader(bitmapShader); if (hasBorder) { //內圓所屬矩形的寬和高應該分別是外圓所屬矩形的寬減去外圓到內圓的距離和高減去外圓到內圓的距離 drawableRect.set(0, 0, borderRect.width() - borderWidth, borderRect.height() - borderWidth); } else { drawableRect.set(0, 0, getWidth(), getHeight()); } circleRadius = Math.min(drawableRect.height() / 2, drawableRect.width() / 2); Log.i("CircleImage3", "initPaint drawable Width :" + drawableRect.width() + " Height " + drawableRect.height() + " View width :" + getWidth() + " height :" + getHeight()); //壓縮並位移圖片用於填充內部圓 setBitmapShaderMtrix(); } private void setBitmapShaderMtrix() { float scale; float dx = 0; float dy = 0; Log.i("CircleImage3", "setBitmapShaderMtrix drawable Width :" + drawableRect.width() + " Height " + drawableRect.height()); Log.i("CircleImage3", "setBitmapShaderMtrix bitmap width :" + bitmap.getWidth() + " height : " + bitmap.getHeight()); mShaderMatrix.set(null); //x方向壓縮還是Y方向壓縮判斷 if (bitmapWidth * drawableRect.height() > drawableRect.width() * bitmapHeight) { // y軸縮放 x軸平移 使得圖片的y軸方向的邊的尺寸縮放到圖片顯示區域(mDrawableRect)一樣) scale = drawableRect.height() / (float) bitmapHeight; dx = (drawableRect.width() - bitmapWidth * scale) * 0.5f; Log.i("CircleImage3", "setBitmapShaderMtrix scale :" + scale + " dx :" + dx); } else { // x軸縮放 y軸平移 使得圖片的x軸方向的邊的尺寸縮放到圖片顯示區域(mDrawableRect)一樣) scale = drawableRect.width() / (float) bitmapWidth; dy = (drawableRect.height() - bitmapHeight * scale) * 0.5f; Log.i("CircleImage3", "setBitmapShaderMtrix scale :" + scale + " dy :" + dy); } // shaeder的變換矩陣,我們這裏主要用於放大或者縮小。 mShaderMatrix.setScale(scale, scale); // 平移 mShaderMatrix.postTranslate((int) (dx + 0.5f) + drawableRect.left, (int) (dy + 0.5f) + drawableRect.top); // 設置變換矩陣 bitmapShader.setLocalMatrix(mShaderMatrix); } @Override public void setImageDrawable(Drawable drawable) { super.setImageDrawable(drawable); } }
MainActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv_animator = ((CircleImage) findViewById(R.id.iv_animator));
}
效果附上: