转载请标明出处:

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

本文出自:【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

Android自定义View实战(SlideTab-可滑动的选择器)的更多相关文章

  1. 转载:android自定义view实战(温度控制表)!

    效果图 package cn.ljuns.temperature.view; import com.example.mvp.R; import android.content.Context;impo ...

  2. Android为TV端助力 转载:android自定义view实战(温度控制表)!

    效果图 package cn.ljuns.temperature.view; import com.example.mvp.R; import android.content.Context;impo ...

  3. android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检索

    我们的手机通讯录一般都有这样的效果,如下图: OK,这种效果大家都见得多了,基本上所有的android手机通讯录都有这样的效果.那我们今天就来看看这个效果该怎么实现. 一.概述 1.页面功能分析 整体 ...

  4. Android 自定义View合集

    自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...

  5. Android自定义View之ProgressBar出场记

    关于自定义View,我们前面已经有三篇文章在介绍了,如果筒子们还没阅读,建议先看一下,分别是android自定义View之钟表诞生记.android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检 ...

  6. android自定义View之NotePad出鞘记

    现在我们的手机上基本都会有一个记事本,用起来倒也还算方便,记事本这种东东,如果我想要自己实现,该怎么做呢?今天我们就通过自定义View的方式来自定义一个记事本.OK,废话不多说,先来看看效果图. 整个 ...

  7. Android自定义View(CustomCalendar-定制日历控件)

    转载请标明出处: http://blog.csdn.net/xmxkf/article/details/54020386 本文出自:[openXu的博客] 目录: 1分析 2自定义属性 3onMeas ...

  8. Android自定义View(RollWeekView-炫酷的星期日期选择控件)

    转载请标明出处: http://blog.csdn.net/xmxkf/article/details/53420889 本文出自:[openXu的博客] 目录: 1分析 2定义控件布局 3定义Cus ...

  9. Android 自定义View——自定义点击事件

    每个人手机上都有通讯录,这是毫无疑问的,我们通讯录上有一个控件,在通讯录的最左边有一列从”#”到”Z”的字母,我们通过滑动或点击指定的字母来确定联系人的位置,进而找到联系人.我们这一节就通过开发这个控 ...

随机推荐

  1. 学习linux的一些指令

    简单说一下我对linux的理解,linux只有一个根目录,所有目录都挂在该根目录上,磁盘进行分区,然后生成文件系统,挂到目录上,/etc/fstab用于记录系统配置,比如分区挂载点,开机自动挂载等等. ...

  2. OpenGL中glUniform1i使用

    在OpenGL中使用glGetUniformLocation和glUniformxxx等函数时,要在之前启用对应的着色器程序,即调用glUseProgram.

  3. ASP.NET CORE系列【六】Entity Framework Core 之数据库迁移

    前言 最近打算用.NET Core写一份简单的后台系统,来练练手 然后又用到了Entity Framework Core 发现园子里有些文章讲得不是那么细节,对于新手小白来说,可能会有点懵. 特意整理 ...

  4. .NET Core2.0+MVC 用Redis/Memory+cookie实现的sso单点登录

    之前发布过使用session+cookie实现的单点登录,博主个人用的很不舒服,为什么呢,博主自己测试的时候,通过修改host的方法,在本机发布了三个站点,但是,经过测试,发现,三个站点使用的sess ...

  5. [LeetCode] Shortest Unsorted Continuous Subarray 最短无序连续子数组

    Given an integer array, you need to find one continuous subarray that if you only sort this subarray ...

  6. 关于Unity中NGUI图片精灵响应鼠标的方法

    我在Unity里做NGUI的时候发现一个问题. 在Unity2D场景当中,一个精灵图片只要加上了Box Collider或者Box Collider2D,就可以相应OnMouseEnter和OnMou ...

  7. 6.19 noip模拟题(题目及解析转自 hzwer 2014-3-15 NOIP模拟赛)

    Problem 1 高级打字机(type.cpp/c/pas) [题目描述] 早苗入手了最新的高级打字机.最新款自然有着与以往不同的功能,那就是它具备撤销功能,厉害吧. 请为这种高级打字机设计一个程序 ...

  8. ●hihocoder #1394 网络流四·最小路径覆盖

    题链: http://hihocoder.com/problemset/problem/1394 题解: 有向图最小路径覆盖:最少的路径条数不重不漏的覆盖所有点. 注意到在任意一个最小路径覆盖的方案下 ...

  9. VK Cup 2017 - Квалификация 2

    因为资格赛1已经通过了,资格赛2随便打打玩.这次题目比上次还简单,FallDream看了两眼觉得太水就不做了,我一个人闲着无聊只好默默做了 A. Новый пароль 题目大意:给出N和K,要求构 ...

  10. bzoj 4448: [Scoi2015]情报传递

    Description 奈特公司是一个巨大的情报公司,它有着庞大的情报网络.情报网络中共有n名情报员.每名情报员口J-能有 若T名(可能没有)下线,除1名大头日外其余n-1名情报员有且仅有1名上线.奈 ...