Android App開發(fā)過程中,很多時候會遇到系統(tǒng)框架中提供的控件無法滿足我們產(chǎn)品的設計需求鞋囊,那么這時候我們可以選擇先Google下有沒有比較成熟的開源項目可以讓我們用踪古,當然現(xiàn)在Github上面的項目非常豐富湘捎,能夠滿足我們絕不多數(shù)的開發(fā)需求,但是在使用這些炫酷的第三方控件時醒叁,我們也要想一想司浪,我們是不是也可以發(fā)揮自己的想象力,動手實現(xiàn)自己想要的控件把沼,盡可能掌控實現(xiàn)的細節(jié)啊易!
View
Android所有的控件都是View或者View的子類,它其實表示的就是屏幕上的一塊矩形區(qū)域饮睬,用一個Rect來表示租谈,left,top表示View相對于它的parent View的起點捆愁,width割去,height表示View自己的寬高,通過這4個字段就能確定View在屏幕上的位置昼丑,確定位置后就可以開始繪制View的內(nèi)容了呻逆。
View繪制過程
View的繪制可以分為下面三個過程:
-
Measure
View會先做一次測量,算出自己需要占用多大的面積菩帝。View的Measure過程給我們暴露了一個接口onMeasure咖城,方法的定義是這樣的,protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}
View類已經(jīng)提供了一個基本的onMeasure實現(xiàn)呼奢,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
其中invoke了setMeasuredDimension()方法宜雀,設置了measure過程中View的寬高,getSuggestedMinimumWidth()返回View的最小Width握础,Height也有對應的方法辐董。插幾句,MeasureSpec類是View類的一個內(nèi)部靜態(tài)類禀综,它定義了三個常量UNSPECIFIED简烘、AT_MOST、EXACTLY定枷,其實我們可以這樣理解它孤澎,它們分別對應LayoutParams中match_parent、wrap_content依鸥、xxxdp亥至。我們可以重寫onMeasure來重新定義View的寬高悼沈。
-
Layout
Layout過程對于View類非常簡單贱迟,同樣View給我們暴露了onLayout方法protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
因為我們現(xiàn)在討論的是View姐扮,沒有子View需要排列,所以這一步其實我們不需要做額外的工作衣吠。插一句茶敏,對ViewGroup類,onLayout方法中缚俏,我們需要將所有子View的大小寬高設置好惊搏,這個我們下一篇會詳細說。
-
Draw
Draw過程忧换,就是在canvas上畫出我們需要的View樣式恬惯。同樣View給我們暴露了onDraw方法protected void onDraw(Canvas canvas) { }
默認View類的onDraw沒有一行代碼,但是提供給我們了一張空白的畫布亚茬,舉個例子酪耳,就像一張畫卷一樣,我們就是畫家刹缝,能畫出什么樣的效果碗暗,完全取決我們。
View中還有三個比較重要的方法
requestLayout
View重新調(diào)用一次layout過程梢夯。invalidate
View重新調(diào)用一次draw過程forceLayout
標識View在下一次重繪言疗,需要重新調(diào)用layout過程。
自定義屬性
整個View的繪制流程我們已經(jīng)介紹完了颂砸,還有一個很重要的知識噪奄,自定義控件屬性,我們都知道View已經(jīng)有一些基本的屬性沾凄,比如layout_width梗醇,layout_height,background等撒蟀,我們往往需要定義自己的屬性叙谨,那么具體可以這么做。
1.在values文件夾下保屯,打開attrs.xml手负,其實這個文件名稱可以是任意的,寫在這里更規(guī)范一點姑尺,表示里面放的全是view的屬性竟终。
-
2.因為我們下面的實例會用到2個長度,一個顏色值的屬性切蟋,所以我們這里先創(chuàng)建3個屬性统捶。
<declare-styleable name="rainbowbar"> <attr name="rainbowbar_hspace" format="dimension"></attr> <attr name="rainbowbar_vspace" format="dimension"></attr> <attr name="rainbowbar_color" format="color"></attr> </declare-styleable>
那么到底怎么用呢,我們會看一個實例。
實現(xiàn)一個比較簡單的Google彩虹進度條喘鸟。
為了簡單起見匆绣,這里我只用一種顏色,多種顏色就留給大家了什黑,我們直接上代碼崎淳。
public class RainbowBar extends View {
//progress bar color
int barColor = Color.parseColor("#1E88E5");
//every bar segment width
int hSpace = Utils.dpToPx(80, getResources());
//every bar segment height
int vSpace = Utils.dpToPx(4, getResources());
//space among bars
int space = Utils.dpToPx(10, getResources());
float startX = 0;
float delta = 10f;
Paint mPaint;
public RainbowBar(Context context) {
super(context);
}
public RainbowBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RainbowBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//read custom attrs
TypedArray t = context.obtainStyledAttributes(attrs,
R.styleable.rainbowbar, 0, 0);
hSpace = t.getDimensionPixelSize(R.styleable.rainbowbar_rainbowbar_hspace, hSpace);
vSpace = t.getDimensionPixelOffset(R.styleable.rainbowbar_rainbowbar_vspace, vSpace);
barColor = t.getColor(R.styleable.rainbowbar_rainbowbar_color, barColor);
t.recycle(); // we should always recycle after used
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(barColor);
mPaint.setStrokeWidth(vSpace);
}
.......
}
View有了三個構(gòu)造方法需要我們重寫,這里介紹下三個方法會被調(diào)用的場景愕把,
- 第一個方法拣凹,一般我們這樣使用時會被調(diào)用,View view = new View(context);
- 第二個方法恨豁,當我們在xml布局文件中使用View時嚣镜,會在inflate布局時被調(diào)用,
<View
layout_width="match_parent"
layout_height="match_parent"/>橘蜜。 - 第三個方法祈惶,跟第二種類似,但是增加style屬性設置扮匠,這時inflater布局時會調(diào)用第三個構(gòu)造方法捧请。
<View
style="@styles/MyCustomStyle"
layout_width="match_parent"
layout_height="match_parent"/>。
上面大家可能會感覺到有點困惑的是棒搜,我把初始化讀取自定義屬性hspace疹蛉,vspace,和barcolor的代碼寫在第三個構(gòu)造方法里面力麸,但是我RainbowBar在線性布局中沒有加style屬性()可款,那按照我們上面的解釋,inflate布局時應該會invoke第二個構(gòu)造方法啊克蚂,但是我們在第二個構(gòu)造方法里面調(diào)用了第三個構(gòu)造方法闺鲸,this(context, attrs, 0); 所以在第三個構(gòu)造方法中讀取自定義屬性,沒有問題埃叭,這是一點小細節(jié)摸恍,避免代碼冗余-,-
Draw
因為我們這里不用關(guān)注measrue和layout過程赤屋,直接重寫onDraw方法即可立镶。
//draw be invoke numbers.
int index = 0;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//get screen width
float sw = this.getMeasuredWidth();
if (startX >= sw + (hSpace + space) - (sw % (hSpace + space))) {
startX = 0;
} else {
startX += delta;
}
float start = startX;
// draw latter parse
while (start < sw) {
canvas.drawLine(start, 5, start + hSpace, 5, mPaint);
start += (hSpace + space);
}
start = startX - space - hSpace;
// draw front parse
while (start >= -hSpace) {
canvas.drawLine(start, 5, start + hSpace, 5, mPaint);
start -= (hSpace + space);
}
if (index >= 700000) {
index = 0;
}
invalidate();
}
//布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_marginTop="40dp"
android:orientation="vertical" >
<com.sw.demo.widget.RainbowBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:rainbowbar_color="@android:color/holo_blue_bright"
app:rainbowbar_hspace="80dp"
app:rainbowbar_vspace="10dp"
></com.sw.demo.widget.RainbowBar>
</LinearLayout>
其實就是調(diào)用canvas的drawLine方法,然后每次將draw的起點向前推進类早,在方法的結(jié)尾媚媒,我們調(diào)用了invalidate方法,上面我們已經(jīng)說明了涩僻,這個方法會讓View重新調(diào)用onDraw方法缭召,所以就達到我們的進度條一直在向前繪制的效果栈顷。下面是最后的顯示效果,制作成gif時好像有色差嵌巷,但是真實效果是藍色的妨蛹。我們只寫了短短的幾十行代碼,自定義View并不是我們想象中那么難晴竞,下一篇我們會繼續(xù)ViewGroup的繪制流程學習。