可自定义图片指示器并支持自定义Tab宽度的TabLayout

来源:互联网 发布:imap服务器端口 编辑:程序博客网 时间:2024/05/23 01:20

[置顶] 可自定义图片指示器并支持自定义Tab宽度的TabLayout

标签: 今日头条tablayout自定义tab宽度
625人阅读 评论(3)收藏举报
分类:
作者同类文章X

    目录(?)[+]

    1. 自定义图片指示器
    2. 自定义Tab宽度
    3. 方案1反射修改Tab宽度
    4. 方案2异步修改Tab宽度
    5. 最后一个问题

    本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

    Tab栏是APP中最常用的组件,如果我们没有太多个性化需求,直接用TabLayout就可以轻松实现。

    但是如果我们的产品大大说不想用一条线作指示器了,想用个图片。或者像我们产品一样,想在主页展示4个半Tab(一共5个)时,原生TabLayout就不能满足了。

    而前面我发过一篇,自定义三角下标的tablayout,来实现类似于今日头条tab效果,同时还有三角下标。

    那时候我是继承HorizontalScrollView,但是滑动过程中有一些bug,因为要自己处理“什么时候该滑动”和“滑动多少”,后来在项目中逻辑太复杂,就想重新推翻重写一个。

    当时也想过用TabLayout,但是TabLayout的滑动指示器是矩形,项目中要使用自定义图片,所以最开始就没继续思考,直接用了HorizontalScrollView。

    现在回头想想,我真是傻,我直接继承TabLayout,把图片指示器画上去不就OK了,而且绘制图片指示器的逻辑跟自定义三角下标的tablayout一文里的逻辑一样!文末会贴上工程源码。

    先看看这次的整体效果:

    这里写图片描述

    这里再简单说一下绘制三角下标的原理。

    自定义图片指示器

    • 初始化指示器坐标:
     private void initTranslationParams(LinearLayout llTab, int screenWidth) {        //mSlideIcon 是图片指示器        if (mSlideIcon == null) {            return;        }        tabWidth = (int) (screenWidth / (mTabVisibleCount + mLastTabVisibleRatio));        View firstView = llTab.getChildAt(0);        if (firstView != null) {            //初始位置:第一个tab正下方            this.mInitTranslationX = (firstView.getLeft() + tabWidth / 2 - this.mSlideIcon.getWidth() / 2);            this.mInitTranslationY = (getBottom() - getTop() - this.mSlideIcon.getHeight());        }    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 监听ViewPager的滑动:
      viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {            @Override            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {                //传入当前滑动位置,重绘指示器                topTabLayout.redrawIndicator(position, positionOffset);            }            @Override            public void onPageSelected(int position) {            }            @Override            public void onPageScrollStateChanged(int state) {            }        });
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 重新计算指示器位置:
    public void redrawIndicator(int position, float positionOffset) {   mTranslationX = (int) ((position + positionOffset) * tabWidth);   invalidate(); }
    • 1
    • 2
    • 3
    • 4
    • 绘制指示器
      @Override  protected void dispatchDraw(Canvas canvas) {       if (mSlideIcon == null) {           return;       }       canvas.save();       // 平移到正确的位置,修正tabs的平移量       canvas.translate(mInitTranslationX + mTranslationX, this.mInitTranslationY);       canvas.drawBitmap(this.mSlideIcon, 0, 0, null);       canvas.restore();       super.dispatchDraw(canvas);   }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    自定义Tab宽度

    本文主要想记录一下自定义Tab宽度,因为我们项目中,顶部tab栏,有5个Tab,但是最后一个只露出半个(想打死产品有木有)。

    我们看下TabLayout的源码,没有任何设置Tab宽度的接口,只能在XML中通过

       app:tabMaxWidth=""   app:tabMinWidth=""
    • 1
    • 2

    来锁定Tab宽度,然后解析出来:

    mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth,                INVALID_WIDTH);mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth,                INVALID_WIDTH);
    • 1
    • 2
    • 3
    • 4

    但是我们这里需要结合手机屏幕宽度和其他业务因素动态的计算出Tab的宽度,在代码里设置。

    怎么办?

    我首先想到利用反射直接修改这两个值,而mRequestedTabMinWidthmRequestedTabMinWidth又是final的,但是!

    但是请注意,这两个变量都不是在定义时初始化的,所以,如果使用反射是可以在代码里设置这两个值的。这里多提一句:

    如果final变量在定义时就已经初始化了,那么反射是无法修改值的

    如果final变量在定义时没有初始化,是可以用反射修改值的

    所以方案1来了。

    方案1:反射修改Tab宽度

     final Class<?> clz = TabLayout.class;    try {        final Field requestedTabMaxWidthField = clz.getDeclaredField("mRequestedTabMaxWidth");        final Field requestedTabMinWidthField = clz.getDeclaredField("mRequestedTabMinWidth");        requestedTabMaxWidthField.setAccessible(true);        requestedTabMaxWidthField.set(this, 宽度);        requestedTabMinWidthField.setAccessible(true);        requestedTabMinWidthField.set(this, 宽度);    } catch (final NoSuchFieldException e) {        e.printStackTrace();    } catch (final SecurityException e) {        e.printStackTrace();    } catch (final IllegalArgumentException e) {        e.printStackTrace();    } catch (final IllegalAccessException e) {        e.printStackTrace();    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这样就可以自定义Tab的宽度了。

    下面是方案2,不使用反射,使用异步方法。

    方案2:异步修改Tab宽度

    当Tab绘制完成以后,我们重新设置TabView的参数,重绘一次,也可以修改Tab的宽度。

    public SlidingTabLayout(Context context, AttributeSet attrs) {    super(context, attrs);    post(new Runnable() {        @Override        public void run() {            resetTabParams();        }    });}private void resetTabParams() {     LinearLayout tabStrip = getTabStrip();         if (tabStrip == null) {             return;         }         for (int i = 0; i < tabStrip.getChildCount(); i++) {             LinearLayout tabView = (LinearLayout) tabStrip.getChildAt(i);             LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(宽度, LinearLayout.LayoutParams                     .WRAP_CONTENT);             tabView.setLayoutParams(params);     }}//反射拿到TabLayout里的mTabStrip(mTabStrip是TabView的父容器)@Nullablepublic LinearLayout getTabStrip() {    Class<?> tabLayout = TabLayout.class;    Field tabStrip = null;    try {        tabStrip = tabLayout.getDeclaredField("mTabStrip");    } catch (NoSuchFieldException e) {        e.printStackTrace();    }    tabStrip.setAccessible(true);    LinearLayout llTab = null;    try {        llTab = (LinearLayout) tabStrip.get(this);    } catch (IllegalAccessException e) {        e.printStackTrace();    }    llTab.setClipChildren(false);    return llTab;}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    如此我们便可以自定义Tab的宽度啦~

    最后一个问题

    最后我还遇到一个问题,我们原来的Tab栏上有飘出的气泡,多余部分不能被截断,因此需要设置:

    setClipChildren(false);setClipToPadding(false);
    • 1
    • 2

    那在TabLayout中要给谁设置呢?

    这里我踩了几个坑,我们先来看下TabLayout的结构:

    这里写图片描述

    其中mCustomView是我们自定义的Tab布局,Tab类是对TabView和mCustomView的包装。很显然,我们需要对TabView设置布局参数,所以方案来了:

    LinearLayout tabStrip = getTabStrip();  if (tabStrip == null) {       return;   }   for (int i = 0; i < tabStrip.getChildCount(); i++) {       LinearLayout tabView = (LinearLayout) tabStrip.getChildAt(i);       //tab中的图标可以超出父容器       tabView.setClipChildren(false);       tabView.setClipToPadding(false);       tabView.setPadding(0, 30, 0, 30);    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    getTabStrip()方法上面贴过。

    这样,我们就可以做出下面类似的气泡效果:

    这里写图片描述

    最后贴上整个项目:TabLayoutDemo,欢迎star~

    2
    0
     
     

      相关文章推荐
    • Android仿今日头条和知乎等App顶部滑动导航实现代码分析及源码下载
    • 腾讯云容器服务架构实现介绍--董晓杰
    • 安卓原生时间选择器,DatePicker和TimePicker并用,弹窗dialog
    • 容器技术在58同城的实践--姚远
    • 带有4个tab的ViewPager,且自定义指示器
    • Tensorflow项目实战-文本分类
    • 自定义View实现顶部Tab指示器
    • MySQL深入浅出
    • Android---Tablayout自定义Tab的背景和字体的颜色变化
    • Python可以这样学(第三季:多线程与多进程编程)
    • TabLayout自定义Tab的title
    • 华为工程师,带你实战C++
    • 自定义tab,viewpager实现仿tablayout切换效果
    • 自定义一个好看点的TabLayout.Tab
    • Android---Tablayout自定义Tab的背景和字体的颜色变化
    • Android---Tablayout自定义tab
    原创粉丝点击