优雅的自定义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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值