仿微博Tab切换 TabIndicator

原文地址: http://blog.csdn.net/likuan0214/article/details/51498859


先看一张效果图:

仿微博Tab切换 TabIndicator_第1张图片


当时第一次接到这种需求,首先想到的是重写LinearLayout,然后监听ViewPager和Tab的点击事件滚动布局,但是写出来以后,遇到一个问题,就是顶部Tab只能随着点击事件或者ViewPager来滚动。

于是分析了一下TabLayout的源码,

TabLayout继承自HorizontalScrollView,如果UI要求不严格,基本的功能都可以实现。但是有一点不好的地方在于指示器(文字下的线)的宽度没有提供修改方法,且指示器位移动画也没办法自定义。

源自TabLayout的灵感,于是自定义了一个MyTabIndicator继承自HorizontalScrollVIew。


代码的基本结构:

MyTabIndicator.java:

[html]  view plain  copy
  1. public class MyTabIndicator extends HorizontalScrollView{  
  2.    private class TabLayout extends LinearLayout{}  
  3. }  

MyTabIndicator 嵌套子view TabLayout。


支持的自定义属性:

[html]  view plain  copy
  1. xml version="1.0" encoding="utf-8"?>  
  2. <resources>  
  3.     <declare-styleable name="MyTabIndicator">  
  4.         <attr name="textNormalColor" format="color"/>  
  5.         <attr name="textSelectedColor" format="color"/>  
  6.         <attr name="textNormalSize" format="dimension"/>  
  7.         <attr name="textSelectedSize" format="dimension"/>  
  8.         <attr name="backGroundColor" format="color"/>  
  9.         <attr name="indicatorColor" format="color"/>  
  10.         <attr name="indicatorWidth" format="dimension"/>  
  11.     declare-styleable>  
  12. 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 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);
    }
}



使用方法:

[html]  view plain  copy
  1. <com.loookapp.loook.View.MyTabIndicator  
  2.       android:id="@+id/myTabIndicator"  
  3.       android:layout_width="match_parent"  
  4.       android:layout_height="44dp"  
  5.       app:textNormalColor="#80FFFFFF"  
  6.       app:textSelectedColor="#FFFFFF"  
  7.       />  

myTabIndicator.setTitles(mDatas);
mViewPager.setAdapter(mAdapter);
//设置关联的ViewPager
myTabIndicator.setViewPager(mViewPager, 0);


源码下载地址:

https://github.com/736791050/TabIndicator

你可能感兴趣的:(自定义控件)