每次聽到某大牛談論自定義View双絮,頓時敬佩之心浴麻,如滔滔江水連綿不絕,心想我什么時候能有如此境界囤攀,好了软免,心動不如行動,于是我開始了自定義View之路抚岗,雖然過程有坎坷或杠,但是結(jié)果我還是挺滿意的。我知道大牛還遙不可及宣蔚,但是我已使出洪荒之力向抢。此篇博客記錄本人初入自定義View之路。
既然是初出茅廬胚委,自然是按部就班的進行,先來一張效果圖
項目源碼【傳送門】
自定義屬性
自定義屬性挟鸠,就是在資源文件夾下values目錄中創(chuàng)建一個attrs.xml文件,
文件結(jié)構(gòu)如下所示亩冬,atrr標簽就是我們要自定義的一些屬性艘希,name就是自定義屬性的名字,那么format是做什么的呢硅急?
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="">
<attr name="centerText" format=""></attr>
<attr name=" ">
<enum name="" value=" "></enum>
<enum name="" value=" "></enum>
</attr>
</declare-styleable>
</resources>
format是屬性對應的值的類型覆享,有十個值
- enm 枚舉類型,例 android:orientation="vertical" 此值有horizontal营袜,和 vertical
- dimension 尺寸值
- color 顏色值撒顿,例 android:textColor = "#00FF00"
- boolean 布爾值,true or false
- flag 位或運算
- float 浮點型
- fraction 百分數(shù)荚板,
- reference 參考某一資源ID,例 android:background = "@drawable/ic_launcher"
- string 字符串類型
- integer 整型值
知道了這些值得含義凤壁,就可以自定義我們自己的屬性了吩屹,對于這個進度條,我們可以自定義圓的半徑拧抖,顏色煤搜,和圓中心文本的大小,顏色唧席,文本擦盾,最后attrs.xml文件為
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomBallView">
<attr name="centerText" format="string"></attr>
<attr name="centerTextSize" format="dimension"></attr>
<attr name="centerTextColor" format="color"></attr>
<attr name="ballColor" format="color"></attr>
<attr name="ballRadius" format="dimension"></attr>
</declare-styleable>
</resources>
布局文件配置相關(guān)內(nèi)容
在布局文件要配置我們自定義的屬性,首先要自定義命名空間袱吆,
如上圖厌衙,如果在as中命名空間寫成http://schemas.android.com/apk/res/包名 此時as會報錯距淫,這是gradle造成的绞绒,在eclipse中如果自定義的屬性 是不能用res-auto的 必須得替換成你自定義view所屬的包名,如果你在恰好使用的自定義屬性被做成了lib 那就只能使用res-auto了榕暇,而在android-studio里蓬衡,無論你是自己寫自定義view 還是引用的lib里的自定義的view 都只能使用res-auto這個寫法。以前那個包名的寫法 在android-studio里是被廢棄無法使用的
所以配置后的布局文件如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:customBallView="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.xh.customball.MainActivity"
tools:showIn="@layout/activity_main">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:text="Hello World!" />
<com.example.xh.customball.CustomBall
android:background="@color/colorPrimary"
android:layout_centerInParent="true"
android:layout_margin="10dp"
customBallView:centerText="30%"
customBallView:centerTextSize="28dp"
customBallView:centerTextColor="#000000"
customBallView:ballColor="@color/colorAccent"
customBallView:ballRadius="30dp"
android:layout_width="260dp"
android:layout_height="260dp">
</com.example.xh.customball.CustomBall>
</LinearLayout>
自定義控件
有了上邊的操作彤枢,接下來就開始到了真正自定義控件的時候了狰晚,創(chuàng)建一個CustomBall類繼承View類,先看構(gòu)造方法缴啡,我們寫成構(gòu)造方法最終調(diào)用三個參數(shù)的構(gòu)造方法壁晒,獲取自定義屬性的值及初始化工作就在三個參數(shù)構(gòu)造方法中進行。下面我先先來繪制一個圓业栅,文字畫在圓心試試手秒咐,效果如圖
當然繪制這個圖形,首先獲取我們自定義屬性值碘裕,可通過下面獲取屬性值
注意通過TypedArray 獲取屬性值后要執(zhí)行typedArray.recycle();回收內(nèi)存携取,防止內(nèi)存泄漏。
/**
* 獲取自定義屬性
*/
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.customBallView);
centerText=typedArray.getString(R.styleable.customBallView_centerText);
Log.e("TAG","centerText"+centerText);
centerTextSize=typedArray.getDimension(R.styleable.customBallView_centerTextSize,24f);
centerTextColor=typedArray.getColor(R.styleable.customBallView_centerTextColor,0xFFFFFF);
ballColor=typedArray.getColor(R.styleable.customBallView_ballColor,0xFF4081);
radius=typedArray.getDimension(R.styleable.customBallView_ballRadius,260f);
typedArray.recycle();
初始化畫筆
/**
* 初始化畫筆
*/
private void initPaint() {
roundPaint = new Paint();
roundPaint.setColor(ballColor);
roundPaint.setAntiAlias(true);//抗鋸齒
fontPaint = new Paint();
fontPaint.setTextSize(centerTextSize);
fontPaint.setColor(centerTextColor);
fontPaint.setAntiAlias(true);
fontPaint.setFakeBoldText(true);//粗體
}
接下來我們先畫一個圓帮孔,先通過下面方法獲取空間本身的寬和高雷滋,然后調(diào)用canvas.drawCircle(width/2, height/2, radius, roundPaint);畫圓,在原點設(shè)置為控件中心位置文兢,即點(width/2, height/2)晤斩,半徑為radius,畫筆roundPaint姆坚,接下來繪制文字澳泵,將位子繪制在圓的中心。
width = getWidth() ;
height = getHeight();
如果我們通過canvas.drawText(centerText, width/2, height/2, fontPaint);繪制文字的話旷偿,發(fā)現(xiàn)文字并不是在中心位置烹俗,那么我們可以做一下調(diào)整爆侣,canvas.drawText(centerText, width/2, height/2, fontPaint);先通過float textWidth = fontPaint.measureText(centerText);獲取文字的寬度,canvas.drawText(centerText, width/2-textWidth /2, height/2, fontPaint);此時文字依然不在中心幢妄,那么此時我們研究一下文字到底是怎么繪制的兔仰,為什么坐標試試中心了,繪制出來的效果依然有偏差呢蕉鸳。
要關(guān)注文字繪制的話乎赴,F(xiàn)ontMetrics這個類是必須要知道的因為它的作用是測量文字,它里面呢就定義了top,ascent,descent,bottom,leading五個成員變量其他什么也沒有潮尝。先看源碼
public static class FontMetrics {
/**
* The maximum distance above the baseline for the tallest glyph in
* the font at a given text size.
*/
public float top;
/**
* The recommended distance above the baseline for singled spaced text.
*/
public float ascent;
/**
* The recommended distance below the baseline for singled spaced text.
*/
public float descent;
/**
* The maximum distance below the baseline for the lowest glyph in
* the font at a given text size.
*/
public float bottom;
/**
* The recommended additional space to add between lines of text.
*/
public float leading;
}
這個類是Paint的靜態(tài)內(nèi)部類榕吼,通過注釋我們就知道了每個變量的含義,為了更生動的理解這幾個變量含義勉失,我們通過下面的一張圖來分別解釋每個變量的含義
- Baseline(基線) 在Android中羹蚣,文字的繪制都是從Baseline處開始的
- ascent(上坡度)Baseline往上至文字“最高處”的距離我們稱之為ascent,
- descent(下坡度)Baseline往下至文字“最低處”的距離我們稱之為descent(下坡度)
- leading(行間距)表示上一行文字的descent到該行文字的ascent之間的距離
- top 對于ascent上面還有一部分內(nèi)邊距乱凿,內(nèi)邊距加上ascent即為top值
- bottom descent和內(nèi)邊距的加上descent距離
值得注意的一點顽素,Baseline上方的值為負,下方的值為正如下圖文字30%的ascent,descent徒蟆,top,bottom胁出。
通過上面的分析,我們就得出了將文本繪制中心的代碼如下
//測量文字的寬度
float textWidth = fontPaint.measureText(centerText);
float x = width / 2 - textWidth / 2;
Paint.FontMetrics fontMetrics = fontPaint.getFontMetrics();
float dy = -(fontMetrics.descent + fontMetrics.ascent) / 2;
float y = height / 2 + dy;
canvas.drawText(centerText, x, y, fontPaint);
至此這個簡單自定義的View基本實現(xiàn)段审,此時我改了布局配置文件為寬高
android:layout_width="wrap_content"
android:layout_height="wrap_content"
或者
android:layout_width="match_parent"
android:layout_height="match_parent"
Oh my God全蝶,為什么效果是一樣的啊,此時再回到自定義的類寺枉,我們發(fā)現(xiàn)我們沒有實現(xiàn)onMeasure里面測量的代碼抑淫,接下來讓我們實現(xiàn)onMeasure操作,如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//測量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//測量規(guī)格大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width=widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width=(int)Math.min(widthSize,radius*2);
} else {
width=windowWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
height=heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height=(int)Math.min(heightSize,radius*2);
} else {
height=windowHeight;
}
setMeasuredDimension(width,height);
}
測量主要依靠MeasureSpec型凳,MeasureSpec(測量規(guī)格)是一個32位的int數(shù)據(jù).其中高2位代表SpecMode即某種測量模式,低32位為SpecSize代表在該模式下的規(guī)格大小丈冬,測量模式有三種
- EXACTLY 確切的,在布局文件中設(shè)置的寬高是固定的甘畅,此時測量大小就是我們設(shè)置的寬高
- AT_MOST 至多埂蕊,不能超出
- UNSPECIFIED 未指定
通過上面的分析,繪制此圖形的完整代碼為 點擊查看
控件升級
上面我們已經(jīng)實現(xiàn)了圓形和文本的繪制疏唾,那么接下來蓄氧,我們先開始實現(xiàn)中心新進度的更新繪制。先看效果圖
通過效果圖槐脏,我們看到實現(xiàn)此效果就是不斷的更新進度值喉童,然后重繪,顿天,那么我們只需開啟一個線程實現(xiàn)更新進度值堂氯,為了更好的控制我們再加點擊事件蔑担,當單機時開始增大進度,雙擊時暫停進度咽白,并彈出Snackbar啤握,其中有一個重置按鈕,點擊重置時將進度設(shè)置為0晶框,重繪界面排抬。
- 響應點擊事件
因為要實現(xiàn)雙擊事件,我們可以直接用GestureDetector(手勢檢測)授段,通過這個類我們可以識別很多的手勢蹲蒲,主要是通過他的onTouchEvent(event)方法完成了不同手勢的識別GestureDetector里有一個內(nèi)部類 SimpleOnGestureListener。SimpleOnGestureListener類是GestureDetector提供給我們的一個更方便的響應不同手勢的類侵贵,這個類實現(xiàn)了上述兩個接口(OnGestureListener, OnDoubleTapListener届搁,但是所有的方法體都是空的),該類是static class模燥,也就是說它實際上是一個外部類咖祭。程序員可以在外部繼承這個類,重寫里面的手勢處理方法
public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener,
OnContextClickListener {
//單擊抬起
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
//長按
public void onLongPress(MotionEvent e) {
}
//滾動
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
return false;
}
//快速滑動
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
return false;
}
//
public void onShowPress(MotionEvent e) {
}
public boolean onDown(MotionEvent e) {
return false;
}
public boolean onDoubleTap(MotionEvent e) {
return false;
}
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
public boolean onContextClick(MotionEvent e) {
return false;
}
}
下面是我們自定繼承SimpleOnGestureListener蔫骂,由于我們只要響應單擊和雙擊事件,那么我們只需要重寫onDoubleTap雙擊()牺汤,onSingleTapConfirmed(單擊)方法即可辽旋,
public class MyGestureDetector extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDoubleTap(MotionEvent e) {
getHandler().removeCallbacks(singleTapThread);
singleTapThread=null;
Snackbar.make(CustomBall.this, "暫停進度,是否重置進度檐迟?", Snackbar.LENGTH_LONG).setAction("重置", new OnClickListener() {
@Override
public void onClick(View v) {
currentProgress=0;
invalidate();
}
}).show();
return super.onDoubleTap(e);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Snackbar.make(CustomBall.this, "單機了", Snackbar.LENGTH_LONG).setAction("Action", null).show();
startProgressAnimation();
return super.onSingleTapConfirmed(e);
}
}
當點擊時Snackbar做個提醒單擊了View,然后調(diào)用startProgressAnimation()方法初始化一個線程补胚,通過postDelayed將線程加入的消息隊列,延遲100ms執(zhí)行追迟,通過singleTapThread == null判斷條件溶其,避免過多的創(chuàng)建對象
private void startProgressAnimation() {
if (singleTapThread == null) {
singleTapThread = new SingleTapThread();
getHandler().postDelayed(singleTapThread, 100);
}
}
我們將SingleTapThread 實現(xiàn)Runnable接口,在run方法里書寫我們的處理邏輯敦间,其實很簡單瓶逃,先判斷當前進度值是不是大于最大進度(100),如果小于最大的值廓块,我們就將currentProgress(當前進度值)加1的操作厢绝,然后調(diào)用invalidate()方法重繪界面,之后還需要再次將線程加入消息隊列带猴,依然延遲100ms執(zhí)行昔汉。對于當如果當前進度已經(jīng)加載到100%,此時我們將此線程從消息隊列移除。
private class SingleTapThread implements Runnable {
@Override
public void run() {
if (currentProgress < maxProgress) {
currentProgress++;
invalidate();
getHandler().postDelayed(singleTapThread, 100);
} else {
getHandler().removeCallbacks(singleTapThread);
}
}
}
接下來還需要注冊事件拴清,我們可以在onDraw()方法中通過GestureDetector的構(gòu)造方法可以將自定義的MyGestureDetector對象傳遞進去靶病,然后通setOnTouchListener設(shè)置監(jiān)聽器会通,這樣GestureDetector能處理不同的手勢了
if (detector==null){
detector = new GestureDetector(new MyGestureDetector());
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return detector.onTouchEvent(event);
}
});
}
還有最重要的一點是,View默認是不可點擊的娄周,所以我們需要 setClickable(true)設(shè)置View可點擊的渴语,OK,到這里我們就完成的中心進度值得更新昆咽,接下來就開始繪制里面的波浪形狀驾凶,效果圖如下
實現(xiàn)水波浪效果
水波紋效果是通過二階貝塞爾曲線實現(xiàn)的,先簡單看下什么是貝塞爾曲線
在數(shù)學的數(shù)值分析領(lǐng)域中掷酗,貝塞爾曲線(英語:Bézier curve)是電腦圖形學中相當重要的參數(shù)曲線调违。更高維度的廣泛化貝塞爾曲線就稱作貝塞爾曲面,其中貝塞爾三角是一種特殊的實例泻轰。
貝塞爾曲線于1962年技肩,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發(fā)表,他運用貝塞爾曲線來為汽車的主體進行設(shè)計浮声。貝塞爾曲線最初由Paul de Casteljau于1959年運用de Casteljau算法開發(fā)虚婿,以穩(wěn)定數(shù)值的方法求出貝塞爾曲線 - - - - -維基百科
- 線性貝塞爾曲線
給定點P0、P1泳挥,線性貝塞爾曲線只是一條兩點之間的直線然痊。這條線由下式給出:
繪制效果為
- 二次方貝塞爾曲線
二次方貝塞爾曲線的路徑由給定點P0、P1屉符、P2的函數(shù)B(t)追蹤:
-
三次方貝塞爾曲線
P0剧浸、P1、P2矗钟、P3四個點在平面或在三維空間中定義了三次方貝塞爾曲線唆香。曲線起始于P0走向P1,并從P2的方向來到P3吨艇。一般不會經(jīng)過P1或P2躬它;這兩個點只是在那里提供方向資訊。P0和P1之間的間距东涡,決定了曲線在轉(zhuǎn)而趨進P2之前冯吓,走向P1方向的“長度有多長”。
曲線的參數(shù)形式為:
當然貝塞爾曲線是一個很復雜的東西软啼,他可以延伸N階貝塞爾曲線桑谍,如果想要真正搞明白,想自定義比較復雜或者比較酷炫的動畫祸挪,那高等數(shù)學知識必須要搞明白锣披,很多時候,我們只需要了解二次貝塞爾曲線就可以了,或者說雹仿,即使貝塞爾曲線不是那么熟悉增热,也不用怕,android API 封裝了常用的貝塞爾曲線胧辽,我們只需要傳入坐標就可以實現(xiàn)很多動畫峻仇。
首先我們需要初始化貝塞爾曲線區(qū)域的畫筆設(shè)置。其中重要的一點就是setXfermode()方法邑商,此方法可以設(shè)置與其他繪制圖形的交集摄咆,合集,補集等運算,在這個項目中人断,我們使用了交集(繪制貝塞爾曲線區(qū)域和圓區(qū)域的交集)
progressPaint = new Paint();
progressPaint.setAntiAlias(true);
progressPaint.setColor(progressColor);
//取兩層繪制交集吭从。顯示上層
progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
初始化畫筆后,就開始繪制我們的圖形恶迈,先初始化一個
寬和高都為radius * 2的正方形畫布作為緩沖區(qū)畫布涩金,我們可以先在緩沖區(qū)畫布繪制,繪制完成后一次再繪制到畫布上暇仲。
bitmap = Bitmap.createBitmap((int) radius * 2, (int) radius * 2, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
然后繪制圓心(width / 2, height / 2)半徑為radius的圓
bitmapCanvas.drawCircle(width / 2, height / 2, radius, roundPaint);
水波從圓的最下方開始(進度為0),到最上方(進度最大值maxProgress)結(jié)束步做,那么我們需要根據(jù)當前進度值動態(tài)計算水波的高度
float y = (1 - (float) currentProgress / maxProgress) * radius * 2
如圖,我們就可以先將path.lineTo將每個點連起來,可以先從(width,y)繪制奈附,那么需要調(diào)用path.moveTo(width, y);方法將操作點移動到該坐標上全度,接下下就開始依次連接其余三個點(width,height),(0,height),(0,y)。由于我們之前畫筆設(shè)置的是取交集(progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)))桅狠,所以此時會繪制與圓相交的部分讼载,也就是圓內(nèi)的部分。
下面就是繪制貝塞爾曲線
path.rQuadTo(space, -d, space * 2, 0);
path.rQuadTo(space, d, space * 2, 0);
第一個是繪制向下彎曲中跌,第二個是繪制向上彎曲。為了從左到右都繪制曲線菇篡,我們根據(jù)圓的直徑計算一下漩符,需要幾次才能平鋪,然后循環(huán)執(zhí)行上面兩句驱还,直到平鋪圓形區(qū)域嗜暴,為了展示當進度增大時將波紋幅度降低的效果(直到進度為100%,幅度降為0)我們根據(jù)當前進度值動態(tài)計算了幅度值,計算方法如下
float d = (1 - (float) currentProgress / maxProgress) *space;
由于我們需要以實心的方式繪制區(qū)域议蟆,那么我們調(diào)用
path.close();將所畫區(qū)域封閉闷沥,也就是實心的效果。
path.close();
bitmapCanvas.drawPath(path, progressPaint);
Ok咐容,到這里舆逃,自定義的水波形狀的進度條就完成了,再次上效果圖
(注:此水波左右移動是后來加的效果,具體實現(xiàn)點擊代碼查看)
由于本人目前水平有限路狮,文字若有不足的地方虫啥,歡迎指正,謝謝。