自定义仿QQ主界面选项卡

自定义QQ主界面选项卡

抽空学习了下大精子的博客,顺便转载下,不过实在是恶心CSDN没有站门一键转载的功能,烦淫啊!!!当然大家也可以去他那xiao习xiao习。点击跳转到原博客文章

QQ Android版本的效果先贴上来
这里写图片描述

可以看到这个 可爱 的选项卡,其实使用xml布局可以很容易的弄出来,但是博主就带大家封装成一个自定义控件!(看我加黑的地方,哈哈,原博主是单身,大家尽管去撩)

博主实现的效果
这里写图片描述

这速度。。。抱歉哈,博主也不知道为啥这么快。。。。

可以看到,支持的还是挺丰富的,还支持包裹,根据自定义属性tabWidht来计算宽度
其实实现起来很简单,下面博主就带小白们来实现一下,大牛请忽略


分析

问题:

实现上述的效果,如果我们是继承View,那么里面的文字、内边框、圆角效果都要自己绘制出来
而且还要支持字体大小的改变和里面文字的排列,显然这样子的代价太大
在我们平常的xml布局中,如果遇到类似的效果,我们很容易就可以想到线性布局
然后里面放几个文本控件并且使用权重进行等分宽度,所以今天的实现的思路就是这样的,只不过平时我们在xml手写的代码都封装起来!

总体思路

1.自定义控件继承LinearLayout,设置本身为水平
2.读取自定义属性到类中
3.根据所有自定义的属性往LinearLayout中添加TextView控件
4.最后就是被系统给绘制显示出来了(这步没我们的事..系统做)

额外的

1.圆角我们使用背景来实现就可以了,GradientDrawable完全可以胜任
2.添加TextView的点击事件,改变选中的下标,然后调用上述的第三步!
3.提供接口给使用者监听被选中的下标和文本

完工

首先国际惯例,决定继承的父类

这里写图片描述

三个参数的构造函数中就是我们上面说的几个步骤啦


支持的属性及其默认的值

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="XTabHost">
        <!--半径-->
        <attr name="radius" format="dimension" />
        <!--文字大小-->
        <attr name="text_size" format="dimension" />
        <!--文本选中的颜色-->
        <attr name="text_select_color" format="color" />
        <!--文本未选中的颜色-->
        <attr name="text_unselect_color" format="color" />
        <!--tab选中的颜色-->
        <attr name="tab_select_color" format="color" />
        <!--tab没选中的颜色-->
        <attr name="tab_unselect_color" format="color" />
        <!--tab间距-->
        <attr name="tab_space" format="dimension" />
        <!--tab的宽,在包裹的时候用到-->
        <attr name="tab_width" format="dimension"/>
        <!--tab的高,在包裹的时候用到-->
        <attr name="tab_height" format="dimension"/>
        <!--整体的背景-->
        <attr name="bg" format="color" />
        <!--默认显示第几个-->
        <attr name="default_index" format="integer" />
        <!--显示的文本数组-->
        <attr name="src" format="reference" />
    </declare-styleable>
</resources>

对应到类中

 /**
     * 自身控件的背景
     */
    private int backBg = Color.WHITE;

    /**
     * 没有选中的tab的背景
     */
    private int unSelectTabBg = Color.BLUE;

    /**
     * 选中的tab的背景
     */
    private int selectTabBg = Color.WHITE;

    /**
     * 一个Tab的宽和高,在自身是包裹的时候会被用到
     * -1表示不起作用,计算的时候按照包裹孩子处理
     * 80是dp的单位
     */
    private int tabWidth = 80, tabHeight = -1;

    /**
     * 未选中的文本的颜色
     */
    private int unSelectTextColor = Color.WHITE;

    /**
     * 选中的文本的颜色
     */
    private int selectTextColor = Color.BLUE;

    /**
     * 默认的字体大小,sp
     */
    private int textSize = 16;

    /**
     * 间距,px
     */
    private int space = 1;

    /**
     * 圆角半径,dp
     */
    private int radius = 0;

    /**
     * 当前的下标
     */
    private int curIndex = 1;

    /**
     * 所有要显示的文本
     */
    private String[] textArr = new String[]{};

可以看到这些属性的效果在效果图中博主基本都使用出来了


读取自定义属性

 /**
     * 读取自定义属性
     *
     * @param context
     * @param attrs
     */
    private void readAttr(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.XTabHost);

        //获取自定义属性

        curIndex = (int) a.getInt(R.styleable.XTabHost_default_index, 0);
        radius = a.getDimensionPixelSize(R.styleable.XTabHost_radius, dpToPx(radius));
        backBg = a.getColor(R.styleable.XTabHost_bg, Color.WHITE);
        unSelectTabBg = a.getColor(R.styleable.XTabHost_tab_unselect_color, 
        Color.parseColor("#51B5EF"));
        selectTabBg = a.getColor(R.styleable.XTabHost_tab_select_color, Color.WHITE);
        Color.WHITE);
        Color.parseColor("#51B5EF"));
        textSize = a.getDimensionPixelSize(R.styleable.XTabHost_text_size, 16);
        space = a.getDimensionPixelSize(R.styleable.XTabHost_tab_space, 1);
        tabWidth = a.getDimensionPixelSize(R.styleable.XTabHost_tab_width, 
        dpToPx(tabWidth));
        tabHeight = a.getDimensionPixelSize(R.styleable.XTabHost_tab_height, -1);

        CharSequence[] arr = a.getTextArray(R.styleable.XTabHost_src);
        if (arr != null) {
            String[] tArr = new String[arr.length];
            for (int i = 0; i < arr.length; i++) {
                tArr[i] = String.valueOf(arr[i]);
            }
            textArr = tArr;
        }

        a.recycle();
    }

由于每一个读取的属性在上面的定义的时候都有注释,就不做解释了


根据支持的属性生成效果

  /**
     * 根据所有的参数,弄出效果
     */
    private void sove() {

        GradientDrawable dd = new GradientDrawable();
        //设置圆角
        dd.setCornerRadii(new float[]{radius, radius, radius, radius, radius, radius, radius, radius});
        //设置背景颜色
        dd.setColor(backBg);
        //兼容低版本
        if (Build.VERSION.SDK_INT >= 16) {
            setBackground(dd);
        } else {
            setBackgroundDrawable(dd);
        }

        //移除所有的孩子
        removeAllViews();

        if (curIndex >= textArr.length || curIndex < 0) {
            curIndex = 0;
        }

        for (int i = 0; i < textArr.length; i++) {

            //创建一个文本
            TextView tv = new TextView(getContext());

            //创建文本的布局对象
            LayoutParams params = new LayoutParams(
                    0, ViewGroup.LayoutParams.MATCH_PARENT
            );

            if (i > 0) {
                params.leftMargin = space;
            }

            GradientDrawable d = getFitGradientDrawable(i);

            //如果选中了设置选中的颜色和背景
            if (curIndex == i) {
                tv.setTextColor(selectTextColor);
                d.setColor(selectTabBg);
            } else {
                tv.setTextColor(unSelectTextColor);
                d.setColor(unSelectTabBg);
            }

            //设置文本
            tv.setText(textArr[i]);
            //设置文本显示在中间
            tv.setGravity(Gravity.CENTER);
            //设置文本大小
            tv.setTextSize(textSize);
            //设置文本的背景,兼容低版本
            if (Build.VERSION.SDK_INT >= 16) {
                tv.setBackground(d);
            } else {
                //noinspection deprecation
                tv.setBackgroundDrawable(d);
            }

            //设置文本(也就是tab)的权重
            params.weight = 1;

            tv.setLayoutParams(params);

            tv.setTag(i);
            tv.setOnClickListener(this);

            //添加孩子
            addView(tv);

        }

    }
  /**
     * 获取每一个tab的背景图,也就是TextView的背景图,最左边是左边有圆角效果的
     * 最右边是右边有圆角效果的
     * 即是左边又是右边的是四个角都有圆角的
     *
     * @param index tab的下标
     * @return
     */
    private GradientDrawable getFitGradientDrawable(int index) {
        GradientDrawable d = null;
        //根据下标决定圆角
        if (index == 0 && index == textArr.length - 1) {//如果只有一个的时候
            d = new GradientDrawable();
            //设置圆角
            d.setCornerRadii(new float[]{radius, radius, radius, radius, radius, radius, radius, radius});
        } else if (index == 0) { //如果是最左边的,那左上角和左下角是圆角
            d = new GradientDrawable();
            //设置圆角
            d.setCornerRadii(new float[]{radius, radius, 0, 0, 0, 0, radius, radius});
        } else if (index == textArr.length - 1) {//如果是最右边的,那右上角和右下角是圆角
            d = new GradientDrawable();
            //设置圆角
            d.setCornerRadii(new float[]{0, 0, radius, radius, radius, radius, 0, 0});
        } else { //如果是中间的,那么没有圆角
            d = new GradientDrawable();
            //设置圆角
            d.setCornerRadii(new float[]{0, 0, 0, 0, 0, 0, 0, 0});
        }
        return d;
    }

这段代码重点说一下

第一段长的方法sove是我们上面分析实现流程中的第三步,根据所有的属性实现效果
其实很简单,首先移除所有的孩子,然后根据文字的数组的个数,添加N个TextView,让每
一个TextView都是宽度平分父容器,高度填充父容器,这和自己在xml中写的效果是一样的
添加的过程中我们需要判断当前的Tab是否是带有圆角的,因为我们可以看到效果图中
左边的有圆角,右边的也有,所以方法getFitGradientDrawable(int index);
就是用来获取指定下标的背景,其实就是获取每一个TextView应该使用的背景
在for循环开始前我们可以看到我们也设置了本身的背景,本身的背景是四个角都有的哦
然后代码的最后再添加每一个TextView的点击事件,然后切换下选中的TextView和取消选中
的TextView的效果即可

剩下的代码

@Override
    public void onClick(View v) {

        //拿到下标
        int index = (int) v.getTag();

        //如果点击的是同一个,不做处理
        if (index == curIndex) {
            return;
        }

        //拿到当前的TextView
        TextView tv = (TextView) getChildAt(curIndex);
        //设置为没有被选中的文本和没有被选中的背景
        tv.setTextColor(unSelectTextColor);
        GradientDrawable d = getFitGradientDrawable(curIndex);
        d.setColor(unSelectTabBg);

        if (Build.VERSION.SDK_INT >= 16) {
            tv.setBackground(d);
        } else {
            //noinspection deprecation
            tv.setBackgroundDrawable(d);
        }

        //记录被选中的下标
        curIndex = index;

        //拿到当前被选中的TextView
        tv = (TextView) getChildAt(curIndex);
        //设置为被选中的文本和被选中的背景
        tv.setTextColor(selectTextColor);
        d = getFitGradientDrawable(curIndex);
        d.setColor(selectTabBg);
        if (Build.VERSION.SDK_INT >= 16) {
            tv.setBackground(d);
        } else {
            //noinspection deprecation
            tv.setBackgroundDrawable(d);
        }

        //如果使用者监听了就通知一下
        if (mOnSelectListener != null) {
            mOnSelectListener.onSelect(index, textArr[index]);
        }

    }

    /**
     * dp的单位转换为px的
     *
     * @param dps
     * @return
     */
    int dpToPx(int dps) {
        return Math.round(getResources().getDisplayMetrics().density * dps);
    }

    /**
     * sp转px
     *
     * @param spVal
     * @return
     */
    int spToPx(float spVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                spVal, getResources().getDisplayMetrics());
    }

    private OnSelectListener mOnSelectListener;

    /**
     * 设置监听
     *
     * @param mOnSelectListener
     */
    public void setOnSelectListener(OnSelectListener mOnSelectListener) {
        this.mOnSelectListener = mOnSelectListener;
    }

    /**
     * 回调接口
     */
    public interface OnSelectListener {

        /**
         * 回调方法
         *
         * @param index
         * @param text
         */
        void onSelect(int index, String text);

    }

下面贴出所有的代码

/**
 * Created by cxj on 2017/2/19.
 * 模仿qq主界面的选项卡
 */
public class XTabHost extends LinearLayout implements View.OnClickListener {


    public XTabHost(Context context) {
        this(context, null);
    }

    public XTabHost(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public XTabHost(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //设置孩子排列的方向是水平的
        setOrientation(HORIZONTAL);

        //读取自定义属性
        readAttr(context, attrs);

        //显示效果
        sove();

    }

    /**
     * 读取自定义属性
     *
     * @param context
     * @param attrs
     */
    private void readAttr(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.XTabHost);

        //获取自定义属性

        curIndex = (int) a.getInt(R.styleable.XTabHost_default_index, 0);
        radius = a.getDimensionPixelSize(R.styleable.XTabHost_radius, dpToPx(radius));
        backBg = a.getColor(R.styleable.XTabHost_bg, Color.WHITE);
        unSelectTabBg = a.getColor(R.styleable.XTabHost_tab_unselect_color, Color.parseColor("#51B5EF"));
        selectTabBg = a.getColor(R.styleable.XTabHost_tab_select_color, Color.WHITE);
        unSelectTextColor = a.getColor(R.styleable.XTabHost_text_unselect_color, Color.WHITE);
        selectTextColor = a.getColor(R.styleable.XTabHost_text_select_color, Color.parseColor("#51B5EF"));
        textSize = a.getDimensionPixelSize(R.styleable.XTabHost_text_size, 16);
        space = a.getDimensionPixelSize(R.styleable.XTabHost_tab_space, 1);
        tabWidth = a.getDimensionPixelSize(R.styleable.XTabHost_tab_width, dpToPx(tabWidth));
        tabHeight = a.getDimensionPixelSize(R.styleable.XTabHost_tab_height, -1);

        CharSequence[] arr = a.getTextArray(R.styleable.XTabHost_src);
        if (arr != null) {
            String[] tArr = new String[arr.length];
            for (int i = 0; i < arr.length; i++) {
                tArr[i] = String.valueOf(arr[i]);
            }
            textArr = tArr;
        }

        a.recycle();
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {


        //获取计算模式
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        //获取推荐的宽和高
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

        if (modeWidth == MeasureSpec.EXACTLY) { //如果是确定的

        } else { //如果是包裹的或者在横向的列表中
            if (tabWidth > -1) {
                for (int i = 0; i < getChildCount(); i++) {
                    TextView view = (TextView) getChildAt(i);
                    LayoutParams lp = (LayoutParams) view.getLayoutParams();
                    lp.width = tabWidth;
                }
            }
        }

        if (modeHeight == MeasureSpec.EXACTLY) { //如果是确定的

        } else { //如果是包裹的或者在纵向的列表中
            if (tabHeight > -1) {
                for (int i = 0; i < getChildCount(); i++) {
                    TextView view = (TextView) getChildAt(i);
                    LayoutParams lp = (LayoutParams) view.getLayoutParams();
                    lp.height = tabHeight;
                }
            }

        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

    /**
     * 自身控件的背景
     */
    private int backBg = Color.WHITE;

    /**
     * 没有选中的tab的背景
     */
    private int unSelectTabBg = Color.BLUE;

    /**
     * 选中的tab的背景
     */
    private int selectTabBg = Color.WHITE;

    /**
     * 一个Tab的宽和高,在自身是包裹的时候会被用到
     * -1表示不起作用,计算的时候按照包裹孩子处理
     * 80是dp的单位
     */
    private int tabWidth = 80, tabHeight = -1;

    /**
     * 未选中的文本的颜色
     */
    private int unSelectTextColor = Color.WHITE;

    /**
     * 选中的文本的颜色
     */
    private int selectTextColor = Color.BLUE;

    /**
     * 默认的字体大小,sp
     */
    private int textSize = 16;

    /**
     * 间距,px
     */
    private int space = 1;

    /**
     * 圆角半径,dp
     */
    private int radius = 0;

    /**
     * 当前的下标
     */
    private int curIndex = 1;

    /**
     * 所有要显示的文本
     */
    private String[] textArr = new String[]{};


    /**
     * 根据所有的参数,弄出效果
     */
    private void sove() {

        GradientDrawable dd = new GradientDrawable();
        //设置圆角
        dd.setCornerRadii(new float[]{radius, radius, radius, radius, radius, radius, radius, radius});
        //设置背景颜色
        dd.setColor(backBg);
        //兼容低版本
        if (Build.VERSION.SDK_INT >= 16) {
            setBackground(dd);
        } else {
            setBackgroundDrawable(dd);
        }

        //移除所有的孩子
        removeAllViews();

        if (curIndex >= textArr.length || curIndex < 0) {
            curIndex = 0;
        }

        for (int i = 0; i < textArr.length; i++) {

            //创建一个文本
            TextView tv = new TextView(getContext());

            //创建文本的布局对象
            LayoutParams params = new LayoutParams(
                    0, ViewGroup.LayoutParams.MATCH_PARENT
            );

            if (i > 0) {
                params.leftMargin = space;
            }

            GradientDrawable d = getFitGradientDrawable(i);

            //如果选中了设置选中的颜色和背景
            if (curIndex == i) {
                tv.setTextColor(selectTextColor);
                d.setColor(selectTabBg);
            } else {
                tv.setTextColor(unSelectTextColor);
                d.setColor(unSelectTabBg);
            }

            //设置文本
            tv.setText(textArr[i]);
            //设置文本显示在中间
            tv.setGravity(Gravity.CENTER);
            //设置文本大小
            tv.setTextSize(textSize);
            //设置文本的背景,兼容低版本
            if (Build.VERSION.SDK_INT >= 16) {
                tv.setBackground(d);
            } else {
                //noinspection deprecation
                tv.setBackgroundDrawable(d);
            }

            //设置文本(也就是tab)的权重
            params.weight = 1;

            tv.setLayoutParams(params);

            tv.setTag(i);
            tv.setOnClickListener(this);

            //添加孩子
            addView(tv);

        }

    }

    /**
     * 获取每一个tab的背景图,最左边是左边有圆角效果的
     * 最右边是右边有圆角效果的
     * 即是左边又是右边的是四个角都有圆角的
     *
     * @param index tab的下标
     * @return
     */
    private GradientDrawable getFitGradientDrawable(int index) {
        GradientDrawable d = null;
        //根据下标决定圆角
        if (index == 0 && index == textArr.length - 1) {
            d = new GradientDrawable();
            //设置圆角
            d.setCornerRadii(new float[]{radius, radius, radius, radius, radius, radius, radius, radius});
        } else if (index == 0) {
            d = new GradientDrawable();
            //设置圆角
            d.setCornerRadii(new float[]{radius, radius, 0, 0, 0, 0, radius, radius});
        } else if (index == textArr.length - 1) {
            d = new GradientDrawable();
            //设置圆角
            d.setCornerRadii(new float[]{0, 0, radius, radius, radius, radius, 0, 0});
        } else {
            d = new GradientDrawable();
            //设置圆角
            d.setCornerRadii(new float[]{0, 0, 0, 0, 0, 0, 0, 0});
        }
        return d;
    }


    @Override
    public void onClick(View v) {

        //拿到下标
        int index = (int) v.getTag();

        //如果点击的是同一个,不做处理
        if (index == curIndex) {
            return;
        }

        //拿到当前的TextView
        TextView tv = (TextView) getChildAt(curIndex);
        //设置为没有被选中的文本和没有被选中的背景
        tv.setTextColor(unSelectTextColor);
        GradientDrawable d = getFitGradientDrawable(curIndex);
        d.setColor(unSelectTabBg);

        if (Build.VERSION.SDK_INT >= 16) {
            tv.setBackground(d);
        } else {
            //noinspection deprecation
            tv.setBackgroundDrawable(d);
        }

        //记录被选中的下标
        curIndex = index;

        //拿到当前被选中的TextView
        tv = (TextView) getChildAt(curIndex);
        //设置为被选中的文本和被选中的背景
        tv.setTextColor(selectTextColor);
        d = getFitGradientDrawable(curIndex);
        d.setColor(selectTabBg);
        if (Build.VERSION.SDK_INT >= 16) {
            tv.setBackground(d);
        } else {
            //noinspection deprecation
            tv.setBackgroundDrawable(d);
        }

        //如果使用者监听了就通知一下
        if (mOnSelectListener != null) {
            mOnSelectListener.onSelect(index, textArr[index]);
        }

    }

    /**
     * dp的单位转换为px的
     *
     * @param dps
     * @return
     */
    int dpToPx(int dps) {
        return Math.round(getResources().getDisplayMetrics().density * dps);
    }

    /**
     * sp转px
     *
     * @param spVal
     * @return
     */
    int spToPx(float spVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                spVal, getResources().getDisplayMetrics());
    }

    private OnSelectListener mOnSelectListener;

    /**
     * 设置监听
     *
     * @param mOnSelectListener
     */
    public void setOnSelectListener(OnSelectListener mOnSelectListener) {
        this.mOnSelectListener = mOnSelectListener;
    }

    /**
     * 回调接口
     */
    public interface OnSelectListener {

        /**
         * 回调方法
         *
         * @param index
         * @param text
         */
        void onSelect(int index, String text);

    }

}

国际惯例的Demo:源码下载

学习了下,顺便现实下,虽然xml也可以实现,但是这样感jiao高大点啊 哈哈 当然主要是学习,最后转载记录下!!!恩 极好的,还有就是撩到原博主的别忘记了给我介绍费哈

发布了11 篇原创文章 · 获赞 24 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/feng40492459/article/details/72523616