仿微博Tab切換 TabIndicator
先看一張效果圖:
當時第一次接到這種需求,首先想到的是重寫LinearLayout,然後監聽ViewPager和Tab的點選事件滾動佈局,但是寫出來以後,遇到一個問題,就是頂部Tab只能隨著點選事件或者ViewPager來滾動。
於是分析了一下TabLayout的原始碼,
TabLayout繼承自HorizontalScrollView,如果UI要求不嚴格,基本的功能都可以實現。但是有一點不好的地方在於指示器(文字下的線)的寬度沒有提供修改方法,且指示器位移動畫也沒辦法自定義。
源自TabLayout的靈感,於是自定義了一個MyTabIndicator繼承自HorizontalScrollVIew。
程式碼的基本結構:
MyTabIndicator.java:
public class MyTabIndicator extends HorizontalScrollView{
private class TabLayout extends LinearLayout{}
}
MyTabIndicator 巢狀子view TabLayout。
支援的自定義屬性:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyTabIndicator"> <attr name="textNormalColor" format="color"/> <attr name="textSelectedColor" format="color"/> <attr name="textNormalSize" format="dimension"/> <attr name="textSelectedSize" format="dimension"/> <attr name="backGroundColor" format="color"/> <attr name="indicatorColor" format="color"/> <attr name="indicatorWidth" format="dimension"/> </declare-styleable> </resources>
上面的xml,主要為了自定義屬性,text的正常模式及大小,選中顏色及大小,背景色,指示器顏色,指示器寬度(指示器寬度最大值為Tab的寬度)
下面介紹幾個關鍵的方法:1.MyTabIndicator的構造方法
public MyTabIndicator(Context context) { this(context, null); } public MyTabIndicator(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyTabIndicator(Context context,AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //讀取xml中 if(attrs != null) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTabIndicator); TEXTNORMALCOLOR = typedArray.getColor(R.styleable.MyTabIndicator_textNormalColor, 0x80FFFFFF); TEXTSELECTEDCOLOR = typedArray.getColor(R.styleable.MyTabIndicator_textSelectedColor, 0xFFFFFFFF); TEXTNORMALSIZE = typedArray.getDimension(R.styleable.MyTabIndicator_textNormalSize, 13); TEXTSELECTEDSIZE = typedArray.getDimension(R.styleable.MyTabIndicator_textSelectedSize, 15); BACKGROUNDCOLOR = typedArray.getColor(R.styleable.MyTabIndicator_backGroundColor, 0xFF242425); INDICATORCOLOR = typedArray.getColor(R.styleable.MyTabIndicator_indicatorColor, 0xFFFFFFFF); INDICATORWIDTH = typedArray.getDimension(R.styleable.MyTabIndicator_indicatorWidth, 0); typedArray.recycle(); }else { TEXTNORMALCOLOR = Color.parseColor("#80FFFFFF"); TEXTSELECTEDCOLOR = Color.parseColor("#FFFFFF"); TEXTNORMALSIZE = 13; TEXTSELECTEDSIZE = 15; BACKGROUNDCOLOR = Color.parseColor("#242425"); INDICATORCOLOR = Color.parseColor("#FFFFFF"); INDICATORWIDTH = 0; } //隱藏滾動條 setHorizontalScrollBarEnabled(false); //新增tab容器 mTabLayout = new TabLayout(context); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); mTabLayout.setLayoutParams(params); addView(mTabLayout); //初始化title寬度 mTitleWidth = getScreenWidth() / mTabVisibleCount; }
這裡主要用於初始化一些自定義屬性,如果使用者在佈局xml檔案中使用了common_attr.xml中的屬性,則會使用使用者填寫的。如果使用者沒有填寫屬性,則使用預設的屬性。
然後在TabIndicator中新增Tab容器TabLayout,這裡需要設定mTabLayout的LayoutParams,寬度和高度都是matchparent。
接下來是初始化Tab標題的寬度,getScreenWidth()是螢幕的寬度。
2.TabLayout構造方法
public TabLayout(Context context) { this(context, null); } public TabLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOrientation(HORIZONTAL); setBackgroundColor(BACKGROUNDCOLOR); mPaintIndicator = new Paint(); mPaintIndicator.setColor(INDICATORCOLOR); mPaintIndicator.setStyle(Paint.Style.FILL); mPaintIndicator.setStrokeWidth(Local.dip2px(2)); }
這裡主要用來初始化TabLayout的佈局方向及背景顏色(這裡遇到一個問題,如果不設定背景色,則onDraw方法得不到執行,為什麼讀者可以自行百度)。
接下來就是指示器畫筆初始化,其中setStrokeWidth()方法是設定指示器的高度。
3.TabLayout的onDraw方法:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //繪製指示器 canvas.save(); float x1 = mInitTranslationX + mTranslationX; float x2 = x1 + mIndicatorWidth; float y1, y2; y1 = y2 = getHeight() - 9; canvas.drawLine(x1, y1, x2, y2, mPaintIndicator); canvas.restore(); }
這裡繪製指示器,讀者也可以自己修改這段程式碼來繪製自己想要的指示器樣式,我這裡繪製的是line。
這個方法中mInitTranslationx 是指示器的偏移量,作用是讓指示器與文字居中對齊。
mTranslationx是指示器的滑動時的偏移量,作用是控制指示器距離父佈局左邊的位移。
兩個變數相加就可以控制指示器左側的x座標值。
y1,y2,是指示器的Y座標值。
(對於Cavas的用法請讀者自行百度)
4.setTitles設定Tab的數量及標題
/** * 填充標題 * @param datas */ public void setTitles(List<String> datas){ this.mTabTitles = datas; // 如果傳入的list有值,則移除佈局檔案中設定的view if (datas != null && datas.size() > 0) { mTabLayout.removeAllViews(); this.mTabTitles = datas; mTabVisibleCount = Math.min(datas.size(), COUNT_DEFAULT_MAX_TAB); mTitleWidth = getScreenWidth() / mTabVisibleCount; mIndicatorWidth = (int) (mTitleWidth * RADIO_TRIANGEL); if(INDICATORWIDTH > 0){ mIndicatorWidth = Math.min(mTitleWidth, (int)INDICATORWIDTH); } // 初始時的偏移量 mInitTranslationX = (int) ((mTitleWidth - mIndicatorWidth) / 2.0); for (String title : mTabTitles) { // 新增view mTabLayout.addView(getTitleView(title)); } // 設定item的click事件 setTitleItemClickEvent(); highLightTextView(0); } }
這個方法很關鍵,只有設定了此方法,才能有tab及title顯示。計算tab寬度,指示器的初始偏移量及title的點選事件。
5.標題點選事件
/** * 標題點選事件 */ private void setTitleItemClickEvent() { int cCount = mTabLayout.getChildCount(); for (int i = 0; i < cCount; i++) { final int j = i; View view = mTabLayout.getChildAt(i); view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if(!isClick){ isClick = true; if(mViewPager == null){ Message msg = handler.obtainMessage(); mTranslationX++; msg.what = 3; msg.arg1 = j; handler.sendMessage(msg); resetTextViewColor(); highLightTextView(j); }else { mViewPager.setCurrentItem(j, false); } }else { if(!handler.hasMessages(3)) { isClick = false; } } } }); } }
點選事件進行了有沒有ViewPager的判斷,如果使用者不需要與ViewPager聯動,這裡也預設保留了動畫效果。如果使用者想要點選事件與其它行為進行聯動,可以在此方法中寫一個回撥介面實現自己的需求。
6.指示器的滑動
int speed = -1; /** * 回滾的速度 */ public int SCROLL_SPEED = -10; private Handler handler = new Handler(){ @Override public void handleMessage(final Message msg) { switch (msg.what){ case 3: if(!isClick)break; final int position = msg.arg1; float a = getWidth() / mTabVisibleCount * position - mTranslationX; if(speed == -1) { speed = (int) (a / mIndicatorWidth * 2); speed = Math.abs(speed); if(speed < 20){ speed = 20; } } if(a > 0){ SCROLL_SPEED = speed; }else if(a < 0){ SCROLL_SPEED = -speed; } mTranslationX += SCROLL_SPEED; if(a > -speed && a < speed){ mTranslationX = getWidth() / mTabVisibleCount * position; isClick = false; speed = -1; }else{ Message msg1 = handler.obtainMessage(); msg1.what = 3; msg1.arg1 = position; handler.sendMessage(msg1); } Log.i("mTranslationX", "" + mTranslationX); mTabLayout.invalidate(); break; } } };
hanlder的作用不斷通過傳遞訊息,重繪介面來達到指示器移動的效果。
speed的作用是用來確定速度,如果使用者點選來tab,handler第一次傳遞訊息就會確定一個滾動速度。
isClick是用來做點選事件的遮蔽,如果指示器滾動中,isClick為true。
7.ViewPager的滾動監聽及對外回撥介面
/** * 對外的ViewPager的回撥介面 */ public interface PageChangeListener { void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); void onPageSelected(int position); void onPageScrollStateChanged(int state); } // 對外的ViewPager的回撥介面 private PageChangeListener onPageChangeListener; // 對外的ViewPager的回撥介面的設定 public void setOnPageChangeListener(PageChangeListener pageChangeListener) { this.onPageChangeListener = pageChangeListener; } // 設定關聯的ViewPager public void setViewPager(ViewPager mViewPager, int pos) { this.mViewPager = mViewPager; mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageSelected(int position) { // 設定字型顏色高亮 resetTextViewColor(); highLightTextView(position); // 回撥 if (onPageChangeListener != null) { onPageChangeListener.onPageSelected(position); } } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { // 滾動 scroll(position, positionOffset); // 回撥 if (onPageChangeListener != null) { onPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); } } @Override public void onPageScrollStateChanged(int state) { // 回撥 if (onPageChangeListener != null) { onPageChangeListener.onPageScrollStateChanged(state); } } }); // 設定當前頁 mViewPager.setCurrentItem(pos); // 高亮 highLightTextView(pos); }
8. ViewPager不為空情況下指示器的滾動控制方法:
/** * 指示器跟隨手指滾動,以及容器滾動 * * @param position * @param offset */ public void scroll(final int position, float offset) { if(offset != 0.0){ isClick = false; } int tabWidth = getScreenWidth() / mTabVisibleCount; int last = 1; // 容器滾動,當移動到倒數最後一個的時候,開始滾動 if (offset > 0 && position >= (mTabVisibleCount - last) && mTabLayout.getChildCount() > mTabVisibleCount) { if (mTabVisibleCount != 1) { scrollTo((position - (mTabVisibleCount - last)) * tabWidth + (int) (tabWidth * offset), 0); } else { // 為count為1時 的特殊處理 scrollTo(position * tabWidth + (int) (tabWidth * offset), 0); } } else if (position < (mTabVisibleCount - last)) { scrollTo(0, 0); } if(!isClick) { // 不斷改變偏移量,invalidate mTranslationX = getWidth() / mTabVisibleCount * (position + offset); mTabLayout.invalidate(); }else { Message msg = handler.obtainMessage(); mTranslationX++; msg.what = 3; msg.arg1 = position; handler.sendMessage(msg); } }
使用方法:
<com.loookapp.loook.View.MyTabIndicator
android:id="@+id/myTabIndicator"
android:layout_width="match_parent"
android:layout_height="44dp"
app:textNormalColor="#80FFFFFF"
app:textSelectedColor="#FFFFFF"
/>
myTabIndicator.setTitles(mDatas); mViewPager.setAdapter(mAdapter); //設定關聯的ViewPager myTabIndicator.setViewPager(mViewPager, 0);
原始碼下載地址:
https://github.com/736791050/TabIndicator