Android自定义控件---导航栏SlideTab(Fragment+ViewPager)

一、前言

好久没有更新过博客了,趁今天有空分享一个导航栏的自定义控件。有关此控件的demo相信在网上已经烂大街了,一搜一大把。
我现在只着重分享一些我认为比较难理解的知识点。整个控件的难点大概有三个
1、游标的绘制。
2、ViewPager监听器的理解。
3、游标的移动。
本文将注重这三个方面重点分析。

先上Demo的最终效果

二、Demo结构图和知识点

样例Module,有四个java文件和两个xml文件
Android自定义控件---导航栏SlideTab(Fragment+ViewPager)_第1张图片
总结一下此控件的主要知识点
1、ViewGroup绘制流程。
2、ViewPager的用法。
3、OnPageChangeListener接口的用法。
4、scrollTo方法的使用。

需要完整代码,请看底部链接,谢谢!(^_^)。下面我直接讲核心代码。

三、SlideTab导航栏控件

(1)SlideTab继承了HorizontalScrollView控件之后,咋们需要重写onDraw方法。接下来需要看个图了解SlideTab控件的内部组成
Android自定义控件---导航栏SlideTab(Fragment+ViewPager)_第2张图片
整个SlideTab控件就是由这三个类型的控件组成。假设SlideTab控件已经初始化完成了。第一次由系统开始调用onDraw方法。

/**
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //获取当前Tab的左右两边的横坐标值
        View currentTabView = horizontalContainer.getChildAt(currentPosition);
        float currentTabLeftX = currentTabView.getLeft();
        float currentTabRightX = currentTabView.getRight();
        int childCount = horizontalContainer.getChildCount();
         //ViewPager在滑动的过程中会重复调用onDraw方法。下面if语句的内容是用来计算游标的起点坐标和终点坐标
        if (currentPositionOffset > 0f && currentPosition < childCount - 1) {
            //获取下一个Tab左右两边的横坐标值
            View nextTab = horizontalContainer.getChildAt(currentPosition + 1);
            float nextTabLeftX = nextTab.getLeft();
            float nextTabRightX = nextTab.getRight();
            //计算起点
            currentTabLeftX = (currentPositionOffset * nextTabLeftX + (1f - currentPositionOffset)
                    * currentTabLeftX);
            //计算终点
            currentTabRightX = (currentPositionOffset * nextTabRightX + (1f - currentPositionOffset)
                    * currentTabRightX);
        }
        //绘制下划线
//        drawUnderline(canvas, horizontalContainer);
        //绘制指示器
        drawIndicator(canvas, currentTabLeftX, currentTabRightX);
    }

currentPosition变量的初始值为0。
第8行代码View currentTabView = horizontalContainer.getChildAt(currentPosition);获取到LinearLayout容器里面第一个View(实际是TextView)视图。

第9,10行分别得到View视图的左上角坐标和右上角坐标。

第11行获取LinearLayout容器中TextView控件的总数。

第13行的判断语句。currentPositionOffset 这是一个记录着当前页面滑动过程中的偏移量(如果不理解先放下,后面再讲)初始值为0,很明显,currentPositionOffset 不大于 0f,if语句不成立,略过(if语句里面的内容稍后再分析)。继续往下走。

来到底28行执行drawIndicator方法,开始绘制游标。

(2)现在来解决此控件的第一个难点,游标的绘制。
这个游标实际上是一条线。要画一条先就必须得确定两点的坐标值(初中知识,两点才能确定一条直线嘛)。

/**
     * 绘制游标
     *
     * @param canvas        SlideTab控件的画板
     * @param currentLeftX  标题控件的左坐标
     * @param currentRightX 标题控件的右坐标
     */
    private void drawIndicator(Canvas canvas, float currentLeftX, float currentRightX) {
        float indicatorMiddle = (indicatorPaint.getStrokeWidth() / 2);
        float indicatorY = getHeight() - indicatorMiddle;
        canvas.drawLine(currentLeftX, indicatorY, currentRightX, indicatorY, indicatorPaint);
    }

为了更好的理解这几段代码,还是画个图。下图黑色框是LinearLayout容器,绿色框是游标。
Android自定义控件---导航栏SlideTab(Fragment+ViewPager)_第3张图片
绿色框高度的值就是indicatorPaint.getStrokeWidth()。接下的工作就是计算下图左右两边的橙色点。
Android自定义控件---导航栏SlideTab(Fragment+ViewPager)_第4张图片
红色横线表示currentLeftX变量。
蓝色横线表示currentRightX变量。
红色竖线表示indicatorMiddle变量。
粉色竖线表示indicatorY变量。

由第9,10行的计算方法得到
左边橙色坐标点(currentLeftX,indicatorY)。
右边橙色坐标点(currentRightX,indicatorY)。

在第11行调用canvas.drawLine方法(这是一个绘制直线的方法)绘制游标。这个方法的最后的参数indicatorPaint是一个画笔(用来描述这个直线的状态,例如颜色,宽度,直线末端是否圆角等等。)

以上的内容就是游标绘制的流程。

(3)绘制好游标之后如何让导航栏控件跟随着ViewPager的滑动而滑动呢?现在我们需要写一个setViewPager方法,将(Fragment+ViewPager)与SlideTab控件关联起来。这个方法是提供给用户(使用你控件的程序猿)调用。他们只需要传来一个ViewPager的实例和一个标题名称数组即可完成此控件的调用。

/**
     * @param viewPager   用户传进来的ViewPager
     * @param titleString 标题名称
     */
    public void setViewPager(ViewPager viewPager, String[] titleString) {
        this.viewPager = viewPager;
        addTab(titleString);
        //设置ViewPager监听事件
        viewPager.addOnPageChangeListener(new SlideTabPageViewListener());
    }
    /**
     * 添加Tab
     *
     * @param titleString 标题数组
     */
    private void addTab(String[] titleString) {
        //清空所有控件
        horizontalContainer.removeAllViews();
        for (int i = 0; i < viewPager.getAdapter().getCount(); i++) {
            //创建垂直容器,用来包裹住下面TextView
            tabVerticalContainer = new LinearLayout(context);
            tabVerticalContainer.setOrientation(LinearLayout.VERTICAL);
            tabVerticalContainer.setHorizontalGravity(Gravity.CENTER_HORIZONTAL);
            //设置点击事件
            tabVerticalContainer.setOnClickListener(new ViewPagerClickListener(i));
            tabVerticalContainer.setVerticalGravity(Gravity.CENTER_VERTICAL);
            //将垂直LinearLayout容器放入水平LinearLayout容器中
            horizontalContainer.addView(tabVerticalContainer, isExtendTab ? expandedTabLayoutParams
                    : defaultTabLayoutParams);
            if (titleString != null) {
                //创建标题
                TextView textViews = new TextView(context);
                textViews.setText(titleString[i]);
                textViews.setTextSize(14);
                textViews.setTextColor(Color.parseColor("#000000"));
                textViews.setSingleLine(true);
                tabVerticalContainer.addView(textViews, textViewLayoutParams);
            }
        }
    }

第8行是一个自定义方法。根据ViewPager的页面总数,设置标题导航栏。逻辑比较简单只是单纯的堆代码,咋们略过吧。
我们重点关注第10行代码。ViewPager注册了一个监听事件的实例。此实例有三个回调方法用来监听用户对屏幕的滑动操作。具体详情请往下看,(SlideTab控件的游标滑动与这个监听事件有很大关系。)

(4)SlideTabPageViewListener是SlideTab控件的内部类,实现了ViewPager.OnPageChangeListener接口,这个接口必须实现3个方法。
现在来解决第2个难点。就是ViewPager的监听事件。
public void onPageScrolled(int position, float positionOffset,int positionOffsetPixels)
当你滑动页面的时候会调用此方法,在滑动停止之前,此方法回一直被调用。
position表示当前页面的下标。例如你有三个选项卡,现在从第一页滑动到第二页的过程中,这个position的下标是0(下标从0开始),滑动到第二页position的时候下标就变成1了。
positionOffset表示当前页面偏移的百分比。这个参数我们待会就会用到。
positionOffsetPixels表示当前页面偏移的像素,一般情况不用。
Android自定义控件---导航栏SlideTab(Fragment+ViewPager)_第5张图片
public void onPageScrollStateChanged(int state)
当ViewPager页面的状态被改变的时候会调用此方法。怎么理解这句话呢?
1、假如你触摸屏幕从第一页滑动到第二页这个过程中,会回调此方法(如果你手指一直处于滑动状态此方法就会一直被调用),传过来的state的值是1表示正在滑动。
2、滑动结束之后,会再次回调此方法,传过来的state的值是2表示滑动结束了。
3、结束滑动之后如果没有其他的滑动操作,会再次回调此方法,传过来的state的值是0,表示ViewPager处理闲置状态。

public void onPageSelected(int position)
此方法是从当前页面滑动到另一个页面才会调用,并且这个position是新页面的下标。注意如果你从当前页往下一页滑动的过程中(不松手)又滑回原页面,此方法不会调用。

以上关于ViewPager的OnPageChangeListener接口的方法详情,有了这些基础之后就比较好解释SlideTabPageViewListener内部类。

(5)SlideTabPageViewListener内部类

 /**
     * ViewPager滚动监听事件
     */
    public class SlideTabPageViewListener implements ViewPager.OnPageChangeListener {
        @Override
        public void onPageScrolled(int position, float positionOffset,
                                   int positionOffsetPixels) {
            currentPosition = position;
            currentPositionOffset = positionOffset;
            scrollToCurrentPosition(position, (int) (positionOffset * (horizontalContainer)
                    .getChildAt(position).getWidth()));
            //重新绘制onDraw方法
            invalidate();
        }
        @Override
        public void onPageScrollStateChanged(int state) {
        }
        @Override
        public void onPageSelected(int position) {
        }
    }

最后,咋们来解决此控件的最后一个难题,如何控制游标的移动。这是我感觉最难讲清楚的一部分。
假设咋们正在触摸屏幕从第一页滑动到第二页,在这个过程中。以上的onPageScrolled方法会一直被调用。

第8,9行的代码是更新当前最新的页面下标(currentPosition )和页面的偏移值(currentPositionOffset )。这两个变量咋们已经在上面的onDraw方法中见过。
第10行调用scrollToCurrentPosition方法(这个方法很重要)。将当前的页面下标(值为0)和当前View(TextView)的偏移值传递过去。具体代码如下。

/*****
     *
     *
     * @param position
     * @param offset
     */
    public void scrollToCurrentPosition(int position, int offset) {
        int currentOffsetX = horizontalContainer.getChildAt(position).getLeft() + offset;
        int startScrollX = currentOffsetX;
        if (position > 0 || offset > 0) {
            //remainOffset表示剩余偏移量
            startScrollX = currentOffsetX - remainOffset;
        }
        //如果位移发生变化,则滑动
        if (startScrollX != lastScrollX) {
            //更新最后一次滑动的距离
            lastScrollX = startScrollX;
            //horizontalContainer控件开始滑动
            scrollTo(startScrollX, 0);
        }
    }

第11行的语句成立。计算得到startScrollX这是horizontalContainer 容器实际的滑动偏移值。
第17行lastScrollX默认初始值为0,因此if语句也成立。
最终在21行开始滑动horizontalContainer容器。(需要注意的是如果startScrollX的值大于0则往左滑动,小于0往右滑动。)

紧接着调用invalidate()方法,其内部代码又会回调咋们刚才所说的onDraw方法。在onDraw方法中,执行了前面的代码后来到了刚才没有讲解的if语句,由于此时处于滑动状态。currentPositionOffset和currentPosition的值肯定是成立的。

(6)我再贴一下onDraw方法中if语句的代码。

if (currentPositionOffset > 0f && currentPosition < childCount - 1) {
            //获取下一个Tab左右两边的横坐标值
            View nextTab = horizontalContainer.getChildAt(currentPosition + 1);
            float nextTabLeftX = nextTab.getLeft();
            float nextTabRightX = nextTab.getRight();
            //计算起点
            currentTabLeftX = (currentPositionOffset * nextTabLeftX + (1f - currentPositionOffset)
                    * currentTabLeftX);
            //计算终点
            currentTabRightX = (currentPositionOffset * nextTabRightX + (1f - currentPositionOffset)
                    * currentTabRightX);
        }

在第3行,由于滑动没有结束,此时currentPosition 的值还是0,又因为currentPosition + 1,所以nextTab得到的是第二个TextView的值。
第4,5行获取nextTab控件(实际就是TextView)左右两边的坐标。

第7,10行计算滑动过程中游标的起点和终点,也就是下图中左右两边的红点的坐标。
Android自定义控件---导航栏SlideTab(Fragment+ViewPager)_第6张图片
计算完毕之后。最后就是再次调用drawIndicator方法重新绘制游标。
至此有关SlideTab控件与ViewPager滑动的流程就走完了。谢谢(^_^)Y

四、结束

鉴于篇幅的关系,我就不再演示控件的使用了。各位可以下载下面的demo看看源码。demo中的SlideTabDemonstration类是入口。
此demo有BUG在所难免。仅限于学习。希望能帮到各位。

Demo源码请戳这里(Android Studio编译器)

你可能感兴趣的:(Android基础,Android自定义控件,android,控件,导航,java)