使用系統TabLayout的app快來修Bug

abcjob發表於2021-09-09

前言

系統 TabLayout 和 ViewPager 配合使用時有個 Bug,當切換 Tab 的時候,Tab 會整體往左抖一下,這個抖動速度很快,大家稍微注意點能看到,那麼筆者在公司做業務時也有使用到系統的 TabLayout ,進行視覺校驗的時候沒逃過設計師的法眼,設計師要求高是件好事,但這個時候重新寫一個也不現實,那麼該怎麼辦呢?

問題描述

先來看看直接使用系統 TabLayout 而出現問題的一些App:

             圖片描述圖片描述圖片描述                           


問題分析

在分析問題之前,我們先回顧下這個 Bug 復現的場景:先選中一個靠後的 Tab,然後滑動 TabLayout 到最左邊,點選第一個 Tab,會發現整個 TabLayout 往左抖了一下,速度很快,但無法忽視

那麼我們要解決的就是快速抖動的問題。

想解決這個問題,TabLayout 的原始碼還是得分析的,TabLayout 直接繼承的 HorizontalScrollView,不難推測,抖動的產生其實就是被執行了 scroll。

我們回想下,讓 TabLayout 發生 scroll 行為的場景會有哪些?

o    直接選中指定 Tab

o    滑動 ViewPager

我們發現的 Bug 出現的場景是點選 Tab 發生的,那麼我們點選了 Tab 後符合上面說的場景一,那麼 Tab 切換後會導致 ViewPager 滑動,那也會觸發 scroll,擦,難道就是因為這樣,導致了閃了一下?只能看看原始碼了:

首先看看點選 Tab 觸發的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 做了啥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 滑動方法裡做了啥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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 :

1
2
3
4
5
6
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 類的實現做點改變:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 變數,我們需要替換它,那隻能反射了。

1
2
3
4
5
6
7
8
9
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();
}

這樣一來,就完成了,看看效果:

圖片描述


尾語

即使是官方的東西但難免也會有點小問題,重視細節,再解決它,這個過程還是不錯的。

原文連結:http://www.apkbus.com/blog-822721-77046.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4798/viewspace-2812522/,如需轉載,請註明出處,否則將追究法律責任。

相關文章