自定义仿QQ主界面选项卡

59 篇文章 0 订阅
15 篇文章 0 订阅

自定义QQ主界面选项卡

写博客、效果、demo都是很花时间的,转载请注明出处!
http://blog.csdn.net/u011692041/article/details/60780603

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);

    }

}

喜欢博主的朋友可以关注一波哦。。。
有问题及时在评论去留言,博主会第一时间解答的
最近博主也是比较忙,文章中必有不详细之处,但是最大的好处
就是博主的注释还是很详细的,弥补一下吧。。

下载源码

源码下载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值