TabLayout的基本使用
以前我们是用TabHost来实现Tab切换的效果。现在谷歌推荐使用TabLayout。
下面以TabLayout+ViewPager+Fragment为例,讲述TabLayout的基本使用。
布局文件如下面所示:
其中,需要关注的属性有:
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));
// ...
}
如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:
我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)。