Android自定義View精品(SlideTab-可滑動的選擇器)

版權聲明:本文爲openXu原創文章【openXu的博客】,未經博主允許不得以任何形式轉載

目錄:


  這篇博客我們來一發自定義控件的實戰,恰好前些天有一個小需求,效果圖如下:
    

  根據效果圖,我們可以確定,用自定義View完全可以搞定,在自定義控件系列博客第一篇中,我們總結了自定義View的幾個步驟:

  • 繼承View,覆蓋構造方法
  • 自定義屬性
  • 重寫onMeasure方法測量寬高
  • 重寫onDraw方法繪製控件

  當然,你沒有必要完全依照步驟去做,這個步驟是你對控件應該怎麼寫已經有了完整的思路和規劃,這在實際情況下是不現實的,往往我們自定義控件都是做到哪裏缺什麼就做什麼,首先我們應該將它畫出來,有一個可視的供我們思考的視圖。所以,這裏我們將這個步驟靈活的變換一下,由於我們現在還不確定需要自定義哪些屬性,以及需要怎樣測量,所以我們把這兩個步驟挪到後面。

1. 初步分析,重寫onDraw繪製

  首先我們分析一下這個控件裏面有哪些元素,有一條直線,上面有n個選項,分佈着n個圓,當選中哪一個後這上面的圓變爲藍色的,還有n項字,當選中後字變爲藍色。下面我們初步確定一下需要的常量和一些簡單的計算:

  • 一個供選擇的數組
    String[] tabNames = new String[]{"tab1","tab2","tab3","tab4"}
  • 一些必要的數據:字體大小mTextSize,字體顏色mColorTextDef,線段和圓圈的顏色mColorDef,被選中後的顏色mColorSelected,直線的高度mLineHight,圓圈的直徑mCircleHight,被選中後藍色空心圓圈的寬度mCircleSelStroke,當前選中的序號selectedIndex
  • 直線的長度float lineLength=整個控件的寬度-左邊圓圈的半徑 -右邊圓圈的半徑(爲了讓直線兩端正好在兩端圓圈的中心)
  • 圓圈的分佈間隔距離float splitLength = lineLength / (n-1);
  • 字體與上面部分的間距mMarginTop

  在動手之前,我們要注意:直線的長度應該在控件完成測量後才能計算,所以應該在onMeasure中計算。現在我們可以動手了,首先繼承View,覆蓋構造方法,然後重寫onDraw,在上面畫出初步的輪廓。

代碼:

public class SlideTab extends View {
    String TAG = "SlidingTab";
    private int mTextSize;          //文本的字體大小
    private int mColorTextDef;      // 默認文本的顏色
    private int mColorDef;          // 線段和圓圈顏色
    private int mColorSelected;     //選中的字體和圓圈顏色
    private int mLineHight;         //基準線高度
    private int mCircleHight;       //圓圈的高度(直徑)
    private int mCircleSelStroke;   //被選中圓圈(空心)的粗細
    private int mMarginTop;         //圓圈和文字之間的距離
    private String[] tabNames;      //需要繪製的文字

    /**
     * 下面需要計算
     */
    private float splitLengh;       //每一段橫線長度
    private int textStartY;         //文本繪製的Y軸座標
    private List<Rect> mBounds;     //保存文本的量的結果

    private int selectedIndex = 0;      //當前選中序號

    private Paint mTextPaint;      //繪製文字的畫筆
    private Paint mLinePaint;      //繪製基準線的畫筆
    private Paint mCirclePaint;    //繪製基準線上灰色圓圈的畫筆
    private Paint mCircleSelPaint; //繪製被選中位置的藍色圓圈的畫筆

    public SlideTab(Context context) {
        this(context, null);
    }
    public SlideTab(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public SlideTab(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //初始化屬性
        tabNames = new String[]{"tab1","tab2","tab3","tab4"};

        mColorTextDef = Color.GRAY;
        mColorSelected = Color.BLUE;
        mColorDef = Color.argb(255,234,234,234);   //#EAEAEA
        mTextSize = 20;

        mLineHight = 5;
        mCircleHight = 20;
        mCircleSelStroke = 10;
        mMarginTop = 50;

        mLinePaint = new Paint();
        mCirclePaint = new Paint();
        mTextPaint = new Paint();
        mCircleSelPaint = new Paint();

        mLinePaint.setColor(mColorDef);
        mLinePaint.setStyle(Paint.Style.FILL);//設置填充
        mLinePaint.setStrokeWidth(mLineHight);//筆寬像素
        mLinePaint.setAntiAlias(true);//鋸齒不顯示

        mCirclePaint.setColor(mColorDef);
        mCirclePaint.setStyle(Paint.Style.FILL);//設置填充
        mCirclePaint.setStrokeWidth(1);//筆寬像素
        mCirclePaint.setAntiAlias(true);//鋸齒不顯示
        mCircleSelPaint.setColor(mColorSelected);
        mCircleSelPaint.setStyle(Paint.Style.STROKE);    //空心圓圈
        mCircleSelPaint.setStrokeWidth(mCircleSelStroke);
        mCircleSelPaint.setAntiAlias(true);

        mTextPaint.setTextSize(mTextSize);
        mTextPaint.setColor(mColorTextDef);
        mLinePaint.setAntiAlias(true);

        measureText();
    }

    /**
     * measure the text bounds by paint
     */
    private void measureText(){
        mBounds = new ArrayList<>();
        for(String name : tabNames){
            Rect mBound = new Rect();
            mTextPaint.getTextBounds(name, 0, name.length(), mBound);
            mBounds.add(mBound);
        }
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        initConstant();
    }

    private void initConstant(){
        int lineLengh = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleHight;
        splitLengh = lineLengh/(tabNames.length-1);
        textStartY = mCircleHight + mMarginTop + getPaddingTop();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //畫灰色基準線
        canvas.drawLine(mCircleHight/2, mCircleHight/2, getWidth()-mCircleHight/2,mCircleHight/2 , mLinePaint);

        float centerY = mCircleHight/2;
        for(int i = 0; i<tabNames.length; i++){
            float centerX = mCircleHight/2+(i*splitLengh);
            //float cx, float cy, float radius, @NonNull Paint paint
            //畫基準線上灰色小圓圈
//            Log.v(TAG, "畫圓:X:"+centerX+"  Y:"+centerY);
            canvas.drawCircle(centerX, centerY,mCircleHight/2,mCirclePaint);

            mTextPaint.setColor(mColorTextDef);
            if(selectedIndex == i){
                //畫選中位置的藍色圓圈
                mCircleSelPaint.setStrokeWidth(mCircleSelStroke);
                mCircleSelPaint.setStyle(Paint.Style.STROKE);
//                    Log.v(TAG, "畫圓:X:"+centerX+"  Y:"+centerY+"  半徑:"+(mCircleHight-mCircleSelHight)/2);
                canvas.drawCircle(centerX, centerY, (mCircleHight-mCircleSelStroke)/2, mCircleSelPaint);
                mTextPaint.setColor(mColorSelected);
            }

            //繪製文字
            float startX;
            if(i == 0){
                startX = 0;
            }else if(i == tabNames.length-1){
                startX = getWidth()-mBounds.get(i).width();
            }else{
                startX = centerX-(mBounds.get(i).width()/2);
            }
//            Log.v(TAG, "寫字:X:"+startX+"  Y:"+textStartY +"  字寬度:"+mBounds.get(i).width());
            canvas.drawText(tabNames[i], startX, textStartY, mTextPaint);
        }
    }
}

佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dip">

    <com.openxu.st.SlideTab
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#aaff0000"/>
</LinearLayout>

運行效果:
    這裏寫圖片描述

2. 重寫onMeasure計算寬高

  基本的效果圖已經出來了,不知道你們有沒有發現,我在寫佈局文件的時候設置的高度是wrap_content,並且爲控件設置了紅色背景以便於參考,運行結果顯示控件的高度卻佔滿的整個屏幕,所以我們應該用重寫onMeasure測量控件的高度(不熟悉onMeasure可以參照博客Android自定義View(三、深入解析控件測量onMeasure))。對於此控件,它的高度設置爲填充父窗體,高度應該是圓圈的直徑+字體的高度+字體與上面部分的距離。

重寫onMeasure:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);   //獲取寬的模式
        int heightMode = MeasureSpec.getMode(heightMeasureSpec); //獲取高的模式
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);   //獲取寬的尺寸
        int heightSize = MeasureSpec.getSize(heightMeasureSpec); //獲取高的尺寸
        int height ;
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            float textHeight = mBounds.get(0).height();
            height = (int) (textHeight + mCircleHight + mMarginTop);
//            Log.v(TAG, "文本的高度:"+textHeight + "控件的高度:"+height);
        }
        //保存測量寬度和測量高度
        setMeasuredDimension(widthSize, height);
        initConstant();
    }

運行結果:
    這裏寫圖片描述

  發現高度還是不對,其實這個地方並不是上面重寫onMeasure有問題,而是繪製文本的Y座標的問題,我們看看drawText方法的註釋:

/**
 * Draw the text, with origin at (x,y), using the specified paint. The
 * origin is interpreted based on the Align setting in the paint.
 *
 * @param text  The text to be drawn
 * @param x     The x-coordinate of the origin of the text being drawn
 * @param y     The y-coordinate of the baseline of the text being drawn
 * @param paint The paint used for the text (e.g. color, size, style)
 */
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
    native_drawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags,
            paint.getNativeInstance(), paint.mNativeTypeface);
}

  對於參數y的說明中,它指的是baseline的y軸座標,而不是文字top的y座標,對於baseline,後面再做說明,所以,我們計算textStartY的時候,應該計算baseline的y座標:

private void initConstant(){
        int lineLengh = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleHight;
        splitLengh = lineLengh/(tabNames.length-1);
        // FontMetrics對象
        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
        textStartY = getHeight() - (int)fontMetrics.bottom;    //baseLine的位置
//        textStartY = mCircleHight + mMarginTop + getPaddingTop();
    }

再看看運行效果:
    這裏寫圖片描述

3. 重寫onTouch加入滑動效果

  現在,文字顯示已經沒有問題了,接下來,我們加入手指滑動的效果。此控件只支持左右滑動,手指滑動到某個位置的時候記錄xy的座標值,然後將藍色選中的圓圈移動到x位置,其實就是在手指的位置畫一個藍色的圓圈,還要根據x的值計算當前偏向於選擇哪一個標籤。這裏需要注意的地方是event.getX()event.getY()獲取到的手指的座標是相對於本控件左上角的座標(本控件左上角爲原點),具體看下面代碼,註釋已經很清楚了:

@Override
    protected void onDraw(Canvas canvas) {
        //畫灰色基準線
        canvas.drawLine(mCircleHight/2, mCircleHight/2, getWidth()-mCircleHight/2,mCircleHight/2 , mLinePaint);

        float centerY = mCircleHight/2;
        for(int i = 0; i<tabNames.length; i++){
            float centerX = mCircleHight/2+(i*splitLengh);
            //float cx, float cy, float radius, @NonNull Paint paint
            //畫基準線上灰色小圓圈
//            Log.v(TAG, "畫圓:X:"+centerX+"  Y:"+centerY);
            canvas.drawCircle(centerX, centerY,mCircleHight/2,mCirclePaint);

            mTextPaint.setColor(mColorTextDef);
            if(selectedIndex == i){
                if(!isSliding){
                    //畫選中位置的藍色圓圈
                    mCircleSelPaint.setStrokeWidth(mCircleSelStroke);
                    mCircleSelPaint.setStyle(Paint.Style.STROKE);
//                    Log.v(TAG, "畫圓:X:"+centerX+"  Y:"+centerY+"  半徑:"+(mCircleHight-mCircleSelHight)/2);
                    canvas.drawCircle(centerX, centerY, (mCircleHight-mCircleSelStroke)/2, mCircleSelPaint);
                }
                mTextPaint.setColor(mColorSelected);
            }

            //繪製文字
            float startX;
            if(i == 0){
                startX = 0;
            }else if(i == tabNames.length-1){
                startX = getWidth()-mBounds.get(i).width();
            }else{
                startX = centerX-(mBounds.get(i).width()/2);
            }
//            Log.v(TAG, "寫字:X:"+startX+"  Y:"+textStartY +"  字寬度:"+mBounds.get(i).width());
            canvas.drawText(tabNames[i], startX, textStartY, mTextPaint);
        }

        //畫手指拖動位置圓圈,最後畫,避免被其他圓圈覆蓋
        if(isSliding){
//            Log.v(TAG, "手指拖動畫圓:X:"+slidX+"  Y:"+centerY+"  半徑:"+mCircleHight/2);
            mCircleSelPaint.setStrokeWidth(1);
            mCircleSelPaint.setStyle(Paint.Style.FILL);
            canvas.drawCircle(slidX, centerY, mCircleHight/2, mCircleSelPaint);
        }

    }
    private boolean isSliding = false;  //手指是否在拖動
    private float slidX, slidY;         //手指當前位置(相對於本控件左上角的座標)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        slidX = event.getX();   //以本控件左上角爲座標原點
        slidY = event.getY();
        //左右越界
        if(slidX< mCircleHight/2)
            slidX = mCircleHight/2;
        if(slidX>(getWidth() - mCircleHight/2))
            slidX = getWidth() - mCircleHight/2;
        Log.e(TAG, "手指位置:  getX:"+slidX+"  getY:"+slidY);
        float select = slidX/splitLengh;
        int xs = (int)(select*10)-(((int)select)*10);
        selectedIndex = (int)select +(xs>5?1:0);
//        Log.w(TAG, "手指位置在第"+select+"位置,小數爲:"+xs+" ,選中的序列爲:"+selectedIndex);
        //TODO 如果要求手指脫離了直線所在矩形之後停止滑動,放開下面代碼
       /* if(slidY>mCircleHight || slidY < 0){
            Log.e(TAG, "手指落在外面了");
            if(isSliding){    //滑動到外面的,這時候需要重新繪製一次,其他事件不用重繪
                isSliding = false;
                invalidate();
            }
            isSliding = false;
            return super.onTouchEvent(event);
        }*/
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                isSliding = true;
//                Log.e(TAG, "手指按下:  getX:"+slidX+"  getY:"+slidY);
                break;
            case MotionEvent.ACTION_MOVE:
//                Log.i(TAG, "手指滑動:  getX:"+slidX+"  getY:"+slidY);
                break;
            case MotionEvent.ACTION_UP:
//                Log.e(TAG, "手指擡起:  getX:"+slidX+"  getY:"+slidY);
                isSliding = false;
                break;
        }
        invalidate();
        return true;
    }

效果圖:
    這裏寫圖片描述

4. 自定義屬性

  目前爲止,控件基本能夠正常使用了,如果你認爲這樣就可以了,那就不用往下看了。這個樣子使用起來很不方便,如果很多地方需要用到此控件,而且控件中的字體大小顏色等都不一樣,那是不是得寫很多這樣的控件(只是改變一下里面一些常量的值)?所以爲了讓這個控件使用更加靈活,可以自定義一些屬性,這樣只需要在佈局文件中設置屬性值即可。自定義屬性具體方法請參見(Android自定義View(二、深入解析自定義屬性))。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--
    private int mColorTextDef;      // 默認文本的顏色
    private int mColorDef;          // 線段和圓圈顏色
    private int mColorSelected;     //選中的字體和圓圈顏色
    private int mLineHight;         //基準線高度
    private int mCircleHight;       //圓圈的高度(直徑)
    private int mCircleSelStroke;   //被選中圓圈(空心)的粗細
    private int mMarginTop;         //圓圈和文字之間的距離
    private String[] tabNames;      //需要繪製的文字
    private int mTextSize;          //文本的字體大小
    -->
    <declare-styleable name="SlidTab">
        <attr name="textColorDef" format="reference|color"/>             <!--默認文本的顏色-->
        <attr name="android:textSize"/>                 <!--文本的字體大小-->
        <attr name="defColor" format="reference|color" />  <!--線段和圓圈顏色-->
        <attr name="selectedColor" format="reference|color" /><!--選中的字體和圓圈顏色-->
        <attr name="lintHight" format="dimension" />   <!--基準線高度-->
        <attr name="circleHight" format="dimension" />    <!--圓圈的高度(直徑)-->
        <attr name="circleSelStroke" format="dimension" />   <!--被選中圓圈(空心)的粗細-->
        <attr name="mMarginTop" format="dimension" />   <!--圓圈和文字之間的距離-->
        <attr name="tabNames" format="reference" />   <!--需要繪製的文字-->
    </declare-styleable>
</resources>

佈局中使用:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:openXu="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dip">

    <com.openxu.st.SlideTab
        android:id="@+id/slideTab"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize = "15sp"
        openXu:textColorDef = "#A4A4A4"
        openXu:defColor = "#EAEAEA"
        openXu:selectedColor = "#5CBB8C"
        openXu:lintHight = "2dip"
        openXu:circleHight = "20dip"
        openXu:circleSelStroke = "5dip"
        openXu:mMarginTop = "15dip"
        openXu:tabNames = "@array/tab_names" />
</LinearLayout>

運行效果:
    這裏寫圖片描述

歡迎關注,希望在這裏有你想要的,博主會持續更新高(di)質(ji)量(shu)的文章和大家交流學習,祝各位學習愉快。

喜歡請點贊,no愛請勿噴~O(∩_∩)O謝謝

##源碼下載:

注:沒有積分的童鞋 請留言索要代碼喔

http://download.csdn.net/detail/u010163442/9698879 CSDN下載平臺太流氓

https://github.com/openXu/SlidingTab

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章