上一篇文章我們利用View進(jìn)行自定義UI,這篇我們將利用Android現(xiàn)有的UI進(jìn)行自定義UI。我們利用現(xiàn)有的UI控件,主要是利用它們的一些屬性,并且根據(jù)這些屬性的改變可以達(dá)到我們預(yù)期的效果。還是看看今天我們實(shí)現(xiàn)的效果吧,No picture,it's so hard。效果圖如下所示,就是我們常見的Tab和SeekBar,看看今天怎么用現(xiàn)有的UI控件實(shí)現(xiàn)它;老樣子我們還是來一步一步分析吧。

分解效果圖
我們看到Horizontal和Vertical兩個(gè)Tab是不可以滑動(dòng)的,只有通過點(diǎn)擊來觸發(fā)左右切換,在Tab的下面會(huì)有一個(gè)帶顏色的bar標(biāo)志當(dāng)前選中的位置。想這樣的UI目前現(xiàn)有的Android控件應(yīng)該沒有可以直接使用的(據(jù)我的了解)。一般我們?cè)趯?shí)現(xiàn)這樣的UI時(shí),我們會(huì)用TextView和一個(gè)帶顏色的View組合實(shí)現(xiàn)。這樣好像也可以,但是還是有一定的代碼量的。那么我們?cè)趺从米钌俚拇a實(shí)現(xiàn)這樣的需求呢!首先我們看到左右切換的選中,類似于RadioButton這樣的控件——在多個(gè)選項(xiàng)中只能選取其中一個(gè),但是RadioButton這樣的控件底部好像也沒有這樣的帶顏色的bar啊,難道還是要利用一個(gè)View和它組合使用嗎,那這樣還是太low了。我們想啊,既然底部沒有這樣一個(gè)bar,那我們可以讓RadioButton在底部畫一個(gè)啊。怎么畫?那當(dāng)然是繼承它,在onDraw()方法里面畫啦。具體怎么畫,我相信畫一個(gè)矩形應(yīng)該很簡(jiǎn)單吧_。
上面分析完了Tab的部分,這下我們來看看這個(gè)Seekbar吧,我們先看看下面分解的4張圖片。




從上面的四張圖片可以看出,這個(gè)Seekbar是通過前面三張一個(gè)一個(gè)疊加過后達(dá)到第四張圖片的效果。當(dāng)我們拖動(dòng)小thumb(小圓球)滑動(dòng)的時(shí)候,不斷的改變第二層上圓角矩形的寬度就可以達(dá)到想要的效果了。那么具體怎么實(shí)現(xiàn),當(dāng)然還是上代碼啦。
實(shí)現(xiàn)分解效果圖
1.實(shí)現(xiàn)Tab的切換效果
上面我們分析了,需要實(shí)現(xiàn)Tab底部的效果主要是繼承RadioButton,并在onDraw()方法中繪制一個(gè)矩形就可以了,還是直接上代碼吧。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height = getMeasuredHeight();
//the indicator's height
int indicatorHeight = getResources().getDimensionPixelSize(R.dimen.radio_button_indicator_height);
if (isChecked()){
mPaint.setColor(getCurrentTextColor());
}else {
mPaint.setColor(Color.TRANSPARENT);
}
canvas.drawRect(0, height - indicatorHeight, getMeasuredWidth(), height, mPaint);
}
這里我們?cè)O(shè)置了選中色塊的高度,再調(diào)用drawRect()方法繪制矩形(選中色塊)。這里簡(jiǎn)單介紹一個(gè)這個(gè)方法
drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)
先看看下面的圖解吧。

結(jié)合上面的圖解,我們這里把top的值設(shè)為height - indicatorHeight,這個(gè)height為整個(gè)RadioButton的高度,由圖應(yīng)該可以很清楚的知道top的坐標(biāo)計(jì)算方法了。我們?cè)赗adioButton選中的時(shí)候通過Paint的值設(shè)置選中顏色,當(dāng)處于未選中狀態(tài)時(shí)設(shè)置為透明色,并且繪制同一塊區(qū)域,達(dá)到切換的效果。
2.實(shí)現(xiàn)Seekbar
在分析Seekbar的時(shí)候,我們把它分解成了幾個(gè)層次的疊加,那接下來我們的任務(wù)就是實(shí)現(xiàn)這些層次的疊加。我們這部分是通過現(xiàn)有的UI控件實(shí)現(xiàn)這個(gè)效果的,那么我們用哪一個(gè)UI控件可以實(shí)現(xiàn)層次的疊加呢,那當(dāng)然只有FrameLayou和RelativeLayout啦!那到底用FrameLayout還是RelativeLayout呢?這里我們使用RelativeLayout,對(duì)于FrameLayout的使用可以自行實(shí)踐。
(1)實(shí)現(xiàn)第一層圓角——progressbar

那我們這一層用什么實(shí)現(xiàn)呢,這里我們可以使用Andorid UI控件中的LinearLayout、FrameLayou等都可以,但是我們這里使用ViewGroup的子類——LinearLayout(當(dāng)然也可以選取其他的),這樣我們可以對(duì)這一層進(jìn)行擴(kuò)展,在里面添加TextView、ImageView等。那么怎么實(shí)現(xiàn)圓角呢,當(dāng)時(shí)是使用drawble進(jìn)行配置的,但是對(duì)于使用drawable進(jìn)行配置是有問題的——不能代碼控制圓角的大小,這個(gè)問題導(dǎo)致可擴(kuò)展性太差。那怎么解決呢,當(dāng)然得想辦法啊,最后找到了使用GradientDrawable,這里我想說的就是,我們?cè)谧远x的過程中總會(huì)出現(xiàn)問題,然后不停的找到具體的解決方法,一步一步實(shí)現(xiàn)。其實(shí)其他的編程問題都是這樣的。好了,我們這里還是直接看看具體代碼吧,如下:
mFirstBar = new LinearLayout(context);
GradientDrawable drawable = new GradientDrawable();
drawable.setColor(Color.parseColor("#FFBB33"));
drawable.setCornerRadius((float) mProgressHeight / 2);
mFirstBar.setBackgroundDrawable(drawable);
//mFirstBar.setBackgroundResource(R.drawable.firtbar_bkg);
mFirstBarLp = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mProgressHeight);
mFirstBarLp.addRule(CENTER_IN_PARENT);
mFirstBar.setClickable(false);
addView(mFirstBar, mFirstBarLp);
好了,第一層的實(shí)現(xiàn)應(yīng)該很簡(jiǎn)單吧。
(2)實(shí)現(xiàn)第二層圓角——secondProgressbar

其實(shí)secondProgressbar的實(shí)現(xiàn)基本上同progressbar的實(shí)現(xiàn)類似,還是直接上代碼吧
mSecondBar = new LinearLayout(context);
GradientDrawable secondDrawable = new GradientDrawable();
secondDrawable.setColor(Color.parseColor("#99CC00"));
secondDrawable.setCornerRadius((float)mProgressHeight/2);
mSecondBar.setBackgroundDrawable(secondDrawable);
mSecondBarLp = new RelativeLayout.LayoutParams(mThumbRadius, mProgressHeight);
//mSecondBarLp.leftMargin = 0;
mSecondBarLp.addRule(CENTER_VERTICAL);
//mSecondBarLp.addRule(LEFT_OF, THUMB_ID);
mSecondBarLp.addRule(ALIGN_PARENT_LEFT);
addView(mSecondBar, 1, mSecondBarLp);
mSecondBar.setClickable(false);
這里我們看到了,在創(chuàng)建LayoutParams的時(shí)候,我們?cè)O(shè)置的width的大小為圓角半徑的大小,為什么要這么做?這里還是先解釋一下吧,這樣做的目的主要是將secondProgressbar的最右端總是在thumb的中心位置,這里記住這一點(diǎn)后面我們?cè)诮榻B。
(3)實(shí)現(xiàn)游標(biāo)——thumb
其實(shí)thumb很簡(jiǎn)單啦,就是一個(gè)圓形的View。這里直接使用TextView,不要問我為什么——我任性$_$。直接看代碼吧。
mThumb = new TextView(context);
GradientDrawable thumb = new GradientDrawable();
thumb.setColor(Color.parseColor("#33b5e5"));
thumb.setCornerRadius((float)mThumbRadius);
mThumb.setBackgroundDrawable(thumb);
mThumbLp = new RelativeLayout.LayoutParams(mThumbRadius*2, mThumbRadius*2);
mThumbLp.addRule(CENTER_VERTICAL);
mThumbLp.addRule(ALIGN_PARENT_LEFT);
mThumbLp.leftMargin = 0;
addView(mThumb, mThumbLp);
mThumb.setId(THUMB_ID);
mThumb.setGravity(Gravity.CENTER);
if (mThumbTextSize != 0)
mThumb.setTextSize(mThumbTextSize);
這里在我們使用TextView,我們還可以在上面顯示一些信息,擴(kuò)展性更好,當(dāng)然你也可以利用其他UI控件,設(shè)置你需要的UI!
(4)實(shí)現(xiàn)滑動(dòng)——Seekbar
好了,上面我們分解的圖都實(shí)現(xiàn)了,現(xiàn)在應(yīng)該要實(shí)現(xiàn)滑動(dòng)了吧!那么怎么實(shí)現(xiàn)按住Thumb就會(huì)滑動(dòng),并且secondProgressbar和滑動(dòng),同時(shí)顯示當(dāng)前的進(jìn)度。當(dāng)然是使用監(jiān)聽OnTouchEvent事件啦,根據(jù)滑動(dòng)的distance不斷更新thumb和secondProgressbar的參數(shù),讓他們動(dòng)起來。那這里就直接實(shí)現(xiàn)RelativeLayout的OnTouchEvent方法,還是先看代碼吧,如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = MotionEventCompat.getActionMasked(event);
boolean isDraged = false;
Rect rect = new Rect();
mThumb.getHitRect(rect);
switch (action){
case MotionEvent.ACTION_DOWN:
float x = event.getX();
float y = event.getY();
boolean contain = rect.contains((int)x, (int)y);
if (contain){
mLastMotionX = event.getX();
isDraged = true;
}
break;
case MotionEvent.ACTION_MOVE:
dragThumb(event.getX());
break;
case MotionEvent.ACTION_UP:
break;
}
return isDraged;
}
private void dragThumb(float x){
float distance = (x - mLastMotionX);
mLastMotionX = x;
mThumbLp.leftMargin = (int) (mThumbLp.leftMargin + distance);
mSecondBarLp.width = (int) (mSecondBarLp.width + distance);
LogUtils.LogD(TAG, " horizontal current distance == " + distance);
//confirm this thumb is show, no anywhere is hide
if (mThumbLp.leftMargin <= 0) {
mThumbLp.leftMargin = 0;
mSecondBarLp.width = mThumbRadius;
} else if (mThumbLp.leftMargin >= getMeasuredWidth() - mThumbRadius * 2) {
mThumbLp.leftMargin = getMeasuredWidth() - mThumbRadius * 2;
mSecondBarLp.width = getMeasuredWidth() - mThumbRadius;
}
updateViewLayout(mThumb, mThumbLp);
updateViewLayout(mSecondBar, mSecondBarLp);
}
這里我們實(shí)現(xiàn)的是整個(gè)RelativeLayout的OnTouchEvent方法,所以它的touch事件是針對(duì)整個(gè)RelativeLayout的。所以這里我們要做一下過濾,點(diǎn)擊范圍在不在thumb上面,只有點(diǎn)擊和拖動(dòng)都在thumb上面這次的touch對(duì)thumb才有效。當(dāng)確定拖動(dòng)有效的時(shí)候,在開始初始化的時(shí)候,設(shè)置了thumb相對(duì)于父控件為ALIGN_PARENT_LEFT,所以通過改變mThumbLp.leftMargin就可以改變thumb于左邊的距離啦。對(duì)于secondProgressbar,只要改變mSecondBarLp.width的大小就可以改變它的寬度,最后調(diào)用updateViewLayout()方法更新UI。這里我們要注意兩點(diǎn):
1.防止thumb滑動(dòng)到最右端時(shí)超出邊界。
2.防止thumb滑動(dòng)回來到最左端時(shí)超出邊界。
好了,到這里通過簡(jiǎn)單的介紹,我們將這個(gè)Seekbar的基本功能完成。
總結(jié)
上面我們利用Android控件實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的Seekbar,現(xiàn)在簡(jiǎn)單的總結(jié)一下:
1.和實(shí)踐自定UI—View的時(shí)候一樣,我們還是把這個(gè)Seekbar進(jìn)行了分解,然后一步一步實(shí)現(xiàn)。
2.在自定義的過程中我們會(huì)遇到很多問題,我在這里遇到了這些問題:圓角怎么可以用代碼控制、在開始的時(shí)候我沒有利用RelativeLayou的onTouchEvent方法實(shí)現(xiàn)滑動(dòng),而是將onTouchEvent事件直接set在thumb上面,結(jié)果滑動(dòng)的時(shí)候出現(xiàn)了問題(有興趣的可以自己試試看看是什么問題)....。這里面遇到了很多問題,但都一個(gè)一個(gè)擊破,所以我們?cè)谧远║I的時(shí)候不要心急,一點(diǎn)一點(diǎn)將沒有問題解決,最后就會(huì)實(shí)現(xiàn)你想要的效果。
3.這里只是通過這個(gè)例子分析怎樣去利用Android UI 自定義我們自己需要的UI。這里只是引導(dǎo),更多的還是靠實(shí)踐、實(shí)踐、實(shí)踐...。重要的事說三遍_
好了,國際慣例,可以自己練習(xí)一下垂直方向的Seekbar,如下圖:

最后還是放上代碼地址吧
如何利用View自定義UI請(qǐng)閱讀實(shí)踐自定UI—View
如何利用ViewGroup自定義UI請(qǐng)閱讀實(shí)踐自定義UI-ViewGroup
希望在Android學(xué)習(xí)的路上,大家共同成長!
如果你也在簡(jiǎn)書上撰寫Android相關(guān)的內(nèi)容,或者也準(zhǔn)備寫可以加入我們的寫作交流群:196537830