修改下划线宽度的坑
效果如下:
代码实现方式:
如果想要实现这种效果,最主要控制的就是下划线部分,现在网上有很多通过反射的方式来修改下划线宽度的文章,但这段代码如果实现我们想要的效果是 不可能 的,因为如果研究过源码就知道 Indicator 的宽度跟 Tab 的宽度一致的,不能让指示器的宽度小于 Tab 的宽度,所以我们只能另辟蹊径:通过 CustomView自己绘制 线,通过添加 OnTabSelectedListener 来展示隐藏下划线,让原生的 Indicaqtor 高度为 0 也不会影响我们的展示。
OK!Talk is cheap, show me the code.
先隐藏原来的下划线,让其不显示:
<android.support.design.widget.TabLayout
android:id="@+id/radio_playlist_tab_layout"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_36.7"
android:background="@color/color_e8e8e8"
app:tabBackground="@null"
app:tabIndicatorHeight="0dp"
app:tabMode="scrollable" />
其次设置 CustomView 效果:
再手动控制切换时 Tab 的下划线:
mTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {override fun onTabReselected(tab: TabLayout.Tab?) {val view = tab?.customView?.findViewById<View>(R.id.tab_indicator)
val textView = tab?.customView?.findViewById<TextView>(R.id.tab_tv_date)
textView?.setTextColor(mTabSelectedColor)
view?.visibility = View.VISIBLE
}
override fun onTabUnselected(tab: TabLayout.Tab?) {val view = tab?.customView?.findViewById<View>(R.id.tab_indicator)
val textView = tab?.customView?.findViewById<TextView>(R.id.tab_tv_date)
textView?.setTextColor(mTabUnSelectedColor)
view?.visibility = View.INVISIBLE
}
override fun onTabSelected(tab: TabLayout.Tab?) {val view = tab?.customView?.findViewById<View>(R.id.tab_indicator)
val textView = tab?.customView?.findViewById<TextView>(R.id.tab_tv_date)
textView?.setTextColor(mTabSelectedColor)
view?.visibility = View.VISIBLE
}
})
间距部分
只是这样的话,下划线的问题解决了。但对于我们的 UI 对于界面还原度要求较高,对于 Tab 之间的间距也有一些要求,所以也要处理,对于间距部分的处理可以按照之前的方式通过反射来完成。注意,这种方式因为需要计算 TextView 的文字宽度,所以要放到设置完所有的 customView 后调用。
private fun customTabWidth(tabLayout: TabLayout) {
try {
// 拿到 tabLayout 的 mTabStrip 属性
val mTabStripField = tabLayout.javaClass.getDeclaredField("mTabStrip")
mTabStripField.isAccessible = true
val mTabStrip = mTabStripField.get(tabLayout) as LinearLayout
val dp2 = DensityUtils.dp2px(context, 2f)
val dp30 = DensityUtils.dp2px(context, 30f)
for (i in 0 until mTabStrip.childCount) {
// 此时获取到 tabView 其实是我们的 CustomView
val tabView = mTabStrip.getChildAt(i)
// 找到我们的 TextView
val mTextView = tabView.findViewById<TextView>(R.id.tab_tv_date)
// 测量出 TextView 文字的宽度
var width = 0
width = mTextView.width
if (width == 0) {mTextView.measure(0, 0)
width = mTextView.measuredWidth
}
// PDL = padding left --- PDR = padding right
// |PDL30 CONTENT PDR30| |PDL2 CONTENT PDR30| |PDL2 CONTENT PDR30| |PDL2 CONTENT PDR30|
val params = tabView.layoutParams as LinearLayout.LayoutParams
// 如果是第一个 View,则 View 左侧还要 30dp 的空间需要 padding
// 有了 TextView 的宽度,我们可以根据自己想要的效果去设置 Tab 的宽度,Tab 的实际间距其实是 0,但我们可以
// 通过改变 Padding 的方式做出 Tab 间距的效果
// 对于为什么用 **padding** 而不是 **margin** 的原因,请向下看
if (i == 0) {
params.width = width + dp30 + dp30
tabView.layoutParams = params
tabView.setPadding(dp30, 0, dp30, 0)
} else {
params.width = width + dp2 + dp30
tabView.layoutParams = params
tabView.setPadding(dp2, 0, dp30, 0)
}
tabView.invalidate()}
} catch (e: NoSuchFieldException) {e.printStackTrace()
} catch (e: IllegalAccessException) {e.printStackTrace()
}
}
修改完下划线宽度后 tab 的滑动位置错乱的坑(TabView 的 MarginLeft & MarginRigt 导致的问题)
开始做的时候看网上相关的文档然后做出的效果如下:
可以看到当滑过去以后,Tab 又位移了一段距离,我想如果你的脑子没有被 驴踢过的话,肯定都知道滑动完位移后的位置是正确的位置,那么 为什么会出现 这种情况已经 如何解决??
看源码!
因为是我们关联的 ViewPager 滑动导致的 Tab 位移,那么就从 ViewPager 的移动时开始找线索,通过 setupWithViewPager()方法中可以看到,TabLayout 代码中给 ViewPager 对象通过 addOnPageChangeListener()方法添加了一个监听,那么我们看一下监听的回调好了!
public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
// 忽略无关代码
// ** 主要看这里哦~!~!~ **
@Override
public void onPageScrolled(final int position, final float positionOffset,
final int positionOffsetPixels) {final TabLayout tabLayout = mTabLayoutRef.get();
if (tabLayout != null) {
// Only update the text selection if we're not settling, or we are settling after
// being dragged
final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
mPreviousScrollState == SCROLL_STATE_DRAGGING;
// Update the indicator if we're not settling after being idle. This is caused
// from a setCurrentItem() call and will be handled by an animation from
// onPageSelected() instead.
final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
&& mPreviousScrollState == SCROLL_STATE_IDLE);
// 这里调用了 setScrollPosition 去让 TabLayout 进行滑动,穿进去目标下标和偏移量
tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
}
}
}
再向下看一下 setScrollPosition 方法!
void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
boolean updateIndicatorPosition) {final int roundedPosition = Math.round(position + positionOffset);
if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) {return;}
// Set the indicator position, if enabled
// 如果更新指示器的话那么会走进这里,我们指示器都隐藏掉了无需看这里
if (updateIndicatorPosition) {mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
}
// Now update the scroll position, canceling any running animation
if (mScrollAnimator != null && mScrollAnimator.isRunning()) {mScrollAnimator.cancel();
}
// 这里调用了 scrollTo 方法去移动位置,那么我们猜测应该是这个 X 位移距离算错了,进去看一下
scrollTo(calculateScrollXForTab(position, positionOffset), 0);
}
private int calculateScrollXForTab(int position, float positionOffset) {if (mMode == MODE_SCROLLABLE) {
// 获取目标的 View
final View selectedChild = mTabStrip.getChildAt(position);
final View nextChild = position + 1 < mTabStrip.getChildCount()
? mTabStrip.getChildAt(position + 1)
: null;
final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
// base scroll amount: places center of tab in center of parent
// 注意这里的,**selectedChild.getLeft()**,getLeft()是计算相对于父布局的左边距,那么如果设置了 margin 的话,childView 相当于父布局的位置就会变化了,那么我们为了避免这种情况可以使用 padding 来改变距离,避免使原来的逻辑收到干扰。一切真想大白了!int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);
// offset amount: fraction of the distance between centers of tabs
int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);
return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR)
? scrollBase + scrollOffset
: scrollBase - scrollOffset;
}
return 0;
}
TabLayout 设置最后一个默认选中时位置错乱
因为的 TabLayout 在 dialog 中,而我想让 dialog 展示之前就把数据设置完毕并且切换到默认的下标。
fun showPos(index: Int) {mTabLayout.getTabAt(index)?.select()
show()}
切换成功了,但是位置不对!
更操蛋的是将对话框 dissmiss 后再重新 show 的时候就正确了!!(这个是最气的
最后 debug 半天多终于搞定了!现在!让我们回归当出问题时的代码(案发现场
private fun customTabWidth(tabLayout: TabLayout) {
// 嫌疑人在这里
// ↓
tabLayout.post {
try {
// 拿到 tabLayout 的 mTabStrip 属性
val mTabStripField = tabLayout.javaClass.getDeclaredField("mTabStrip")
mTabStripField.isAccessible = true
val mTabStrip = mTabStripField.get(tabLayout) as LinearLayout
// balabala 修改 Tab 间距的代码
tabView.invalidate()}
} catch (e: NoSuchFieldException) {e.printStackTrace()
} catch (e: IllegalAccessException) {e.printStackTrace()
}
}
}
没错就是这个 tabLayout.post(Runnable runnable)啊!当我第一次运行的时候,TabLayout 虽然已经创建但是并没有依附到任何 Window 中,导致 runnable 会被添加到运行队列中,然后等到这个 View 已经添加到 Window 时,再一起运行!那么会导致的现象就是,我第一次修改这个宽度的时候,其实并没有真的修改,真的修改是在对话框展示后,TabLayout 依附到 View了,那时才会运行!所以才会出现第一次的位置是错误的,第二次的位置是正确的情况!!
关于本人使用 TabLayout 时的坑就介绍到这里了!希望能够帮助的了大家!!
以上。