美文网首页
Android TabLayout

Android TabLayout

作者: 没有颜色的菜 | 来源:发表于2019-02-21 15:36 被阅读0次

前言

很久很久没写过源码解析了,不是自己没有看了,只是没有记录了,却发现不记录的话,似懂非懂,时间久了就忘得差不多了,用到了还是得再学一遍,忍住提笔一篇 TabLayout 源码学习。

Hello World

依赖

添加 support design 包

implementation 'com.android.support:design:27.1.1'
xml

添加一个 TabLayout 就可以了

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TabLayoutActivity">
    
    <android.support.design.widget.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</android.support.constraint.ConstraintLayout>
MainActivity.java

import android.support.design.widget.TabLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class TabLayoutActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab_layout);

        TabLayout mTabLayout = findViewById(R.id.tab_layout);
        // 添加 tab item
        mTabLayout.addTab(mTabLayout.newTab().setText("TAB1"));
        mTabLayout.addTab(mTabLayout.newTab().setText("TAB2"));
        mTabLayout.addTab(mTabLayout.newTab().setText("TAB3"));
        mTabLayout.addTab(mTabLayout.newTab().setText("TAB4"));
    }
}
效果
Screenshot_1550732046.png

源码学习

其实,实现这样一个布局并不难,让我们来看看里面所有的内容

前世今生

继承自 HorizontalScrollView 因为他支持滚动

public class TabLayout extends HorizontalScrollView 

TabLayout 支持两种模式,一种是固定的,一种是可滚动的(tab 太多,一屏显示不下,可使用这种模式,否则默认为平分)

    /**
     * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab
     * labels and a larger number of tabs. They are best used for browsing contexts in touch
     * interfaces when users don’t need to directly compare the tab labels.
     *
     * @see #setTabMode(int)
     * @see #getTabMode()
     */
    public static final int MODE_SCROLLABLE = 0;

    /**
     * Fixed tabs display all tabs concurrently and are best used with content that benefits from
     * quick pivots between tabs. The maximum number of tabs is limited by the view’s width.
     * Fixed tabs have equal width, based on the widest tab label.
     *
     * @see #setTabMode(int)
     * @see #getTabMode()
     */
    public static final int MODE_FIXED = 1;

    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Mode {}

Tab 的位置有两种,一种是居中,一种是平分

    /**
     * Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect
     * when used with {@link #MODE_FIXED}.
     *
     * @see #setTabGravity(int)
     * @see #getTabGravity()
     */
    public static final int GRAVITY_FILL = 0;

    /**
     * Gravity used to lay out the tabs in the center of the {@link TabLayout}.
     *
     * @see #setTabGravity(int)
     * @see #getTabGravity()
     */
    public static final int GRAVITY_CENTER = 1;

    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface TabGravity {}

居中模式

屏幕快照 2019-02-21 下午3.07.13.png

创建 Tab

使用代码创建 Tab

    public Tab newTab() {
        Tab tab = sTabPool.acquire();
        if (tab == null) {
            tab = new Tab();
        }
        tab.mParent = this;
        tab.mView = createTabView(tab);
        return tab;
    }

Tab 还使用了 Pool,还是挺细心的

    private static final Pools.Pool<Tab> sTabPool = new Pools.SynchronizedPool<>(16);
可滑动的指示条形图

自定义 ViewGroup

    private class SlidingTabStrip extends LinearLayout 
onMeasure

如果设置了 MODE_FIXED 和 GRAVITY_CENTER 则需要重新测量,目的就是让居中,每个 ITEM 的宽度都是一样的,而且等于最大的一个,如果一屏放得下则需要重新设置每个 ITEM 的大小,并且重新测量。如果发不下,那么侧设置GRAVITY_FILL

@Override
        protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);

            if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
                // HorizontalScrollView will first measure use with UNSPECIFIED, and then with
                // EXACTLY. Ignore the first call since anything we do will be overwritten anyway
                return;
            }
            // 重新测量
            if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
                final int count = getChildCount();

                // First we'll find the widest tab
                int largestTabWidth = 0;
                for (int i = 0, z = count; i < z; i++) {
                    View child = getChildAt(i);
                    if (child.getVisibility() == VISIBLE) {
                        largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
                    }
                }

                if (largestTabWidth <= 0) {
                    // If we don't have a largest child yet, skip until the next measure pass
                    return;
                }
                // 间隔
                final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
                boolean remeasure = false;
                // 一屏放得下
                if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
                    // If the tabs fit within our width minus gutters, we will set all tabs to have  
                    // the same width
                    for (int i = 0; i < count; i++) {
                        final LinearLayout.LayoutParams lp =
                                (LayoutParams) getChildAt(i).getLayoutParams();
                        if (lp.width != largestTabWidth || lp.weight != 0) {
                            lp.width = largestTabWidth;
                            lp.weight = 0;
                            remeasure = true;
                        }
                    }
                } else {
                    // If the tabs will wrap to be larger than the width minus gutters, we need
                    // to switch to GRAVITY_FILL
                    mTabGravity = GRAVITY_FILL;
                    updateTabViews(false);
                    remeasure = true;
                }

                if (remeasure) {
                    // Now re-measure after our changes
                    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                }
            }
        }

如何实现动画?

通过移动 IndicatorView

onLayout

mIndicatorAnimator 是动画辅助类,在 onLayout 中,非空而且正在运行则看取消,然后调用 animateIndicatorToPosition,动画调用,否则直接设置位置,不支持动画

    @Override                                                                               
    protected void onLayout(boolean changed, int l, int t, int r, int b) {                  
        super.onLayout(changed, l, t, r, b);                                                
                                                                                            
        if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {                 
            // If we're currently running an animation, lets cancel it and start a          
            // new animation with the remaining duration                                    
            mIndicatorAnimator.cancel();                                                    
            final long duration = mIndicatorAnimator.getDuration();                         
            animateIndicatorToPosition(mSelectedPosition,                                   
                    Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration));
        } else {                                                                            
            // If we've been layed out, update the indicator position                       
            updateIndicatorPosition();                                                      
        }                                                                                   
    } 

updateIndicatorPosition 首先获取选中的 View,然后看 mSelectionOffset 是否大于零,说明发生滚动,则需要重新计算新位置

    private void updateIndicatorPosition() {                                       
        final View selectedTitle = getChildAt(mSelectedPosition);                  
        int left, right;                                                           
                                                                                   
        if (selectedTitle != null && selectedTitle.getWidth() > 0) {               
            left = selectedTitle.getLeft();                                        
            right = selectedTitle.getRight();                                      
                                                                                   
            if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
                // Draw the selection partway between the tabs                     
                View nextTitle = getChildAt(mSelectedPosition + 1);                
                left = (int) (mSelectionOffset * nextTitle.getLeft() +             
                        (1.0f - mSelectionOffset) * left);                         
                right = (int) (mSelectionOffset * nextTitle.getRight() +           
                        (1.0f - mSelectionOffset) * right);                        
            }                                                                      
        } else {                                                                   
            left = right = -1;                                                     
        }                                                                          
                                                                                   
        setIndicatorPosition(left, right);                                         
    } 
    // mIndicatorLeft 和 mIndicatorRight 控制了线的起始位置
    void setIndicatorPosition(int left, int right) {                
        if (left != mIndicatorLeft || right != mIndicatorRight) {   
            // If the indicator's left/right has changed, invalidate
            mIndicatorLeft = left;                                  
            mIndicatorRight = right;                                
            ViewCompat.postInvalidateOnAnimation(this);             
        }                                                           
    }                                                               

移动动画,移动间隔大的话,并不会从当前位置直接移动,而是跳跃一段距离再移动,通过 startLeft 和 startRight 控制,并且使用 ValueAnimator 来实现动画
这个不错,同意了 fraction 0-1 ,通过函数计算进度

setIndicatorPosition(                                           
        AnimationUtils.lerp(startLeft, targetLeft, fraction),   
        AnimationUtils.lerp(startRight, targetRight, fraction));
   void animateIndicatorToPosition(final int position, int duration) {              
       if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {          
           mIndicatorAnimator.cancel();                                             
       }                                                                            
                                                                                    
       final boolean isRtl = ViewCompat.getLayoutDirection(this)                    
               == ViewCompat.LAYOUT_DIRECTION_RTL;                                  
                                                                                    
       final View targetView = getChildAt(position);                                
       if (targetView == null) {                                                    
           // If we don't have a view, just update the position now and return      
           updateIndicatorPosition();                                               
           return;                                                                  
       }                                                                            
                                                                                    
       final int targetLeft = targetView.getLeft();                                 
       final int targetRight = targetView.getRight();                               
       final int startLeft;                                                         
       final int startRight;                                                        
                                                                                    
       if (Math.abs(position - mSelectedPosition) <= 1) {                           
           // If the views are adjacent, we'll animate from edge-to-edge            
           startLeft = mIndicatorLeft;                                              
           startRight = mIndicatorRight;                                            
       } else {                                                                     
           // Else, we'll just grow from the nearest edge                           
           final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET);                   
           if (position < mSelectedPosition) {                                      
               // We're going end-to-start                                          
               if (isRtl) {                                                         
                   startLeft = startRight = targetLeft - offset;                    
               } else {                                                             
                   startLeft = startRight = targetRight + offset;                   
               }                                                                    
           } else {                                                                 
               // We're going start-to-end                                          
               if (isRtl) {                                                         
                   startLeft = startRight = targetRight + offset;                   
               } else {                                                             
                   startLeft = startRight = targetLeft - offset;                    
               }                                                                    
           }                                                                        
       }                                                                            
       // 开始移动位置                                                                             
       if (startLeft != targetLeft || startRight != targetRight) {                  
           ValueAnimator animator = mIndicatorAnimator = new ValueAnimator();       
           animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);  
           animator.setDuration(duration);                                          
           animator.setFloatValues(0, 1);                                           
           animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
               @Override                                                            
               public void onAnimationUpdate(ValueAnimator animator) {              
                   final float fraction = animator.getAnimatedFraction();           
                   setIndicatorPosition(                                            
                           AnimationUtils.lerp(startLeft, targetLeft, fraction),    
                           AnimationUtils.lerp(startRight, targetRight, fraction)); 
               }                                                                    
           });                                                                      
           animator.addListener(new AnimatorListenerAdapter() {                     
               @Override                                                            
               public void onAnimationEnd(Animator animator) {                      
                   mSelectedPosition = position;                                    
                   mSelectionOffset = 0f;                                           
               }                                                                    
           });                                                                      
           animator.start();                                                        
       }                                                                            
   }                                                                                

onDraw 很简单

    @Override                                                                          
    public void draw(Canvas canvas) {                                                  
        super.draw(canvas);                                                            
                                                                                       
        // Thick colored underline below the current selection                         
        if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {                 
            canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,    
                    mIndicatorRight, getHeight(), mSelectedIndicatorPaint);            
        }                                                                              
    }                                                                                  

TabView

接下来再看看上面的内容

  class TabView extends LinearLayout {
        private Tab mTab;                  
        private TextView mTextView;        
        private ImageView mIconView;       
                                           
        private View mCustomView;          
        private TextView mCustomTextView;  
        private ImageView mCustomIconView; 
                                           
        private int mDefaultMaxLines = 2;  
  }

如何实现监听的,对每个 TabView 设置点击事件,重写了 performClick,其中调用 mTab.select,mTab 拥有 TabLayout 的引用,

    @Override                                               
    public boolean performClick() {                         
        final boolean handled = super.performClick();       
                                                            
        if (mTab != null) {                                 
            if (!handled) {                                 
                playSoundEffect(SoundEffectConstants.CLICK);
            }                                               
            mTab.select();                                  
            return true;                                    
        } else {                                            
            return handled;                                 
        }                                                   
    }                                                       
    /**                                                                             
     * Select this tab. Only valid if the tab has been added to the action bar.     
     */                                                                             
    public void select() {                                                          
        if (mParent == null) {                                                      
            throw new IllegalArgumentException("Tab not attached to a TabLayout");  
        }                                                                           
        mParent.selectTab(this);                                                    
    }                                                                                                                                   

tabLayout 回调事件,

    void selectTab(Tab tab) {                                                              
        selectTab(tab, true);                                                              
    }                                                                                      
                                                                                           
    void selectTab(final Tab tab, boolean updateIndicator) {                               
        final Tab currentTab = mSelectedTab;                                               
                                                                                           
        if (currentTab == tab) {                                                           
            if (currentTab != null) {                                                      
                dispatchTabReselected(tab);                                                
                animateToTab(tab.getPosition());                                           
            }                                                                              
        } 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) {                          
                    // 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;                                                            
            if (tab != null) {                                                             
                dispatchTabSelected(tab);                                                  
            }                                                                              
        }                                                                                  
    }                                                                                      

移动 Tab,动画

    private void animateToTab(int newPosition) {                                        
        if (newPosition == Tab.INVALID_POSITION) {                                      
            return;                                                                     
        }                                                                               
                                                                                        
        if (getWindowToken() == null || !ViewCompat.isLaidOut(this)                     
                || mTabStrip.childrenNeedLayout()) {                                    
            // If we don't have a window token, or we haven't been laid out yet just dra
            // position now                                                             
            setScrollPosition(newPosition, 0f, true);                                   
            return;                                                                     
        }                                                                               
                                                                                        
        final int startScrollX = getScrollX();                                          
        final int targetScrollX = calculateScrollXForTab(newPosition, 0);               
                                                                                        
        if (startScrollX != targetScrollX) {                                            
            ensureScrollAnimator();                                                     
                                                                                        
            mScrollAnimator.setIntValues(startScrollX, targetScrollX);                  
            mScrollAnimator.start();                                                    
        }                                                                               
                                                                                        
        // Now animate the indicator                                                    
        mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);          
    }                                                                                   

计算移动的距离,让选中的 tab 位于中间位置,由于 Android ScrollView 默认不会滚动超出边界,所以如果到达边界也不会继续滚动了

    private int calculateScrollXForTab(int position, float positionOffset) {                                  
        if (mMode == MODE_SCROLLABLE) {                                                                       
            final View selectedChild = mTabStrip.getChildAt(position);                                        
            final View nextChild = position + 1 < mTabStrip.getChildCount()                                   
                    ? mTabStrip.getChildAt(position + 1)                                                      
                    : null;                                                                                   
            final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;                   
            final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;                               
                                                                                                              
            // base scroll amount: places center of tab in center of parent                                   
            int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);                
            // offset amount: fraction of the distance between centers of tabs                                
            int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);                   
                                                                                                              
            return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR)                   
                    ? scrollBase + scrollOffset                                                               
                    : scrollBase - scrollOffset;                                                              
        }                                                                                                     
        return 0;                                                                                             
    }                                                                                                         

ViewPager

其实很简单,就是给 ViewPager 添加一个 OnPageChangeListener 就行了,代码也很简单,在 onPageScrolled 中改变 指示条 的位置,在 onPageSelected 中改变 选中状态

    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;
        }
    }
小结

基本上看完了,但对于一些细节,滚动边界问题还没有深刻的理解,只知道大概的逻辑

相关文章

网友评论

      本文标题:Android TabLayout

      本文链接:https://www.haomeiwen.com/subject/dgiryqtx.html