TabLayout 踩坑之 onTabSelected 沒有被回撥的問題

依然範特稀西發表於2017-04-17

TabLayout 踩坑之 onTabSelected 沒有被回撥的問題
封面.png

一、 問題描述

最近專案中有個需求:一個頁面頂部有3個tab,每一個tab分別展示一個不同的頁面,點選tab 切換到對應頁面。進入頁面是預設選中第一個頁面。

這不很簡單的一個需求嘛?很明顯,用TabLayout 分分鐘實現,於是開啟Android Studio ,幾分鐘後寫下了如下程式碼:

public class TabActivity extends AppCompatActivity {
    private TabLayout mTabLayout;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tab_layout_ac2);
        mTabLayout = (TabLayout) findViewById(R.id.tab_layout2);

        mTabLayout.addTab(mTabLayout.newTab().setText("個性推薦"),true);//設定預設選中
        mTabLayout.addTab(mTabLayout.newTab().setText("歌單"));
        mTabLayout.addTab(mTabLayout.newTab().setText("主播電臺"));

        final List<Fragment> fragments = new ArrayList<>();
        fragments.add(FirstFragment.newInstance());
        fragments.add(SecondFragment.newInstance());
        fragments.add(ThirdFragment.newInstance());

        mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                Log.e("TAG","tab position:"+tab.getPosition());
                replaceFragment(fragments.get(tab.getPosition()));
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {

            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {

            }
        });
    }

    private void replaceFragment(Fragment fragment){
        getSupportFragmentManager().beginTransaction().replace(R.id.tab_container,fragment).commit();
    }
}複製程式碼

寫完之後,一執行,發現tab 顯示出來了,第一個tab也選中了(效果如下圖),但是第一頁的內容咋沒展示出來呢? 然後點選tab切換,切換到後面2個tab時,可以載入出頁面,然後再次點選第一個tab ,第一個tab 的頁面也展示出來了。

TabLayout 踩坑之 onTabSelected 沒有被回撥的問題
未展示頁面效果.png

第一次進來時,tab 下面的頁面內容沒有展示出來,很明顯,那就是第一次進來的時候onTabSelected 回撥沒有被執行。因為我們是在onTabSelected 來載入頁面的。經過幾次反覆測試(日誌和斷點除錯),確定了是第一次進入的時候,onTabSelected沒有被回撥。

那麼,為什麼第一次進入的時候,onTabSelected沒有被回撥了?反覆檢查了幾次程式碼,沒有發現問題。既然沒有發現問題,那麼,我們就只有去看原始碼了,看一下TabLayoout 初始化完成後,在什麼時候呼叫的onTabSelected 回撥方法?

二、原始碼追蹤

我們要看一下原始碼中TabLayout初始化後,在什麼時候呼叫的onTabSelected。我們注意到,新增Tab的時候,有這麼一個方法:

 mTabLayout.addTab(mTabLayout.newTab().setText("個性推薦"),true);複製程式碼

addTab 方法有2個引數,第一個是要新增的Tab,第二個引數是是否設定為預設選中。上面這行程式碼的意思是,新增一個Tab,並且設定這個tab為預設選中的Tab。

接下來就走讀一下原始碼,看一下在何時回撥的onTabSelected方法:

1,首先看一下OnTabSelectedListener 的設定

 public void addOnTabSelectedListener(@NonNull OnTabSelectedListener listener) {
        if (!mSelectedListeners.contains(listener)) {
            mSelectedListeners.add(listener);
        }
    }複製程式碼

很簡單,就是將OnTabSelectedListener儲存到一個列表裡。沒有做其他事情。

2, 以 addTab 方法為入口,順藤摸瓜。

    // 新增一個Tab ,並且設定為是否選中
    // 實際呼叫方法 addTab(@NonNull Tab tab, int position, boolean setSelected) 
    public void addTab(@NonNull Tab tab, boolean setSelected) {
        addTab(tab, mTabs.size(), setSelected);
    }

 //1, 首先將Tab 儲存到一個列表中,記錄位置
 //2, 將tab 新增到TabLayout 中
 //3, 判斷時候選中(這就是我們要的)
 public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
        if (tab.mParent != this) {
            throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
        }
        configureTab(tab, position);
        addTabView(tab);
        // 是否選中
        if (setSelected) {
            tab.select();
        }
    }複製程式碼

如上程式碼,如果setSelectedtrue,就呼叫了tab 的select()方法,我們看一下select()方法:

 public void select() {
            if (mParent == null) {
                throw new IllegalArgumentException("Tab not attached to a TabLayout");
            }
            mParent.selectTab(this);
        }複製程式碼

呼叫了Parent的select方法,Parent 是誰?當然是TabLayout 啦。所以繼續深入,看看TabLayout的select方法。

  void selectTab(Tab tab) {
       //實際呼叫 selectTab(final Tab tab, boolean updateIndicator) 
        selectTab(tab, true);
    }

 // 
  void selectTab(final Tab tab, boolean updateIndicator) {
        final Tab currentTab = mSelectedTab;

        if (currentTab == tab) { // 如果新選中的Tab 和當前Tab 相同,回撥onTabReselected 方法
            if (currentTab != null) {
                dispatchTabReselected(tab);
                animateToTab(tab.getPosition());
            }
        } else { // 如果不同,則回撥 onTabSelected方法
            final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION;
            if (updateIndicator) {
                if ((currentTab == null || currentTab.getPosition() == Tab.INVALID_POSITION)
                        && newPosition != Tab.INVALID_POSITION) {
                    // If we don't currently have a tab, just draw the indicator
                    setScrollPosition(newPosition, 0f, true);
                } else {
                    animateToTab(newPosition);
                }
                if (newPosition != Tab.INVALID_POSITION) {
                    setSelectedTabView(newPosition);
                }
            }
            if (currentTab != null) {
                dispatchTabUnselected(currentTab);
            }
            mSelectedTab = tab; // 記錄選中的Tab 
            if (tab != null) {
                dispatchTabSelected(tab); // 處理選中Tab 
            }
        }
    }複製程式碼

TabLayout 的selectTab方法中最終呼叫了dispatchTabSelected方法:

 private void dispatchTabSelected(@NonNull final Tab tab) {
        for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
            mSelectedListeners.get(i).onTabSelected(tab); // 找到了,在這裡迴圈列表,分別呼叫onTabSelected 方法。
        }
    }複製程式碼

好了,到這裡就找到了onTabSelected回撥的地方了,有沒有很熟悉 mSelectedListeners?當然了,在我們呼叫addOnTabSelectedListener設定監聽器的時候,就是儲存到mSelectedListeners的。我們來捋一捋:

TabLayout 踩坑之 onTabSelected 沒有被回撥的問題
原始碼流程.png

如上圖:
當我們執行addTab方法新增Tab的時候,最後會呼叫到 dispatchTabSelected方法,在dispatchTabSelected方法裡面呼叫addOnTabSelectedListeneronTabSelected()方法。但是這個時候,mSelectedListeners 為空(因為這個時候我們還沒有設定OnTabSelectedListener),因此,就沒有回撥到onTabSelected

分析到此我們也就明白了,第一次沒有回撥到 onTabSelected 方法,是因為我們寫的程式的順序問題,應該在新增Tab 之前 新增OnTabSelectedListener 監聽。

三、解決方案

通過上面的原始碼分析,我們知道了,第一次沒有執行OnTabSelected回撥,是因為我們的程式碼順序問題(這個是Google 的坑,設計得有問題啊),因此,要想第一次進入的時候回撥到OnTabSelected方法,我們應該先設定
addOnTabSelectedListener 監聽器,再新增Tab
,我們原來的程式順序調整一下:

        // 1, 設定 addOnTabSelectedListener
        // 設定 addOnTabSelectedListener 必須在 addTab 之前。
        mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                Log.e("TAG","tab position:"+tab.getPosition());
                replaceFragment(fragments.get(tab.getPosition()));
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {

            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {

            }
        });
        // 2.新增Tab 
        mTabLayout.addTab(mTabLayout.newTab().setText("個性推薦"),true);
        mTabLayout.addTab(mTabLayout.newTab().setText("歌單"));
        mTabLayout.addTab(mTabLayout.newTab().setText("主播電臺"));複製程式碼

通過如上調整,再執行程式,完美解決,第一次進入如下:

TabLayout 踩坑之 onTabSelected 沒有被回撥的問題
展示了頁面內容效果.png

四、總結

本篇文章是對TabLayout 使用過程中遇到的一個的坑記錄和總結,可能有的人不會遇到,也沒有人會注意到(如果你一開始就把順序寫對了的話),但是,如果遇到的話,要看原始碼才知道原因。這也是我們解決問題的一種思路。很多時候,我們需要去了解一下原理和原始碼的實現,這樣才能幫助我們更好的理解,以免使用的時候踩坑。本文沒有介紹TabLayout的詳細使用方法和使用場景,要了解更多請看我前面的文章Material Design 之 TabLayout 使用

相關文章