Android自定义View实战(SlideTab-可滑动的选择器)

时间:2022-11-21 19:16:21

转载请标明出处:

http://blog.csdn.net/xmxkf/article/details/52178553

本文出自:【openXu的博客】

目录:

  这篇博客我们来一发自定义控件的实战,恰好前些天有一个小需求,效果图如下:

    Android自定义View实战(SlideTab-可滑动的选择器)

  根据效果图,我们可以确定,用自定义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>

运行效果:

    Android自定义View实战(SlideTab-可滑动的选择器)

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

运行结果:

    Android自定义View实战(SlideTab-可滑动的选择器)

  发现高度还是不对,其实这个地方并不是上面重写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();
    }

再看看运行效果:

    Android自定义View实战(SlideTab-可滑动的选择器)

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

效果图:

    Android自定义View实战(SlideTab-可滑动的选择器)

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>

运行效果:

    Android自定义View实战(SlideTab-可滑动的选择器)

欢迎关注,希望在这里有你想要的,博主会持续更新高(di)质(ji)量(shu)的文章和大家交流学习,祝各位学习愉快。

喜欢请点赞,no爱请勿喷~O(∩_∩)O谢谢

源码下载:

注:没有积分的童鞋 请留言索要代码喔

http://download.csdn.net/detail/u010163442/9698879