TabLayout的基本使用
以前我们是用TabHost来实现Tab切换的效果。现在谷歌推荐使用TabLayout。
下面以TabLayout+ViewPager+Fragment为例,讲述TabLayout的基本使用。
布局文件如下面所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <android.support.design.widget.TabLayout android:id="@+id/tablayout" android:layout_width="match_parent" android:layout_height="wrap_content" app:tabGravity="center" app:tabIndicatorColor="#4ce91c" app:tabMode="scrollable" app:tabSelectedTextColor="#4ce91c" app:tabTextColor="#ccc" app:tabIndicatorHeight="5dp" /> <android.support.v4.view.ViewPager android:id="@+id/vp" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
其中,需要关注的属性有:
app:tabIndicatorColor="@color/colorPrimary_pink"//指示器的颜色 app:tabTextColor="@color/colorPrimary_pink"//tab的文字颜色 app:tabSelectedTextColor="@color/colorPrimary_pinkDark"//选中的tab的文字颜色 app:tabMode="fixed"//scrollable:可滑动;fixed:不能滑动,平分tabLayout宽度 app:tabGravity="center"// fill:tab平均填充整个宽度;center:tab居中显示
需要切换的Fragment,为了方便,我们重用一个Fragment:
public class NewsDetailFragment extends Fragment { @Override @Nullable public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { TextView tv = new TextView(getContext()); Bundle bundle = getArguments(); String title = bundle.getString("title"); tv.setBackgroundColor(Color.rgb((int)(Math.random()*255), (int)(Math.random()*255), (int)(Math.random()*255))); tv.setText(title); return tv; } }
Activity的代码:
public class TabLayoutActivity extends AppCompatActivity { private TabLayout tabLayout; private String[] title = { "头条", "新闻", "娱乐", "体育", "科技", "美女", "财经", "汽车", "房子", "头条" }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_tab_layout); final ViewPager viewPager = (ViewPager) findViewById(R.id.vp); tabLayout = (TabLayout) findViewById(R.id.tablayout); MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager()); //1.TabLayout和Viewpager关联 // tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { // // @Override // public void onTabUnselected(TabLayout.Tab arg0) { // // } // // @Override // public void onTabSelected(TabLayout.Tab tab) { // // 被选中的时候回调 // viewPager.setCurrentItem(tab.getPosition(), true); // } // // @Override // public void onTabReselected(TabLayout.Tab tab) { // // } // }); //2.ViewPager滑动关联tabLayout // viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout)); //设置tabLayout的标签来自于PagerAdapter // tabLayout.setTabsFromPagerAdapter(adapter); //设置tabLayout的标签来自于PagerAdapter tabLayout.setupWithViewPager(viewPager); viewPager.setAdapter(adapter); //设置Indicator的左右间距(Indicator的宽度) setIndicator(this, tabLayout, 15, 15); } class MyPagerAdapter extends FragmentPagerAdapter { public MyPagerAdapter(FragmentManager fm) { super(fm); } @Override public CharSequence getPageTitle(int position) { return title[position]; } @Override public Fragment getItem(int position) { Fragment f = new NewsDetailFragment(); Bundle bundle = new Bundle(); bundle.putString("title", title[position]); f.setArguments(bundle); return f; } @Override public int getCount() { return title.length; } } //下面三个方法是设置Indicator public static void setIndicator(Context context, TabLayout tabs, int leftDip, int rightDip) { Class<?> tabLayout = tabs.getClass(); Field tabStrip = null; try { tabStrip = tabLayout.getDeclaredField("mTabStrip"); } catch (NoSuchFieldException e) { e.printStackTrace(); } tabStrip.setAccessible(true); LinearLayout ll_tab = null; try { ll_tab = (LinearLayout) tabStrip.get(tabs); } catch (IllegalAccessException e) { e.printStackTrace(); } int left = (int) (getDisplayMetrics(context).density * leftDip); int right = (int) (getDisplayMetrics(context).density * rightDip); for (int i = 0; i < ll_tab.getChildCount(); i++) { View child = ll_tab.getChildAt(i); child.setPadding(0, 0, 0, 0); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1); params.leftMargin = left; params.rightMargin = right; child.setLayoutParams(params); child.invalidate(); } } public static DisplayMetrics getDisplayMetrics(Context context) { DisplayMetrics metric = new DisplayMetrics(); ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(metric); return metric; } public static float getPXfromDP(float value, Context context) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, context.getResources().getDisplayMetrics()); } }
代码比较简单,需要注意的是,新提供的tabLayout.setupWithViewPager(viewPager);方法代替了注释中的3个方法了,其实内部做的事都是一样的。TabLayout默认没有提供修改Indicator宽度的函数,需要我们通过反射的方式去设置。
用TabLayout实现底部导航(相对于传统的TabHost,它是可滑动的)
只需要三个步骤:
在布局中就把TabLayout放在布局底部
去掉底部的indicator,app:tabIndicatorHeight="0dp"
实现自己的效果,自定义的标签布局
代码如下:
for (int i = 0; i < tabLayout.getTabCount(); i++) { TabLayout.Tab tab = tabLayout.getTabAt(i); tab.setCustomView(view); }
TabLayout源码分析
TabLayout是可以水平滑动的,因此继承了HorizontalScrollView
public class TabLayout extends HorizontalScrollView { }
TabLayout里面有那么多小tab,tab有分为是否填充,是否可滑动,那么测量的时候就会很复杂:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //推荐的高度 final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom(); switch (MeasureSpec.getMode(heightMeasureSpec)) { case MeasureSpec.AT_MOST: //最大值模式下,最终的高度是推荐的高度与所给的高度的最小值 heightMeasureSpec = MeasureSpec.makeMeasureSpec( Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), MeasureSpec.EXACTLY); break; case MeasureSpec.UNSPECIFIED: //没有指定的模式下,最终的高度就是推荐的高度 heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY); break; } final int specWidth = MeasureSpec.getSize(widthMeasureSpec); if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { // If we don't have an unspecified width spec, use the given size to calculate // the max tab width mTabMaxWidth = mRequestedTabMaxWidth > 0 ? mRequestedTabMaxWidth : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); } // 父容器(TabLayout)测量自身 super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getChildCount() == 1) { // so we don't scroll final View child = getChildAt(0); boolean remeasure = false; switch (mMode) { case MODE_SCROLLABLE: // 如果Tab有多个,那么需要重新测量 remeasure = child.getMeasuredWidth() < getMeasuredWidth(); break; case MODE_FIXED: // 固定模式下,每一个Tab都是不可滑动的,只有一个的情况下需要重新测量 remeasure = child.getMeasuredWidth() != getMeasuredWidth(); break; } if (remeasure) { // 重新测量每一个子View(Tab) int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), child.getLayoutParams().height); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( getMeasuredWidth(), MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
Tab是如何添加进来:
public void addTab(@NonNull Tab tab, int position, boolean setSelected) { if (tab.mParent != this) { throw new IllegalArgumentException("Tab belongs to a different TabLayout."); } configureTab(tab, position); addTabView(tab); if (setSelected) { tab.select(); } } private void addTabView(Tab tab) { final TabView tabView = tab.mView; mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs()); }
可以看见最终是通过addView添加进来的,其中,每一个TabView又是一个LinearLayout:
class TabView extends LinearLayout implements OnLongClickListener { private Tab mTab; private TextView mTextView; private ImageView mIconView; private View mCustomView; private TextView mCustomTextView; private ImageView mCustomIconView; //... }
细心的你会发现TabView本来就支持Icon的设置,并且提供我们添加自定义Tab的View的接口。
mTabStrip也是一个LinearLayout:
private class SlidingTabStrip extends LinearLayout { }
mTabStrip是在TabLayout构造的时候创建的,作为根布局:
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)); // ... }