1. 程式人生 > >仿微博Tab切換 TabIndicator

仿微博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