優雅的自定義TabLayout
阿新 • • 發佈:2019-01-06
為啥要自己定義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<String> 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