TabIndicator简单实现

前言

使用TabIndicator+ViewPager布局多个不同类别的页面,使它们展示在同一个地方,用户通过手指滑动切换关注的内容,这是很多应用都会使用的一种高效布局。在页面切换过程中ViewPager会做页面切换,TabIndicator通常会在不同Tab底部移动一条横线实现Tab之间的切换效果,这种看似简单的动画真正实现起来却相当费事,现在就来尝试手动实现一下。

实现效果

这里写图片描述

实现接口

如果仔细观察TabIndicator底部的线条运动会发现线条的位置和ViewPager的切换过程是同步的,比如切换页面只展示的一半那么横线会在两个Tab之间展示。ViewPager提供了一个监听接口来回调切换页面的百分比情况,接口如下:

public interface OnPageChangeListener {
    // 该接口会在用户手动会动ViewPager或者调用ViewPager.scrollXXX类方法的时候回调,
    // 第一个参数代表要展示的页面索引,第二个参数表示切换的当前比例,
    // 最后一个是切换已经偏移的像素值
    void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);

    // 当新页面被选中展示时回调,动画可能完成可能没完成
    void onPageSelected(int position);

    // 当ViewPager切换的时滚动状态发生变化会被回调
    // ViewPager#SCROLL_STATE_IDLE 滑动停止
    // ViewPager#SCROLL_STATE_DRAGGING 用户正在拖动
    // ViewPager#SCROLL_STATE_SETTLING ViewPager在惯性滑动中
    void onPageScrollStateChanged(int state);
}

通过ViewPager提供的接口查看,onPageScrolled回调的positionOffset正式我们需要的切换比例值,用这个比例值可以计算当前横线应该在两个Tab之间的位置。现在为ViewPager添加回调接口,并且添加3个页面元素,向右依次点击滑动页面,打印一下position和positionOffset的值:

onPageScrolled() position = 0, positionOffset = 0.0
onPageScrolled() position = 0, positionOffset = 0.073148146
onPageScrolled() position = 0, positionOffset = 0.40925926
onPageScrolled() position = 0, positionOffset = 0.65092593
onPageScrolled() position = 0, positionOffset = 0.78518516
onPageScrolled() position = 0, positionOffset = 0.8842593
onPageScrolled() position = 0, positionOffset = 0.9314815
onPageScrolled() position = 0, positionOffset = 0.9685185
onPageScrolled() position = 0, positionOffset = 0.98703706
onPageScrolled() position = 0, positionOffset = 0.9953704
onPageScrolled() position = 0, positionOffset = 0.9990741
onPageScrolled() position = 1, positionOffset = 0.0
onPageScrolled() position = 1, positionOffset = 0.20555556
onPageScrolled() position = 1, positionOffset = 0.48703706
onPageScrolled() position = 1, positionOffset = 0.6925926
onPageScrolled() position = 1, positionOffset = 0.82592595
onPageScrolled() position = 1, positionOffset = 0.9046296
onPageScrolled() position = 1, positionOffset = 0.95370376
onPageScrolled() position = 1, positionOffset = 0.98796296
onPageScrolled() position = 1, positionOffset = 0.9962963
onPageScrolled() position = 1, positionOffset = 0.9990741
onPageScrolled() position = 2, positionOffset = 0.0

观察数据可以发现除了positionOffset = 0.0和1.0的特殊位置外,其他位置都是切换的中间状态,positionOffset逐渐增大,并且position是当前页的索引。再从最后一页向前一次滑动页面观察打印结果:

onPageScrolled() position = 1, positionOffset = 0.912037
onPageScrolled() position = 1, positionOffset = 0.8370371
onPageScrolled() position = 1, positionOffset = 0.7787037
onPageScrolled() position = 1, positionOffset = 0.7574074
onPageScrolled() position = 1, positionOffset = 0.72870374
onPageScrolled() position = 1, positionOffset = 0.5842593
onPageScrolled() position = 1, positionOffset = 0.47685182
onPageScrolled() position = 1, positionOffset = 0.375
onPageScrolled() position = 1, positionOffset = 0.29537034
onPageScrolled() position = 1, positionOffset = 0.22592592
onPageScrolled() position = 1, positionOffset = 0.17129624
onPageScrolled() position = 1, positionOffset = 0.1268518
onPageScrolled() position = 1, positionOffset = 0.094444394
onPageScrolled() position = 1, positionOffset = 0.06759262
onPageScrolled() position = 1, positionOffset = 0.047222257
onPageScrolled() position = 1, positionOffset = 0.032407403
onPageScrolled() position = 1, positionOffset = 0.021296263
onPageScrolled() position = 1, positionOffset = 0.013888836
onPageScrolled() position = 1, positionOffset = 0.008333325
onPageScrolled() position = 1, positionOffset = 0.004629612
onPageScrolled() position = 1, positionOffset = 0.0027778149
onPageScrolled() position = 1, positionOffset = 9.2589855E-4
onPageScrolled() position = 1, positionOffset = 0.0
onPageScrolled() position = 0, positionOffset = 0.9546296
onPageScrolled() position = 0, positionOffset = 0.8518519
onPageScrolled() position = 0, positionOffset = 0.74444443
onPageScrolled() position = 0, positionOffset = 0.5833333
onPageScrolled() position = 0, positionOffset = 0.4037037
onPageScrolled() position = 0, positionOffset = 0.2851852
onPageScrolled() position = 0, positionOffset = 0.19074073
onPageScrolled() position = 0, positionOffset = 0.12037037
onPageScrolled() position = 0, positionOffset = 0.072222225
onPageScrolled() position = 0, positionOffset = 0.042592593
onPageScrolled() position = 0, positionOffset = 0.022222223
onPageScrolled() position = 0, positionOffset = 0.011111111
onPageScrolled() position = 0, positionOffset = 0.0046296297
onPageScrolled() position = 0, positionOffset = 0.0018518518
onPageScrolled() position = 0, positionOffset = 9.259259E-4
onPageScrolled() position = 0, positionOffset = 0.0

观察打印结果可以发现positionOffset的值是逐渐的减小,position的值是切换的目标页索引。打印完成之后再打印一下直接从第一个点击跳转到第三个,打印结果如下:

onPageScrolled() position = 0, positionOffset = 0.033333335
onPageScrolled() position = 0, positionOffset = 0.50555557
onPageScrolled() position = 0, positionOffset = 0.8833333
onPageScrolled() position = 1, positionOffset = 0.19629633
onPageScrolled() position = 1, positionOffset = 0.4351852
onPageScrolled() position = 1, positionOffset = 0.61296296
onPageScrolled() position = 1, positionOffset = 0.73703706
onPageScrolled() position = 1, positionOffset = 0.86481476
onPageScrolled() position = 1, positionOffset = 0.91851854
onPageScrolled() position = 1, positionOffset = 0.95370376
onPageScrolled() position = 1, positionOffset = 0.9759259
onPageScrolled() position = 1, positionOffset = 0.98796296
onPageScrolled() position = 1, positionOffset = 0.9944445
onPageScrolled() position = 1, positionOffset = 0.9981482
onPageScrolled() position = 1, positionOffset = 0.9990741
onPageScrolled() position = 2, positionOffset = 0.0

观察结果可以发现从第一个到第三个position是逐渐增大的,并没有出现直接从第一个跳到第三个的情况,positionOffset在相邻页之间是逐渐增大的。
通过上面打印的结果我们知道:
- 向左滑动时postionOffset逐渐增大,position代表当前展示页索引
- 向右滑动时positionOffset逐渐减小,position代表目标展示页索引
- 多个页面之间的跳转其实是第一个先跳到第二个,第二个再跳到第三个,最后在跳到目标页。

了解了ViewPager的页面切换原理之后,再观察底部线条的运动会发现它是跨越多个Tab的,所以最终实现在包含Tab视图的ViewGroup中,ViewGroup会在dispatchDraw里请求它的子控件绘制自己,这个动画就可以在这里做。这条线的宽高都是确定的,要画它只需要确定这条线经过的一个点,通常都是用线端的中心点作为定位点。

实现过程

首先定义TabIndicator继承自LinearLayou布局,然后初始化画笔工具,当然需要为它指定需要联动的ViewPager对象,并且初始化它的布局。

public void setViewPager(final ViewPager viewPager) {
    this.viewPager = viewPager;
    if (viewPager.getAdapter() != null) {
        this.pagerAdapter = viewPager.getAdapter();
    } else {
        throw new IllegalArgumentException("ViewPager has no pagerAdapter");
    }

    if (pagerAdapter.getCount() == 0) {
        setVisibility(View.GONE);
        return;
    }

    setVisibility(View.VISIBLE);
    int count = pagerAdapter.getCount();
    int width = getTabWidth(count);

    // 根据ViewPager的adapter获取标题,并且将标题TextView加入TabIndicator布局
    for (int i = 0; i < count; i++) {
        TextView textView = new TextView(getContext());
        textView.setGravity(Gravity.CENTER);
        textView.setTextColor(getResources().getColor(R.color.colorAccent));
        textView.setTextSize(15f);
        textView.setText(pagerAdapter.getPageTitle(i));
        final int index = i;
        textView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                viewPager.setCurrentItem(index, true);
            }
        });
        LinearLayout.LayoutParams params = new LayoutParams(width, ViewGroup.LayoutParams.WRAP_CONTENT);
        params.gravity = Gravity.CENTER;
        addView(textView, params);
    }

    // 初始化Tab的宽度为屏幕的四分之一
    tabWidth = width;
    // 底部线条的中心点在Tab宽度一半的位置,也就是说选中的是第一个Tab
    curPos = width / 2;
    selected = 0;
    postInvalidate();
}

// Tab宽度大于3个就屏幕四分之一,否则就2、3个平分屏幕宽度
private int getTabWidth(int count) {
    if (count >= 4) {
        return CommonUtils.getScreenWidth() / 4;
    } else {
        return CommonUtils.getScreenWidth() / count;
    }
}

接下来为自定义控件增加onMeasure测量宽高的功能,如果和添加画线功能,画线就是在curPos为中心点绘制。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int height = MeasureSpec.getSize(heightMeasureSpec);
    int mode = MeasureSpec.getMode(heightMeasureSpec);
    // 如果设置的高度不是精确值,那么直接使用45dp
    int realHeight = CommonUtils.dp2px(45);
    if (mode == MeasureSpec.EXACTLY) {
        realHeight = height;
    }

    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        getChildAt(i).getLayoutParams().height = realHeight;
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    setMeasuredDimension(getMeasuredWidth(), realHeight);
}

@Override
protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
    drawBottomLine(canvas);
}

// 以curPos为中心点画一条线
private void drawBottomLine(Canvas canvas) {
    canvas.save();
    canvas.drawRect(curPos - tabWidth / 2,
            getMeasuredHeight() - CommonUtils.dp2px(3),
            curPos + tabWidth / 2, getMeasuredHeight(), paint);
    canvas.restore();
}

前面的过程都非常简单,只是普通的自定义控件画图和添加子控件操作,下面开始真正的随着ViewPager的切换动态计算curPos的位置。为了动态跟踪ViewPager切换数值,需要在前面的回调里将数据传递给TabIndicator:

viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        Log.d(TAG, "onPageScrolled() position = " + position + ", positionOffset = " + positionOffset);
        arrowTabIndicator.setPositionAndOffset(position, positionOffset);
    }

    @Override
    public void onPageSelected(int position) {

    }

    @Override
    public void onPageScrollStateChanged(int state) {
        if (state == ViewPager.SCROLL_STATE_IDLE) {
            arrowTabIndicator.reset();
        }
    }
});

获取了postion和positionOffset之后首先要确定ViewPager向那个方向移动,可以先采集最开始的两个positionOffset进行比较,如果逐渐增大就是向右移动,那么目标页索引就是position + 1;如果逐渐减小就是向左移动,目标页索引就是position。由于跨多个Tab跳转也是逐个跳转的,只需要实现当前页到前一个或者后一个页面的滑动就可以了。那么滚动什么时候停止呢,当然就在onPageScrollStateChanged的state为SCROLL_STATE_IDLE的时候就代表滚动停止了。这里为了区分这些步骤采用了简单的状态机实现计算curPos。

// 初始状态,ViewPager没有滑动时TabIndicator就处于这种状态
private static final int STATE_INIT = 0;
// 决策ViewPager滚动的方向状态
private static final int STATE_DECIDE_DIRECT = 1;
// 正在滚动画底部线条状态
private static final int STATE_DRAW_SCROLL_ARROW = 2;

private ViewPager viewPager;
private PagerAdapter pagerAdapter;
// ViewPager静止时选中的Tab索引
private int selected = -1;
// 初始状态
private int state = STATE_INIT;

定义好这些状态和属性值之后,开始通过回调采集到的数据进行处理。

public void setPositionAndOffset(int position, float offset) {
    // 如果ViewPager没有初始化,或者传入的是特殊值,也就是滚动基本完成,不做处理
    if (selected < 0 || offset < 0.00000001 || offset > 0.9999999999) {
        return;
    }

    switch (state) {
        // 初始状态,记录下第一个offset值和position,并且将状态改成决定滚动方向状态
        case STATE_INIT:
            lastOffset = offset;
            lastPosition = position;
            state = STATE_DECIDE_DIRECT;
            Log.d(TAG, "setPositionAndOffset(): init state = " + state
                    + ", lastPosition = " + lastPosition + ", lastOffset = " + lastOffset);
            break;
        case STATE_DECIDE_DIRECT:
            // 使用第二个值与第一个值比较,确定滚动方向
            if (lastPosition == position) {
                leftToRight = (offset - lastOffset) > 0;
                state = STATE_DRAW_SCROLL_ARROW;
            } else { // 为了防止一个页面只采集到一个滚动数据
                lastOffset = offset;
                lastPosition = position;
            }
            Log.d(TAG, "setPositionAndOffset(): init state = " + state
                    + ", lastPosition = " + lastPosition + ", lastOffset = " + lastOffset);
            break;
        case STATE_DRAW_SCROLL_ARROW:
            // 确定方向后,开始根据positionOffset去顶curPos位置
            if (leftToRight) {
                int target = position + 1;
                Log.d(TAG, "setPositionAndOffset(): init state = " + state
                        + ", leftToRight = " + leftToRight + ", target = " + target + ", selected = " + selected);
                if (target >= pagerAdapter.getCount() || target == selected) {
                    return;
                }
                curPos = (int) (((target + offset) * tabWidth) - tabWidth / 2);
            } else {
                int target = position;
                Log.d(TAG, "setPositionAndOffset(): init state = " + state
                        + ", leftToRight = " + leftToRight + ", target = " + target + ", selected = " + selected);
                curPos = (int) (((target + 1 + offset) * tabWidth) - tabWidth / 2);
            }
            // 重回当前的底部线条,一次只会画一个位置的线,但是这个方法会在滚动过程中不
            // 停调用直到滚动结束,这样就看到一条中心点不断变化的线。
            invalidate();
        default:
            break;
    }

}

// 在停止滑动后重置状态和选中Tab索引
public void reset() {
    state = STATE_INIT;
    selected = viewPager.getCurrentItem();
}

以上就是TabIndicator的简单实现,至于更多的Tab滑动支持暂时还没有添加,后面学习滑动的时候在来完善,查看详细代码实现请点击查看源码

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值