转载请标明出处:

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. STL源码剖析 — 空间配置器(allocator)

    前言 以STL的实现角度而言,第一个需要介绍的就是空间配置器,因为整个STL的操作对象都存放在容器之中. 你完全可以实现一个直接向硬件存取空间的allocator. 下面介绍的是SGI STL提供的配 ...

  2. 原生js代码挑战之动态添加双色球

    var ballArr = []; //存放已有的红球,用来排除重复和排序window.onload = function(){ var btn = document.createElement(&q ...

  3. [ZooKeeper] 1 基本概念

    ZooKeeper: A Distributed Coordination Service for Distributed Applications ZooKeeper is a distribute ...

  4. [HNOI 2012]集合选数

    Description 题库链接 对于任意一个正整数 \(n\) ,求出集合 \(\{1,2,\cdots,n\}\) 的满足约束条件"若 \(x\) 在该子集中,则 \(2x\) 和 \( ...

  5. [TJOI2017]可乐

    题目描述 加里敦星球的人们特别喜欢喝可乐.因而,他们的敌对星球研发出了一个可乐机器人,并且放在了加里敦星球的1号城市上.这个可乐机器人有三种行为: 停在原地,去下一个相邻的城市,自爆.它每一秒都会随机 ...

  6. ●BZOJ 2839 集合计数

    题链: http://www.lydsy.com/JudgeOnline/problem.php?id=2839 题解: 容斥原理 真的是神题!!! 定义 f[k] 表示交集大小至少为 k时的方案数怎 ...

  7. 2015 ICL, Finals, Div. 1 Ceizenpok’s formula(组合数取模,扩展lucas定理)

    J. Ceizenpok’s formula time limit per test 2 seconds memory limit per test 256 megabytes input stand ...

  8. Java Servlet 笔记1

    1. 什么是Servlet. Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序 ...

  9. Windows下设置 ssh key,配置GitHub ssh key

    1.新建一个目录,利用git工具打开 Git Bash Here 2.执行如下命令 ssh-keygen -t rsa -C "email@email.com" 其中邮箱为GitH ...

  10. Big Christmas Tree(poj-3013)最短路

    Big Christmas Tree Time Limit: 3000MS   Memory Limit: 131072K Total Submissions: 25823   Accepted: 5 ...