基于TabLayout源碼實(shí)現(xiàn)自定義TabLayout

目錄

  • TabLayout原理
  • 具體實(shí)現(xiàn)
  • 遇到的問(wèn)題
  • 總結(jié)

一蜓陌、TabLayout原理

1.1 TabLayout與ViewPager的綁定原理

往往TabLayout都是和ViewPager聯(lián)動(dòng)使用晴竞,下面就從TabLayout源碼進(jìn)行分析ViewPager和TabLayout如何配合使用卵史。
下面的代碼是最簡(jiǎn)單的一個(gè)viewpager+tablayout+fragment的使用場(chǎng)景烛占,那么最開(kāi)始就從setupWithViewPage()對(duì)源碼進(jìn)行分析。

        mFragments = new ArrayList<>();
        mFragments.add(new NewsTypeFragment());
        mFragments.add(new NewsTypeFragment());
        mFragments.add(new NewsTypeFragment());
        mViewPagerFragmentAdapter = new ViewPagerFragmentAdapter(getChildFragmentManager(), mFragments);
        viewpager.setAdapter(mViewPagerFragmentAdapter);
        tablayout.setupWithViewPager(viewpager);

viewpager和tablayout存在雙向綁定的機(jī)制:

image

綁定流程如下:


屏幕快照 2018-02-04 下午5.13.29.png

通過(guò)監(jiān)聽(tīng)viewpager, 與之綁定的TabLayout也隨viewpager更改視圖浑玛,以下是TabLayoutOnPageChangeListener的源碼甲锡。

public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
        private final WeakReference<TabLayout> mTabLayoutRef;
        private int mPreviousScrollState;
        private int mScrollState;

        public TabLayoutOnPageChangeListener(TabLayout tabLayout) {
            mTabLayoutRef = new WeakReference<>(tabLayout);
        }

        @Override
        public void onPageScrollStateChanged(final int state) {
            mPreviousScrollState = mScrollState;
            mScrollState = state;
        }

        @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);
                tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
            }
        }

        @Override
        public void onPageSelected(final int position) {
            final TabLayout tabLayout = mTabLayoutRef.get();
            if (tabLayout != null && tabLayout.getSelectedTabPosition() != position
                    && position < tabLayout.getTabCount()) {
                // Select the tab, only updating the indicator if we're not being dragged/settled
                // (since onPageScrolled will handle that).
                final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE
                        || (mScrollState == SCROLL_STATE_SETTLING
                        && mPreviousScrollState == SCROLL_STATE_IDLE);
                tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
            }
        }

        void reset() {
            mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
        }
    }

其中onPageScrollStateChanged() 得到viewpager的三種狀態(tài),并保存前置狀態(tài)和當(dāng)前狀態(tài),影響后續(xù)頁(yè)面布局和動(dòng)畫效果中燥。

    /**
     * Indicates that the pager is in an idle, settled state. The current page
     * is fully in view and no animation is in progress.
     * 表示viewpager的狀態(tài)為靜止?fàn)顟B(tài)(無(wú)動(dòng)畫寇甸、無(wú)滑動(dòng))
     */
    public static final int SCROLL_STATE_IDLE = 0;

    /**
     * Indicates that the pager is currently being dragged by the user.
     * 表示viewpager的狀態(tài)滑動(dòng)狀態(tài)
     */
    public static final int SCROLL_STATE_DRAGGING = 1;

    /**
     * Indicates that the pager is in the process of settling to a final position.
     */
    public static final int SCROLL_STATE_SETTLING = 2;

public void onPageScrolled(final int position, final float positionOffset,final int positionOffsetPixels)該方法監(jiān)聽(tīng)的是Viewpager的位置以及每個(gè)page的偏移量(這里解釋一下positionOffset,它對(duì)應(yīng)ViewPager當(dāng)前page的偏移量疗涉,其中左劃數(shù)值從0-1,右滑數(shù)值從1-0拿霉,后續(xù)會(huì)根據(jù)positionOffset計(jì)算整個(gè)HorizontalScrollView的位置)、對(duì)應(yīng)的像素位置咱扣,onPageScrolled()和下面onPageSelected() 是與TabLayout聯(lián)動(dòng)最關(guān)鍵的兩個(gè)方法 ,在這個(gè)方法中绽淘,會(huì)將position和positionOffset傳遞給setScrollPosition(),并通過(guò)這個(gè)方法更新TabLayout視圖,其中包括闹伪,底部indicater(tab追蹤條)沪铭,text(tab的名稱)壮池,HorizontalScrollView的偏移位置,運(yùn)用對(duì)偏移量四舍五入的計(jì)算方法杀怠,設(shè)置tab標(biāo)題顏色椰憋。這里要尤其注意,onPageScrolled返回的position會(huì)根據(jù)滑動(dòng)方向改變赔退,左滑position保持當(dāng)前pager的值橙依,而從靜止開(kāi)始往右滑動(dòng)則變成當(dāng)前page-1,尤其區(qū)分這里的position和onPageSelected返回的position离钝。

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(calculateScrollXForTab(position, positionOffset), 0);

        // Update the 'selected state' view as we scroll, if enabled
        if (updateSelectedText) {
            setSelectedTabView(roundedPosition);
        }
    }

public void onPageSelected(final int position) 該方法只有在動(dòng)畫完成票编,頁(yè)面靜止的時(shí)候調(diào)用,position顯示當(dāng)前page的頁(yè)數(shù)(從0開(kāi)始)

二卵渴、具體實(shí)現(xiàn)

2.1 tab底部indicator自定義

原生TabLayout的底部indicator默認(rèn)是矩形條慧域,并且只能修改其高度,所以它的可定制性非常低浪读,而繪制矩形條的類SlidingTabStrip是私密內(nèi)部類昔榴,所以為了自定義indcator需要將tablayout整體移植到自己的工程項(xiàng)目?jī)?nèi),并修改SlidingTabStrip這個(gè)類碘橘。這里提供簡(jiǎn)單的三種自定義圖形

        @Override
        public void draw(Canvas canvas) {
            super.draw(canvas);

            // Thick colored underline below the current selection
            if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
                //自定義畫圓
                //canvas.drawCircle((mIndicatorLeft + mIndicatorRight) / 2, getHeight() - mSelectedIndicatorHeight, mSelectedIndicatorHeight, mSelectedIndicatorPaint);
                //自定義三角形
                Path path = new Path();
                path.moveTo((mIndicatorLeft + mIndicatorRight) / 2, getHeight() - mSelectedIndicatorHeight - 10);
                path.lineTo((mIndicatorLeft + mIndicatorRight) / 2 - mSelectedIndicatorHeight - 10, getHeight());
                path.lineTo((mIndicatorLeft + mIndicatorRight) / 2 + mSelectedIndicatorHeight + 10, getHeight());
                path.close();
                canvas.drawPath(path, mSelectedIndicatorPaint);
                //自定義矩形互订、條形(默認(rèn))
                //canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
                // mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
            }
        }

2.2 tab滑動(dòng)機(jī)制自定義

通常TabLayout與fragment+ViewPager一起使用,不知道大家有沒(méi)有遇到過(guò)這種情況痘拆,當(dāng)設(shè)置ViewPager的setCurrentItem方法時(shí)仰禽,可以選擇pager的滑動(dòng)是否是smooth夯缺,true的時(shí)候赔桌,tablayout也是smooth,false的時(shí)候工闺,tablayout的切換也變得生硬桥氏,包括現(xiàn)在的網(wǎng)易新聞温峭,今日頭條的tablayout就是這種機(jī)制。產(chǎn)生這種不協(xié)調(diào)的原因是因?yàn)樯鲜霰O(jiān)聽(tīng)ViewPager的onPageScrolled方法字支,點(diǎn)擊tab的時(shí)候onPageScrolled方法返回的positionOffset一直為0凤藏,每次點(diǎn)擊tab時(shí),最后一次調(diào)用的是onPageScrolled方法而不是onPageSelected方法堕伪,通過(guò)debug點(diǎn)擊tab時(shí)候的log可以看出來(lái):

02-04 08:35:59.965 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled:1 
02-04 08:36:08.591 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageSelected: 2
02-04 08:36:08.597 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled:1 

所以揖庄,以最后一次onPageScrolled的監(jiān)聽(tīng)為主,同時(shí)positionOffset為0欠雌,就導(dǎo)致沒(méi)有動(dòng)畫效果抠艾,也就是導(dǎo)致點(diǎn)擊tab很生硬的主要原因!

那有沒(méi)有一種機(jī)制一能防止viewpager產(chǎn)生過(guò)渡動(dòng)畫桨昙,又能讓tablayout有過(guò)渡動(dòng)畫检号。 其實(shí)很簡(jiǎn)單,就是監(jiān)聽(tīng)positionOffset蛙酪,當(dāng)positionOffset大于0時(shí)執(zhí)行setScrollPosition方法:

@Override
        public void onPageScrolled(final int position, final float positionOffset,
                                   final int positionOffsetPixels) {
            final TabLayout tabLayout = mTabLayoutRef.get();
            Log.d("mTabLayoutRef", "onPageScrolled:1 ");
            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);
                if (positionOffset>0)
                tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
            }
        }

三齐苛、遇到的問(wèn)題

遇到的最主要的問(wèn)題就是在tab滑動(dòng)機(jī)制自定義時(shí),由于兩個(gè)監(jiān)聽(tīng)用了同一種動(dòng)畫桂塞,所以監(jiān)聽(tīng)結(jié)果的順序就很重要凹蜂,不然顯示的結(jié)果差強(qiáng)人意,通過(guò)debug發(fā)現(xiàn)返回position的順序是最后返回onPageScrolled方法而不是onPageSelected阁危,才發(fā)現(xiàn)問(wèn)題所在玛痊。

四、總結(jié)

這次在寫自己的demo的時(shí)候狂打,本來(lái)是想仿寫網(wǎng)易新聞和今日頭條的頂部滑動(dòng)菜單欄擂煞,然后發(fā)現(xiàn)都有這種點(diǎn)擊tab時(shí)菜單欄無(wú)滾動(dòng)效果的問(wèn)題,通過(guò)看了TabLayout的源碼趴乡,并改寫才完善了這個(gè)功能对省,提高了用戶體驗(yàn),自己也積累了不少知識(shí)晾捏,總之再小的功能都有不斷發(fā)掘和革新的價(jià)值蒿涎!

附:Demo地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市惦辛,隨后出現(xiàn)的幾起案子劳秋,更是在濱河造成了極大的恐慌,老刑警劉巖胖齐,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玻淑,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡市怎,警方通過(guò)查閱死者的電腦和手機(jī)岁忘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)区匠,“玉大人干像,你說(shuō)我怎么就攤上這事〕叟” “怎么了麻汰?”我有些...
    開(kāi)封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)戚篙。 經(jīng)常有香客問(wèn)我五鲫,道長(zhǎng),這世上最難降的妖魔是什么岔擂? 我笑而不...
    開(kāi)封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任位喂,我火速辦了婚禮浪耘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘塑崖。我一直安慰自己七冲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布规婆。 她就那樣靜靜地躺著澜躺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪抒蚜。 梳的紋絲不亂的頭發(fā)上掘鄙,一...
    開(kāi)封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音嗡髓,去河邊找鬼操漠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛器贩,可吹牛的內(nèi)容都是我干的颅夺。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蛹稍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼吧黄!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起唆姐,我...
    開(kāi)封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拗慨,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后奉芦,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體赵抢,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年声功,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了烦却。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡先巴,死狀恐怖其爵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伸蚯,我是刑警寧澤摩渺,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站剂邮,受9級(jí)特大地震影響摇幻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一绰姻、第九天 我趴在偏房一處隱蔽的房頂上張望枉侧。 院中可真熱鬧,春花似錦龙宏、人聲如沸棵逊。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至徒像,卻和暖如春黍特,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锯蛀。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工灭衷, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人旁涤。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓翔曲,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親劈愚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瞳遍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容