关键字:TabLayout、材料设计
项目地址:Github
注意:代码基于 android.support.design 26.0.0
谷歌在 2014 年 I/O 大会上发布 Material Design 已经有三年之久,现在应用市场中的应用,或多或少都能找到材料设计的影子。这足以证明,材料设计在开发过程中也是必不可少的一项技能。为此,做下学习笔记,希望通过一段时间的学习,能够在将来重构项目的时候有备无患。
今天主要通过官网的介绍进行学习,官网需要科学上网访问。
先看效果:
一、TabLayout介绍
继承关系:
java.lang.Object
android.view.View
android.view.ViewGroup
android.widget.FrameLayout
android.widget.HorizontalScrollView
android.support.design.widget.TabLayout
TabLayout 是一个继承于 ScrollView 的容器,它能帮助我们快速完成多个标签的展示。如果想看 TabLayout 源码的话,主要需要注意两个类
1.Tab 封装了单个标签的大多属性
2.SlidingTabStrip 封装了指示器的大部分逻辑
官网对 TabLayout 的介绍主要分两部分,第一是 TabLayout 的使用,第二为 TabLayout 和 ViewPager 的整合。
第一部分
第一部分提到了两种添加标签项的方法:代码添加和布局添加,但是通常情况下,我们不这么做。想附图,然而官网不知道为什么字体变得特别细,看不清。所以贴文字。
TabLayout provides a horizontal layout to display tabs.
Population of the tabs to display is done through TabLayout.Tab instances. You create tabs via newTab().From there you can change the tab's label or icon via setText(int) and setIcon(int) respectively. To display the tab, you need to add it to the layout via one of the addTab(Tab) methods. For example:
//总结上面
newTab: 新建标签项
setText:设置文字
setIcon:设置图片
addTab:向 TabLayout 中添加标签项
//方法一
TabLayout tabLayout = ...;
tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
//方法二
第二部分
第二部分主要提到了两种 TabLayout 和 ViewPager 联动的写法:
//方法一:利用 setUpWithViewPager 实现与 ViewPager 的联动
//Tablayout 中的 text 可以通过 PagerAdapter 中的 getPageTitle() 设置
tablayout.setUpWithViewPager(viewpager)
//方法二:
以上,官网文档的介绍就结束了,剩下的就是 TabLayout 各种方法和参数的表格了
二、xml 中可以设置的属性
XML attributes
XML attrs | 备注 |
---|---|
app:tabBackground | Reference to a background to be applied to tabs. |
app:tabContentStart | Position in the Y axis from the starting edge that tabs should be positioned from. |
app:tabGravity | Gravity constant for tabs. |
app:tabIndicatorColor | Color of the indicator used to show the currently selected tab. |
app:tabIndicatorHeight | Height of the indicator used to show the currently selected tab. |
app:tabMaxWidth | The maximum width for tabs. |
app:tabMinWidth | The minimum width for tabs. |
app:tabMode | The behavior mode for the Tabs in this layout. Must be one of the following constant values. |
app:tabPadding | The preferred padding along all edges of tabs. |
app:tabSelectedTextColor | The text color to be applied to the currently selected tab. |
app:tabTextAppearance | A reference to a TextAppearance style to be applied to tabs. |
app:tabTextColor | The default text color to be applied to tabs. |
Constants
Type | Name | 备注 |
---|---|---|
int | GRAVITY_CENTER | 用于 TabLayout 的 Gravity 属性,标签项居中 |
int | GRAVITY_FILL | 用于 TabLayout 的 Gravity 属性,标签项横向充满 |
int | MODE_FIXED | 用于 TabLayout 的 Mode 属性,标签项不可滚动 |
int | MODE_SCROLLABLE | 用于 TabLayout 的 Mode 属性,标签项可以滚动 |
---- 以上为所有xml可见的属性,大多数有对应的 java 设置代码
三、使用 TabLayout
一般使用:
/*====[TabLayout init start]====================================================================================*/
tablayout = ((TabLayout) findViewById(R.id.activity_tablayout));
/**
Tablayout.newTab() 创建标签
Tablayout.addTab() 添加标签
Tablayout.removeTab() 删除标签
Tablayout.removeTabAt() 通过索引删除标签
Tablayout.removeAllTabs() 删除全部标签
Tablayout.Tab方法
setText () 设置Tab文本内容
getText () 获取Tab文本内容
setIcon () 为Tab添加图标
getIcon () 获取Tab的图标
setCustomView() 设置用户自定义的Tab,参数为资源id或者View对象
Tablayout.getSelectedTabPosition() 获取当前选中的Tab位置
Tablayout.getTabAt() 根据索引获取Tab
Tablayout.getTabCount() 获取Tab总数
*/
tablayout.setSelectedTabIndicatorColor(
getResources().getColor(R.color.colorPrimary)); //指示器颜色 - app:tabIndicatorColor=""
tablayout.setSelectedTabIndicatorHeight(10); //指示器高度 - app:tabIndicatorHeight=""
tablayout.setTabTextColors(Color.BLACK, //Tab 颜色 - app:tabTextColor=""
getResources().getColor(R.color.colorAccent)); // - app:tabSelectedTextColor=""
// - app:tabTextAppearance="" 为Tab文本设置样式
tablayout.setTabMode(TabLayout.MODE_FIXED); //Tab 模式 - app:tabMode=""
tablayout.setTabGravity(TabLayout.GRAVITY_FILL); //Tab 对齐 - app:tabGravity=""
/*====[TabLayout init end]====================================================================================*/
/***************************************************************************************************************/
/*====[ViewPager init start]====================================================================================*/
viewpager = ((ViewPager) findViewById(R.id.activity_tablayout_viewpager));
viewpager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
@Override
public Fragment getItem(int position) {
return fragments == null ? null : fragments.get(position);
}
@Override
public int getCount() {
return fragments == null ? 0 : fragments.size();
}
@Override
public CharSequence getPageTitle(int position) {
return titles == null ? "" : titles.get(position);
}
});
/*====[ViewPager init end]====================================================================================*/
//设置联动
tablayout.setupWithViewPager(viewpager);
四、一些特别的自定义需求
其实对于大多数的应用来说,能用到上面的程度就已经够了。不过因为业务的需求,常常也会有个性化的设置,所以才有了下面的东西。不过由于 design 包的代码每次更新过之后,都有比较大的改动,之前看过的方法,很有可能在你升级了 design 包之后就不能用了,所以请确保你的 design 包和你看的帖子相同,不然看再多也是对不上口的。比如,这个 demo 之前,我用的是 design 25.3.1,但是这个 demo 是 design 26.0.0 的。点进源码变了好多。
Q1: Tab 中需要有图片或者其他个性化定制
对于大多数的定制要求,其实都可以利用 setCustomView 解决,只是代码量会增加很多。TabLayout 提供了两个方法用于文字和图片的设置
TabLayout.Tab.setIcon(...)
TabLayout.Tab.setText(...)
//其中 TabLayout.Tab 可以通过 TabLayout.getChildAt(..) 获得
那么这个 Text 和 Icon 是怎么排列的呢?我们查看 TabLayout.Tab 关于 setText 和 setIcon 的代码,发现它们都和一个变量有关 —— mView
public static final class Tab {
//...省去许多变量
TabView mView;
//...省去许多代码
@Nullable
public CharSequence getText() {
return mText;
}
@NonNull
public Tab setIcon(@Nullable Drawable icon) {
mIcon = icon;
updateView();
return this;
}
@NonNull
public Tab setText(@Nullable CharSequence text) {
mText = text;
updateView();
return this;
}
void updateView() {
if (mView != null) {
mView.update();
}
}
}
检索这个 mView 发现是在 TabLayout.createTabView(..) 中创建的。
/**
* Create and return a new {@link Tab}. You need to manually add this using
* {@link #addTab(Tab)} or a related method.
*
* @return A new Tab
* @see #addTab(Tab)
*/
@NonNull
public Tab newTab() {
Tab tab = sTabPool.acquire();
if (tab == null) {
tab = new Tab();
}
tab.mParent = this;
tab.mView = createTabView(tab);
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;
}
最后我们在 TabView 中找到了默认的布局:
public TabView(Context context) {
super(context);
if (mTabBackgroundResId != 0) {
ViewCompat.setBackground(
this, AppCompatResources.getDrawable(context, mTabBackgroundResId));
}
ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, //这是 xml attr 中 TabLayout 设置的属性
mTabPaddingEnd, mTabPaddingBottom);
setGravity(Gravity.CENTER);
setOrientation(VERTICAL);
setClickable(true);
ViewCompat.setPointerIcon(this,
PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND));
}
final void update() {
//...
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);
} else {
// ...
}
//...
}
我想看看 R.layout.design_layout_tab_icon 和 R.layout.design_layout_tab_text 里的布局长什么样,但是资源文件并不可见,测试代码很好写,如下:
//测试默认的布局
mTablayout = (TabLayout) findViewById(R.id.tablayout);
mTablayout.addTab(mTablayout.newTab().setText("A").setIcon(R.drawable.pic_1));
mTablayout.addTab(mTablayout.newTab().setText("B").setIcon(R.drawable.pic_2));
mTablayout.addTab(mTablayout.newTab().setText("C").setIcon(R.drawable.pic_3));
效果其实挺不理想的,在大多数的情况下,不管是 UI 还是业务,都不可能用这种方法糊弄过关。因此,大多数的自定义都是利用 setCustomView 来完成的。上面看源码的流程再走一边也能看到 customView 最终加入 TabLayout 内部的全部过程。
注意:
1.CustomView 和 TabLayout 内部默认的 View 是横向排列的,设置了 CustomView 并不会使默认的 View 失效
2.CustomView 的 Select 状态在不同版本的 design 包下都会有一些问题,尤其是有图的情况,即使是默认 View 的情况,有的版本也是不能响应的。需要做好充分的测试
3.CustomView 如果出现被“挤没了”的情况,请注意 TabLayout 的 padding 属性,默认值并非 0
使用 CustomView 并不是难事,相信多数人都能完成,代码简单贴一点:
//attention 需要确保此时 TabLayout 是有数据的,
//例如,使用 setUpWithViewPager 联动的时候,下面的代码需要在 setUpWithViewPager 之后
for (int i = 0; i < tablayout.getChildCount(); i++) {
View customView = LayoutInflater.from(this).inflate(R.layout.custom_view, null);
//做一些 customView 的初始化
tablayout.getTabAt(i).setCustomView(customView);
}
Q2:指示器长度
1.曾经可以通过反射某个方法更改,但是不知道到哪个版本,这个方法已经没有了,指示器的长度没有方法可以控制
2.后来的版本,我们根据 SlidingTabStrip.ondraw 方法中的绘制参数,发现宽度是受其 Margin 影响的,所以有了利用反射获取 TabLayout 中的 SlidingTabStrip 对象,再设置其 margin 的方法,这种方法可以使指示器变短,但是无法比内容更短,且动画效果会受影响。
你可以点进现在使用的 TabLayout 源码中,查看 SlidingTabStrip 的 ondraw 方法,如果是下面这样的,就可以通过设置 margin 方法,得到指示器缩短的效果:
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if(mIndicatorLeft>=0&&mIndicatorRight>mIndicatorLeft) {
canvas.drawRect(mIndicatorLeft+ getTabMargin(), getHeight() -mSelectedIndicatorHeight,
mIndicatorRight- getTabMargin(), getHeight(),mSelectedIndicatorPaint);
}
//==========================[对应设置方法]========================================
private void setIndicatorMargin(TabLayout tablayout,int left,int right) {
try {
Field mTabStrip = tablayout.getClass().getDeclaredField("mTabStrip");
mTabStrip.setAccessible(true);
LinearLayout ltab = (LinearLayout) mTabStrip.get(tablayout);
int childCount = ltab.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = ltab.getChildAt(i);
child.setPadding(0, 0, 0, 0);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, -1);
params.weight = 1;
params.leftMargin = left;
params.rightMargin = right;
child.setLayoutParams(params);
child.invalidate();
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
而 design 26.0.0 版本中 ondraw 方法已经更改相关代码只在三个方法里,代码如下:
private class SlidingTabStrip extends LinearLayout {
//...省略代码
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);
}
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);
}
}
@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);
}
}
}
我们发现 ondraw 方法中的绘制,已经完全收住了口,没有可以修改的方法。
目前的解决方案是自己提取代码,修改这个绘制计算的部分。 design 包的 TabLayout 代码我已经提取好了。
链接:ATabLayout
如果觉得麻烦,也可以用这个开源库,4k赞,应该很好用。
链接:很好用的改造TabLayout
Q3:其他的一些坑
- 1.不知道有没有人在使用 customView 的时候遇到过这个问题,TabLayout 内部有个 DEFAULT_HEIGHT 限制住高度,因此, customView 的高度并不是 TabLayout 的高度,会被阉割。
- 2.设置 customView 里面有图片的时候,即便设置了选择器,在正常点击选择和滑动时是正常的,但是测试发现第一次展示的时候图片处于未选中状态。根据网上的材料, tab.select 方法并不奏效,查看源码发现应该是内部已经置了 selected 状态,但是没有显示出来。这个问题目前只有通过代码调用 addTab(tab,selected) 可以解决,或者。。。。我在进入页面的时候先切了第二页再切回来,也勉强做到,加载数据量不大的时候可以用,不然还是自定义吧。