自定义Tab + ViewPager控件

看了关于自定义控件的很多视频还有书籍等,也写了不少自定义控件。拿出来与大家共享一下。

一、概述

1.前言

直奔主题吧,我们当同一个页面内容过多时,比如新闻类app,要分很多栏目,但是在同一个activity中显示,这时需要tab栏+ViewPager+Fragment来实现这样的效果。因此在此处来讲述一下这个tab栏的实现。这个很早以前做的,很多地方都忘了,也当时回顾一下吧。当时做的时候是参考的Hongyang在慕课网上的视频,然后加以自己的需求实现。现在在Android的support包中也有PagerTabStrip的类似控件实现,就不多说了。

2.实现原理

关于原理,先简单说说,后面再看实现。自定义ViewGroup有两种实现方式,一个继承自ViewGroup,另一个就是继承已经实现的控件。先来分析一下我们所需要的效果:

  1. tab栏要横向排列;
  2. 每个tab要有点击事件 ;
  3. tab点击对应样式变化,以及viewpager的联动变化;
  4. tab底部有可自定义的下划线;

来分析一下:

  • tab要横向排列,其实最好的就是继承LinearLayout来实现,这样就可以跳过measure和layout的过程;
  • tab的点击事件,当然view也能实现点击事件,但是由于要区分每个tab,如果在同一个view 实现,还要根据触摸点去辨别点击的tab,相当麻烦,所以使用ViewGroup是最简单的;
  • tab点击对应样式变化,这里可以在点击事件中重置或是联动的ViewPager中重置;而与viewpager的联动变化则可以通过viewPager的OnPageChangeListener接口来改变标题栏变化;
  • 这个可以在onDraw中绘制ViewGroup内部显示的内容,但是要计算位置和变化。

效果图:
这里写图片描述

二、实现

1.继承LinearLayout并添加变量

public class SimpleTabStripView extends LinearLayout {
    private static final String TAG = "SimpleTabStripView";
    private static final String[] DEFAULT_TITLES = new String[]{"Tab1", "Tab2", "Tab3"};// 默认标题
    private static final int DEFAULT_VISIBLE = 3;//默认可见三个tab
    private static final int DEFAULT_SELECTED_COLOR = Color.RED;//默认的选中tab和下划线相同颜色
    private static final int DEFAULT_UNSELECTED_COLOR = Color.GRAY;//默认下划线颜色
    private static final int DEFAULT_UNDERLINE_HEIGHT = 20;//px,默认下划线高度
    private static final int DEFAULT_TXT_SIZE = 16;//sp,默认字体大小

    private int mSelectedTxtColor;  //选中文本颜色
    private CharSequence[] mTitles; //tab标题
    private int mWidth; //控件宽度
    private int mVisibleTabsCount;  //可见tab数量
    private int mTabWidth; //tab宽度
    private ViewPager mViewPager;
    private int mTranslationX;  //偏移量
    private int mUnderlineColor;    //下划线颜色
    private int mUnselectedTxtColor;    //未选中文本颜色
    private int mSelectedPosition = 0;//此处主要是为了标记当前选中的tab,因为设置viewpager时不知道此时view是否已经添加,所以只能先做标记
    private int mUnderlineHeight;   //下划线高度
    private UnderlineStyle mUnderlineStyle; //下划线样式(已经定义好了条形和三角形的样式,扩展可实现UnderlineStyle接口)
    private int mTabTextSize;   //文字颜色

    public SimpleTabStripView(Context context) {
        this(context, null);
    }

    public SimpleTabStripView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SimpleTabStripView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOrientation(HORIZONTAL);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SimpleTabStripView);
        mTitles = ta.getTextArray(R.styleable.SimpleTabStripView_titles);
        if (mTitles == null) {
            mTitles = DEFAULT_TITLES;
        }
        mVisibleTabsCount = ta.getInt(R.styleable.SimpleTabStripView_visible_tabs_count, DEFAULT_VISIBLE);
        mSelectedTxtColor = ta.getColor(R.styleable.SimpleTabStripView_selected_txt_color, DEFAULT_SELECTED_COLOR);
        mUnderlineColor = ta.getColor(R.styleable.SimpleTabStripView_underline_color, mSelectedTxtColor);
        mUnselectedTxtColor = ta.getColor(R.styleable.SimpleTabStripView_unselected_txt_color, DEFAULT_UNSELECTED_COLOR);
        mUnderlineHeight = ta.getDimensionPixelSize(R.styleable.SimpleTabStripView_underline_height, DEFAULT_UNDERLINE_HEIGHT);
        mTabTextSize = (int) ta.getDimension(R.styleable.SimpleTabStripView_tab_txt_size, sp2px(context, DEFAULT_TXT_SIZE));
        ta.recycle();
        mUnderlineStyle = new RectUnderlineStyle();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mTabWidth = w / mVisibleTabsCount;
        updateTabs();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mUnderlineStyle != null) {
            mUnderlineStyle.draw(canvas, mTranslationX, mTabWidth, mUnderlineColor, getMeasuredHeight(), mUnderlineHeight);
        }
    }

    /**
     * sp转px
     *
     * @param context 上下文
     * @param spVal   sp值
     * @return px值
     */
    public static int sp2px(Context context, float spVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                spVal, context.getResources().getDisplayMetrics());
    }

    /**
     * px转sp
     *
     * @param context 上下文
     * @param pxVal   px值
     * @return sp值
     */
    public static float px2sp(Context context, float pxVal) {
        return (pxVal / context.getResources().getDisplayMetrics().scaledDensity);
    }
}

这个,基本上不用过多的解释,已经在上面进行了注释。提一下,在上面的构造函数中我们实现了自定义属性的获取,这个我到后面再稍微提一下。之后在onDraw中调用自定义的下划线绘制接口进行下划线绘制。具体绘制的实现下面再进行分析。

2.添加tab与点击事件

由于在上面已经添加了一个字符串数组mTiltles,因此提供一个方法用于添加tab即可。

/**
 * 创建添加tab
 */
private void updateTabs() {
    removeAllViews();
    for (int i = 0; i < mTitles.length; i++) {
        final int j = i;
        CharSequence title = mTitles[i];
        TextView tab = new TextView(getContext());
        tab.setLayoutParams(new LinearLayout.LayoutParams(mTabWidth, LinearLayout.LayoutParams.MATCH_PARENT));
        tab.setText(title);
        tab.setTextColor(i == mSelectedPosition ? mSelectedTxtColor : mUnselectedTxtColor);
        tab.setTextSize(px2sp(getContext(), mTabTextSize));
        tab.setGravity(Gravity.CENTER);
        tab.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mViewPager.setCurrentItem(j);
            }
        });
        addView(tab);
    }
}

ok,从代码中可以看出,在我们更新tab时,首先清空所有的tab,然后根据mTitles中的内容来逐个创建tab,并分别设置其文本,颜色,尺寸,以及居中方式。最后我们通过添加点击事件,调用ViewPager的选择item方法,做到tab与mViewPager的联动。关于update的调用时机,我们选择在onSizeChanged()方法中进行调用,这样可以解决几个问题,首先,onSizeChanged方法中,我们可以得到测量后的整个ViewGroup的尺寸,进而可以计算出每个tab应有的尺寸。后面还会添加手动添加tab的方法以及显示的tab数量的方法,同时要在这几个方法中计算出tab该有的尺寸。

3.tab点击样式变化,与viewpager的联动

public void setViewPager(ViewPager viewpager) {
    this.mViewPager = viewpager;
    viewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            scrollToChild(position, positionOffset);
        }

        @Override
        public void onPageSelected(int position) {
            highLightTextView(position);
            mSelectedPosition = position;
        }

        @Override
        public void onPageScrollStateChanged(int state) {

        }
    });
    mViewPager.setCurrentItem(mSelectedPosition);
    //此处不hightLight文本,主要是因为,set的时候不能确定宽度是否已经测量。
}

/**
 * 滚动到指定位置
 *
 * @param position       起始位置
 * @param positionOffset 偏移量
 */
private void scrollToChild(int position, float positionOffset) {
    mTranslationX = (int) ((position + positionOffset) * mTabWidth);
    invalidate();
    if (position >= mVisibleTabsCount - 2 && getChildCount() > mVisibleTabsCount && position < getChildCount() - 2) {
        if (mVisibleTabsCount != 1) {
            //例:可见3个,当前在1位置,由1滚动到2,
            // 则position为1,而滚动的距离则为positionOffset * mTabWidth
            scrollTo((int) ((position + 2 - mVisibleTabsCount + positionOffset) * mTabWidth), 0);
        } else {
            scrollTo((int) ((position + positionOffset) * mVisibleTabsCount), 0);
        }
    }
}

/**
 * 重置所有tab的字体颜色
 */
private void resetAllTabsColor() {
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        View tab = getChildAt(i);
        if (tab instanceof TextView) {
            ((TextView) tab).setTextColor(mUnselectedTxtColor);
        }
    }
}

/**
 * 高亮文本
 *
 * @param pos 选中的tab位置
 */
 private void highLightTextView(int pos) {
    resetAllTabsColor();
    View tab = getChildAt(pos);
    if (tab instanceof TextView) {
        ((TextView) tab).setTextColor(mSelectedTxtColor);
    }
}

首先在下面提供对应tab点击的样式变化方法,resetAllTabsColor和highLightTextView的方法,故名思议,先重置,再高亮指定的tab即可。

与viewPager的联动,我们设置了ViewPager的OnPageChangeListener。重写其onPageScrolled执行tab的下划线的滚动;重写onPageSelected方法执行tab的选中的样式变化。

在scrollToChild(int position,float positionOffset)方法中,position(0~tab数-1)是指选中的tab(或者说是ViewPager的item),positionOffset(-1~1)是相对于当前选中的item的滑动偏移量。该方法首先计算偏移量,再调用绘制tab的下划线,之后再使用scrollTo方法来滚动LinearLayout的内容部分,则可以实现滑动的效果。
解释一下其中的判断,下面提到的滑动都是指viewGroup内容的滑动,不是下划线:

  • 首先,选中position大于等于可见tab数-2,也就是从可见数的倒数第2个才可开始进行滑动(比如,总共有4个tab可见,那么就从position为2的tab位置可以滑动,也就是第三个tab;6个可见,就从第5个tab选中开始可以滑动),这样做是为了良好的用户体验,总不能从开始就滑动;
  • getChildCount() > mVisibleTabsCount,即tab总数大于可见tab数才会发生滚动,都放不满一屏还滑动条毛啊;
  • position < getChildCount() - 2,这里与第一个条件稍微有点关系,第一个条件中从倒数第二个可见tab数开始滑动,那么在滑动结束时,后面总会有1个没被选中的tab显示。此时,如果从倒数第二个开始往最后一个tab滑动时,其实最后1个已经显示出来了,不需要再滚动了。比如,可见数4个,则tab总数至少5个(假设5个),从第3个tab(即position为2)选中可滑动,但第4个tab也是可见的,position < getChildCount() - 2 满足,到第4个被选中时,即position为3,则第5个就可见了,后面没tab了就不用滑动了,此时position < getChildCount() - 2 不满足则不可滑动。
    这里或许有点难懂,或许改为position < (getChildCount() - 1) -1这样会更好理解,即position比最后一个tab的前一个tab还小时可以滑动,即倒数第三个tab选中时可滑动,倒数第二个tab选中时不可滑动。
  • 最后如果可见数不等于1,则每次滑动到的位置就需要position+ positionOffset的同时还有减去不会滑动的mVisibleTabsCount-2的数量。当然如果可见就是1个,那就正常滑动好了。

4.tab底部下划线

这里就很简单了,根据偏移量在onDraw中画出线即可。
写了个接口,用于之后可以扩展底部样式:

/**
 * 应对不同的底色
 */
interface UnderlineStyle {
    /**
     * 绘制选中的按钮的下划线样式(三角形,长方形等等)
     *
     * @param canvas          canvas
     * @param translationX    X偏移量
     * @param tabWidth        每个tab宽度
     * @param underlineColor  下划线颜色
     * @param height          tab高度
     * @param underlineHeight 下划线高度(xml设置)
     */
    void draw(Canvas canvas, int translationX, int tabWidth, int underlineColor, int height, int underlineHeight);
}

再给出我矩形指示器和三角指示器的实现:
矩形指示器(图就是上面那张):

@Override
public void draw(Canvas canvas, int translationX, int tabWidth, int underlineColor, int height, int underlineHeight) {
    mPaint.setColor(underlineColor);
    float offset = RADIO_OFFSET * tabWidth / 2; // RADIO_OFFSET = 0.35f
    canvas.save();
    canvas.translate(translationX, height - underlineHeight);
    canvas.drawRect(offset, 0, tabWidth - offset, underlineHeight, mPaint);
    canvas.restore();
}

上面的偏移比例是指相对于tab宽度,指示器应该相对小一点,所以,前面留0.35/2的比例为空白,然后画矩形。

mPaint.setColor(underlineColor);
    int mTriangleWidth = (int) (tabWidth * RADIO_TRIANGLE_WIDTH);  RADIO_TRIANGLE_WIDTH = 1/6f
    int mInitTriangleWidth = tabWidth / 2 - mTriangleWidth / 2;
    Path path = new Path();
    path.moveTo(0, 0);
    path.lineTo(mTriangleWidth, 0);
    path.lineTo(mTriangleWidth / 2, -underlineHeight);
    path.close();
    canvas.save();
    canvas.translate(mInitTriangleWidth + translationX, height - 2);
    canvas.drawPath(path, mPaint);
    canvas.restore();

在后面绘制时相对向上偏移了一点位置,防止三角形被底部覆盖。
这里写图片描述

5.其他部分功能

当然还给出了一些设置tab的标题、可见tab数、选中文本色、未选中文本色等功能的方法。请自己在代码中看一下。还有就是自定义属性,这个要在attrs.xml中定义其属性,之后才可在view中获取:

<declare-styleable name="SimpleTabStripView">
    <attr name="visible_tabs_count" format="integer" />
    <attr name="titles" format="reference" />
    <attr name="selected_txt_color" format="color" />
    <attr name="unselected_txt_color" format="color" />
    <attr name="underline_color" format="color" />
    <attr name="tab_selected_bg" format="color" />
    <attr name="underline_height" format="dimension" />
    <attr name="tab_txt_size" format="dimension" />
</declare-styleable>

三、总结

其实也没啥可说的。讲讲怎样自定义View吧。首先理清需要的View所含功能到底是啥,然后把这些功能按照每个步骤实现划分开来,保证每一步都很清晰的能够达到目标。之后了解需要的api就可以实现了。当然这只是简单的需求。如果涉及到touch事件,还需要对Android的事件分发机制有所了解。说到这,想到其实我们这个tab还有个缺陷,如果过多的tab,需要一个个滑动到最后面,我们可以通过拦截滑动事件,来定义其为可随意滑动的tab控件。这个就不写了,我自己写过了,留个思考空间给大家吧。当然涉及到Touch事件,还是需要下一番功夫的。
补充:其实上述的实现中还有个问题,如果按照上面的实现,我们在AndroidStudio的preview中其实是看不到指定的tab的,我一开始还没在意。后来用到项目中,发现华为的荣耀8以上,p8等都不显示,但是其他机子都显示。根据其测量过程的log,我发现一般我们在addView之后都会重新onMeasure再layout,但是在华为的部分机子上不是,他们直接layout了,导致其宽高度为0,不显示。只要在addView之后重新measure一下就可以了,这个我加在代码中了。



代码下载地址:自定义tab栏控件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值