AndroidX TabLayout使用、扩展及解析All In One

14,239 阅读7分钟

前言

TabLayout仍旧是移动端比较常用的一个控件,这里分析一下TabLayout,分别从下面几个方面进行解析:

  1. 基本构成及使用TabLayout
  2. 原理解析
  3. 开发扩展

1.TabLayout的引用变化

先看下支持库指南。之前的TabLayout是在support中使用,新的引用全部放到老AndroidX中

支持库指南

使用老的库需要用

implementation 'com.android.support:design:28.0.0'

Android 支持库的最新版本是28.0.0,这是最后一个google发布的支持库版本,现在google已将所有support包下的库都迁移至androidx包下面,以后的更新都只会在androidx包中进行。具体对照表点击进入迁移说明

迁移及工程说明

这些变动是由于android的jetpack项目,意在帮助开发者快速实现应用开发,将一些常用的框架都整合进来了。

官网详细介绍

使用新的库需要引用:

implementation 'androidx.appcompat:appcompat:1.0.2'

TabLayout类的继承关系:

java.lang.Object

android.view.View

android.view.ViewGroup

android.widget.FrameLayout

android.widget.HorizontalScrollView

com.google.android.material.tabs.TabLayout

2.基本功能及使用

TabLayout继承自 HorizontalScrollView

控件的基本层次关系

2.1 代码添加tab

TabLayout提供了用于显示选项卡的水平布局。要显示的选项卡的填充是通过TabLayout.Tab实例完成的。可以通过创建标签 newTab()。在此处,您可以分别通过setText(int) 和更改选项卡的标签或图标setIcon(int)。要显示选项卡,需要通过一种addTab(Tab)方法将其添加到布局中。例如:

 TabLayout tabLayout = ...;
 tabLayout.addTab(tabLayout.newTab()。setText(“ Tab 1”));
 tabLayout.addTab(tabLayout.newTab()。setText(“ Tab 2”));
 tabLayout.addTab(tabLayout.newTab()。setText(“ Tab 3”));

应该添加一个监听器,addOnTabSelectedListener(OnTabSelectedListener)以在任何选项卡的选择状态更改时得到通知。

2.2 xml配置tab

还可以通过使用将项目添加到布局中的TabLayout TabItem。用法示例如下:

 <com.google.android.material.tabs.TabLayout
         android:layout_height =“ wrap_content”
         android:layout_width =“ match_parent”>

     <com.google.android.material.tabs.TabItem
             android:text =“ @ string / tab_text” />

     <com.google.android.material.tabs.TabItem
             android:icon =“ @ drawable / ic_android” />

 </com.google.android.material.tabs.TabLayout>

2.3 tab配置viewpager

如果ViewPager将此布局与一起使用,则可以调用setupWithViewPager(ViewPager)将两者链接在一起。该版式将从PagerAdapter的页面标题中自动填充。

此视图还支持用作ViewPager装饰的一部分,并且可以像这样在布局资源文件中直接添加到ViewPager:

 <androidx.viewpager.widget.ViewPager
     android:layout_width =“ match_parent”
     android:layout_height =“ match_parent”>

     <com.google.android.material.tabs.TabLayout
         android:layout_width =“ match_parent”
         android:layout_height =“ wrap_content”
         android:layout_gravity =“ top” />

 </androidx.viewpager.widget.ViewPager>

3.使用详解

这里很多人都使用的都是design 28,主工程的gradle的配置根据不同情况改。

工程库引用

上面三种使用方法,我们使用新的库androidx看下使用的效果图:

基础库使用效果

3.1 使用扩展

这里列举一下主要使用到到属性,只列举几个,具体可看官方文档。

属性

说明

tabMode

scrollable/fixed

tab是水平可滚动的还是固定宽,个数较少的时候可以使用fixed,如果标签超出了屏幕范围,设置为scrollable比较好

tabGravity

fill/center

tab的布局是布满空间还是居中

tabIndicatorHeight

(dp/pix)

底部滑动线条的高度

tabMaxWidth

(dp/pix)

Tab的最大宽度

tabTextColor

颜色值

默认样式未选中颜色

app:tabSelectedTextColor

颜色值

选中颜色

3.2 典型的使用场景

(1)代码添加tab

  //TabLayout的基本使用
        for(int i=0;i<mTitles.length;i++){
            TabLayout.Tab tab=mTabLayout.newTab();
            tab.setTag(i);
            tab.setText(mTitles[i]);
            mTabLayout.addTab(tab);
        }

(2)不需要指示器

    属性设置
    app:tabIndicatorHeight="0dp" 

有时候想指示器的宽度小一些,可以参考文章Tablayout使用全解,一篇就够了 修改指示线长度(利用的反射,感觉不如自己基于源码封装一个,可以自定义长度)。

(3)添加图标

TabItem有个上下结构的默认布局,这里使用默认的

tabLayout1.addTab(tabLayout1.newTab().setText("Tab 4").setIcon(R.mipmap.ic_launcher));

(4)加入Padding

设置Tab内部的子控件的Padding:

    app:tabPadding="xxdp"
    app:tabPaddingTop="xxdp"
    app:tabPaddingStart="xxdp"
    app:tabPaddingEnd="xxdp"
    app:tabPaddingBottom="xxdp"

设置整个TabLayout的Padding:

    app:paddingEnd="xxdp"
    app:paddingStart="xxdp"

(5)Tab的宽度限制

设置最大的tab宽度:

    app:tabMaxWidth="xxdp"

设置最小的tab宽度:

    app:tabMinWidth="xxdp"

(6)Tab的“Margin”

TabLayout开始位置的偏移量:

    app:tabContentStart="50dp"

(7)Tab默认选中

如果你要设置默认选中第三项,你可以这样:

    mTabLayout.getTabAt(2).select();

(8)监听事件

初始化进入的时候,监听事件的三个方法都不会执行

tabLayout1.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
    @Override
    public void onTabSelected(TabLayout.Tab tab) {
        //选中了tab的逻辑
        Log.i(TAG, "======我选中了===="+tab.getTag());
    }

    @Override
    public void onTabUnselected(TabLayout.Tab tab) {
        //未选中tab的逻辑
        Log.i(TAG, "======我未被选中===="+tab.getTag());
    }

    @Override
    public void onTabReselected(TabLayout.Tab tab) {
        //再次选中tab的逻辑
        Log.i(TAG, "======我再次被选中===="+tab.getTag());
    }
});

onTabReselected为已经选中的tab再次点击会走到。

(9)判读是否选中

mTabLayout.getTabAt(position).isSelected()

有时候要监听某个Tab的点击事件,可以参考TabLayout基本属性全解 或者 tablayout增加选择tab 的事件和重写tab点击事件

(10)自定义Tab布局

这里有两种方式添加TabItem的自定义布局,其一种方式是在TabItem的xml中定义

<com.google.android.material.tabs.TabItem
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:icon="@drawable/two"
    android:layout="@layout/custom_indicator1"
    android:text="娱乐" />
custom_indicator1.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:id="@android:id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        />
    <ImageView
        android:id="@android:id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        />
</LinearLayout>

custom_indicator1.xml文件内容,值得注意的是这里的TextView的id必须是“@android:id/text1”,ImageView的id必须是“@android:id/icon”,原因来自于与TabLayout的源码中TabView的update方法。

这种方式只能事先确定有几个Tab的时候用到,当这个Tab个数需要动态的创建的时候不能使用此方法。

另外一种方式通过代码动态设置布局,布局的选中和未选中态的更新采用监听器动态修改的方式。

/**
 * 设置Tab的样式
 */
private void setTabView() {
    holder = null;
    for (int i = 0; i < tabs.size(); i++) {
        //依次获取标签
        TabLayout.Tab tab = tabLayout.getTabAt(i);
        //为每个标签设置布局
        tab.setCustomView(R.layout.tab_item);
        holder = new ViewHolder(tab.getCustomView());
        //为标签填充数据
        holder.tvTabName.setText(tabs.get(i));
        holder.tvTabNumber.setText(String.valueOf(tabNumbers.get(i)));
        //默认选择第一项
        if (i == 0){
            holder.tvTabName.setSelected(true);
            holder.tvTabNumber.setSelected(true);
            holder.tvTabName.setTextSize(18);
            holder.tvTabNumber.setTextSize(18);
        }
    }

    //tab选中的监听事件
    tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            holder = new ViewHolder(tab.getCustomView());
            holder.tvTabName.setSelected(true);
            holder.tvTabNumber.setSelected(true);
            //选中后字体变大
            holder.tvTabName.setTextSize(18);
            holder.tvTabNumber.setTextSize(18);
            //让Viewpager跟随TabLayout的标签切换
            viewPager.setCurrentItem(tab.getPosition());

        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {
            holder = new ViewHolder(tab.getCustomView());
            holder.tvTabName.setSelected(false);
            holder.tvTabNumber.setSelected(false);
            //恢复为默认字体大小
            holder.tvTabName.setTextSize(16);
            holder.tvTabNumber.setTextSize(16);
        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {

        }
    });
}

class ViewHolder{
    TextView tvTabName;
    TextView tvTabNumber;

    public ViewHolder(View tabView) {
        tvTabName = (TextView) tabView.findViewById(R.id.tv_tab_name);
        tvTabNumber = (TextView) tabView.findViewById(R.id.tv_tab_number);
    }
}

有一些写的比较好的文章分享了一些比较高级的功能。如,TabLayout的简单运用和若干问题的解决。

这篇中介绍了怎么加分割线,设置原有字体大小,自定义标签等。

在源码中可以看到再newTab中,customView的的创建。

@NonNull
public Tab newTab() {
    Tab tab = sTabPool.acquire();
    if (tab == null) {
        tab = new Tab();
    }
    tab.mParent = this;
    tab.mView = createTabView(tab);
    if (mViewPagerTabEventListener != null) {
        tab.setCustomView(mViewPagerTabEventListener.onCreateTab(tab.mView));
    }
    return tab;
}

4.源码分析

最新的TabLayout源码可以通过下面的地址中看到,看到Google是由专门的material设计和工程团队负责此库。

github.com/material-co…

先看下整体的类关系图

结构图
  • (1)整体的构成

TabLayout继承HorizontalScrollView是一个横向滚动的ViewGroup,他包含一个子View(只能包含一个)SlidingTabStrip。

SlidingTabStrip为一个LinearLayout为TabLayout的内部类。所有的TabView都是它的子View.

TabView继承于LinearLayout,以Tab为数据源,来展示Tab的样式。最终用for循环被add进SlidingTabStrip

Tab是一个简单的View Model实体类,控制TabView的title, icon, custom layout id等属性。

TabItem继承于View. 用于在layout xml中来描述Tab. 需要注意的是,它不会add到SlidingTabStrip中去。它的作用是从xml中获取到text,icon,custom layout id等属性。TabLayout inflate到TabItem并获取属性到装配到Tab中,最终add到SlidingTabStrip中的还是TabView.

  • (2)Tab的创建

这里调newTab()方法创建了一个tab对象,并且用对象池把创建的tab对象缓存起来。然后将TabItem对象的属性都赋值给tab对象。在createTabView(Tab tab)这个方法中,首先从TabView池中获取TabView对象,如果不存在,则实例化一个对象,并调用tabView.setTab(tab)方法来进行了数据绑定。

private static final Pools.Pool<Tab> sTabPool = new Pools.SynchronizedPool<Tab>(16);
public Tab newTab() {
    Tab tab = sTabPool.acquire();
    if (tab == null) {
        tab = new Tab();
    }
    tab.mParent = this;
    tab.mView = createTabView(tab);
    if (mViewPagerTabEventListener != null) {
        tab.setCustomView(mViewPagerTabEventListener.onCreateTab(tab.mView));
    }
    return tab;
}
private TabView createTabView(@NonNull final Tab tab) {
    TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null;
    if (tabView == null) {
        tabView = new TabView(getContext());
    }
    tabView.setTab(tab);
    tabView.setFocusable(true);
    tabView.setMinimumWidth(getTabMinWidth());
    return tabView;
}
  • (3)TabLayout的TabView协同滚动

mTabStrip本身在HorizonScrollView中,所以直接通过scrollTo方法即可实现滚动的操作,这里只需要计算位置即可。

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 selectedLeft = selectedChild != null ? selectedChild.getLeft() : 0;
        final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;

        // base scroll amount: places center of tab in center of parent
        int scrollBase = selectedLeft + (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;
}
  • (4)ViewPager的绑定是通过一些监听器实现,这里代码不复杂,不详细说明。可参见引用文章。

5.引用文章

[1] 官方介绍

[2] MaterialDesign--(7)TabLayout的使用及其源码分析

[3] TabLayout基本属性全解

[4] Tablayout使用全解,一篇就够了

[5] TabLayout的简单运用和若干问题的解决

[6] MaterialDesign之对TabLayout的探索

[7] https://github.com/itgoyo/AndroidSource-Analysis/blob/master/chapter1/TabLayout%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md