TabLayout修改

文章目录

文章修修补补添加了不少,主要分3个时间段

  1. 最早实现,就是在tab切换的时候在下边画一条线
  2. 不画线了,因为看源码知道线条宽度和tabview一样的,所以反射修改TabView的宽度
  3. viewPager?.addOnPageChangeListener 根据offset,动态计算view当前的位置,线条应该偏移的位置。
  4. 进化后,想画啥画啥,不一定是线条,反正位置都算出来了。

结构分析

TabLayout修改_第1张图片
QQ截图20171026091245.png

TabLayout这个导航控件,父类关系如下
public class TabLayout extends HorizontalScrollView
public class HorizontalScrollView extends FrameLayout
里边子空间的类型
class TabView extends LinearLayout implements OnLongClickListener
private class SlidingTabStrip extends LinearLayout
tablayout的结构图

TabLayout修改_第2张图片
QQ截图20171025161612.png

大概看下源码,整体布局的添加如下,首先加了一个SlidingTabStrip也就是个线性布局.

 public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        ThemeUtils.checkAppCompatTheme(context);

        // Disable the Scroll Bar
        setHorizontalScrollBarEnabled(false);

        // Add the TabStrip
        mTabStrip = new SlidingTabStrip(context);
        super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(
                LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));

完事通过绑定viewPager或者直接addTab来添加TabView

private void addTabView(Tab tab) {
        final TabView tabView = tab.mView;
        mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());
    }

添加的tabView的params如下,根据mode和gravity设置为比重为1或是wrap

    private LinearLayout.LayoutParams createLayoutParamsForTabs() {
        final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        updateTabViewLayoutParams(lp);
        return lp;
    }
    private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
        if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
            lp.width = 0;
            lp.weight = 1;
        } else {
            lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
            lp.weight = 0;
        }
    }

至于TabView,最上边也说了也是个线性布局,垂直布局的,简单看下,默认的布局,里边添加了一个图片和一个TextView,自定义的就不说了。添加自定义的也就是把这两个默认的隐藏,完事add那个自定义的控件到TabView里而已

if (mCustomView == null) {
                // If there isn't a custom view, we'll us our own in-built layouts
                if (mIconView == null) {
                    ImageView iconView = (ImageView) LayoutInflater.from(getContext())
                            .inflate(R.layout.design_layout_tab_icon, this, false);
                    addView(iconView, 0);
                    mIconView = iconView;
                }
                if (mTextView == null) {
                    TextView textView = (TextView) LayoutInflater.from(getContext())
                            .inflate(R.layout.design_layout_tab_text, this, false);
                    addView(textView);
                    mTextView = textView;
                    mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView);
                }
                TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance);
                if (mTabTextColors != null) {
                    mTextView.setTextColor(mTabTextColors);
                }
                updateTextAndIcon(mTextView, mIconView);
            }

说了半天貌似和线条都没关系,好吧,再看下SlidingTabStrip的代码。里边有个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);
            }
        }

我们要做的就是如何修改这个线条的左右边界。下边先说下老的做法和思路

前提条件

下边都是为了修改mode=fixed,tabGravity="fill"这种,完事要求线条宽度和文字宽度差不多这种需求。
如果是mode=scollable这种,你要求文字和线条宽度一样,那么设置如下属性基本都能满足需求的

      app:tabMinWidth="2dp"
        app:tabPadding="1dp"
        app:tabPaddingStart="1dp"
        app:tabPaddingEnd="1dp"

简单分析下,一些默认属性

mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED);
mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL);//横屏的时候默认style里这个是center
        final Resources res = getResources();
        mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width);
  //添加tabview的时候会setminwidth的,就是调用如下方法获取最小宽度
  //可以看到tab是有个最小宽度的,design_tab_scrollable_min_width手机是72dp,pad之类的是160dp
    private int getTabMinWidth() {
        if (mRequestedTabMinWidth != INVALID_WIDTH) {
            // If we have been given a min width, use it
            return mRequestedTabMinWidth;
        }
        // Else, we'll use the default value
        return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0;
    }

老的做法和思路

最早比较笨的想法,既然你这线条是画出来了,那我也画一个好了,不过因为那线条左右边界是动态的,想着麻烦,就弄个固定的好了,也就是tab切换的时候才改变线条,少了滑动效果。
我们的需求是线条和文字宽度差不多,那第一步肯定是获取到文字的宽度了,文字的宽度哪来的,当然是获取到tabView里的那个TextView的宽度了。

获取方法有两种,第一种反射,第二种直接getChildAt

TabLayout修改_第3张图片
QQ截图20171025170903.png

看下Tab里的那个mView就是我们要的TabView,里边就包含有TextView
下边就是重写TabLayout,给他画条线,缺点就是线不能滑动,我们通过监听tab选中状态的改变,来invalidate这个布局刷新线条。另外因为有了自己的线条了,所以需要把TabLayout的线条高度设置为0或者线条颜色弄为透明
那个factor就是线条长度和文字宽度的比例,为1就是一样,比1大就是稍微出去一点

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        TabCheck();
        canvas.drawRect(rectF.left,getHeight()-indicatorHeight,rectF.right,getHeight(),paintLine);

    }
    RectF rectF=new RectF();
    private void TabCheck(){
        try {
            //通过反射获取那个textView
            Tab tab=getTabAt(getSelectedTabPosition());
            Field field=tab.getClass().getDeclaredField("mView");
            field.setAccessible(true);
            LinearLayout linearLayout= (LinearLayout) field.get(tab);
            /**child1就是tab上的文字控件,第一个是图片控件,第二个就是这个文本控件*/
            View child1=linearLayout.getChildAt(1);//
            float add=(factor-1)*child1.getWidth()/2;
            rectF.left=linearLayout.getLeft()+child1.getLeft()-add;
            rectF.right=linearLayout.getLeft()+child1.getRight()+add;
        } catch (Exception e) {
            e.printStackTrace();
        }
        //根据整体控件的结构,我们也能拿到那个textView
//        int selectedPosition=getSelectedTabPosition();
//        LinearLayout slidingTabStrip=(LinearLayout)getChildAt(0);
//        LinearLayout tabView= (LinearLayout) slidingTabStrip.getChildAt(selectedPosition);
//        View textView=tabView.getChildAt(1);
    }

新的做法和思路

SlidingTabStrip里有下边的代码是来计算线条左右边距的,根据viewpager的偏移量动态改变
left 和right就是对应的线条的左右点位置

 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);
        }

既然线条的宽度位置都和tabView有关,那么我们改变tabView的大小即可,默认的tabView的大小就是平分TabLayout的。
这里再强调下,我们的这个自定义只支持下边的属性,默认的就是这两个

app:tabGravity="fill"
app:tabMode="fixed"

完整的代码如下

import android.content.Context
import android.support.design.widget.TabLayout
import android.util.AttributeSet
import android.view.ViewTreeObserver
import android.widget.LinearLayout
import android.widget.TextView
import java.lang.reflect.Field

/**
 * Created by Sage on 2017/10/25.
 * Description:此控件只适用于 app:tabMode="fixed"
 */
class TabLayoutIndicatorShort : TabLayout {
    constructor(context: Context?) : super(context) {
        initSomeThing()
    }

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        initSomeThing()
    }

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        initSomeThing()
    }


    private fun initSomeThing() {
        viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                viewTreeObserver.removeOnGlobalLayoutListener(this)
                changeIndicator()
            }
        })

    }

    val factor = 1.1f
    fun changeIndicator() {
if(tabCount==0){
            return
        }
        val tabLayout:Class<*> = javaClass.superclass
        var tabStrip: Field? = null
        try {
            tabStrip = tabLayout.getDeclaredField("mTabStrip")
        } catch (e: Exception) {
            e.printStackTrace()
            return
        }
        tabStrip!!.isAccessible = true
        var ll_tab: LinearLayout? = null
        try {
            ll_tab = tabStrip.get(this) as LinearLayout
        } catch (e: Exception) {
            e.printStackTrace()
            return
        }
        /**每个tab的宽,总宽度除以tabCount*/
        val widthTab = width / tabCount
        for (i in 0..ll_tab.childCount - 1) {
            val child = ll_tab.getChildAt(i)
            child.setPadding(0, 0, 0, 0)
            try {
                val tv = (child as LinearLayout).getChildAt(1) as TextView
                var margin = ((widthTab - tv.width * factor) / 2).toInt()
                println("i==" + i + "==widthTab=" + widthTab + "==child w=" + tv.width + "==margin=" + margin)
                if (margin < 0) {
                    margin = 0
                }
                val params = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, tv.width.toFloat())
                params.leftMargin = margin
                params.rightMargin = margin
                child.setLayoutParams(params)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

    }

}

最后看下修改后的效果和原生的效果。

TabLayout修改_第4张图片
QQ截图20171026094315.png

原生的线条是平分的,点击范围也很大,我们修改后的线条是小了,可点击范围也小了。我还是喜欢原生的,可惜啊,很多时候ui设计的都是线条和文字宽度一样。如果不要求滚动的时候线条动画,我还是喜欢老的那种,直接画条线,也不影响点击范围。

最后的实现

以前懒得写啊,最近闲了,就抽空把这个实现吧
先看下效果图


TabLayout修改_第5张图片
image.png

移动到一半效果如下图


TabLayout修改_第6张图片
image.png

移动了超过一半,如下图
TabLayout修改_第7张图片
image.png

移动完成
TabLayout修改_第8张图片
image.png

简单说下思路:

隐藏掉原生画的线条【把颜色设置为透明,或者你把高度弄为0也可以】
然后给viewpager添加监听滑动的偏移量,我们来计算线条的位置
偏移量在0.5以下,
线条left位置从第一个textview的left位置移动到第一个的中心位置,
线条right位置从第一个textview的right位置移动到第二个textview的中心位置
偏移量0.5到1之间的话,
线条left位置是从第一个textview的中心位置到第二个textview的left位置
线条right位置是从第二个textview的中心位置到第二个textview的rigth位置
库一直在更新,下边的使用design库,新版的androidX也就是material库,TabLayout的tabIndicatorHeight属性默认高度没有了,它是通过一张默认图片获取的高度
所以老的代码,给个默认值,或者xml必须设置一个tabIndicatorHeight的高度才行

 indicotorHeight = a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 3)

代码如下

import android.content.Context
import android.graphics.Canvas
import android.support.design.widget.TabLayout
import android.support.v4.view.ViewPager
import android.util.AttributeSet
import android.graphics.Color
import android.graphics.Paint
import android.widget.LinearLayout
import android.graphics.RectF
import android.support.design.R
import android.support.v4.view.ViewCompat

class TabLayoutFixedFill : TabLayout {
    constructor(context: Context) : this(context, null)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr){
        initAttrs(context,attrs,defStyleAttr)
    }

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs,R.attr.tabStyle)

    private fun initAttrs(context: Context, attrs: AttributeSet?,defStyleAttr: Int) {
        val a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,
                defStyleAttr, R.style.Widget_Design_TabLayout)
        indicotorHeight = a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0)
        paintLine.color = a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0)
        a.recycle()
        setSelectedTabIndicatorColor(Color.TRANSPARENT)//隐藏掉原生画的线
    }

    var factor = 1f//线条的长度和文字宽度的比例,因为有的需求是比文字稍微长点。所以这里可以修改
    var indicotorHeight = 2;//线条的高度
    var paintLine = Paint()
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawRect(rectIndicator.left, getHeight().toFloat() - indicotorHeight, rectIndicator.right, getHeight().toFloat(), paintLine);
    }

    override fun setupWithViewPager(viewPager: ViewPager?, autoRefresh: Boolean) {
        super.setupWithViewPager(viewPager, autoRefresh)
        viewPager?.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
            override fun onPageScrollStateChanged(state: Int) {
            }

            override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
                updateIndicator(position, positionOffset)
            }

            override fun onPageSelected(position: Int) {
            }
        })

    }

    var rectIndicator = RectF()//记录下要画的线条的left和right位置
    fun updateIndicator(position: Int, positionOffset: Float) {
    if(position>=tabCount){
            return
        }
        var rectF = getTextViewRect(position)
        var rectF2 = rectF
        if (position < tabCount - 1) {
            rectF2 = getTextViewRect(position + 1)
        }
        if (positionOffset < 0.5) {
            rectIndicator.left = rectF.left + rectF.width * positionOffset //第一个最左边移动到第一个的中心位置
            rectIndicator.right = rectF.right + (rectF2.center - rectF.right) * positionOffset * 2 //移动范围,从第一个右边,移动到另一个控件的中心位置
        } else {
            rectIndicator.left = rectF2.left - (rectF2.left - rectF.center) * (1 - positionOffset) * 2 //移动范围,从第一个中心到另一个最左边
            rectIndicator.right = rectF2.left + rectF2.width * positionOffset//第二个中心点到第二个的右边
        }
        ViewCompat.postInvalidateOnAnimation(this@TabLayoutFixedFill)
    }

    /**找出某个tabview里Textview的left和right位置*/
    private fun getTextViewRect(selectedPosition: Int): ViewOption {
        var slidingTabStrip = getChildAt(0) as LinearLayout
        var tabView = slidingTabStrip.getChildAt(selectedPosition) as LinearLayout
        var textView = tabView.getChildAt(1);
        val add = (factor - 1) * textView.width / 2
        return ViewOption(tabView.left + textView.left - add, tabView.left + textView.right + add)
    }

    /**记录下view的left,right,center ,and  width*/
    data class ViewOption(var left: Float, var right: Float, var center: Float = (right + left) / 2f, var width: Float = (right - left))
}

如果有人说我没有绑定viewpager咋办,也很简单,我上边的updateIndicator方法,你可以在切换tab的时候调用这个方法也可以。或者你也可以直接用最老的那种方法,就是画条线的那个也可以。

带背景的

上边介绍的是画了一条线,其实画啥都行,只要你愿意。
看下这种效果,就是在上边的代码上稍微修改下线条的高度,弄个圆角就可以了


TabLayout修改_第9张图片
20181119_134552.gif

布局如下
设置下左右的padding,完事设置下高度。padding是高度的一半,为了实现两边半圆的效果,这样弄最简单。
不想设置高度,你自己在里边算下文本的高度,然后自己算下padding也随你了,

    

代码如下
增加了2个参数 var tabPaddingStart=0
var tabPaddingEnd=0;其实正常这两个是一样的,要不圆角就不一样拉。
然后onDraw里画个带圆角的
canvas.drawRoundRect(rect,tabPaddingStart/1f,height/2f, paintLine)

import android.content.Context
import android.graphics.Canvas
import android.support.design.widget.TabLayout
import android.support.v4.view.ViewPager
import android.util.AttributeSet
import android.graphics.Color
import android.graphics.Paint
import android.widget.LinearLayout
import android.graphics.RectF
import android.support.design.R
import android.support.v4.view.ViewCompat

class TabLayoutFixedWrap : TabLayout {
    constructor(context: Context) : this(context, null)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr){
        initAttrs(context,attrs,defStyleAttr)
    }

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs,R.attr.tabStyle)

    private fun initAttrs(context: Context, attrs: AttributeSet?,defStyleAttr: Int) {
        val a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,
                defStyleAttr, R.style.Widget_Design_TabLayout)
        paintLine.color = a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0)
        val padding = a.getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0)
        tabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart,padding)
        tabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd,padding)
        a.recycle()
        setSelectedTabIndicatorColor(Color.TRANSPARENT)
    }
    var tabPaddingStart=0
    var tabPaddingEnd=0;
    var factor = 1f//这里没啥用了
    var paintLine = Paint()
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val rect=RectF(rectIndicator.left-tabPaddingStart, 0f, rectIndicator.right+tabPaddingEnd, getHeight().toFloat())
        canvas.drawRoundRect(rect,tabPaddingStart/1f,height/2f, paintLine)
    }

    override fun setupWithViewPager(viewPager: ViewPager?, autoRefresh: Boolean) {
        super.setupWithViewPager(viewPager, autoRefresh)
        viewPager?.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
            override fun onPageScrollStateChanged(state: Int) {
            }

            override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
                updateIndicator(position, positionOffset)
            }

            override fun onPageSelected(position: Int) {
            }
        })

    }

    var rectIndicator = RectF() //记录下要画的线条的left和right位置

     fun updateIndicator(position: Int, positionOffset: Float) {
        if(position>=tabCount){
            return
        }
        var rectF = getTextViewRect(position)
        var rectF2 = rectF
        if (position < tabCount - 1) {
            rectF2 = getTextViewRect(position + 1)
        }
        if (positionOffset < 0.5) {
            rectIndicator.left = rectF.left + rectF.width * positionOffset //第一个最左边移动到第一个的中心位置
            rectIndicator.right = rectF.right + (rectF2.center - rectF.right) * positionOffset * 2 //移动范围,从第一个右边,移动到另一个控件的中心位置
        } else {
            rectIndicator.left = rectF2.left - (rectF2.left - rectF.center) * (1 - positionOffset) * 2 //移动范围,从第一个中心到另一个最左边
            rectIndicator.right = rectF2.left + rectF2.width * positionOffset//第二个中心点到第二个的右边
        }
        ViewCompat.postInvalidateOnAnimation(this@TabLayoutFixedWrap)
    }

    /**找出某个tabview里Textview的left和right位置*/
    private fun getTextViewRect(selectedPosition: Int): ViewOption {
        var slidingTabStrip = getChildAt(0) as LinearLayout
        var tabView = slidingTabStrip.getChildAt(selectedPosition) as LinearLayout
        var textView = tabView.getChildAt(1);
        val add = (factor - 1) * textView.width / 2
        return ViewOption(tabView.left + textView.left - add, tabView.left + textView.right + add)
    }

    /**记录下view的left,right,center ,and  width*/
    data class ViewOption(var left: Float, var right: Float, var center: Float = (right + left) / 2f, var width: Float = (right - left))
}

如果你需要的是那种scrollable的效果,那么添加如下的属性即可
系统默认tabview有个最小宽度的,你不设置的话,可能看到文字很少的tab宽度也很大。

        app:tabMode="scrollable"
        app:tabMinWidth="2dp"

其他一些看源码的收获

如果我们不设置tablayout的高度的话,用个warp,那么他的高度其实是固定的。
图片文字同时存在,是72dp,只有文字的话48dp
看下系统的一些默认值
tab是有最大宽度一说的,而且scroll模式下默认有个最小宽度的design_tab_scrollable_min_width

    
    264dp
    72dp
    14sp
    12sp

    

如果是pad的话,values-sw600dp目录默认如下

160dp
    

可以看到,手机默认是fill,而平板默认是center,所以如果不设置tabGravity,平板下你会发现tab不是平分宽度的,而是居中显示的。

补充

除了比较复杂的需求,如果只是要求修改选中的文字的效果,比如加粗,字体方法这些,还是可以简单实现的,还是那句话,tablayout的整体布局结构开头也都说了,那么拿到tab也就拿到了position,有了position自然能拿到那个textview控件了。

        tab_page.addOnTabSelectedListener(object :TabLayout.OnTabSelectedListener{
            override fun onTabReselected(tab: TabLayout.Tab?) {

            }

            override fun onTabUnselected(tab: TabLayout.Tab) {
                changeTextStyle(tab.position,false)
            }

            override fun onTabSelected(tab: TabLayout.Tab) {
                changeTextStyle(tab.position,true)
            }
        })

    private fun changeTextStyle(position: Int,selected:Boolean){
        var parent=tab_page.getChildAt(0) as LinearLayout
        var tabview=parent.getChildAt(position) as LinearLayout
       //tabview也可以通过反射tab的mView这个字段来获取
        var tv=tabview.getChildAt(1) as TextView
        tv.setTypeface(if(selected) Typeface.DEFAULT_BOLD else Typeface.DEFAULT)
        tv.setTextSize(if(selected) 40f else 15f)
    }

问题

需求是2个tab平分显示,可在平板上看到2个tab挤在一起了


image.png

添加如下的属性,修改下最大值,弄大点,另外游标长度默认是和文字长度一样的,下边的true可以保证和tab一样宽

app:tabIndicatorFullWidth="true"
        app:tabMaxWidth="2000dp"

你可能感兴趣的:(TabLayout修改)