TabLayout改造计划

大多数的应用程序都采用了多Fragment+底部导航栏,Android提供了Fragment+ViewPager+TabLayout这套解决方案,但是TabLayout在使用中有两处局限:

  • Tab无法溢出TabLayout
  • 无法设置中心按钮(一般中心按钮不用来切换Fragment,而是发帖、扫码等其他功能)

第一处局限已有开源项目XTabLayout解决了这个问题,实现这种效果主要是把Clipchildren属性设置为false,具体实现和Demo可以参考这篇文章:http://www.jianshu.com/p/d1931528e4da,以及对应的GitHub项目:https://github.com/sltpaya/XTabLayout

第二处局限由本文尝试分析并解决。

TabLayout继承于HorizontalScrollView(如果TabLayout包含的Tab过多,超出屏幕可以进行滚动),Tab的容器是SlidingTabStrip这个内部类,继承于LinearLayout。

在调用setupWithViewPager时,TabLayout得到ViewPager中包含的Fragment数量,通过addTab方法向SlidingTabStrip中添加同等数量的Tab视图对象(使用内置默认布局),并且为ViewPager切换和Tab点击都设置默认的监听方法,两者通过位置参数Position对应起来,比如第一个Tab对应的一定是第一个Fragment。


解决思路一:

向SlidingTabStrip中循环添加Tab视图对象时,额外再在中间插入一个Tab,插入后Tab数量比ViewPager的Fragment数量多一个,由于Tab和ViewPager是通过位置关联的,所以需要位置转换的方法:点击中央按钮右侧的Tab,需要对Position-1作为ViewPager的位置;滑动到后一半的Fragment时,需要对Position+1作为Tab的位置。

举例:4个Fragment页面,4个普通Tab和1个中央Tab。当点击最右侧Tab时,由于中央Tab的占位,实际点击的Position参数为4(从0开始),需要对其-1,viewPager.setCurrentItem传入的参数应该为3。同理,滑动到最后一个Fragment时,这个Fragment在ViewPager中的位置为3(从0开始),由于中央Tab的占位,对应的Tab位置应该是4,需要对其+1。

缺点:转换位置参数时,需要判断诸多前置条件,如是否添加了中心按钮、事件来源是滑动ViewPager还是点击Tab、点击中心按钮还是普通Tab等,进而确定如何处理点击事件,是否需要位置转换,位置+1还是-1等等…而且中心按钮也是Tab的一种,与其他Tab的行为类似,比如:点击中心按钮,中心按钮被选中后,其他按钮会变为非选中状态,但是一般情况下,点击中心按钮并不会影响其他按钮的行为。如果需要为中心按钮定制特殊的行为,需要在多处增加判断条件,过于繁琐。

解决思路二:

使TabLayout继承于FrameLayout或RelativeLayout,中心按钮可以单独悬浮在顶层,不需要向SlidingTabStrip中添加TabView,不会占用其他Tab的位置。这样Tab和ViewPager一一对应,不需要做位置转换。也不需要特殊处理中心按钮的行为,因为这是个独立的可点击的View,和其他Tab完全没有关系。

但是实际实现中会遇到问题:

  • TabLayout继承关系改变,影响TabLayout原有行为

TabLayout原本继承于HorizontalScrollView,当Tab数量超过1屏时可以进行滚动,但是HorizontalScrollView只能有唯一一个子布局,即SlidingTabStrip,无法再向其添加中心按钮。修改TabLayout,使其继承于FrameLayout或RelativeLayout后,TabLayout失去滑动功能,违背了与兼容TabLayout原有功能的需求。

改进思路:增加一个RelativeLayout作为TabLayout(也即HorizontalScrollView)的直接、唯一的子布局,SlidingTabStrip和中心按钮添加到这个RelativeLayout中,既可添加按钮,也可避免HorizontalScrollView有多个子布局的问题。(待实现)

  • 向TabLayout添加中心按钮后,SlidingTabStrip及其Tab布局出现错误

效果如图:
这里写图片描述

这个问题出现时觉得非常诡异,通过LayoutInspector工具查看布局,发现SlidingTabStrip的宽度没有占满整个屏幕宽度,那么在SlidingTabStrip的onMeasure方法中,通过下面两个方法,把宽度约束的模式和值打到日志里:

MeasureSpec.getMode(widthMeasureSpec)
MeasureSpec.getSize(widthMeasureSpec)

onMeasure方法会被调用很多次,最终的布局以最后一次调用的结果最准。发现添加中心按钮前的正常情况下,最后一次的Mode为1073741824(即MeasureSpec.EXACTLY,对应MATCH_PARENT),Size为1440(即手机屏幕实际宽度);而添加中心按钮后,最后一次的Mode变为-2147483648(即MeasureSpec.AT_MOST,对应WRAP_CONTENT)。

onMeasure方法的作用是,父布局为了确定自身的宽高,首先需要获取到子布局所需的宽高,这一过程通过父控件调用子控件的onMeasure方法来实现。同时,子布局在测量自身的宽高时,还必须考虑到父布局的约束条件,约束条件分为Mode和Size,包含在widthMeasureSpec和heightMeasureSpec参数中。

约束条件的含义是,子布局必须在满足父布局的约束条件下,进行测量。以宽度为例,如父布局传入的约束条件的模式是MeasureSpec.EXACTLY,EXACTLY即确切模式,即意味着子布局的宽度甚至都不需要测量,直接使用父控件给定的Size即可,这种模式对应MATCH_PARENT,即子控件不需要测量,直接使用父控件能提供的最大宽度即可。同样,若模式为MeasureSpec.AT_MOST,即父布局约束了子布局的最大宽度,在这个范围内宽度是多少都行,这时子布局就只能继续测量其包含的控件,以便确定自身宽度。

那么,添加按钮后,Mode为MeasureSpec.AT_MOST,即WRAP_CONTENT,显然是不对的,因为当SlidingTabStrip为固定模式(MODE_FIXED)时,它应当是占满整个屏幕宽度的,也就是说,从父布局传过来的约束参数有误,那么应该继续检查XTabLayout的onMeasure方法。

最终定位到问题出现在TabLayout的onMeasure方法中,有一行判断条件:

if (getChildCount() == 1) {
...
    int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可见,只有getChildCount为1时,才会把测量子布局的模式设定为MeasureSpec.EXACTLY(即相当于width为match_parent),否则使用的测量方式会是AT_MOST(即wrap_content),所以需要修改判断条件为:

if (getChildCount() == 1 || mHasCenterButton)

同时要注意,现在TabLayout的子布局不一定只有SlidingTabStrip,所以getChildAt()方法的index也不应该直接传入0,经测试,无论是先添加中心按钮,还是先添加Tab,SlidingTabStrip的位置始终是最后一个(因为在setupWithViewPager时,TabView就已经创建添加完成,所以中心按钮一定是最后添加的,所以index是最小的),代码应修改为:

final View child = getChildAt(getChildCount() - 1);

其他想法:

在ViewPager添加一个空白Fragment对应中央Tab,使Tab和ViewPager一一对应。并且单独处理中央Tab点击事件,使ViewPager始终不会切换到这个空白Fragment。
但是显然这种方法并没有上面的两个方法好,暂不考虑。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值