前言
我在之前的一篇文章提到了使用系统 TabLayout 的一个Bug,详情见这篇文章,使用了系统TabLayout的app来领bug啦
系统 TabLayout 和 ViewPager 配合使用时有个 Bug,当切换 Tab 的时候,Tab 会整体往左抖一下,这个抖动速度很快,大家稍微注意点能看到,那么笔者在公司做业务时也有使用到系统的 TabLayout ,进行视觉校验的时候没逃过设计师的法眼,设计师要求高是件好事,但这个时候重新写一个也不现实,那么该怎么办呢?
问题描述
先来看看直接使用系统 TabLayout 而出现问题的一些知名 App:
慕*网:
慕*网稀土*金
稀土*金开源*国:
开源*国问题分析
在分析问题之前,我们先回顾下这个 Bug 复现的场景:先选中一个靠后的 Tab,然后滑动 TabLayout 到最左边,点击第一个 Tab,会发现整个 TabLayout 往左抖了一下,速度很快,但无法忽视
那么我们要解决的就是快速抖动的问题。
想解决这个问题,TabLayout 的源码还是得分析的,TabLayout 直接继承的 HorizontalScrollView,不难推测,抖动的产生其实就是被执行了 scroll。
我们回想下,让 TabLayout 发生 scroll 行为的场景会有哪些?
- 直接选中指定 Tab
- 滑动 ViewPager
我们发现的 Bug 出现的场景是点击 Tab 发生的,那么我们点击了 Tab 后符合上面说的场景一,那么 Tab 切换后会导致 ViewPager 滑动,那也会触发 scroll,擦,难道就是因为这样,导致了闪了一下?只能看看源码了:
首先看看点击 Tab 触发的方法
// TabLayout#selectTab
void selectTab(final Tab tab, boolean updateIndicator) {
final Tab currentTab = mSelectedTab;
if (currentTab == tab) {
...
} else {
final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION;
if (updateIndicator) {
if ((currentTab == null || currentTab.getPosition() == Tab.INVALID_POSITION)
&& newPosition != Tab.INVALID_POSITION) {
...
} else {
// 让 Tab 做动画
animateToTab(newPosition);
}
...
}
...
}
}
再看 TabLayout#animateToTab
private void animateToTab(int newPosition) {
if (newPosition == Tab.INVALID_POSITION) {
return;
}
...
final int startScrollX = getScrollX();
final int targetScrollX = calculateScrollXForTab(newPosition, 0);
if (startScrollX != targetScrollX) {
// 创建 scroll 动画
ensureScrollAnimator();
mScrollAnimator.setIntValues(startScrollX, targetScrollX);
// scroll 动画开始执行
mScrollAnimator.start();
}
// Now animate the indicator
mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
}
再看看 ensureScrollAnimator 做了啥:
private void ensureScrollAnimator() {
if (mScrollAnimator == null) {
mScrollAnimator = new ValueAnimator();
mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
mScrollAnimator.setDuration(ANIMATION_DURATION);
mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
// 属性动画回调中调用 scrollTo
scrollTo((int) animator.getAnimatedValue(), 0);
}
});
}
}
可以看到点击 Tab 最终会使 TabLayout 发生 scroll 行为。
继续顺着刚才说的点击 Tab 的时候也会触发 ViewPager 的滑动,我们看看 ViewPager 滑动方法里做了啥:
// TabLayout#TabLayoutOnPageChangeListener
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) {
...
// 这里又调用了设置 Scroll 的位置的方法
tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
}
}
...
}
再看看 setScrollPosition :
void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
boolean updateIndicatorPosition) {
...
scrollTo(calculateScrollXForTab(position, positionOffset), 0);
...
}
看到了吧,这里也调用了 scrollTo。
那么之前抖动的问题就很显然了,点击 Tab 的时候会触发 TabLayout 的scrollTo,而点击 Tab 会触发ViewPager 滑动,ViewPager的滑动也特么触发了 scrollTo,这 ViewPager 滑动导致的 scrollTo 就是我们闪烁的原因!
分析完毕,如何解决呢?
解决
其实解决方案很简单,我们只要使点击 Tab 的时候不触发 ViewPager 滑动的那个 scrollTo 就行了。
How?
我们在自己滑动 ViewPager 的时候 scrollTo 还是要走的,那么自己滑动和点击 Tab 触发的 ViewPager 滑动有啥区别呢?当然有!pageScrollState 不同!自己滑动的时候是 SCROLL_STATE_DRAGGING,而点击 Tab 时是 SCROLL_STATE_IDLE。
那么显而易见了,通过 pageScrollState 来区分下就行了。
我们需要对刚刚分析的 TabLayoutOnPageChangeListener 类的实现做点改变:
public static class FixedTabLayoutOnPageChangeListener
extends TabLayout.TabLayoutOnPageChangeListener {
private boolean isTouchState;
public FixedTabLayoutOnPageChangeListener(TabLayout tabLayout) {
super(tabLayout);
}
@Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
if (state == SCROLL_STATE_DRAGGING) {
isTouchState = true;
} else if (state == SCROLL_STATE_IDLE) {
isTouchState = false;
}
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (isTouchState) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
}
}
只有 pageScrollState 是 SCROLL_STATE_DRAGGING 的时候才触发 TabLayoutOnPageChangeListener 的 onPageScrolled。
但是 TabLayoutOnPageChangeListener 是 TabLayout 的 mPageChangeListener 变量,我们需要替换它,那只能反射了。
try {
Field field = TabLayout.class.getDeclaredField("mPageChangeListener");
field.setAccessible(true);
field.set(this, new FixedTabLayoutOnPageChangeListener(this));
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
这样一来,就完成了,看看效果:
FixedTabLayout尾语
即使是官方的东西但难免也会有点小问题,重视细节,再解决它,这个过程还是不错的。
写了个小 demo,地址:https://github.com/JeasonWong/FixedTabLayout
网友评论