反射改變TabLayout屬性

目錄介紹

  • 01.遇到的實際需求分析
  • 02.原生TabLayout局限
  • 03.TabLayout源碼解析
    • 3.1 Tab選項卡如何實現(xiàn)
    • 3.2 滑動切換Tab選項卡
    • 3.3 Tab選項卡指示線寬度
  • 04.設(shè)置自定義tabView選項卡
  • 05.自定義指示器的長度
  • 06.設(shè)置滑動改變選項卡顏色
  • 07.使用反射的注意要點
  • 08.混淆時用到反射注意項

想換個工作,渴望同行內(nèi)推我

  • 個人信息
    • 姓名:楊充【26歲】
    • 郵箱:yangchong211@163.com
    • 微信:13667225184
    • GitHub:https://github.com/yangchong211
    • 博客匯總:https://github.com/yangchong211/YCBlogs
    • 干活集中營:Android端技術(shù)博客和開源項目審核員
    • 目前工作情況:在職狀態(tài)
    • 技術(shù)項目和博客:GitHub項目7k以上star,follower1.1k以上,發(fā)表博客100多篇。
    • 熱愛技術(shù):開源項目和博客多次被鴻洋,郭霖,Android技術(shù)周刊,干活集中營等等推薦。
    • 學(xué)歷:武漢軟件工程職業(yè)學(xué)院,大專學(xué)歷
    • 工作年限:3年多
    • 工作地點:北京
  • 關(guān)于近期投遞簡歷一點感想
    • 從進入Android這個行業(yè)以來,前兩次幾乎都是朋友內(nèi)推,面試機會相對容易,都是一個App一個人做或者兩個人做,用戶相對來說并不多。這次想著離職,主要是想進入一個較大的平臺,大概可以理解為Android端有個至少四五人,可以進行技術(shù)交流,渴望自己能夠在技術(shù)上突破,這就像自己平時獨自跑步,和跟著一群跑馬拉松的人跑步,那種緊張感肯定是不一樣的。
    • 近段時間,嘗試著向一些較大的公司投遞簡歷,大概在拉鉤上投了15個左右(不喜歡海投),發(fā)現(xiàn)絕大多數(shù)簡歷到不了技術(shù)那里,就被人事說學(xué)歷不夠,經(jīng)驗不夠,工作不匹配等情況回絕。不過也可以理解,看簡歷無非就是學(xué)歷和經(jīng)驗,貌似自己的履歷是差了一點。
    • 這大概是第一次在網(wǎng)上發(fā)一個主動希望同行內(nèi)推的介紹,如果你的公司有Android方面的招聘,能否內(nèi)推一下我這個小人物,感謝。

好消息

  • 博客筆記大匯總【16年3月到至今】,包括Java基礎(chǔ)及深入知識點,Android技術(shù)博客,Python學(xué)習(xí)筆記等等,還包括平時開發(fā)中遇到的bug匯總,當(dāng)然也在工作之余收集了大量的面試題,長期更新維護并且修正,持續(xù)完善……開源的文件是markdown格式的!同時也開源了生活博客,從12年起,積累共計N篇[近100萬字,陸續(xù)搬到網(wǎng)上],轉(zhuǎn)載請注明出處,謝謝!
  • 鏈接地址:https://github.com/yangchong211/YCBlogs
  • 如果覺得好,可以star一下,謝謝!當(dāng)然也歡迎提出建議,萬事起于忽微,量變引起質(zhì)變!

01.遇到的實際需求分析

  • 實際開發(fā)中UI的效果圖
    • 一般要求文字內(nèi)容和指示線的寬度要一樣
    • image
  • 使用TabLayout的效果圖
    • 一般指示線的寬度要大于文字內(nèi)容
    • image
  • 遇到問題分析
    • 設(shè)置tabPaddingStart和tabPaddingEnd,但是布局填上去后發(fā)現(xiàn)并沒有用。
  • 實現(xiàn)方案
    • 第一種:自定義類似TabLayout的控件,代碼量巨大,且GitHub上有許多已經(jīng)比較成熟的庫,代碼質(zhì)量是層次不齊。
    • 第二種:在原有基礎(chǔ)上通過繼承TabLayout控件,重寫其中幾個方法,并且通過反射來修改部分屬性,也能達到第一種方案效果。
    • 下面就來講一下我自己通過第二種方案實現(xiàn)步驟和原理!
  • 最終UI效果圖展示
    • image

02.原生TabLayout局限

  • 一張圖看懂TabLayout的結(jié)構(gòu)
    • image
    • 如果要用代碼進行表示的話,大概是這樣的。TabLayout繼承自HorizontalScrollView,而都知道ScrollView只能添加一個子 View,所以SlidingTabIndicator就是那個用來添加子View 的橫向LinearLayout。
    //28版本代碼
    public class TabLayout extends HorizontalScrollView {
    
        private class SlidingTabIndicator extends LinearLayout {
    
        }
    }
    
  • 存在的局限性
    • 第一個無法改變指示線的寬度
    • 第二個無法做到滑動改變tab選項卡顏色漸變的效果【有的還需要放大效果】

03.TabLayout源碼解析

3.1 Tab選項卡如何實現(xiàn)

  • 第一種方式,直接通過addTab方法添加tab選項卡,代碼如下所示
    TabLayout.Tab tab = tabLayout.newTab();
    View tabView = new TextView(this);
    tabLayout.setCustomView(tabView);
    tabLayout.addTab(tab);
    
  • 第二種方式,通過設(shè)置FragmentPagerAdapter中的getPageTitle也可以添加tab選項卡,代碼如下所示
    mTitleList.add("瀟湘劍雨");
    FragmentManager supportFragmentManager = getSupportFragmentManager();
    PagerAdapter myAdapter = new PagerAdapter(supportFragmentManager, mFragments, mTitleList);
    tabLayout.setAdapter(myAdapter);
    
    
    public class PagerAdapter extends FragmentPagerAdapter {
    
        private List<?> mFragment;
        private List<String> mTitleList;
    
        public PagerAdapter(FragmentManager fm, List<?> mFragment, List<String> mTitleList) {
            super(fm);
            this.mFragment = mFragment;
            this.mTitleList = mTitleList;
        }
    
        @Override
        public CharSequence getPageTitle(int position) {
            if (mTitleList != null) {
                return mTitleList.get(position);
            } else {
                return "";
            }
        }
    }
    
    • 接下來看一下tabLayout源碼是如何拿到getPageTitle方法的內(nèi)容而達到設(shè)置addTab的目的。主要看源碼中的populateFromPagerAdapter方法??吹较旅娲a是不是豁然開朗了……
    void populateFromPagerAdapter() {
        this.removeAllTabs();
        if (this.pagerAdapter != null) {
            int adapterCount = this.pagerAdapter.getCount();
    
            int curItem;
            for(curItem = 0; curItem < adapterCount; ++curItem) {
                this.addTab(this.newTab().setText(this.pagerAdapter.getPageTitle(curItem)), false);
            }
    
            if (this.viewPager != null && adapterCount > 0) {
                curItem = this.viewPager.getCurrentItem();
                if (curItem != this.getSelectedTabPosition() && curItem < this.getTabCount()) {
                    this.selectTab(this.getTabAt(curItem));
                }
            }
        }
    }
    
  • 不管是上面那種方式,那么如何將tab添加到SlidingTabIndicator布局中呢?
    • 通過下面代碼可以看到,最終是通過slidingTabIndicator對象調(diào)用addView將tabView添加到SlidingTabIndicator布局之中的。
    public void addTab(@NonNull TabLayout.Tab tab, int position, boolean setSelected) {
        if (tab.parent != this) {
            throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
        } else {
            this.configureTab(tab, position);
            this.addTabView(tab);
            if (setSelected) {
                tab.select();
            }
        }
    }
    
    private void addTabView(TabLayout.Tab tab) {
        TabLayout.TabView tabView = tab.view;
        this.slidingTabIndicator.addView(tabView, tab.getPosition(), this.createLayoutParamsForTabs());
    }
    
  • 為什么要分析這個addTab?
    • 因為需求說了,需要在滑動的時候,隨著滑動而改變tabView的文字顏色,這一點原生TabLayout并沒有實現(xiàn)。所以要實現(xiàn)這個邏輯,就必須重寫TabLayout的addTab方法,然后將自己自定義的tabView添加到tab中,這個下面會講如何實現(xiàn)……

3.2 滑動切換Tab選項卡

  • 第一步:隨著頁面的滑動文字顏色漸變那么肯定少不了ViewPager的頁面監(jiān)聽,這個在我們調(diào)用setupWithViewPager的時候TabLayout就已經(jīng)添加監(jiān)聽。那么先來看下源碼監(jiān)聽滑動是如何實現(xiàn)的?
    • 綁定 ViewPager 只需要一行代碼mTabLayout.setupWithViewPager(mViewPager)即可。
    • 可以看到當(dāng)viewPager不為null的時候,先移除listener監(jiān)聽事件。然后在創(chuàng)建listener監(jiān)聽,并且重置狀態(tài)。
    private void setupWithViewPager(@Nullable ViewPager viewPager, boolean autoRefresh, boolean implicitSetup) {
        if (this.viewPager != null) {
            if (this.pageChangeListener != null) {
                this.viewPager.removeOnPageChangeListener(this.pageChangeListener);
            }
    
            if (this.adapterChangeListener != null) {
                this.viewPager.removeOnAdapterChangeListener(this.adapterChangeListener);
            }
        }
    
        if (this.currentVpSelectedListener != null) {
            this.removeOnTabSelectedListener(this.currentVpSelectedListener);
            this.currentVpSelectedListener = null;
        }
    
        if (viewPager != null) {
            this.viewPager = viewPager;
            if (this.pageChangeListener == null) {
                this.pageChangeListener = new TabLayout.TabLayoutOnPageChangeListener(this);
            }
    
            this.pageChangeListener.reset();
            viewPager.addOnPageChangeListener(this.pageChangeListener);
            this.currentVpSelectedListener = new TabLayout.ViewPagerOnTabSelectedListener(viewPager);
            this.addOnTabSelectedListener(this.currentVpSelectedListener);
            PagerAdapter adapter = viewPager.getAdapter();
            if (adapter != null) {
                this.setPagerAdapter(adapter, autoRefresh);
            }
    
            if (this.adapterChangeListener == null) {
                this.adapterChangeListener = new TabLayout.AdapterChangeListener();
            }
    
            this.adapterChangeListener.setAutoRefresh(autoRefresh);
            viewPager.addOnAdapterChangeListener(this.adapterChangeListener);
            this.setScrollPosition(viewPager.getCurrentItem(), 0.0F, true);
        } else {
            this.viewPager = null;
            this.setPagerAdapter((PagerAdapter)null, false);
        }
    
        this.setupViewPagerImplicitly = implicitSetup;
    }
    
  • 那么滑動是如何切換選項卡和指示線呢,具體看一下TabLayoutOnPageChangeListener滑動監(jiān)聽源碼。
    • 主要是看onPageSelected方法,該方法是通過tabLayout.selectTab來切換選項卡的。
    public static class TabLayoutOnPageChangeListener implements OnPageChangeListener {
    
        public TabLayoutOnPageChangeListener(TabLayout tabLayout) {
            this.tabLayoutRef = new WeakReference(tabLayout);
        }
    
        public void onPageScrollStateChanged(int state) {
            this.previousScrollState = this.scrollState;
            this.scrollState = state;
        }
    
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            TabLayout tabLayout = (TabLayout)this.tabLayoutRef.get();
            if (tabLayout != null) {
                boolean updateText = this.scrollState != 2 || this.previousScrollState == 1;
                boolean updateIndicator = this.scrollState != 2 || this.previousScrollState != 0;
                tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
            }
    
        }
    
        public void onPageSelected(int position) {
            TabLayout tabLayout = (TabLayout)this.tabLayoutRef.get();
            if (tabLayout != null && tabLayout.getSelectedTabPosition() != position && position < tabLayout.getTabCount()) {
                boolean updateIndicator = this.scrollState == 0 || this.scrollState == 2 && this.previousScrollState == 0;
                tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
            }
        }
    }
    
  • 知道了滑動切換選項卡后,就思考一下,能否通過反射來使用自己的滑動監(jiān)聽事件,然后在onPageSelected方法中,滑動改變選項卡中文字的顏色,或者縮放的功能呢。答案是可以的。

3.3 Tab選項卡指示線寬度

  • 具體可以看updateIndicatorPosition源碼
    • 可以看到先獲取當(dāng)前滑動位置的tabView,如果內(nèi)容不為空,則獲取左右的位置。
    • 在滑塊滑動的時候,如果滑動超過了上一個或是下一個滑塊一半的話。那就說明移動到了上一個或是下一個滑塊,然后取出left和right
    • 最后設(shè)置滑塊的位置
    private void updateIndicatorPosition() {
        //根據(jù)當(dāng)前滑塊的位置拿到當(dāng)前TabView
        View selectedTitle = this.getChildAt(this.selectedPosition);
        int left;
        int right;
        if (selectedTitle != null && selectedTitle.getWidth() > 0) {
            //拿到TabView的左、右位置
            left = selectedTitle.getLeft();
            right = selectedTitle.getRight();
            if (!TabLayout.this.tabIndicatorFullWidth && selectedTitle instanceof TabLayout.TabView) {
                this.calculateTabViewContentBounds((TabLayout.TabView)selectedTitle, TabLayout.this.tabViewContentBounds);
                left = (int)TabLayout.this.tabViewContentBounds.left;
                right = (int)TabLayout.this.tabViewContentBounds.right;
            }
    
            //在滑塊滑動的時候,如果滑動超過了上一個或是下一個滑塊一半的話
            //那就說明移動到了上一個或是下一個滑塊,然后取出left和right
            if (this.selectionOffset > 0.0F && this.selectedPosition < this.getChildCount() - 1) {
                View nextTitle = this.getChildAt(this.selectedPosition + 1);
                int nextTitleLeft = nextTitle.getLeft();
                int nextTitleRight = nextTitle.getRight();
                if (!TabLayout.this.tabIndicatorFullWidth && nextTitle instanceof TabLayout.TabView) {
                    this.calculateTabViewContentBounds((TabLayout.TabView)nextTitle, TabLayout.this.tabViewContentBounds);
                    nextTitleLeft = (int)TabLayout.this.tabViewContentBounds.left;
                    nextTitleRight = (int)TabLayout.this.tabViewContentBounds.right;
                }
    
                left = (int)(this.selectionOffset * (float)nextTitleLeft + (1.0F - this.selectionOffset) * (float)left);
                right = (int)(this.selectionOffset * (float)nextTitleRight + (1.0F - this.selectionOffset) * (float)right);
            }
        } else {
            right = -1;
            left = -1;
        }
        //設(shè)置滑塊的位置
        this.setIndicatorPosition(left, right);
    }
    
  • 然后看一下setIndicatorPosition的代碼
    • 設(shè)置滑塊的寬度是根據(jù)子TabView的寬度來設(shè)置的,也就是說,TabView的寬度是多少,那么滑塊的寬度就是多少。
    void setIndicatorPosition(int left, int right) {
        if (left != this.indicatorLeft || right != this.indicatorRight) {
            this.indicatorLeft = left;
            this.indicatorRight = right;
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
    
  • 為何要分析這個?
    • 因為如果你要改變指示器的寬度,那么必須要能夠動態(tài)改變左右的位置。知道了這個大概的原理,那么下面利用反射設(shè)置選項卡左右的間距來改變指示器的長度就知道怎么實現(xiàn)呢。

04.實現(xiàn)滑動改變顏色

  • 滑動改變指示器文字變色
    • TabLayout中可以設(shè)置文字內(nèi)容,通過上面3.2源碼分析,可以知道通過addTab添加自定義選項卡,那么滑動改變選項卡tabView的顏色,可以會涉及到監(jiān)聽滑動。因此這里需要用反射替換成自己的滑動監(jiān)聽,然后在TabLayoutOnPageChangeListener的監(jiān)聽類中的onPageScrolled方法,改變tabView的顏色。
  • 通過反射找到源碼中pageChangeListener成員變量,然后設(shè)置暴力訪問權(quán)限。
    • 然后獲取TabLayoutOnPageChangeListener的對象,刪除自帶的監(jiān)聽,同時將自己自定義的滑動監(jiān)聽listener添加上。
    @Override
    public void setupWithViewPager(@Nullable ViewPager viewPager, boolean autoRefresh) {
        super.setupWithViewPager(viewPager, autoRefresh);
        try {
            //通過反射找到mPageChangeListener
            Field field = getPageChangeListener();
            field.setAccessible(true);
            TabLayoutOnPageChangeListener listener = (TabLayoutOnPageChangeListener) field.get(this);
            if (listener!=null && viewPager!=null) {
                //刪除自帶監(jiān)聽
                viewPager.removeOnPageChangeListener(listener);
                OnPageChangeListener mPageChangeListener = new OnPageChangeListener(this);
                mPageChangeListener.reset();
                viewPager.addOnPageChangeListener(mPageChangeListener);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    • 然后看一下反射的代碼,我在網(wǎng)上看到好多博客,沒有區(qū)分27前和28后的問題。這個地方一定要注意一下!
    /**
     * 反射獲取私有的mPageChangeListener屬性,考慮support 28以后變量名修改的問題
     * @return Field
     * @throws NoSuchFieldException
     */
    private Field getPageChangeListener() throws NoSuchFieldException {
        Class clazz = TabLayout.class;
        try {
            // support design 27及一下版本
            return clazz.getDeclaredField("mPageChangeListener");
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            // 可能是28及以上版本
            return clazz.getDeclaredField("pageChangeListener");
        }
    }
    
  • 然后看一下自定義的OnPageChangeListener
    • 采用弱引用方式防止監(jiān)聽listener內(nèi)存泄漏,算是一個小的優(yōu)化
    /**
     * 滑動監(jiān)聽,核心邏輯
     * 建議如果是activity退到后臺,或者關(guān)閉頁面,將listener給remove掉
     * 采用弱引用方式防止監(jiān)聽listener內(nèi)存泄漏,算是一個小的優(yōu)化
     */
    private static class OnPageChangeListener extends TabLayoutOnPageChangeListener {
    
        private final WeakReference<CustomTabLayout> mTabLayoutRef;
        private int mPreviousScrollState;
        private int mScrollState;
    
        OnPageChangeListener(TabLayout tabLayout) {
            super(tabLayout);
            mTabLayoutRef = new WeakReference<>((CustomTabLayout) tabLayout);
        }
    
        /**
         * 這個方法是滾動狀態(tài)發(fā)生變化是調(diào)用
         * @param state                     樁體
         */
        @Override
        public void onPageScrollStateChanged(final int state) {
            mPreviousScrollState = mScrollState;
            mScrollState = state;
        }
    
        /**
         * 正在滾動時調(diào)用
         * @param position                  索引
         * @param positionOffset            offset偏移
         * @param positionOffsetPixels      offsetPixels
         */
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            super.onPageScrolled(position, positionOffset, positionOffsetPixels);
            CustomTabLayout tabLayout = mTabLayoutRef.get();
            if (tabLayout == null) {
                return;
            }
            final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
                    mPreviousScrollState == SCROLL_STATE_DRAGGING;
            if (updateText) {
                tabLayout.tabScrolled(position, positionOffset);
            }
        }
    
        /**
         * 選中時調(diào)用
         * @param position                      索引
         */
        @Override
        public void onPageSelected(int position) {
            super.onPageSelected(position);
            CustomTabLayout tabLayout = mTabLayoutRef.get();
            mPreviousScrollState = SCROLL_STATE_SETTLING;
            tabLayout.setSelectedView(position);
        }
    
        /**
         * 重置狀態(tài)
         */
        void reset() {
            mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
        }
    }
    

05.自定義指示器的長度

  • 通過反射的方式修改指示器長度,如果需要指示器寬度等于文字寬度需要自己微調(diào),或者28版本直接通過設(shè)置app:tabIndicatorFullWidth="false"屬性即可讓內(nèi)容和指示器寬度一樣。
    • 原理就是通過反射的方式獲取TabLayout的字段mTabStrip(27之前)或者slidingTabIndicator(28之后),然后再去遍歷修改每一個子 View 的 Margin 值。代碼如下:
    /**
     * 通過反射設(shè)置TabLayout每一個的長度
     * @param left                      左邊 Margin 單位 dp
     * @param right                     右邊 Margin 單位 dp
     */
    public void setIndicator(int left, int right) {
        Field tabStrip = null;
        try {
            tabStrip = getTabStrip();
            tabStrip.setAccessible(true);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    
        LinearLayout llTab = null;
        try {
            if (tabStrip != null) {
                llTab = (LinearLayout) tabStrip.get(this);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    
        int l = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, left,
                Resources.getSystem().getDisplayMetrics());
        int r = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, right,
                Resources.getSystem().getDisplayMetrics());
    
        if (llTab != null) {
            for (int i = 0; i < llTab.getChildCount(); i++) {
                View child = llTab.getChildAt(i);
                child.setPadding(0, 0, 0, 0);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                        0, LinearLayout.LayoutParams.MATCH_PARENT, 1);
                params.leftMargin = l;
                params.rightMargin = r;
                child.setLayoutParams(params);
                child.invalidate();
            }
        }
    }
    
  • 然后看一下反射獲取tabStrip的代碼
    /**
     * 反射獲取私有的mTabStrip屬性,考慮support 28以后變量名修改的問題
     * @return Field
     * @throws NoSuchFieldException
     */
    private Field getTabStrip() throws NoSuchFieldException {
        Class clazz = TabLayout.class;
        try {
            // support design 27及一下版本
            return clazz.getDeclaredField("mTabStrip");
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            // 可能是28及以上版本
            return clazz.getDeclaredField("slidingTabIndicator");
        }
    }
    
  • 這里其實也可以不用反射,那么該怎么實現(xiàn)呢?
    • 需要注意一點,需要在Tablayout設(shè)置完成后操作,并且必須等所有繪制操作結(jié)束,使用tabLayout.post拿到屬性參數(shù),然后設(shè)置下margin。
    public void setTabWidth(TabLayout tabLayout){
        //拿到slidingTabIndicator的布局
        LinearLayout mTabStrip = (LinearLayout) tabLayout.getChildAt(0);
        //遍歷SlidingTabStrip的所有TabView子view
        for (int i = 0; i < mTabStrip.getChildCount(); i++) {
            View tabView = mTabStrip.getChildAt(i);
            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)tabView.getLayoutParams();
            //給TabView設(shè)置leftMargin和rightMargin
            params.leftMargin = dp2px(10);
            params.rightMargin = dp2px(10);
            tabView.setLayoutParams(params);
            //觸發(fā)繪制
            tabView.invalidate();
        }
    }
    

06.設(shè)置滑動改變選項卡顏色

  • 滑動時如何改變選項卡的顏色呢?當(dāng)然在滾動的時候去動態(tài)改變屬性,具體的做法:
  • 在TabLayoutOnPageChangeListener中監(jiān)聽,主要看onPageScrolled方法
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        super.onPageScrolled(position, positionOffset, positionOffsetPixels);
        CustomTabLayout tabLayout = mTabLayoutRef.get();
        if (tabLayout == null) {
            return;
        }
        final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
                mPreviousScrollState == SCROLL_STATE_DRAGGING;
        if (updateText) {
            tabLayout.tabScrolled(position, positionOffset);
        }
    }
    
  • 然后看一下tabScrolled方法,代碼如下所示
    • 這個方法里,主要是拿到當(dāng)前tabView和下一個tabView,然后依次改變Progress進度,以此達到更改文字的顏色。
    /**
     * 滑動改變自定義tabView的顏色
     * @param position                      索引
     * @param positionOffset                偏移量
     */
    private void tabScrolled(int position, float positionOffset) {
        if (positionOffset == 0.0F) {
            return;
        }
        //當(dāng)前tabView
        CustomTabView currentTrackView = getCustomTabView(position);
        //下一個tabView
        CustomTabView nextTrackView = getCustomTabView(position + 1);
        if (currentTrackView != null) {
            currentTrackView.setDirection(1);
            currentTrackView.setProgress(1.0F - positionOffset);
        }
        if (nextTrackView != null) {
            nextTrackView.setDirection(0);
            nextTrackView.setProgress(positionOffset);
        }
    }
    
  • 然后在CustomTabView中,看代碼如下所示
    • 調(diào)用invalidate()方法會調(diào)用onDraw()方法,然后去達到重繪view的目的。
    public void setProgress(float progress) {
        this.mProgress = progress;
        invalidate();
    }
    
  • 接著看看onDraw這個方法做了什么操作
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDirection == DIRECTION_LEFT) {
            drawChangeLeft(canvas);
            drawOriginLeft(canvas);
        } else if (mDirection == DIRECTION_RIGHT) {
            drawOriginRight(canvas);
            drawChangeRight(canvas);
        } else if (mDirection == DIRECTION_TOP) {
            drawOriginTop(canvas);
            drawChangeTop(canvas);
        } else if (mDirection == DIRECTION_BOTTOM){
            drawOriginBottom(canvas);
            drawChangeBottom(canvas);
        }
    }
    
  • 然后看其中的一個drawChangeLeft方法
    private void drawChangeLeft(Canvas canvas) {
        drawTextHor(canvas, mTextChangeColor, mTextStartX,  (int) (mTextStartX + mProgress * mTextWidth));
    }
    
    /**
     * 橫向
     * @param canvas                    畫板
     * @param color                     顏色
     * @param startX                    開始x
     * @param endX                      結(jié)束x
     */
    private void drawTextHor(Canvas canvas, int color, int startX, int endX) {
        mPaint.setColor(color);
        if (debug) {
            mPaint.setStyle(Style.STROKE);
            canvas.drawRect(startX, 0, endX, getMeasuredHeight(), mPaint);
        }
        canvas.save();
        canvas.clipRect(startX, 0, endX, getMeasuredHeight());
        // right, bottom
        canvas.drawText(mText, mTextStartX, getMeasuredHeight() / 2
                        - ((mPaint.descent() + mPaint.ascent()) / 2), mPaint);
        canvas.restore();
    }
    

07.使用反射的注意要點

  • 比如或者mTabStrip屬性,網(wǎng)上許多沒有區(qū)分27和28名稱的變化。如果因為名稱的問題,會導(dǎo)致反射獲取不到Field,那么所做的操作也就失效了,這是一個很大的風(fēng)險。
    /**
     * 反射獲取私有的mTabStrip屬性,考慮support 28以后變量名修改的問題
     * @return Field
     * @throws NoSuchFieldException
     */
    private Field getTabStrip() throws NoSuchFieldException {
        Class clazz = TabLayout.class;
        try {
            // support design 27及一下版本
            return clazz.getDeclaredField("mTabStrip");
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            // 可能是28及以上版本
            return clazz.getDeclaredField("slidingTabIndicator");
        }
    }
    

08.混淆時用到反射注意項

  • 還有一點就是有的人這么使用會報錯,是因為混淆產(chǎn)生的問題,反射slidingTabIndicator或者pageChangeListener的時候可能會出問題,可以在混淆配置里面設(shè)置下TabLayout不被混淆。
    -keep class android.support.design.widget.TabLayout{*;}
    

其他介紹

01.關(guān)于博客匯總鏈接

02.關(guān)于我的博客

博客匯總項目開源地址:https://github.com/yangchong211/YCBlogs

TabLayout項目開源地址:https://github.com/yangchong211/YcTabLyout

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 開源項目效果 調(diào)用實例 必練基本功 Android studio 項目導(dǎo)入依賴compile路徑 dependen...
    隨心隨性_0a25閱讀 2,511評論 1 0
  • 這篇主要要介紹Tablayout使用,因為在實際開發(fā)中TabLayout和ViewPager一起使用,所以下面主要...
    mm_cuckoo閱讀 17,403評論 2 55
  • 一、概述 它也是design中新出的一個控件,用來實現(xiàn)選項卡切換的效果,以前我們常用RadioGroup+Radi...
    Serenity那年閱讀 2,559評論 0 9
  • 夏不睡石,秋不睡板。春不露臍,冬不蒙頭。白天多動,夜里少夢。 睡前洗腳,勝吃補藥。晚上開窗,一覺都香。貪涼失蓋,不...
    凡高瀟湘花子閱讀 1,371評論 0 2
  • 接觸定投,從嚴格意義上來說,應(yīng)該可以追溯到上初中的時候給自己定下的十二存單法的方案。 鄉(xiāng)下孩子,初中就開始寄宿了,...
    正版江湖走馬閱讀 408評論 0 0

友情鏈接更多精彩內(nèi)容