catalogue

  • 01. Analysis of actual requirements encountered
  • 02. Native TabLayout limitations
  • 03.TabLayout source code analysis
    • 3.1 How to implement the Tab Tab
    • 3.2 Slide to switch the Tab Tab
    • 3.3 Tab indicates line width
  • 04. Set the custom tabView TAB
  • 05. Customize the length of the indicator
  • 06. Set slider to change TAB color
  • Tips for using reflection
  • 08. Use reflection considerations for confusion

Good news

  • Summary of blog notes [March 2016 to present], including Java basic and in-depth knowledge points, Android technology blog, Python learning notes, etc., including the summary of bugs encountered in daily development, of course, I also collected a lot of interview questions in my spare time, updated, maintained and corrected for a long time, and continued to improve… Open source files are in Markdown format! At the same time, ALSO open source life blog, from 12 years, accumulated a total of N [nearly 1 million words, gradually moved to the Internet], reprint please indicate the source, thank you!
  • Link address:Github.com/yangchong21…
  • If you feel good, you can star, thank you! Of course, also welcome to put forward suggestions, everything starts from small, quantitative change causes qualitative change!

01. Analysis of actual requirements encountered

  • A rendering of the UI in actual development
    • It is generally required that the text content and the width of the indicator line be the same
  • Use the TabLayout renderings
    • Generally, the width of the indicator line should be larger than the text content
  • Problem analysis
    • TabPaddingStart and tabPaddingEnd are set, but the layout is not used.
  • Implementation scheme
    • The first: custom TabLayout similar controls, code volume is huge, and GitHub has many more mature libraries, code quality is uneven.
    • Second: on the original basis by inheriting TabLayout control, rewrite some of the methods, and through reflection to modify some properties, can also achieve the first scheme effect.
    • Will tell myself below through the second kind of plan to achieve steps and principles!
  • Final UI renderings are displayed

02. Native TabLayout limitations

  • To understand the structure of a TabLayout
    • If I were to write it in code, it would look something like this. A TabLayout inherits from a HorizontalScrollView, and a ScrollView can only add one child View, so the SlidingTabIndicator is the horizontal LinearLayout used to add child Views.
    Public Class TabLayout extends HorizontalScrollView {private Class SlidingTabIndicator extends LinearLayout { }}Copy the code
  • Existing limitations
    • The first cannot change the width of the indicator line
    • The second one is not able to change the gradient effect of the TAB TAB by sliding.

03.TabLayout source code analysis

3.1 How to implement the Tab Tab

  • The first way is to add the TAB TAB directly through the addTab method, as shown below
    TabLayout.Tab tab = tabLayout.newTab();
    View tabView = new TextView(this);
    tabLayout.setCustomView(tabView);
    tabLayout.addTab(tab);
    Copy the code
  • Second, you can also add a TAB TAB by setting getPageTitle in the FragmentPagerAdapter, as shown below
    mTitleList.add("Rain of Xiaoxiang Swords"); 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 ""; }}}Copy the code
    • Let’s take a look at the tabLayout source code to get the contents of the getPageTitle method to set the addTab. See the populateFromPagerAdapter method in the source code. See the following code is not suddenly clear…
    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)); }}}}Copy the code
  • Either way, how do you add tabs to a SlidingTabIndicator layout?
    • As you can see in the code below, the tabView is finally added to the slidingTabIndicator layout by calling addView on the slidingTabIndicator object.
    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());
    }
    Copy the code
  • Why analyze this addTab?
    • Because of the requirement to change the tabView text color as you slide, the native TabLayout doesn’t do that. To implement this logic, you must override the TabLayout addTab method and add your own custom tabView to the TAB.

3.2 Slide to switch the Tab Tab

  • Step 1: With the color gradient of the sliding text on the page, there must be a ViewPager page listener, which is added to TabLayout when we call setupWithViewPager. So let’s take a look at how the source listening slide is implemented.
    • Binding ViewPager need only one line of code mTabLayout. SetupWithViewPager (mViewPager).
    • If viewPager is not null, remove the listener to listen for events. Then create a listener to listen and reset the state.
    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); Enclosing setScrollPosition (viewPager getCurrentItem (), 0.0 F,true);
        } else {
            this.viewPager = null;
            this.setPagerAdapter((PagerAdapter)null, false);
        }
    
        this.setupViewPagerImplicitly = implicitSetup;
    }
    Copy the code
  • So how is the sliding switch TAB and indicates the line, specific see TabLayoutOnPageChangeListener sliding to monitor the source code.
    • Look at the onPageSelected method, which switches tabs via 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);
            }
        }
    }
    Copy the code
  • Now that you know how to swipe to switch tabs, consider whether you can use reflection to use your own swipe listening events and then, in the onPageSelected method, swipe to change the color of text in the TAB, or zoom. The answer is yes.

3.3 Tab indicates line width

  • For details, see updateIndicatorPosition source code
    • You can see that the tabView gets the current slide position first, and the left and right positions if the contents are not empty.
    • When the slider slides, if more than half of the previous slider or the next slider. That means to move to the previous slider or the next slider, and then pull out left and right
    • Finally, set the position of the slider
    private void updateIndicatorPosition() {TabView View selectedTitle = this.getChildAt(this.selectedPosition); int left; int right;if(selectedTitle ! = null && selectedTitle.getwidth () > 0) {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; } // If the slider slides more than half of the previous slider or the next slider // then move to the previous slider or the next slider, then remove left and rightif(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 * (floatNextTitleLeft + (1.0f - this.selectionOffset) * (float)left);
                right = (int)(this.selectionOffset * (floatNextTitleRight + (1.0f - this.selectionOffset) * (float)right); }}else{ right = -1; left = -1; } / / set the position of the slider enclosing setIndicatorPosition (left, right); }Copy the code
  • Then take a look at the code for setIndicatorPosition
    • The width of the slider is set according to the width of the child TabView, that is, the width of the slider is the same as the width of the TabView.
    void setIndicatorPosition(int left, int right) {
        if(left ! = this.indicatorLeft || right ! = this.indicatorRight) { this.indicatorLeft = left; this.indicatorRight = right; ViewCompat.postInvalidateOnAnimation(this); }}Copy the code
  • Why analyze this?
    • Because if you want to change the width of the indicator, you must be able to dynamically change the left and right positions. With that in mind, let’s use the spacing between the left and right of the reflection Settings TAB to change the length of the indicator.

04. Realize sliding to change color

  • Slide to change indicator text color
    • TabLayout can set text content, through the above 3.2 source analysis, you can know that addTab add custom TAB, then sliding to change the color of tabView TAB, can involve listening to slide. So here need to use reflection to replace their slide listening, then in TabLayoutOnPageChangeListener onPageScrolled methods of listening in class, change the color of the tabView.
  • Find the pageChangeListener member variable in the source code by reflection, and then set violent access.
    • And then obtain TabLayoutOnPageChangeListener object, delete their own listening, and add their own custom slide to monitor the listener.
    @Override public void setupWithViewPager(@Nullable ViewPager viewPager, boolean autoRefresh) { super.setupWithViewPager(viewPager, autoRefresh); MPageChangeListener Field Field = getPageChangeListener(); field.setAccessible(true);
            TabLayoutOnPageChangeListener listener = (TabLayoutOnPageChangeListener) field.get(this);
            if(listener! =null && viewPager! = null) {/ / delete their own monitoring viewPager removeOnPageChangeListener (the listener); OnPageChangeListener mPageChangeListener = new OnPageChangeListener(this); mPageChangeListener.reset(); viewPager.addOnPageChangeListener(mPageChangeListener); } } catch (Exception e) { e.printStackTrace(); }}Copy the code
    • And then looking at the reflection code, I see a lot of blogs on the Internet that don’t distinguish between pre-27 and post-28 issues. This place must be watched!
    /** * Reflection gets the private mPageChangeListener attribute, considering variable name changes after support 28 * @returnField * @throws NoSuchFieldException */ private Field getPageChangeListener() throws NoSuchFieldException { Class clazz = TabLayout.class; // Support Design 27 and later versionsreturn clazz.getDeclaredField("mPageChangeListener"); } catch (NoSuchFieldException e) { e.printStackTrace(); // May be version 28 or laterreturn clazz.getDeclaredField("pageChangeListener"); }}Copy the code
  • Then look at the custom OnPageChangeListener
    • Using weak references to prevent listener memory leaks is a minor optimization
    /** * If the activity is in the background, or the page is closed, remove the listener * use weak reference mode to prevent listener memory leakage. Even a small optimization * / 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); } public void onPageScrollStateChanged(final int state) { mPreviousScrollState = mScrollState; mScrollState = state; } @param position index @param positionOffset offset 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); @override public void onPageSelected(int position) {public void onPageSelected(int position) { super.onPageSelected(position); CustomTabLayout tabLayout = mTabLayoutRef.get(); mPreviousScrollState = SCROLL_STATE_SETTLING; tabLayout.setSelectedView(position); } /** * resets the state */ voidreset() { mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE; }}Copy the code

05. Customize the length of the indicator

  • Modified indicator length by means of reflection, if you need any indicator width is equal to the width need to fine tune, or 28 version directly through the Settings app: tabIndicatorFullWidth = “false” attribute can make content and width of indicator.
    • The principle is to get the TabLayout field mTabStrip(before 27) or slidingTabIndicator(after 28) by reflection and then iterate over and modify the Margin value of each sub-view. The code is as follows:
    /** * Set the length of each TabLayout by reflection * @param left Margin dp * @param right Margin dp */ public voidsetIndicator(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(); }}}Copy the code
  • Then take a look at the code for reflection to get the tabStrip
    /** * Reflection gets a private mTabStrip property, considering variable names changed after support 28 * @returnField * @throws NoSuchFieldException */ private Field getTabStrip() throws NoSuchFieldException { Class clazz = TabLayout.class; // Support Design 27 and later versionsreturn clazz.getDeclaredField("mTabStrip"); } catch (NoSuchFieldException e) { e.printStackTrace(); // May be version 28 or laterreturn clazz.getDeclaredField("slidingTabIndicator"); }}Copy the code
  • You can actually do without reflection here, so how do you do that?
    • Note that you need to do this after Tablayout is set, and you must wait for all drawing operations to finish, use tabLayout.post to get the property parameters, and then set the margin.
    public void setTabWidth(TabLayout TabLayout){// Get slidingTabIndicator LinearLayout mTabStrip = (LinearLayout) tabLayout.getChildAt(0); // Walk through all TabView sub-views of SlidingTabStripfor(int i = 0; i < mTabStrip.getChildCount(); i++) { View tabView = mTabStrip.getChildAt(i); LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)tabView.getLayoutParams(); // Set TabView to leftMargin and rightMargin params.leftMargin = dp2px(10); params.rightMargin = dp2px(10); tabView.setLayoutParams(params); // Trigger drawing tabview.invalidate (); }}Copy the code

06. Set slider to change TAB color

  • How do I change the color of the tabs when I swipe? Of course, when scrolling to dynamically change the property, specific approach:
  • Listening in TabLayoutOnPageChangeListener, basically see onPageScrolled method
    @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); }}Copy the code
  • Then look at the tabScrolled method as shown below
    • In this method, we take the current tabView and the next tabView, and then change the Progress in order to change the color of the text.
    @param positionOffset offset */ private void tabScrolled(int position, rolled)float positionOffset) {
        if(positionOffset = = 0.0 F) {return; } // currentTrackView = getCustomTabView(position); CustomTabView nextTrackView = getCustomTabView(position + 1);if(currentTrackView ! = null) { currentTrackView.setDirection(1); CurrentTrackView. SetProgress (F - 1.0 positionOffset); }if (nextTrackView != null) {
            nextTrackView.setDirection(0);
            nextTrackView.setProgress(positionOffset);
        }
    }
    Copy the code
  • Then in CustomTabView, look at the code shown below
    • Calling the invalidate() method calls the onDraw() method, which then redraws the view.
    public void setProgress(float progress) {
        this.mProgress = progress;
        invalidate();
    }
    Copy the code
  • Now what does this onDraw method do
    @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); }}Copy the code
  • And then look at one of the drawChangeLeft methods
    private void drawChangeLeft(Canvas canvas) {
        drawTextHor(canvas, mTextChangeColor, mTextStartX,  (int) (mTextStartX + mProgress * mTextWidth));
    }
    
    /**
     * 横向
     * @param canvas                    画板
     * @param color                     颜色
     * @param startX                    开始x
     * @param endX                      结束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();
    }
    Copy the code

Tips for using reflection

  • For example, or the mTabStrip property, many web sites do not distinguish between 27 and 28 name changes. There is a big risk that the reflection will not get the Field because of the name, and the operation will be invalidated.
    /** * Reflection gets a private mTabStrip property, considering variable names changed after support 28 * @returnField * @throws NoSuchFieldException */ private Field getTabStrip() throws NoSuchFieldException { Class clazz = TabLayout.class; // Support Design 27 and later versionsreturn clazz.getDeclaredField("mTabStrip"); } catch (NoSuchFieldException e) { e.printStackTrace(); // May be version 28 or laterreturn clazz.getDeclaredField("slidingTabIndicator"); }}Copy the code

08. Use reflection considerations for confusion

  • You can also set the TabLayout in the obfuscation configuration to avoid obfuscation. If the TabLayout is not obfuscation, you can set the TabLayout in the obfuscation configuration to avoid obfuscation.
    -keep class android.support.design.widget.TabLayout{*; }Copy the code

The other is introduced

01. About blog summary links

  • 1. Tech blog round-up
  • 2. Open source project summary
  • 3. Life Blog Summary
  • 4. Himalayan audio summary
  • 5. Other summaries

02. About my blog

  • Github:github.com/yangchong21…
  • Zhihu: www.zhihu.com/people/yczb…
  • Jane: www.jianshu.com/u/b7b2c6ed9…
  • csdn:my.csdn.net/m0_37700275
  • The Himalayan listening: www.ximalaya.com/zhubo/71989…
  • Source: China my.oschina.net/zbj1618/blo…
  • Soak in the days of online: www.jcodecraeer.com/member/cont.
  • Email address: [email protected]
  • Blog: ali cloud yq.aliyun.com/users/artic… 239.headeruserinfo.3.dT4bcV
  • Segmentfault headline: segmentfault.com/u/xiangjian…
  • The Denver nuggets: juejin. Cn/user / 197877…

Blog summary project open source address:Github.com/yangchong21…

TabLayout projectGithub.com/yangchong21…