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

标签: android  安卓

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

标签: 今日头条tablayout自定义tab宽度
2017-11-01 14:42 625人阅读 评论(3) 收藏 举报
category_icon.jpg分类:

本篇文章已授权微信公众号 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的父容器)
@Nullable
public 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

版权声明:本文为dianziagen原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/dianziagen/article/details/78688229

智能推荐

spring与redis整合和序列化问题

spring与redis整合 首先用docker下载redis 下载:docker pull redis 运行:docker run -d -p 6379:6379 --name myredis docker.io/redis 连接redis Desktop Manager 然后开始在springboot上开始配置 application.yml: 自动配置好StringRedisTemplate...

CentOS 7配置南大docker镜像

文章目录 CentOS 7配置南大docker镜像 0.帮助页面 1.系统要求 2.卸载旧版本(没有旧版本可跳过) 3.安装方式 4.准备工作 5.可选操作 Stable Test Nightly 6.安装docker引擎 7. (可选)修改配置文件防止与xshell连接冲突 8.启动docker CentOS 7配置南大docker镜像 0.帮助页面 南大docker源:https://mirr...

Qcon演讲纪实:详解如何在实时视频通话中实现AR功能

2018年4月20日-22日,由 infoQ 主办的 Qcon 2018全球软件开发大会在北京如期举行。声网首席 iOS 研发工程师,iOS 端移动应用产品设计和技术架构负责人龚宇华,受邀分享了《基于 ARkit 和 ARcore,在实时视频通话中实现 AR 功能》,在演讲中剖析了 AR 与 VR 差异,ARKit 的工作原理,以及逐步讲解如何基于 ARKit 与声网Agora SDK 创建 AR...

POJ2348 UVa10368 HDU1525 Euclid's Game【博弈】

Euclid's GameTime Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submission(s): 4106    Accepted Submission(s): 1947 Probl...

使用Breeze.js编写更好的查询

这篇文章是由同行评审Agbonghama柯林斯 。 感谢所有SitePoint的审稿作出SitePoint内容也可以是最好的! 数据量正在迅速发展,他们正在变得越来越复杂,维护。 许多开发人员希望避免由数据问题他们的工作过程中造成的问题和头痛。 一个使我们的工作更轻松的图书馆是Breeze.js 。 在这篇文章中,我们将讨论我们如何能够写出更好的查询与Breeze.js。 但是首先,我们应该知道什...

猜你喜欢

Netty框架构建Nio编程

~~~ 随手点赞,养成习惯 ~~~ 为什么选择Netty框架 Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的。 优点: ① API使用简单,开发门槛低 ②功能强大,预置了多种编解码功能,支持多种主流协议 ③ 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展; ④性能高,通过与其他业界主流的NIO框架对比,Nett...

【JZOJ5262】【GDOI2018模拟8.12】树(DP,性质题)

Description Solution 首先我们可以知道两个性质:1、路径u-v和路径v-w可以合并为路径u-w;2、路径u1-v1加路径u2-v2和路径u1-v2加路径u2-v1是等价的(就是起始点和终点可以互换) 那么知道这些性质之后就很好做了。我们只用知道每个点多少次做起点和多少次做终点。 我们设f[i]表示满足i子树的需求i上的值要是多少。 那么枚举i的所有儿子,判断a[i]-f[i],...

【String-easy】541. Reverse String II 反转的元素,有反转个数和间隔

1. 题目原址 https://leetcode.com/problems/reverse-string-ii/ 2. 题目描述 3. 题目大意 给定一个字符串,和字符串的间隔k, 这个k表示每k个数反转一次,然后再间隔k个元素再反转k个元素。 4. 解题思路 只要按照间隔去反转就可以了。然后间隔k个元素不反转是通过让i每次递增 2*k完成的。 5. AC代码 6. 相似题型 【1】344. Re...

【C语言笔记结构体】

我们都知道C语言中变量的类型决定了变量存储占用的空间。当我们要使用一个变量保存年龄时可以将其声明为int类型,当我们要使用一个变量保存某一科目的考试成绩时可以将其声明为float。 那么,当我们要做一个学生信息管理系统时,需要保存学生的姓名、学号、年龄等信息,该怎么做呢? 如当要保存三个学生的信息时, 方法一是: 方法二是: 显然,方法二跟更清晰,因为它把name、num、age都集成在一个模板,...

39. Combination Sum 回溯算法简析

LeetCode传送门     这道题要求给你一组正数 C,然后给你一个目标数 T,让你从那组C中找到加在一起等于 T 的那些组合。     例如:给你 [2,3,6,7] 和 7,则返回 [[2,2,3],[7] ] 。      想解决这个问题前,我们首先引入一个新问题,图(树)的遍历问题。  ...