Android TabLayout的Indicator如何设置为图片

前言

TabLayout 我相信大家都有使用过,但是官方并没有对Indicator可以设置为图片的支持,只能是简单的水平线,也就是滑动杆那个玩意,那么如何做到可以用图片来做指示器呢?作为一个喜欢探索新颖解决方案的我,现在给大家带来了大家都很期待的问题的解决方案,下面我来讲讲我的思路和解决方案


先上效果

这是我将要做到的事情,实现TabLayoutIndicator可自定义image

效果图

分析源码

既然TabLayout 能够和ViewPager手势联动那就说明TablayoutViewPager有关联,先来看一下他们是如何关联的

tab_layout.setupWithViewPager(viewpager)

通过这个方法TablayoutViewPager相遇了,继续跟进这个方法执行最终会执行到这里

  private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh,
            boolean implicitSetup) {
    
        ...
        if (viewPager != null) {
            //保存一个成员变量引用
            mViewPager = viewPager;

            // Add our custom OnPageChangeListener to the ViewPager
            if (mPageChangeListener == null) {
                mPageChangeListener = new TabLayoutOnPageChangeListener(this);
            }
            mPageChangeListener.reset();
            //关注点1
            viewPager.addOnPageChangeListener(mPageChangeListener);
            //关注点2
            // Now we'll add a tab selected listener to set ViewPager's current item
            //注释很明显翻译过来就是添加一个选项选择监听器来设置ViewPager的当前项
            mCurrentVpSelectedListener = new ViewPagerOnTabSelectedListener(viewPager);
            addOnTabSelectedListener(mCurrentVpSelectedListener);

        }  
        ...
    }

关注点1添加监听ViewPager的滑动事件,关注点2添加Tablayout选择事件来设置ViewPager的当前项,关注点2注释已经解释得很清楚,重点分析关注点1ViewPager滑动事件,我们lou一眼

   public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
        
        //Viewpager滑动偏移量回调
        @Override
        public void onPageScrolled(final int position, final float positionOffset,
                final int positionOffsetPixels) {
            final TabLayout tabLayout = mTabLayoutRef.get();
            if (tabLayout != null) {
                // Only update the text selection if we're not settling, or we are settling after
                // being dragged
                final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
                        mPreviousScrollState == SCROLL_STATE_DRAGGING;
                // Update the indicator if we're not settling after being idle. This is caused
                // from a setCurrentItem() call and will be handled by an animation from
                // onPageSelected() instead.
                final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
                        && mPreviousScrollState == SCROLL_STATE_IDLE);
                //将ViewPager的滑动信息设置给tablayout,这是一个关键的信息!!!
                tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
            }
        }
  
    }
  ...
  //在这个方法里面调用了SlidingTabStrip的setIndicatorPositionFromTabPosition方法获取滑动数据
   void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
            boolean updateIndicatorPosition) {
       ···
        // Set the indicator position, if enabled
        if (updateIndicatorPosition) {
              //设置Indicator的位置
            mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
        }
      ···
    }
//用变量保存参数并执行 updateIndicatorPosition方法
void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
            if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
                mIndicatorAnimator.cancel();
            }

            mSelectedPosition = position;
            mSelectionOffset = positionOffset;
            //更新Indicator位置
            updateIndicatorPosition();
        }
     //通过viewpager的滑动偏移量计算选项卡左边和右边的位置
     private void updateIndicatorPosition() {
            final View selectedTitle = getChildAt(mSelectedPosition);
            int left, right;

            if (selectedTitle != null && selectedTitle.getWidth() > 0) {
                left = selectedTitle.getLeft();
                right = selectedTitle.getRight();

                if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
                    // Draw the selection partway between the tabs
                    View nextTitle = getChildAt(mSelectedPosition + 1);
                    left = (int) (mSelectionOffset * nextTitle.getLeft() +
                            (1.0f - mSelectionOffset) * left);
                    right = (int) (mSelectionOffset * nextTitle.getRight() +
                            (1.0f - mSelectionOffset) * right);
                }
            } else {
                left = right = -1;
            }
            
            setIndicatorPosition(left, right);
        }

        void setIndicatorPosition(int left, int right) {
            if (left != mIndicatorLeft || right != mIndicatorRight) {
                // If the indicator's left/right has changed, invalidate
                mIndicatorLeft = left;
                mIndicatorRight = right;
                //执行这个方法最终会执行view.postInvalidate()触发view的重绘
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }
   @Override
        public void draw(Canvas canvas) {
            super.draw(canvas);

            // Thick colored underline below the current selection
            // 根据前面计算过后的位置信息绘制indicator滑动杆
            if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
                canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
                        mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
            }
        }

看到这里那么恭喜你TablayoutIndicatorViewPager联动的流程你已经掌握了。我来总结一下流程
1.通过调用setupWithViewPagerViewPager添加滑动监听
2.并在监听回调里面把数据传递给Tablayout的内部类SlidingTabStrip就是指示器
3.SlidingTabStrip接收到数据之后调用 ViewCompat.postInvalidateOnAnimation(this);这个方法将会执行view.postInvalidate()触发view的重绘

实现想法

自己造个轮子实现将TablayoutIndecator换成图片,说干就干

遇到问题

问题来了,Tablayout的的滑动杆是在SlidingTabStrip这个类中的draw方法里面进行的绘制的,在外部不能对其进行修改,于是我就想既然不能动你,那我就干掉你,另辟蹊径找其他方法代替你,所以我决定自己写个Indecator类去监听viewpager的滑动事件,并更新显示位置

解决思路

从源码分析中我们已经知道要更新Indecator的位置就是要拿到viewPager的滑动偏移量,计算出他的左右位置然后通知view重绘,那我也自己写个Indecator来做同样的事代码如下

/**
 * Created by xwc on 2018/6/6
 * 
 */
public class ImageIndicator extends View {

    public ImageIndicator(Context context) {
        super(context);
    }

    public ImageIndicator(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public ImageIndicator(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    private float mIndicatorLeft = -1;
    private int mIndicatorRight = -1;

    //滑动的图片
    private Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.progressbar);
    private Paint mSelectedIndicatorPaint = new Paint();

    int mSelectedPosition = -1;
    float mSelectionOffset;

    LinearLayout tab;
    TabLayout tabs;

    public void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
        mSelectedPosition = position;
        mSelectionOffset = positionOffset;
        updateIndicatorPosition();
    }



    public void setupWithTabLayout(TabLayout tabs) {
        this.tabs = tabs;
        if (tab == null) {
            tab = getTabStrip();
        }
    }

    //计算滑动杆位置
    private void updateIndicatorPosition() {
        if (tab == null) {
            return;
        }
        final View selectedTitle = tab.getChildAt(mSelectedPosition);
        int left, right;

        if (selectedTitle != null && selectedTitle.getWidth() > 0) {
            left = selectedTitle.getLeft();
            right = selectedTitle.getRight();

            if (mSelectionOffset > 0f && mSelectedPosition < tab.getChildCount() - 1) {

                View nextTitle = tab.getChildAt(mSelectedPosition + 1);
                left = (int) (mSelectionOffset * nextTitle.getLeft() +
                        (1.0f - mSelectionOffset) * left);
                right = (int) (mSelectionOffset * nextTitle.getRight() +
                        (1.0f - mSelectionOffset) * right);
            }
        } else {
            left = right = -1;
        }

        setIndicatorPosition(left, right);
    }


    void setIndicatorPosition(int left, int right) {
        if (left != mIndicatorLeft || right != mIndicatorRight) {

            mIndicatorLeft = left;
            mIndicatorRight = right;
            //通知view重绘
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //绘制图片
        if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
            canvas.drawBitmap(mBitmap, mIndicatorLeft, getHeight() - mBitmap.getHeight(), mSelectedIndicatorPaint);
        }
    }

    /**
     * tabs : TabLayout
     * 通过反射TabLayout拿到SlidingTabStrip这个内部类
     */
    public LinearLayout getTabStrip() {
        Class<?> tabLayout = tabs.getClass();
        Field tabStrip = null;
        try {
            tabStrip = tabLayout.getDeclaredField("mTabStrip");
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }

        tabStrip.setAccessible(true);

        try {
            tab = (LinearLayout) tabStrip.get(tabs);
            return tab;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return tab;
    }
}

使用:

        viewPager.setAdapter(new MyViewPagerAdapter());
        tabLayout.setupWithViewPager(viewPager);
        imageIndicator.setupWithTabLayout(tabLayout);


        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                // 将滑动偏移量传给Indicator               
                imageIndicator.setIndicatorPositionFromTabPosition(position, positionOffset);
            }

            @Override
            public void onPageSelected(int position) {
            }

            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });

番外

如果你反射了Tablayout,想要改变他属性或者他的内部类属性,请在项目发布时加上混淆,避免出现反射无效!

-keep public class android.support.design.widget.TabLayout{
     *;
}
-keep class android.support.design.widget.TabLayout$* {
     *;
}

总结

借鉴老罗的一句话我们在项目中碰到问题的时候,通常第一反应都是到网上去搜索答案。但是有时候有些问题,网络并不能给出满意的答案。这时候就千万不要忘了你所拥有的一个大招——从代码中找答案!以上就是我的解决方案,查看源码从中获得灵感,解决实际问题!坚决不做码农,我是工程师

具体使用请查看 Demo

欢迎评论点赞交流

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容