效果圖
app中下面這樣的控件很常見,像默認的TabHost表現上不夠靈活,下面就簡單寫一個可以結合ViewPager切換內容顯示,提供底部“滑動條”指示所顯示頁簽的效果。
這里控件應對的場景是“水平等長度”的若干標題,標題不可滾動。
控件設計
下面是要實現的控件TabIndicator的組成部分:
- 底部指示器:也就是藍色滑動條,記為Indicator。
- 分割線,寬度固定為1px的線條,可以不顯示。記為Divider。
- 頁簽標題:記為TabView。
- 最底部的邊框線,高度固定1px,就是給整個View的bottom部分一個分割線。
整體思路
整個TabIndicator是一個LinearLayout的子類,它包含水平方向的TabView——用來顯示頁簽標題。
分割線、底部的指示器、底部的水平邊框線都直接在TabIndicator.onDraw()中繪制。
方式很多,這里盡可能使用更少的View實現目標。當然標題文本可以不使用TextView自己繪制。如果需要按下標簽時的背景切換效果,使用TextView更好些,而且文本換行,大小等也好控
制。
TabIndicator的設置
TabIndicator作為一個ViewGroup,它需要繪制內容的話就需要設置屬性setWillNotDraw(false);
以保證它的onDraw()被執行。
要知道childView繪制會覆蓋ViewGroup本身的內容,所以這里的思路是利用paddingBottom為要繪制的底部Indicator和BorderLine預留空間。
在其構造方法中:
public TabIndicator(Context context, AttributeSet attrs) {
...
setWillNotDraw(false);
setGravity(Gravity.CENTER_VERTICAL);
setPadding(0, 0, 0, mIndicatorHeight);
}
標簽標題:TabView
將要顯示的標題使用TextView進行顯示,為了讓水平方向等分寬度,childView設置weight為1。
然后為了顯示容器繪制的Divider,倆個TabView之間需要預留空間,使用marginRight即可。
private void buildTabStrip() {
removeAllViews();
PagerAdapter adapter = mViewPager.getAdapter();
TabClickListener tabClickListener = new TabClickListener();
int tabCount = adapter.getCount();
int dividerWidth = (int) mDividerWidth;
for (int i = 0; i < tabCount; i++) {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
params.weight = 1;
if (dividerWidth > 0) {
if (i != 0) {
// use marginRight to make space for divider line.
params.setMargins(dividerWidth, 0, 0, 0);
}
}
TextView tabTitleView = createTabTitleView(params);
tabTitleView.setText(adapter.getPageTitle(i));
tabTitleView.setOnClickListener(tabClickListener);
addView(tabTitleView);
}
}
private TextView createTabTitleView(LinearLayout.LayoutParams params) {
TextView textView = new TextView(getContext());
textView.setGravity(Gravity.CENTER);
textView.setBackgroundColor(Color.WHITE);
textView.setLayoutParams(params);
return textView;
}
代碼中params.weight、params.setMargins()的調用完成了上述操作。
要顯示的TabView的個數是根據ViewPager關聯的PagerAdapter.getCount()決定的,這里明確
一點:此處的TabIndicator不會像ActionBar自帶Tabs視圖那樣水平滾動,它是一個等寬的頁簽指示器控件,適合2-6個TabView這樣的場景,如果需求不是這樣的,這里僅僅是一個思路。
TabClickListener用來監聽各個TabView的點擊,然后將ViewPager切換到對應位置:
private class TabClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
for (int i = 0; i < getChildCount(); i++) {
if (v == getChildAt(i)) {
mViewPager.setCurrentItem(i);
return;
}
}
}
}
底部邊界線
具體的繪制操作在onDraw()中進行。
邊界線就是一條緊貼TabIndicator底部bottom的一個線條,canvas.drawLine()可以完成。
只需要注意一點:繪制的BorderLine的位置必須在TabIndicator的區域內,所以這里應該讓
line的y坐標是TabIndicator本身的y減去1。
protected void onDraw(Canvas canvas) {
...
canvas.drawLine(getLeft(), tabHostHeight - 1, getRight(), tabHostHeight - 1, mBottomLinePaint);
}
分割線:Divider
Divider需要在每兩個TabView的中間進行繪制,在創建各個TabView時,已經使用marginRight預留了它的顯示位置。其高度會在上下各減去一定的值int mDividerPadding,為了美觀:
protected void onDraw(Canvas canvas) {
...
if (mEnableDivider && mDividerWidth > 0 && tabCount > 1) {
View tab = getChildAt(0);
if (mDividerPadding > tab.getHeight()) {
mDividerPadding = tab.getHeight() / 2.0f;
}
float startY = tab.getY() + mDividerPadding;
float stopY = tab.getY() + tab.getHeight() - mDividerPadding;
mDividerPaint.setStrokeWidth(mDividerWidth);
float halfDividerWidth = mDividerWidth / 2.0f;
for (int i = 0; i < tabCount - 1; i++) {
tab = getChildAt(i);
canvas.drawLine(tab.getRight() + halfDividerWidth,
startY, tab.getRight() + halfDividerWidth,
stopY,
mDividerPaint);
}
}
}
同樣是一個canvas.drawLine()指令進行繪制,其參數的計算代碼是最好的解釋。
底部指示器:滑動條
滾動條是有厚度的,所以使用canvas.drawRect()來進行繪制,方法需要繪制的矩形的四個坐標。
top、bottom是固定的。
left、right需要根據ViewPager的拖動進行確定:
假設從n滑動到n+1,那么計算出兩個childView之間的水平距離,然后監聽ViewPager的切換進度得到offset即可。
監聽ViewPager的拖動使用OnPageChangeListener接口,這里為需要的交互規則定義了它的實現類:
private class PageChangeListener extends ViewPager.SimpleOnPageChangeListener {
private int mScrollState;
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
int tabCount = getChildCount();
if ((tabCount == 0) || (position < 0) || (position >= tabCount)) {
return;
}
onViewPagerPageChanged(position, positionOffset);
if (mOuterPageListener != null) {
mOuterPageListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
}
@Override
public void onPageScrollStateChanged(int state) {
mScrollState = state;
if (mOuterPageListener != null) {
mOuterPageListener.onPageScrollStateChanged(state);
}
}
@Override
public void onPageSelected(int position) {
// this is called before the onPageScrolled progress finished.
// do not conflict with drag or setting-scroll.
// ViewPager.setCurrentItem(index, animating) may need this?
if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
onViewPagerPageChanged(position, 0f);
}
if (mOuterPageListener != null) {
mOuterPageListener.onPageSelected(position);
}
}
}
為了讓使用TabIndicator的代碼可以繼續監聽ViewPager頁面切換的事件,mOuterPageListener
用來保存外部代碼提供的監聽器。
回調方法onPageScrolled()用來通知ViewPager的拖動進度,positionOffset就是當前頁面和目標頁面切換的進度:0~1的一個float值。
監聽器調用onViewPagerPageChanged()來做處理:
public void onViewPagerPageChanged(int position, float positionOffset) {
if (mSelectedPosition == position
&& mIndicatorOffset == positionOffset) return;
mSelectedPosition = position;
mIndicatorOffset = positionOffset;
invalidate();
}
記錄下位置mSelectedPosition和切換進度mIndicatorOffset,然后通知當前TabIndicator進行繪制即可。緊接著在onDraw()中:
protected void onDraw(Canvas canvas) {
...
if (tabCount > 0) {
int left = selectedTitle.getLeft();
int right = selectedTitle.getRight();
if (mIndicatorOffset > 0f && mSelectedPosition < (tabCount - 1)) {
int offsetPixels = (int) (tabWidth * mIndicatorOffset);
left += offsetPixels;
right += offsetPixels;
}
canvas.drawRect(left, tabHostHeight - mIndicatorHeight, right,
tabHostHeight, mIndicatorPaint);
}
}
對offsetPixels的計算很簡單——這里的TabView是等寬的!!!
如果不是等寬的TabView,那么它們之間的水平位置差就是偏移的基準量。
NOTE:
在PageChangeListener.onPageSelected()中的調用onViewPagerPageChanged(position, 0f)
用來通知ViewPager發生的瞬間切換,這個在無動畫的ViewPager.setCurrentItem()時會發生。------我沒實驗,這里為了以防萬一。
記得對onViewPagerPageChanged()的調用為了不和onPageScrolled()中的調用沖突,它只在
ViewPager處在SCROLL_STATE_IDLE狀態時進行。
小結
以上就是TabIndicator的所有內容,這類控件實在是可以很簡單,更多的功能意味著更多的代碼。
這里沒有提供各種property/attrs的代碼,保持關鍵代碼的簡單。
實際上不一定需要結合ViewPager,代碼稍微修改,就可以滿足一般的TabHost這類效果的需求。
源碼在這里:
https://github.com/everhad/ViewPagerTabIndicator
(本文使用Atom編寫)
文章列表