1. 程式人生 > >自定義仿QQ主介面選項卡

自定義仿QQ主介面選項卡

自定義QQ主介面選項卡

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

    }

}

喜歡博主的朋友可以關注一波哦。。。
有問題及時在評論去留言,博主會第一時間解答的
最近博主也是比較忙,文章中必有不詳細之處,但是最大的好處
就是博主的註釋還是很詳細的,彌補一下吧。。

下載原始碼

原始碼下載