本篇文章已授权微信公众号 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());
}
}
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) {
}
});
public void redrawIndicator(int position, float positionOffset) {
mTranslationX = (int) ((position + positionOffset) * tabWidth);
invalidate();
}
@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);
}
本文主要想记录一下自定义Tab宽度,因为我们项目中,顶部tab栏,有5个Tab,但是最后一个只露出半个(想打死产品有木有)。
我们看下TabLayout的源码,没有任何设置Tab宽度的接口,只能在XML中通过
app:tabMaxWidth=""
app:tabMinWidth=""
来锁定Tab宽度,然后解析出来:
mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth,
INVALID_WIDTH);
mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth,
INVALID_WIDTH);
但是我们这里需要结合手机屏幕宽度和其他业务因素动态的计算出Tab的宽度,在代码里设置。
怎么办?
我首先想到利用反射直接修改这两个值,而mRequestedTabMinWidth
和mRequestedTabMinWidth
又是final的,但是!
但是请注意,这两个变量都不是在定义时初始化的,所以,如果使用反射是可以在代码里设置这两个值的。这里多提一句:
如果final变量在定义时就已经初始化了,那么反射是无法修改值的
如果final变量在定义时没有初始化,是可以用反射修改值的
所以方案1来了。
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();
}
这样就可以自定义Tab的宽度了。
下面是方案2,不使用反射,使用异步方法。
当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;
}
如此我们便可以自定义Tab的宽度啦~
最后我还遇到一个问题,我们原来的Tab栏上有飘出的气泡,多余部分不能被截断,因此需要设置:
setClipChildren(false);
setClipToPadding(false);
那在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);
}
getTabStrip()
方法上面贴过。
这样,我们就可以做出下面类似的气泡效果:
最后贴上整个项目:TabLayoutDemo,欢迎star~