为啥要自己定义TabLayout?
1.design包中的TabLayout很多时候不能满足UI的需求
2.我们需要自定义tab的位置和tab内容的字体和style
3.我们自定义的控件比较容易适配
有人可能会百度,改变tab字体大小和style不是有方法吗?但是当你要加入自定义布局的时候,就无法实现了。但是字体大小和字体的style还是可以通过反射来修改的,TabLayout中Tab的字段textView来设置字体的大小和style,值得注意的是TabLayout设计了一个最大的textSize,这里我们要通过反射区修改这个最大值,这样下来我们才能说我们已经完美的自定义了每个Tab,嗯?是这样的吗?那么布局怎么搞?Tab都是居中的,我们想让左右的Tab对齐父布局,那我们怎么处理呢?so,今天带给大家一个比较完善的自定义View–YzzTabLayout.
第一:我们要实现自定义Tab的位置,让它出现在我们预期的位置
(1)测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
this.widthMeasureSpec = widthMeasureSpec;
this.heightMeasureSpec = heightMeasureSpec;
//获取宽高的大小和模式
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int wModel = MeasureSpec.getMode(widthMeasureSpec);
int hModel = MeasureSpec.getMode(heightMeasureSpec);
int count = getChildCount();
switch (mModel) {
case MODEL_CENTER:
measureCenter(count, wModel, hModel, width, height);
break;
case MODEL_DEFAULT:
measureDefault(count, wModel, hModel, width, height);
break;
}
}
private void measureCenter(int count, int wModel, int hModel, int width, int height) {
int w = 0;
int h = 0;
if (wModel == MeasureSpec.EXACTLY) {
w = width;
//计算margin
getMargin(count, width);
} else {
w = getCenterW(count, width);
}
if (hModel == MeasureSpec.EXACTLY) {
h = height;
} else {
h = getH(count);
}
setMeasuredDimension(w, h);
}
//获取center模式下的宽度(default模式下就是普通的LinearLayout逻辑),这里是当YzzTab的宽度的model为ATMOST的时候,我们就有一个默认的margin
private int getCenterW(int count, int pW) {
int w = 0;
int childW = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child.getLayoutParams();
} else {
lp = new MarginLayoutParams(child.getLayoutParams());
}
childW += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
}
// if (w + getPaddingLeft() + getPaddingRight() < pW) {
// getMargin(count, pW);
// return pW;
// }
w = childW + getPaddingRight() + getPaddingLeft() + (count - 1) * margin;
//当
if (w > pW) getMargin(count, pW);
//这里我为了统一,Tab的宽度不得超过父容器
return w > pW ? pW : w;
}
//获取margin,这是该View的核心,UI是这样的左右两边必须对其父布局,然后整体平分父容器。
private void getMargin(int count, int w) {
int childW = 0;
int num = 0;
for (int i = 0; i < count; i++) {
num++;
View child = getChildAt(i);
//通过这种方式获取View的margin,标准形式。
MarginLayoutParams lp;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child.getLayoutParams();
} else {
lp = new MarginLayoutParams(child.getLayoutParams());
}
childW += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
//这里我设置了一个最小margin,当小于此margin后,我们就讲最小margin作为我们的值。不明白的童鞋这里可以画一个草图算算这个margin要怎么求,我这里就不做详细说明了,应该问题不大。
if (num > 1) {
margin = (w - (childW + getPaddingLeft() + getPaddingRight())) / (num - 1);
if (margin < MINE_MARGIN) {
childW -= child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
num--;
margin = (w - (childW + getPaddingLeft() + getPaddingRight())) / (num - 1);
break;
}
}
// if (childW > w - getPaddingLeft() - getPaddingRight()) {
// childW -= child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
// num--;
// break;
// }
}
//if (num<=one)return;
//margin = (int) ((w - (childW + getPaddingLeft() + getPaddingRight())) / (num - one));
}
//这里就是普通模式下的测量,思路跟LinearLayout是差不多的,下面的getW()和getH()方法就比较常见了,就不做介绍了。
private void measureDefault(int count, int wModel, int hModel, int width, int height) {
int w = 0;
int h = 0;
if (wModel == MeasureSpec.EXACTLY) {
w = width;
} else {
w = getW(count, width);
}
if (hModel == MeasureSpec.EXACTLY) {
h = height;
} else {
h = getH(count);
}
setMeasuredDimension(w, h);
}
/**
* 获取高度
*/
private int getH(int count) {
int h = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child.getLayoutParams();
} else {
lp = new MarginLayoutParams(child.getLayoutParams());
}
h = Math.max(h, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
return h + getPaddingBottom() + getPaddingTop();
}
/**
* 获取宽度
*/
private int getW(int count, int pW) {
int w = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child.getLayoutParams();
} else {
lp = new MarginLayoutParams(child.getLayoutParams());
}
w += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth();
// if (w + getPaddingLeft() + getPaddingRight() < pW) {
// return pW;
// }
}
return (w + getPaddingLeft() + getPaddingRight()) > pW ? pW : (w + getPaddingLeft() + getPaddingRight());
}
(2)layout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
switch (mModel) {
case MODEL_DEFAULT:
layoutDefault(count);
break;
case MODEL_CENTER:
layoutCenter(count);
break;
}
}
//普通模式下的layout
private void layoutDefault(int count) {
int w = getPaddingLeft();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child.getLayoutParams();
} else {
lp = new MarginLayoutParams(child.getLayoutParams());
}
int l = w + lp.leftMargin;
int centerH = (getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - child.getMeasuredHeight()) / 2;
int t = centerH + lp.topMargin;
int r = l + child.getMeasuredWidth();
int b = t + child.getMeasuredHeight();
if (r > getMeasuredWidth()) {
isNeedScroll = true;
} else {
isNeedScroll = false;
}
child.layout(l, t, r, b);
w = r + lp.rightMargin;
if (i == count - 1) {
maxScroll = child.getRight() - getMeasuredWidth();
}
}
}
/居中效果的测量
private void layoutCenter(int count) {
int w = getPaddingLeft();
int h = getPaddingTop();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child.getLayoutParams();
} else {
lp = new MarginLayoutParams(child.getLayoutParams());
}
//lp = (LayoutParams) child.getLayoutParams();
int l = w + lp.leftMargin;
//这里我们处理的原因是,前面我在计算margin的时候用到了除法运算,并且转型成int类型造成了精度损失,那么我们只好控制最后一个Tab的位置了,让其准确的居于父容器的右侧,这样就能解决这个精度丢失的问题。
if (i == count - 1 && getMeasuredWidth() - w > 0) {
l = getMeasuredWidth() - child.getMeasuredWidth() - lp.leftMargin;
}
int centerH = (getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - child.getMeasuredHeight()) / 2;
int t = lp.topMargin + centerH;
int r = l + child.getMeasuredWidth();
int b = t + child.getMeasuredHeight();
//这里要得到我们时候要滑动的值,当子View的宽度大于容器宽度了,我们是需要滑动的。
if (r > getMeasuredWidth()) {
isNeedScroll = true;
} else {
isNeedScroll = false;
}
child.layout(l, t, r, b);
w = r + lp.rightMargin + margin;
//这里我们要得到可滑动的最大距离,方便后面的滑动处理
if (i == count - 1) {
maxScroll = child.getRight() - getMeasuredWidth();
}
}
}
第二:我们要处理Tab的点击滑动
这里我是用YzzTabLayout这个控件来处理该任务的
(1)测量和layuout
//这里的测量和layout就是简单的居中效果,不再重复
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() == 0) return;
yzzTab = (YzzTab) getChildAt(0);
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int wModel = MeasureSpec.getMode(widthMeasureSpec);
int hModel = MeasureSpec.getMode(heightMeasureSpec);
int w;
int h;
if (wModel == MeasureSpec.EXACTLY) {
w = width;
} else {
w = yzzTab.getMeasuredWidth();
}
if (hModel == MeasureSpec.EXACTLY) {
h = height;
} else {
h = yzzTab.getMeasuredHeight();
}
setMeasuredDimension(w, h);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() == 0) return;
MarginLayoutParams lp;
if (yzzTab.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) yzzTab.getLayoutParams();
} else {
lp = new MarginLayoutParams(yzzTab.getLayoutParams());
}
int ll = getPaddingLeft() + lp.leftMargin;
int centerH = (getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - yzzTab.getMeasuredHeight()) / 2;
int tt = centerH + lp.topMargin;
int rr = ll + yzzTab.getMeasuredWidth();
int bb = tt + yzzTab.getMeasuredHeight();
yzzTab.layout(ll, tt, rr, bb);
maxScroll = yzzTab.getMaxScroll();
}
(2)处理Tab的添加修改,这里由Tab这个类来进行管理
public static class Tab {
private TextView textView;
private View customView;
private LayoutParams layoutParams;
public static final int TEXT_SIZE = 15;
public static final int TEXT_COLOR = 0xff333333;
public static final int TEXT_SELECTOR_COLOR = 0xff00ff00;
private static int selectColor = TEXT_SELECTOR_COLOR;
private static int nomalColor = TEXT_COLOR;
//这里初始化textView,设置默认的颜色,字体,style
private Tab(Context context) {
layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
textView = new TextView(context);
textView.setLayoutParams(layoutParams);
textView.setTextSize(TEXT_SIZE);
textView.setTypeface(Typeface.DEFAULT);
textView.setTextColor(TEXT_COLOR);
tabList.add(this);
tabViews.add(textView);
}
//这里让外部创建Tab对象
public static Tab newTab() {
return new Tab(mContext);
}
//设置字体的颜色
public Tab setText(String textContent) {
if (TextUtils.isEmpty(textContent)) return this;
textView.setText(textContent);
return this;
}
//设置字体的size
public Tab setTextSize(int size) {
textView.setTextSize(size);
return this;
}
//设置color
public Tab setTexrColor(int color) {
nomalColor = color;
textView.setTextColor(color);
return this;
}
//设置字体的style
public Tab setTextStyle(Typeface typeface) {
textView.setTypeface(typeface == null ? Typeface.DEFAULT : typeface);
return this;
}
//设置选中颜色的color
public Tab setSelectColor(int color) {
selectColor = color;
return this;
}
//添加自定义的布局
public void setCustomView(View customView) {
if (customView == null) return;
this.customView = customView;
tabViews.remove(textView);
tabViews.add(customView);
}
//修改tab属性
public void changeFace(int textSize, int textColor, Typeface typeface) {
nomalColor = textColor;
setTextSize(textSize).setTexrColor(textColor).setTextStyle(typeface == null ? Typeface.DEFAULT : typeface);
}
}
(3)处理触摸事件
//默认拦截所有的事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//这里需要为点击事件设置isClickEvent
firstTouch = event.getRawX();
touchFirst = event.getRawX();
isClickEvent = true;
break;
case MotionEvent.ACTION_MOVE:
//当滑动小于系统最小的滑动距离的时候,认为是点击操作
if (Math.abs(event.getRawX() - touchFirst) < mineScrollDistance) {
isClickEvent = true;
} else if (yzzTab.isNeedScroll) {
//否则我们将沿着手势滑动YzzTab
isClickEvent = false;
if (getScrollX() < 0) scrollTo(0, 0);
if (getScrollX() > maxScroll) scrollTo((int) maxScroll, 0);
isClickEvent = false;
float d = firstTouch - event.getRawX();
if (d < 0 && yzzTab.getScrollX() == 0) {
break;
}
if (yzzTab.getScrollX() < 0) {
yzzTab.scrollTo(0, 0);
break;
}
if (yzzTab.getScrollX() == maxScroll && d > 0) {
break;
}
if (yzzTab.getScrollX() > maxScroll) {
yzzTab.scrollTo((int) maxScroll, 0);
return false;
}
yzzTab.scrollTo((int) (d + yzzTab.getScrollX()), 0);
firstTouch = event.getRawX();
}
break;
case MotionEvent.ACTION_UP:
if (isClickEvent) {
//处理点击事件
clickEvent(event);
}
break;
}
return true;
}
/**
* 处理点击事件
*
* @param event
*/
private void clickEvent(MotionEvent event) {
int size = tabViews.size();
for (int i = 0; i < size; i++) {
View tab = tabViews.get(i);
float x = event.getRawX() + yzzTab.getScrollX();
if (x >= tab.getLeft() && x <= tab.getRight()) {
//该tab点击事件发生
//记录选中的位置记录
lastChosePosition = currentChosePosition;
currentChosePosition = i;
//改变文字的颜色
changeTabTextColor();
//当超过父布局的宽度后,我们移动tab个宽度
if (tab.getLeft() - yzzTab.getScrollX() < 0) {
int scroll = tab.getLeft();
yzzTab.scrollTo(scroll, 0);
}
int ll = tab.getRight() - yzzTab.getMeasuredWidth() - yzzTab.getScrollX();
if (ll > 0 && ll < tab.getMeasuredWidth()) {
int scroll = tab.getRight() - yzzTab.getMeasuredWidth();
yzzTab.scrollTo(scroll, 0);
}
break;
}
}
}
private void changeTabTextColor(boolean isFirst) {
//当viewpager的count和tab的个数不等式我们要破异常提醒
if (viewPager != null && viewPager.getAdapter() != null) {
if (viewPager.getAdapter().getCount() != tabViews.size()) {
throw new RuntimeException("the tab's num must equal the viewpager's child num");
}
viewPager.setCurrentItem(currentChosePosition);
}
//下面就是回调事件的调用
if (isFirst && yzzTabSelectListener != null) {
yzzTabSelectListener.onSelect(tabViews.get(currentChosePosition), currentChosePosition);
}
if (!isFirst && yzzTabSelectListener != null) {
if (currentChosePosition == lastChosePosition) {
yzzTabSelectListener.onReSelect(tabViews.get(currentChosePosition), currentChosePosition);
} else {
yzzTabSelectListener.onSelect(tabViews.get(currentChosePosition), currentChosePosition);
yzzTabSelectListener.onUnSelect(tabViews.get(lastChosePosition), lastChosePosition);
}
}
DrawHelper.changeTextColor(yzzTab, isNeedIndicator, paint);
}
//这里我们要写一个方法专门为ViewPager的切换来改变Tab,防止混乱
private void changeTabTextColorByViewPager() {
if (yzzTabSelectListener != null) {
if (currentChosePosition == lastChosePosition) {
yzzTabSelectListener.onReSelect(tabViews.get(currentChosePosition), currentChosePosition);
} else {
yzzTabSelectListener.onSelect(tabViews.get(currentChosePosition), currentChosePosition);
yzzTabSelectListener.onUnSelect(tabViews.get(lastChosePosition), lastChosePosition);
}
}
DrawHelper.changeTextColor(yzzTab, isNeedIndicator, paint);
}
//这里在第一次的时候我们需要判断一下,否则出现第一次tab未选中的现象
private void changeTabTextColor() {
changeTabTextColor(false);
}
提供两种形式去添加Tab形式和TabLayout相似
public YzzTabLayout setTab(Tab tab) {
return this;
}
public YzzTabLayout setTabList(List tabList) {
if (tabList == null) return this;
int size = tabList.size();
for (int i = 0; i < size; i++) {
if (TextUtils.isEmpty(tabList.get(i))) return this;
Tab.newTab().setText(tabList.get(i));
}
return this;
}
public void changeTabView(int textSize, int textColor, Typeface typeface) {
if (tabList == null) return;
int size = tabList.size();
for (int i = 0; i < size; i++) {
tabList.get(i).changeFace(textSize, textColor, typeface);
}
}
public void commit() {
yzzTab.removeAllViews();
int count = tabViews.size();
for (int i = 0; i < count; i++) {
yzzTab.addView(tabViews.get(i));
if (i == 0) changeTabTextColor(true);
}
// if (count > 0)
// yzzTab.setCurrentPosition(currentChosePosition, paint);
}
(4)绘制指示器,由YzzTab来完成
protected void setCurrentPosition(int position, Paint paint) {
currentPosition = position;
linePaint = paint;
invalidate();
}
private void drawLine(Canvas canvas) {
if (linePaint == null) return;
View tab = getChildAt(currentPosition);
fx = tab.getLeft();
sx = fx+tab.getMeasuredWidth();
int fy = getMeasuredHeight();
int sy = fy;
canvas.drawLine(fx, fy, sx, sy, linePaint);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawLine(canvas);
}
以上几个步骤,我们大致就完成了该TabLayout的定义,由于时间关系,添加更多的功能我就以后再做,后面我将实时更新音视频处理方面的文章。github地址:https://github.com/yzzAndroid/YzzTab