LZ-Says: 我把我后背留给你,你却对他人说,瞧,那个傻子。。。
前言
随着年龄的增长,日子仿佛一天天的加快了步伐,总是回头看,不知不觉过了这么久。
回头看,17年,又所剩无几了。
在这17年最后几个月里,好好努力,奋进~
今天学习了有关TabLayout相关知识,分享给大家~
演示效果
TabLayout简述
简单介绍下TabLayout:
1) TabLayout来自于design包内,是一种具有Material Design效果的选项卡。
那么它是怎么用的呢?
1) 一般的话,配合Fragment使用,同样也可以TabLayout+ViewPager+Fragment一起使用。
这里为大家简单介绍其中关键Api,这些同样是比较常用的。
- app:tabBackground: 设置Tab选项卡背景色;
- app:tabIndicatorHeight: 设置指示器高度;
- app:tabGravity: 设置Tab显示方式:
1) center:居中显示;
2) fill:填充整个宽度
- app:tabIndicatorColor: 设置Tab指示器颜色;
- app:tabTextColor: 设置Tab文字颜色;
- app:tabSelectedTextColor: 设置Tab选中颜色;
- app:tabMode: 设置Tab模式:
1) fixed:不能滑动,子控件平分父容器,类似LinearLayout权重;
2) scrollable:可滑动
ok,基于以上关键Api,我们开始着手实现撸码~
着手撸码
第一步:搭建布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.materialdesignstudy.tablayout.TabLayoutActivity">
<android.support.design.widget.TabLayout
android:id="@+id/id_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabBackground="@color/blue"
app:tabGravity="center"
app:tabIndicatorColor="@color/colorPrimary_pink"
app:tabIndicatorHeight="10dp"
app:tabMode="scrollable"
app:tabSelectedTextColor="@color/colorPrimary_pinkDark"
app:tabTextColor="@color/colorPrimary_pink" />
<android.support.v4.view.ViewPager
android:id="@+id/id_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
第二步:定义测试Fragment
package com.materialdesignstudy.tablayout;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
/**
* Created by HLQ on 2017/11/16
* 测试使用Fragment
*/
public class MyFragment extends Fragment {
@Nullable
@Override
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.setText(title);
tv.setBackgroundColor(Color.rgb((int) (Math.random() * 255), (int) (Math.random() * 255), (int) (Math.random() * 255)));
return tv;
}
}
第三步:初始化以及设置监听
private TabLayout mTabLayout;
private ViewPager mViewPager;
private MyPagerAdapter mAdapter;
private String[] titles = {
"今日头条", "热点新闻", "娱乐",
"房地产", "体育", "科技",
"互联网","CSDN Blog", "体育",
"APK Bus", "健康医疗", "民生"
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tab_layout);
initView();
}
private void initView() {
// 实例化
mTabLayout = (TabLayout) findViewById(R.id.id_tab_layout);
mViewPager = (ViewPager) findViewById(R.id.id_view_pager);
// 设置分割线
LinearLayout linearLayout= (LinearLayout) mTabLayout.getChildAt(0);
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
linearLayout.setDividerDrawable(ContextCompat.getDrawable(this,R.drawable.shape_tablayout_divider));
mAdapter = new MyPagerAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mAdapter);
// 关联tabLayout与ViewPager
mTabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// 当前被选中
mViewPager.setCurrentItem(tab.getPosition(), true);
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
// viewPager滑动关联TabLayout
mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(mTabLayout));
// 实现方式一
// 设置tabLayout标签来自于pagerAdapter
mTabLayout.setTabsFromPagerAdapter(mAdapter);
}
class MyPagerAdapter extends FragmentPagerAdapter {
public MyPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
@Override
public Fragment getItem(int position) {
Fragment fragment = new MyFragment();
Bundle bundle = new Bundle();
bundle.putString("title", titles[position]);
fragment.setArguments(bundle);
return fragment;
}
@Override
public int getCount() {
return titles.length;
}
}
一起来看下目前实现的效果:
还记得当初写这个,死费劲儿,一个别憋屈了好久,才实现了部分功能,当初肿么没想到看看api呢!
下面我们反手实现一个特殊的需求(TabLayout怎么就能在下方且无指示器效果呢?),并且为大家介绍第二种玩法,细心的小伙伴一定发现了,代码中写的方式一~
首先要做的第一步,布局变更:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.materialdesignstudy.tablayout.TabLayoutActivity">
<android.support.v4.view.ViewPager
android:id="@+id/id_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<android.support.design.widget.TabLayout
android:id="@+id/id_tab_layout"
android:layout_width="match_parent"
android:layout_height="80dp"
app:tabBackground="@color/blue"
app:tabGravity="fill"
app:tabIndicatorColor="@color/colorPrimary_pink"
app:tabIndicatorHeight="0dp"
app:tabMode="fixed"
app:tabSelectedTextColor="@color/colorPrimary_pinkDark"
app:tabTextColor="@color/colorPrimary_pink" />
</LinearLayout>
第二步:初始化以及设置方式二
private TabLayout mTabLayout;
private ViewPager mViewPager;
private MyPagerAdapter mAdapter;
private String[] titles = {
"今日头条", "热点新闻", "娱乐"
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bottom_tab);
initView();
}
private void initView() {
// 实例化
mTabLayout = (TabLayout) findViewById(R.id.id_tab_layout);
mViewPager = (ViewPager) findViewById(R.id.id_view_pager);
mAdapter = new MyPagerAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mAdapter);
// 方式二:
mTabLayout.setupWithViewPager(mViewPager);
for (int i = 0; i < mTabLayout.getTabCount(); i++) {
TabLayout.Tab tab = mTabLayout.getTabAt(i);
View view = View.inflate(this, R.layout.item_bottom_tab_layout, null);
TextView tv = view.findViewById(R.id.tv_item_content);
tv.setText(titles[i]);
tab.setCustomView(view);
}
}
class MyPagerAdapter extends FragmentPagerAdapter {
public MyPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
@Override
public Fragment getItem(int position) {
Fragment fragment = new MyFragment();
Bundle bundle = new Bundle();
bundle.putString("title", titles[position]);
fragment.setArguments(bundle);
return fragment;
}
@Override
public int getCount() {
return titles.length;
}
}
没错,就是这么6。看看效果~
以上内容便是有关TabLayout使用,而下面,我们开始简要分析TabLayout源码,一起来瞅瞅~
TabLayout源码简要分析
首先,点击进入源码,我们得知此神器依旧继承HorizontalScrollView,不信?源码附上~
public class TabLayout extends HorizontalScrollView
又是似曾相识的感觉哈~
// 检测是否使用Theme或者引入design库
ThemeUtils.checkAppCompatTheme(context);
// 关闭HorizontalScrollBar
setHorizontalScrollBarEnabled(false);
// Add the TabStrip
mTabStrip = new SlidingTabStrip(context);
super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
通过查看构造,不知道大家有没有注意到最下面俩行代码,实例化一个SlidingTabStrip,并添加到View中。
看到这里,我们有些疑问,SlidingTabStrip是什么东西呢?一起来看。
继续深入:
private class SlidingTabStrip extends LinearLayout
SlidingTabStrip是存在于TabLayout的内部类,此类继承了LinearLayout,也就是说,我们的TabLayout父布局为线性布局,这里也就很好的诠释了我们添加分割线为什么直接getCharAt(0)拿到第一个,从而设置其分割线显示以及样式。
private ValueAnimatorCompat mIndicatorAnimator;
SlidingTabStrip(Context context) {
super(context);
setWillNotDraw(false);
mSelectedIndicatorPaint = new Paint();
}
void setSelectedIndicatorColor(int color) {
if (mSelectedIndicatorPaint.getColor() != color) {
mSelectedIndicatorPaint.setColor(color);
ViewCompat.postInvalidateOnAnimation(this);
}
}
void setSelectedIndicatorHeight(int height) {
if (mSelectedIndicatorHeight != height) {
mSelectedIndicatorHeight = height;
ViewCompat.postInvalidateOnAnimation(this);
}
}
上面截取部分,我们发现其内部采用属性动画,通过画笔进行绘制内容,包含指示器颜色、高度。
boolean childrenNeedLayout() {
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
if (child.getWidth() <= 0) {
return true;
}
}
return false;
}
而上面则是通过循环去判断所包含子控件是否有效,也就是getWidth()<=0的时候。
而下面,便是将Tab的位置设置到指示器的位置上。
void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
mIndicatorAnimator.cancel();
}
mSelectedPosition = position;
mSelectionOffset = positionOffset;
updateIndicatorPosition();
}
接下来我们来看看绘制方法中进行了什么操作。
// 校验当前模式是否有效
if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
// 获取子控件个数
final int count = getChildCount();
// First we'll find the widest tab 首先查找最大宽度
int largestTabWidth = 0;
for (int i = 0, z = count; i < z; i++) {
View child = getChildAt(i); // 拿到子控件
if (child.getVisibility() == VISIBLE) {
largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
}
}
if (largestTabWidth <= 0) {
// If we don't have a largest child yet, skip until the next measure pass
return;
}
// 获取dp转px数值
final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
boolean remeasure = false;
if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
// 如果tab匹配我们宽度,我们将为所有的tabs设置相同宽度
// If the tabs fit within our width minus gutters, we will set all tabs to have
// the same width
for (int i = 0; i < count; i++) {
final LinearLayout.LayoutParams lp =
(LayoutParams) getChildAt(i).getLayoutParams();
if (lp.width != largestTabWidth || lp.weight != 0) {
lp.width = largestTabWidth;
lp.weight = 0;
remeasure = true;
}
}
} else {
// 如果大于,则需要选择填充
// If the tabs will wrap to be larger than the width minus gutters, we need
// to switch to GRAVITY_FILL
mTabGravity = GRAVITY_FILL;
updateTabViews(false);
remeasure = true;
}
// 绘制
if (remeasure) {
// Now re-measure after our changes
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
其实主要就是根据宽度做比对去计算,绘制。
继续往下看。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
// If we're currently running an animation, lets cancel it and start a
// new animation with the remaining duration
mIndicatorAnimator.cancel();
final long duration = mIndicatorAnimator.getDuration();
// 下面通过大量的计算 去放置子控件
animateIndicatorToPosition(mSelectedPosition,
Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration));
} else {
// If we've been layed out, update the indicator position
updateIndicatorPosition();
}
}
其中,animateIndicatorToPosition方法则通过计算重而绘制移动时指示器的动画效果。很nice。
这里为大家截取有关动画相关代码:
if (startLeft != targetLeft || startRight != targetRight) {
ValueAnimatorCompat animator = mIndicatorAnimator = ViewUtils.createAnimator();
animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
animator.setDuration(duration);
animator.setFloatValues(0, 1);
animator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimatorCompat animator) {
final float fraction = animator.getAnimatedFraction();
setIndicatorPosition(
AnimationUtils.lerp(startLeft, targetLeft, fraction),
AnimationUtils.lerp(startRight, targetRight, fraction));
}
});
animator.addListener(new ValueAnimatorCompat.AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(ValueAnimatorCompat animator) {
mSelectedPosition = position;
mSelectionOffset = 0f;
}
});
animator.start();
}
而最后,则开始绘制。
@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);
}
}
好了 到此结束 以后 好好研究下关于自定义View相关知识点
而关于我们在文章开头方式一与方式二之间的区别 或者说是某种联系 具体可自行查看源码。相信你一看就秒懂~
GitHub查看地址
https://github.com/HLQ-Struggle/MaterialDesignStudy/tree/master/app/src/main/java/com/materialdesignstudy/tablayout