1. 程式人生 > >優雅的自定義TabLayout

優雅的自定義TabLayout

為啥要自己定義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