1 前言
上篇 TabLayout系列之簡單使用更新也有一段時間了,由于工作任務比較多且遇上國慶出去玩,就很久沒做更新了。還有一點關于TabLayout的使用沒有介紹完,公司的事忙得差不多了,今天來繼續介紹這個系列。再啰嗦下,前兩篇文章介紹了TabLayout的 屬性 和 簡單使用,需要的便宜可以先去熟悉熟悉。這篇文章主要是簡單使用的一個補充,以及對自定義TabItem的一個說明。廢話不多說,我們直奔主題。
2 TabLayout默認Style
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,
defStyleAttr, R.style.Widget_Design_TabLayout);
<style name="Base.Widget.Design.TabLayout" parent="android:Widget">
<item name="tabMaxWidth">@dimen/design_tab_max_width</item>
<item name="tabIndicatorColor">?attr/colorAccent</item>
<item name="tabIndicatorHeight">2dp</item>
<item name="tabPaddingStart">12dp</item>
<item name="tabPaddingEnd">12dp</item>
<item name="tabBackground">?attr/selectableItemBackground</item>
<item name="tabTextAppearance">@style/TextAppearance.Design.Tab</item>
<item name="tabSelectedTextColor">?android:textColorPrimary</item>
</style>
<style name="TextAppearance.Design.Tab" parent="TextAppearance.AppCompat.Button">
<item name="android:textSize">@dimen/design_tab_text_size</item>
<item name="android:textColor">?android:textColorSecondary</item>
<item name="textAllCaps">true</item>
</style>
上面三段代碼片段,可以知道TabLayout默認使用Base.Widget.Design.TabLayout,里面有預設好的一些屬性。其中tabTextAppearance在 Android TabLayout系列之簡單使用中已經介紹到了,通過繼承它來改變默認英文字母大寫與字體大小問題,由此我們也可以自定義自己的style來實現自己的需求。
3 帶圖片的Tab
Android TabLayout系列之簡單使用中,介紹到了TabLayout的使用,但基本上都是純文本的TabItem,并沒有實現帶圖片的TabItem。這次就閑來擼一個帶圖片的TabItem,還是老規矩先上代碼(碼注釋) -> 效果圖 -> 分析。
public class MainActivity extends BaseActivity {
private TabLayout mTabLayout;
private ViewPager mViewPager;
private MainPagerAdapter mAdapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
AppStatusTracker.init(getApplication());
super.onCreate(savedInstanceState);
}
@Override
protected void initContentView() {
setContentView(R.layout.activity_main);
}
@Override
protected void initView() {
mTabLayout = findView(R.id.tabLayout);
mViewPager = findView(R.id.viewPager);
mAdapter = new MainPagerAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mAdapter);
//官方推薦的綁定ViewPager方式
mTabLayout.setupWithViewPager(mViewPager);
int tabCount = mTabLayout.getTabCount();
for (int i = 0; i < tabCount; i++) {
//這里tab可能為null 根據實際情況處理吧
mTabLayout.getTabAt(i).setText("Tab" + i);
//設置圖片icon
mTabLayout.getTabAt(i).setIcon(R.mipmap.ic_launcher);
}
}
@Override
protected void initData(@Nullable Bundle savedInstanceState) {
}
//ViewPager適配器 10個Fragment
private class MainPagerAdapter extends FragmentPagerAdapter {
public MainPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return BlankFragment.newInstance(position);
}
@Override
public int getCount() {
return 10;
}
}
}
這里的demo代碼和Android TabLayout系列之簡單使用中的一致,只是增加了 mTabLayout.getTabAt(i).setIcon(R.mipmap.ic_launcher)來設置TabView中Tab的圖片。這里顯示效果是官方提供的默認實現,TabView繼承至Linearlayout且被設置成了豎直方向。這點可以從TabView的構造器中看到:
public TabView(Context context) {
super(context);
......
setGravity(Gravity.CENTER);
setOrientation(VERTICAL); //設置成豎直方向
......
}
至于為什么顯示在第一個,可以從TabView的update方法了解到:
final void update() {
final Tab tab = mTab;
final View custom = tab != null ? tab.getCustomView() : null;
......
......
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); //將ImageView放在了LinearLayout的0位
mIconView = iconView;
}
if (mTextView == null) {
TextView textView = (TextView) LayoutInflater.from(getContext())
.inflate(R.layout.design_layout_tab_text, this, false);
addView(textView); //添加TextView
mTextView = textView;
mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView);
}
TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance);
if (mTabTextColors != null) {
mTextView.setTextColor(mTabTextColors);
}
updateTextAndIcon(mTextView, mIconView); //更新文本和icon
} else {
// Else, we'll see if there is a TextView or ImageView present and update them
if (mCustomTextView != null || mCustomIconView != null) {
updateTextAndIcon(mCustomTextView, mCustomIconView);
}
}
// Finally update our selected state
setSelected(tab != null && tab.isSelected());
}
這里面的TabView是沒有get方法的,所以我們取不到,可以通過反射拿到做一些需要的操作,就不驗證了,因為我們這里是需要自定義TabView。
4 自定義帶圖片的Tab
看源碼或API的話可以知道Tab有個setCustomView(@Nullable View view)方法,可用于添加自定義的TabItem。
- 首先得來個TabItem的Layout Resource文件:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Tab1"
android:textSize="14sp"/>
<ImageView
android:id="@+id/image_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginBottom="0dp"
android:src="@mipmap/ic_indicator"/>
</LinearLayout>
- 其次在我們的Activity實現我們的效果
public class MainActivity extends BaseActivity implements TabLayout.OnTabSelectedListener {
private TabLayout mTabLayout;
private ViewPager mViewPager;
private MainPagerAdapter mAdapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
AppStatusTracker.init(getApplication());
super.onCreate(savedInstanceState);
}
@Override
protected void initContentView() {
setContentView(R.layout.activity_main);
}
@Override
protected void initView() {
mTabLayout = findView(R.id.tabLayout);
mViewPager = findView(R.id.viewPager);
mAdapter = new MainPagerAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mAdapter);
//官方推薦的綁定ViewPager方式
mTabLayout.setupWithViewPager(mViewPager);
int tabCount = mTabLayout.getTabCount();
for (int i = 0; i < tabCount; i++) {
TabLayout.Tab tab = mTabLayout.getTabAt(i);
if (tab == null) return;
//設置自定義的View
tab.setCustomView(mAdapter.getTabView(i));
}
//需要自己實現選中監聽,來實現自己需要的效果
mTabLayout.addOnTabSelectedListener(this);
}
@Override
protected void initData(@Nullable Bundle savedInstanceState) {
}
@Override
public void onTabSelected(TabLayout.Tab tab) {
//Toast.makeText(this, "onTabSelected", Toast.LENGTH_SHORT).show();
changeTabStatus(tab, true);
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
//Toast.makeText(this, "onTabUnselected", Toast.LENGTH_SHORT).show();
changeTabStatus(tab, false);
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
//Toast.makeText(this, "onTabReselected", Toast.LENGTH_SHORT).show();
new AlertDialog.Builder(this)
.setMessage("再次選中,顯示對話框!")
.show();
}
private void changeTabStatus(TabLayout.Tab tab, boolean selected) {
View view = tab.getCustomView();
TextView txtTitle = (TextView) view.findViewById(R.id.text_title);
if (selected) {
txtTitle.setTextColor(Color.parseColor("#03ce97"));
} else {
txtTitle.setTextColor(Color.parseColor("#333333"));
}
}
//ViewPager適配器 10個Fragment
private class MainPagerAdapter extends FragmentPagerAdapter {
public MainPagerAdapter(FragmentManager fm) {
super(fm);
}
//自定義獲取Tab View的方法
public View getTabView(int position) {
View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.custom_tab_item, null);
TextView tv = (TextView) view.findViewById(R.id.text_title);
tv.setText("Tab " + position);
return view;
}
@Override
public Fragment getItem(int position) {
return BlankFragment.newInstance(position);
}
@Override
public int getCount() {
return 10;
}
}
}
從效果圖上看,已經實現了自定義的TabItem,需要注意的是需要根據Tab的選中狀態來,實現自己想要的效果,包括Tab字體顏色的改變,圖標的旋轉效果,選中Tab放大等等...... 我這里只實現了文字顏色的改變,和再次選中彈出對話框。如果我們只是需要圖文顯示,我們不自定義TabItem也可以實現類似這種效果,直接在getPageTitle中使用ImageSpan讓文字和圖片一起顯示。
//TabLayout會根據當前page的title自動綁定tab
@Override
public CharSequence getPageTitle(int position) {
Drawable image = ContextCompat.getDrawable(MainActivity.this, R.mipmap.ic_indicator);
image.setBounds(0, 0, image.getIntrinsicWidth(), image.getIntrinsicHeight());
ImageSpan imageSpan = new ImageSpan(image, ImageSpan.ALIGN_BOTTOM);
SpannableString ss = new SpannableString("Tab" + position + " ");
ss.setSpan(imageSpan, ss.length() - 1, ss.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return ss;
}
5 源碼分析實現上述效果
不分析波源碼都不夠裝13的,其實也不是我想分析,只是偶然看到,然后使用了一下感覺并不是很靈活,不太明白官方為什么要這樣做。網上有很多分析TabLayout,關于這個自定義TabItem源碼,但是沒有看見分析這一段的。有興趣的可以跟著看看,直接跟蹤Tab的setCustomView,找到TabView的update方法,代碼不長60來行:
final void update() {
final Tab tab = mTab;
//獲取Tab里面的CustomView
final View custom = tab != null ? tab.getCustomView() : null;
//根據獲取出來的custom做相應操作,得到最終的CustomView
if (custom != null) {
final ViewParent customParent = custom.getParent();
if (customParent != this) {
if (customParent != null) {
((ViewGroup) customParent).removeView(custom);
}
addView(custom);
}
mCustomView = custom;
//custom不為空,且判斷和處理系統本身的mTextView、mIconView
if (mTextView != null) {
mTextView.setVisibility(GONE);
}
if (mIconView != null) {
mIconView.setVisibility(GONE);
mIconView.setImageDrawable(null);
}
//注意這里!!!從custom去獲取ID為android.R.id.text1
//我猜想系統是想讓我們自定義CustomView的時候使用這個id
mCustomTextView = (TextView) custom.findViewById(android.R.id.text1);
if (mCustomTextView != null) {
mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView);
}
//注意這里!!!從custom去獲取ID為android.R.id.icon
//我猜想系統是想讓我們自定義CustomView的時候使用這個id
mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon);
} else {
//這里注釋很清楚了,就不多說了
// We do not have a custom view. Remove one if it already exists
if (mCustomView != null) {
removeView(mCustomView);
mCustomView = null;
}
mCustomTextView = null;
mCustomIconView = null;
}
//接下來就是判斷CustomView做一些操作了
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 {
// Else, we'll see if there is a TextView or ImageView present and update them
if (mCustomTextView != null || mCustomIconView != null) {
//更新文本和圖標
updateTextAndIcon(mCustomTextView, mCustomIconView);
}
}
// Finally update our selected state
setSelected(tab != null && tab.isSelected());
}
關于源碼的分析都注釋在代碼里,對應著代碼看,更加清晰。通過源碼分析,系統好像想讓我們使用那兩個id(如果只有TextView和ImageView的話),這樣的話是不是我們就不用處理一些邏輯了,使用起來更加easy了呢?答案當然是否定的,我測試的效果是文本的顏色不會根據TabLayout里面設置的一樣改變,而且圖標距底部有一個8dp的margin值。為什么呢?文本顏色不變從上面那段代碼可以看出來,當我們有CustomView時,并沒有給我們調用相應的setTextColor。底邊距就得繼續看updateTextAndIcon方法:
private void updateTextAndIcon(@Nullable final TextView textView,
@Nullable final ImageView iconView) {
//下面一段沒什么好說的,就是獲取Drawable 和CharSequence并設置顯示或隱藏
final Drawable icon = mTab != null ? mTab.getIcon() : null;
final CharSequence text = mTab != null ? mTab.getText() : null;
final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null;
if (iconView != null) {
if (icon != null) {
iconView.setImageDrawable(icon);
iconView.setVisibility(VISIBLE);
setVisibility(VISIBLE);
} else {
iconView.setVisibility(GONE);
iconView.setImageDrawable(null);
}
iconView.setContentDescription(contentDesc);
}
final boolean hasText = !TextUtils.isEmpty(text);
if (textView != null) {
if (hasText) {
textView.setText(text);
textView.setVisibility(VISIBLE);
setVisibility(VISIBLE);
} else {
textView.setVisibility(GONE);
textView.setText(null);
}
textView.setContentDescription(contentDesc);
}
if (iconView != null) {
//獲取出layout參數
MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams());
int bottomMargin = 0;
if (hasText && iconView.getVisibility() == VISIBLE) {
/這里說得很清楚,如果兩者都顯示,就給icon的bottom加一些底邊距
// If we're showing both text and icon, add some margin bottom to the icon
bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON); //8dp
}
//判斷它加的一些底邊距和布局設置的不一致,就使用它加的,并請求重繪
if (bottomMargin != lp.bottomMargin) {
lp.bottomMargin = bottomMargin;
iconView.requestLayout();
}
}
if (!hasText && !TextUtils.isEmpty(contentDesc)) {
setOnLongClickListener(this);
} else {
setOnLongClickListener(null);
setLongClickable(false);
}
}
實際效果就不給出了,還是大家手動測測,關于布局將TextView和ImageView的id改為@android:id/text1和@android:id/icon就OK了。需要注意的是還得手動在代碼里設置上文本的顏色和根據需求的MarginLayoutParams,下面直接給出代碼。
public class MainActivity extends BaseActivity implements TabLayout.OnTabSelectedListener {
private TabLayout mTabLayout;
private ViewPager mViewPager;
private MainPagerAdapter mAdapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
AppStatusTracker.init(getApplication());
super.onCreate(savedInstanceState);
}
@Override
protected void initContentView() {
setContentView(R.layout.activity_main);
}
@Override
protected void initView() {
mTabLayout = findView(R.id.tabLayout);
mViewPager = findView(R.id.viewPager);
mAdapter = new MainPagerAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mAdapter);
//官方推薦的綁定ViewPager方式
mTabLayout.setupWithViewPager(mViewPager);
int tabCount = mTabLayout.getTabCount();
for (int i = 0; i < tabCount; i++) {
TabLayout.Tab tab = mTabLayout.getTabAt(i);
if (tab == null) return;
//設置自定義的View
tab.setCustomView(R.layout.custom_tab_item);
tab.setText("Tab" + i);
tab.setIcon(R.mipmap.ic_indicator);
View customView = tab.getCustomView();
if (customView == null) return;
//注意設置了文本顏色和MarginLayoutParams
((TextView) customView.findViewById(android.R.id.text1)).setTextColor(mTabLayout.getTabTextColors());
View icon = customView.findViewById(android.R.id.icon);
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) icon.getLayoutParams();
layoutParams.bottomMargin = 0;
icon.setLayoutParams(layoutParams);
}
//需要自己實現選中監聽,來實現自己需要的效果
mTabLayout.addOnTabSelectedListener(this);
}
@Override
protected void initData(@Nullable Bundle savedInstanceState) {
}
@Override
public void onTabSelected(TabLayout.Tab tab) {
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
new AlertDialog.Builder(this)
.setMessage("再次選中,顯示對話框!")
.show();
}
//ViewPager適配器 10個Fragment
private class MainPagerAdapter extends FragmentPagerAdapter {
public MainPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return BlankFragment.newInstance(position);
}
@Override
public int getCount() {
return 10;
}
}
}
廢話太多了,原生TabLayout使用就這么多,源碼中真是變化萬千。如果希望實現更多酷炫效果,可以自定義或者GitHub.....
附: