好久以前看到 Google News 的 TabLayout
的样式挺有意思的,以下图: java
因而,我模仿的结果:(截图来自个人一个小项目里GeekNews) git
开始模仿以前,先问个问题,这个控件是 TabLayout 吗?答案:是的,我用 monitor 看过了。
因此能够获得结论:直接魔改源码是最简单最快的方法。github
简单看一下 TabLayout 这个类的结构能够看出, TabLayout 内的指示器是由一个 私有内部类 SlidingTabStrip
来控制的,再看一下其中的 draw
方法实现,canvas
public void draw(Canvas canvas) {
super.draw(canvas);
// Thick colored underline below the current selection
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
}
复制代码
这里画了一个矩形,从mIndicatorLeft
、mIndicatorRight
、mSelectedIndicatorPaint
这几个变量的名字上就很是很是明显能够看出,这个矩形就是tab下面的那个矩形指示器条 (此处应有️配图)bash
这个地方咱们能够先实现半个圆角矩形的效果,圆角很简单,把drawRect
换成 drawRoundRect
便可,半个矩形只要画一个超过控件最底部的rectF,让控件本身裁掉这个rectF的一半高度,圆角则取这个一半高度,就是mSelectedIndicatorHeight
的值post
public void draw(Canvas canvas) {
super.draw(canvas);
// Thick colored underline below the current selection
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
RectF rectF = new RectF(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight, mIndicatorRight, getHeight() + mSelectedIndicatorHeight);
mSelectedIndicatorPaint.setAntiAlias(true);
canvas.drawRoundRect(rectF, mSelectedIndicatorHeight, mSelectedIndicatorHeight, mSelectedIndicatorPaint);
}
}
复制代码
能够看到这里咱们仅利用已有的变量就能实现半个圆角矩形的效果。this
从上一步咱们能够看到,指示器的宽度是由 mIndicatorLeft
和 mIndicatorRight
这两个变量决定的,那直接在draw方法里改?显然不行,想一想,tab切换涉及两个tab的宽度计算,mIndicatorLeft
和 mIndicatorRight
的计算不只跟着位置改变还受到tab自己宽度的影响(其实我偷偷试过了,在这里改确实不行)。
首先,咱们要找到 mIndicatorLeft
和 mIndicatorRight
被修改的地方,发现一个方法:spa
void setIndicatorPosition(int left, int right) {
if (left != mIndicatorLeft || right != mIndicatorRight) {
// If the indicator's left/right has changed, invalidate
mIndicatorLeft = left;
mIndicatorRight = right;
ViewCompat.postInvalidateOnAnimation(this);
}
}
复制代码
咱们顺着这个方法被调用的地方终于找到 left 和 right 计算的地方:翻译
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);
}
复制代码
终于找到修改的地方了,首先咱们要了解一下第一行 getChildAt 返回的 view 是个什么,这里就不贴代码了,直接说结论:阅读源码可知是个名为 TabView 的类,TabView 是个 LinearLayout,TabLayout 的文字是由其内部的mTextView来显示的。计算的思路就是下面的灵魂示意图:3d
spacing = (view.width -view.mTextView.width)/2
newLeft = view.left + spacing
newRight = view.right - spacing
复制代码
因此修改以后的 updateIndicatorPosition()
方法以下,要注意须要修改两组left和right,一个是当前选中的Tab,一个是下一个Tab
private void updateIndicatorPosition() {
final TabView selectedTitle = (TabView) getChildAt(mSelectedPosition);
int left, right;
if (selectedTitle != null && selectedTitle.getWidth() > 0) {
int spacing = (selectedTitle.getWidth() - selectedTitle.mTextView.getMeasuredWidth()) / 2;
left = selectedTitle.getLeft() + spacing;
right = selectedTitle.getRight() - spacing;
if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
// Draw the selection partway between the tabs
TabView nextTitle = (TabView) getChildAt(mSelectedPosition + 1);
int nextSpacing = (nextTitle.getWidth() - nextTitle.mTextView.getMeasuredWidth()) / 2;
int nextLeft = nextTitle.getLeft() + nextSpacing;
int nextRight = nextTitle.getRight() - nextSpacing;
left = (int) (mSelectionOffset * nextLeft +
(1.0f - mSelectionOffset) * left);
right = (int) (mSelectionOffset * nextRight +
(1.0f - mSelectionOffset) * right);
}
} else {
left = right = -1;
}
setIndicatorPosition(left, right);
}
复制代码
这就修改完了吗?不!要知道tab之间切换有两种方式,一个是 viewPager 划过去,还有一个是点击任意一个tab跳过去,因此还有一个地方要改,不须要找了,setIndicatorPosition 紧跟着的下一个方法就是,void animateIndicatorToPosition(final int position, int duration)
,咱们只要改其中的这一段:
final View targetView = getChildAt(position);
...
final int targetLeft = targetView.getLeft();
final int targetRight = targetView.getRight();
复制代码
只须要改目标 view 的 left 和 right,由于这个方法里调用了一次 updateIndicatorPosition()
,当前选中的 view 已经被计算过一次了。 修改后:
final TabView targetView = (TabView) getChildAt(position);
...
int targetSpacing = (targetView.getWidth() - targetView.mTextView.getMeasuredWidth()) / 2;
final int targetLeft = targetView.getLeft() + targetSpacing;
final int targetRight = targetView.getRight() - targetSpacing;
复制代码
至此,真的就所有完成了。 修改好的源码地址:TabLayout.java
终于水完了 2019 年的第一篇 blog