SprintNBA项目模仿笔记(三)如何做一个优秀的tab标签-ViewPager联动

今天是模仿这个项目的第三天,看到风格优秀的标签-viewpager联动,我有点头痛,怎样做才能达到像这个项目一样的优秀效果呢?
效果图1
效果图2

要点:

  1. 标签和viewpager是联动的,标签动,viewpager动,viewpager动,标签也要动
    难度:☆
  2. 标签选中时是有中间的过度动画的,同时之前选择的标签颜色渐变为浅色,新选择的标签颜色渐变为深色,同时尽量将标签移动到中间
    难度:☆☆
  3. 标签和viewpager之间的联动性非常好,标签和viewpager几乎同时移动
    难度:☆☆☆

根据难度,由易到难,一条一条的来

标签联动:

第一条,好简单,估计很多人都做过,思路如下:
(1)自定义一个HorizontalScrollView
(2)为这个HorizontalScrollView动态添加标签【动态有助于扩展,部分标签可能需要刷新】
这条略难,可以添加一些textview进来,定义选中,未选中两种状态。
(3)为自定义标签和viewpager添加联动类,点击标签时,判断是否已选,已选不处理,否则触发setCurrentItem(),进而触发onPageSelected()
注意,这里一定要对标签页是否选择进行判断,否则会造成死循环,死循环有两种情况,
1.从标签开始
点击标签->setCurrentItem->onPageSelected()->设置标签->点击标签。。。
2.从onPageSelected开始
setCurrentItem->onPageSelected()->设置标签->点击标签->setCurrentItem()

如果判断已选,循环顺序如下:
onPageSelected->设置标签->点击标签->和当前标签不同,选择标签-》setCurrentItem->onPageSelected->点击标签-》和当前标签相同,返回

这样就结束,或者也可以在setCurrentItem中也判断一次,就变成:
onPageSelected->设置标签->点击标签->和当前标签不同,选择标签-》setCurrentItem->和当前标签相同,返回

关键代码:
标签方面的添加标签:

  /**
     * 添加标签
     * @param tabs                  标签列表
     * @param paddingLeftRight    左右间距
     * @param paddingTopBottom    上下间距
     * @param textSize              字号
     */
    public void addTabs(String[] tabs, int paddingLeftRight, int paddingTopBottom, int textSize) {
        container = new LinearLayout(getContext().getApplicationContext());
        container.setOrientation(LinearLayout.HORIZONTAL);
        addView(container);
        for (int i = 0; i < tabs.length; i++) {
            String name = tabs[i];
            TextView tv = new TextView(getContext().getApplicationContext());
            tv.setText(name);
            tv.setTextSize(textSize);
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
            tv.setLayoutParams(params);
            tv.setGravity(Gravity.CENTER);
            tv.setPadding(paddingLeftRight, paddingTopBottom, paddingLeftRight, paddingTopBottom);
            container.addView(tv);
        }
        initData();
    }

初始化标签的选中项,初始化标签代号,初始化标签点击事件:

private void initData() {
        container = (LinearLayout) getChildAt(0);

        for (int i = 0; i < container.getChildCount(); i++) {
            TextView view = (TextView) container.getChildAt(i);
            view.setTag(i);
            if (i == 0)
                view.setTextColor(selectWordColor);
            else
                view.setTextColor(noSelectWordColor);
            view.setOnClickListener(onClickListener = new OnClickListener() {
                @Override
                public void onClick(View v) {
                    fromTab = true;
                    newPos = (int) v.getTag();
                    if(selectedPos == newPos)
                        return;
                     select(newPos);//这个是给外面的接口用的
                }
            });
        }
        invalidate();
    }

移动到中间的逻辑:

 final int scrollPos = (int) (realLeft - (getWidth() - realWidth) / 2);
            smoothScrollTo(scrollPos, 0);

realLeft是当前左边距,可用container.getChildAt(newPos).getLeft()获取
realWidth是当前宽度,可用container.getChildAt(newPos).getWidth()获取

然后绘制,这里贴个大概,主要是提供思路。具体的请看下面源码链接:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //去除滚动条和半弧背景
        setHorizontalScrollBarEnabled(false);
        setHorizontalFadingEdgeEnabled(false);
        setOverScrollMode(OVER_SCROLL_NEVER);
        if (isInEditMode()) {
            return;
        }
        if (width == 0) {
            width = container.getWidth();
            ceilCount = container.getChildCount();
            ceils = new float[ceilCount];
            if (container.getChildCount() != 0) {
                for (int i = 0; i < container.getChildCount(); i++) {
                    ceils[i] = container.getChildAt(i).getWidth();
                }
            }
            startWidth = ceils[0];
        }

        if (underlineHeight != 0) {
             float posLeft =  container.getChildAt(selectedPos).getLeft();
            float aidLeft = container.getChildAt(newPos).getLeft();
            float realLeft = (aidLeft - posLeft) * followOffset + posLeft;

            float postWidth =ceils[selectedPos];
            float aidWidth = ceils[newPos];
            float realWidth = (aidWidth - postWidth) * followOffset + postWidth;

            float left = realLeft + underlineLeftRight;
            float top = getHeight() - underlineHeight;
            float right = realLeft + realWidth - underlineLeftRight;
            float bottom = getHeight();

        //下滑线/背景
            if (underlineDrawable == null) {
                canvas.drawRect(left, top, right, bottom, underlinePaint);
            }

    }

然后看下联动部分:

        tabLayout.setOnSelectTabListener(new TabLayout.SelectTabListener() {
            @Override
            public void onSelect(int position) {
                if (position == nowPos)
                    return;
                nowPos = position;
                if (fromVp) {
                    fromVp = false;
                    return;
                }

                fromTab = true;
                viewPager.setCurrentItem(position);
            }
        });

        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            }

            @Override
            public void onPageSelected(int position) {
                if (fromTab) {
                    fromTab = false;
                    return;
                }
                fromVp = true;
                tabLayout.setTab(position);
            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });

然后setTab中,只要模拟点击就行:

     public void setTab(int position){
        if(ceils == null || ceils.length == 0)
            return;

        if(container!=null && container.getChildCount()>position){
            container.getChildAt(position).performClick();
        }
    }

因为本来是别的项目移植来的,那个项目的每个标签的宽度不一样,所以需要计算宽度,如果宽度一致,可以直接将realWidth写成:realWidth = container.getChildAt(0).getWidth();就行了,如果读者想自定义个标签,不想看过程,可以直接移动到最下面看源码,本文只提供思路,直接复制本文的代码,不能直接使用,但有助于大家学会怎样做标签,viewpager联动,如果要学习,请务必仔细阅读本文。

过渡动画:

过渡动画包含两种效果,从标签a移动到标签b:

  • 指示器(代码选中的背景)从a向b移动
  • a颜色变浅,b颜色变深

这里可以用ValueObject:

ValueAnimator animator = ValueAnimator.ofInt(0,100);
                animator.setTarget(targetBtn);
                animator.setDuration(1000).start();
                final int height = targetBtn.getHeight();
                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation){
                        int value = (int) animation.getAnimatedValue();
                        //value代表进度
                        followOffset = 1.0 * value / 100;
                    }
                });

关于颜色计算,我这里也是百度的,感谢仁兄的提供:

http://blog.csdn.net/super_spy/article/details/49332719

其实这部分的难度主要是颜色部分,动画倒是没什么难度。

然后,在你做完这方面的工作后,会发现一个坑爹的问题,不同步!标签页和viewpager两者根本就没有达到联动效果,原因如下:
1.按照逻辑,是选择标签后,标签动画,触发viewpager,viewpager变化
2.viewpager滚动时候的逻辑根本没写,这个时候标签根本没动过

好用同步的联动性:

这个时候,我发现,滚动的时候会调用一个监听方法,addOnPageChangeListener中的onPageScrolled()
这里面有三个参数,先解释一下:
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels)
position:偏移从这个位置开始计算,注意,这个position并不一定代表你选中的position,这是这个方法的坑点,因为这个调整了好久
positionOffset:
position位置移动到右边页面的百分比数字在[0,1),注意,最后会变为0而不是1

positionOffsetPixels:
position位置移动到右边的偏移像素值,永远>=0

于是有了大概的思路:
(1)删除之前写的标签页动画
(2)在viewpager移动的时候,同时移动标签位置
(3)标签本身的点击事件不做处理,只用于触发setCurrentItem()

 @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                if (fromTab) {
                    return;
                }

                tabLayout.followPos(position, positionOffset);
            }

跟随处理:

public void followPos(int position, float positionOffset) { 
            followPos = position;

            if(positionOffset != 0)
                nextPos = followPos +  1;
            else
                nextPos = followPos;
            followOffset = positionOffset;
        invalidate();
    }
private void drawFollowMode(Canvas canvas) {
        if(ceils == null || ceils.length == 0)
            return;
        int nowIndex = 0;
        int nextIndex = 0;

        nowIndex = followPos;
        nextIndex = nextPos;


        float posLeft =  container.getChildAt(nowIndex).getLeft();
        float aidLeft = container.getChildAt(nextIndex).getLeft();
        float realLeft = (aidLeft - posLeft) * followOffset + posLeft;

        float postWidth =ceils[nowIndex];
        float aidWidth = ceils[nextIndex];
        float realWidth = (aidWidth - postWidth) * followOffset + postWidth;

        float left = realLeft + underlineLeftRight;
        float top = getHeight() - underlineHeight;
        float right = realLeft + realWidth - underlineLeftRight;
        float bottom = getHeight();

        //下滑线/背景
        if (underlineDrawable == null) {
            canvas.drawRect(left, top, right, bottom, underlinePaint);
        } else {
            underlineDrawable.setBounds((int) left, (int) top, (int) right, (int) bottom);
            underlineDrawable.draw(canvas);
        }

        //渐变颜色计算
        int color = ColorUtil.caculateColor(selectWordColor, noSelectWordColor, followOffset);
        int color2 = ColorUtil.caculateColor(noSelectWordColor, selectWordColor, followOffset);

        TextView tv = (TextView) container.getChildAt(nowIndex);
        TextView tv2 = null;
            tv2 = (TextView) container.getChildAt(nextIndex);


        tv2.setTextColor(color2);
        tv.setTextColor(color);
        if(followOffset == 0) {
            if(selectedPos == followPos)
                return;
            final int scrollPos = (int) (realLeft - (getWidth() - realWidth) / 2);
            smoothScrollTo(scrollPos, 0);
            selectedPos = followPos;
        }
    }

大概是这样,简单总结:
viewpager滑动到哪里,标签也跟着滑动到哪里,这样伴随效果比较好。

可是,上述方法存在后续问题,本篇博客本来是2017.9.6号开始写的,后面发现该问题于是2017.9.7解决才继续写。问题出在颜色渐变上。因为viewpager的onPageScrolled那个position总是在变化,所以在隔一个以上标签点击标签而非滑动viewpager的时候,颜色会有残留。所以,需要对选择标签和viewpager直接滚动两种情况进行分别处理:
(1)viewpager滚动:正常,按照之前的方式处理
(2)标签页滚动:a.标签页滚动保存新的位置,根据这个位置进行单独处理
(3)标签页滚动方式处理完后,注意还原成viewpager方式处理也正确的方式,目的是让invalidate时效果正确,另外,滚动是不会发生闪烁。
下面代码请看和fromTab相关部分:

 @Override
 public void onClick(View v) {
        fromTab = true;
        newPos = (int) v.getTag();
        if(selectedPos == newPos)
              return;
              select(newPos);
        }
public void followPos(int position, float positionOffset) {
        if(fromTab){
            int moved = 0;
            if(selectedPos!=container.getChildCount()-1){
                int leftNext = container.getChildAt(selectedPos+1).getLeft();
                int leftNow = container.getChildAt(selectedPos).getLeft();
                 moved = (int) ((leftNext - leftNow)*positionOffset);

            }else{
                int leftNow = container.getChildAt(selectedPos).getLeft();
                int leftLast = container.getChildAt(selectedPos-1).getLeft();
                moved = (int) ((leftNow - leftLast)*positionOffset);
            }

            int newLeft = container.getChildAt(newPos).getLeft();
            int oldLeft = container.getChildAt(selectedPos).getLeft();
            int posLeft = container.getChildAt(position).getLeft();
            int moveDistance = posLeft - oldLeft + moved;

            int allDistance = newLeft-oldLeft;
            followOffset = moveDistance*1.0f / allDistance;
            followPos = position;
        }else{
            followPos = position;

            if(positionOffset != 0)
                nextPos = followPos +  1;
            else
                nextPos = followPos;
            followOffset = positionOffset;
        }

        invalidate();
    }

这里的followOffset重新计算了,思路是a和a+1的偏移量=宽度*百分比
移动距离是a左边距-起始左边距+a到a+1偏移量
总距离是结束位置左边距-起始位置左边距
移动偏移为移动距离除以总距离,注意这里最后positionOffset会变成1不是0,所以下面的判断算的是positionOffset==1

private void drawFollowMode(Canvas canvas) {
        if(ceils == null || ceils.length == 0)
            return;
        int nowIndex = 0;
        int nextIndex = 0;
        if(fromTab){
            nowIndex = selectedPos;
            nextIndex = newPos;
        }else{
            nowIndex = followPos;
            nextIndex = nextPos;
        }

        float posLeft =  container.getChildAt(nowIndex).getLeft();
        float aidLeft = container.getChildAt(nextIndex).getLeft();
        float realLeft = (aidLeft - posLeft) * followOffset + posLeft;

        float postWidth =ceils[nowIndex];
        float aidWidth = ceils[nextIndex];
        float realWidth = (aidWidth - postWidth) * followOffset + postWidth;

        float left = realLeft + underlineLeftRight;
        float top = getHeight() - underlineHeight;
        float right = realLeft + realWidth - underlineLeftRight;
        float bottom = getHeight();

        //下滑线/背景
        if (underlineDrawable == null) {
            canvas.drawRect(left, top, right, bottom, underlinePaint);
        } else {
            underlineDrawable.setBounds((int) left, (int) top, (int) right, (int) bottom);
            underlineDrawable.draw(canvas);
        }

        //渐变颜色计算
        int color = ColorUtil.caculateColor(selectWordColor, noSelectWordColor, followOffset);
        int color2 = ColorUtil.caculateColor(noSelectWordColor, selectWordColor, followOffset);

        TextView tv = (TextView) container.getChildAt(nowIndex);
        TextView tv2 = null;
            tv2 = (TextView) container.getChildAt(nextIndex);


        tv2.setTextColor(color2);
        tv.setTextColor(color);
        if(fromTab && followPos == newPos && followOffset == 1){
            //数据还原,模拟viewpager滑动时数据
            fromTab = false;
            selectedPos = newPos;
            nextPos = selectedPos;
            followOffset = 0;
            final int scrollPos = (int) (realLeft - (getWidth() - realWidth) / 2);
            smoothScrollTo(scrollPos, 0);
        }else if(followOffset == 0 && !fromTab) {
            if(selectedPos == followPos)
                return;
            final int scrollPos = (int) (realLeft - (getWidth() - realWidth) / 2);
            smoothScrollTo(scrollPos, 0);
            selectedPos = followPos;
        }
    }

好,大概这么多,总结几个关键点:
1.单联动不要动画注意保存位置信息,防止死循环
2.不严格跟随可添加动画,严格跟随不必添加动画
3.根据onPageScrolled来更新标签页位置,可以实现完全的跟随效果
4.onPageScrolled提供的position不完全可靠,只能当做偏移量的参照坐标。如果需要渐变等特殊状态,需要将viewpager滚动和标签滚动分开处理。

原项目的封装性更好一些,如果只需要移植,不想尝试自己做可以从原项目地址中提取;代码阅读能力强的可直接参看原项目地址,模仿项目的滑动写的封装性不大好,但是好理解一些:
模仿项目地址:https://github.com/nfwuzhongdemeng/ImitateNBA
原项目地址:https://github.com/smuyyh/SprintNBA

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值