自定义刻度盘View--详解

Posted TC风之翼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义刻度盘View--详解相关的知识,希望对你有一定的参考价值。

简介

本篇是接上一篇seekbar的自定义view进阶版。
本自定义view主要功能:

  1. 可自定义起始时间以及最大时间,设置总格数,每格均分时间差。
  2. 可自定义界面颜色字体大小,文本提示。
  3. 单击触摸可触发刻度以及时间的变动动画效果,动画效果更自然,从上一次位置开始变更。触摸范围为大圆内到圆心距离大于1/2半径距离的坐标范围。触摸事件为action_move时不会触发动画。
  4. 提供禁用触摸操作,以便特殊需求。
  5. 提供是否清零设置(开启后设置时间等周边位置可清零),默认是0格,0格代表的是你设置的初始时间值。
  6. 提供适用于自动倒计时模式下的方法,以便更好更新view的显示。
  7. 提供时间以及刻度变化的监听。

效果图如下。
这里写图片描述

1.主要思路

1)首先老规矩还是先分析有哪些绘制模块,以及根据功能分析需要什么配置参数。根据Gif图,我们从视图效果看有刻度、时间提示、底部文本提示等元素。

  • a.为了绘制这个刻度,我们肯定是围绕一个圆的边进行绘制。也就是说我们需要知道大圆半径,以及圆心坐标。本view的半径是根据view的大小以及内边距进行计算,并且圆心始终是自定义view控件的几何中心。刻度绘制方式并非采用熟知的画布翻转remote,而是通过刻度总数以及起始角度135、终点角度45度(总跨度270度,这里的角度是指绘制刻度的角度,0度为水平方法向向右。下面有个图解释坐标轴)来计算每格的跨度,每次drawArc画刻度时不断调整当前绘制的角度位置。

  • b.时间提示根据当前选中刻度来调整,或者set方法的设置值。

  • c.底部文本提示的基准线为45度或者135度的刻度的Y坐标。

2.重要方法描述

  1. onTouchEvent:负责处理触摸事件,并且触发重绘界面的代码。
  2. onDraw:绘制界面元素,绘制逻辑按照上面分析。
  3. init:初始化自定义属性以及创建paint等
  4. initValues:计算大圆半径、圆心坐标等
  5. judgeQuadrantAndSetCurrentProgress(float x, float y, double angle, boolean isAnim):onTouchEvent里如果处于action_down或者action_move,触发本方法。这里是计算当前触摸坐标,求弧度然后求出角度(求弧度、角度公式请看代码),根据角度以及触摸坐标判断处于第几象限,然后计算当前所在位置的选中刻度为多少。
  6. getSelectCount(double percent):根据当前计算出的进度百分比获取四舍五入的刻度值
  7. getCoordinatePoint(int radius, float cirAngle):获取当前角度所在的y坐标
  8. formatTime(long mss) :格式化当前时间
  9. autoCountDown(long time, boolean isLockTouch) :倒计时自动刷新方法
  10. setSelectTickCount(int selectTickCount, boolean isAnim) :设置当前选中刻度值,setCurrentProgress与setCurrentTime都会最终走入本方法。

3.绘制流程

先调用initValues计算当前view的圆心坐标、半径、设置绘制参数。绘制时,先绘制选中的刻度,然后以选中刻度的终点角度开始绘制未选中的刻度。利用圆心坐标以及FontMetricsInt绘制居中的时间提示文本。最后根据getCoordinatePoint方法求出45度或者135度位置的刻度的y坐标绘制底部文本。

4.核心方法解析

下面方法为如何处理触摸事件。判断当前触摸事件,获取当前触摸点的x、y坐标。根据求弧度公式,求出坐标所在的弧度,然后Math.abs(180 * i / Math.PI)转换为角度,这里取绝对值。

     @Override
    public boolean onTouchEvent(MotionEvent event) {
//        Log.i(TAG, "onTouchEvent: ");
        if (mIsLockTouch) {
            return false;
        }
        float x = event.getX();
        float y = event.getY();
        if ((x - mCircleCenterX) * (x - mCircleCenterX) + (y - mCircleCenterY) *
                (y - mCircleCenterY) <= ((float)
                1 / 2 * mCircleRadius) * ((float) 1 / 2 * mCircleRadius)) {
            // 圆内触摸点在半径的1/2范围内点击无效
            return false;
        }
//        Log.i(TAG, "onTouchEvent: x:" + x + "  y:" + y);
//        Log.i(TAG, "onTouchEvent: mCircleCenterX:" + mCircleCenterX + "  mCircleCenterY:" +
//                mCircleCenterY);
        float result = (y - mCircleCenterY) / (x - mCircleCenterX);
        double i = Math.atan((double) result);//计算点击坐标到圆心的弧度
        double angle = Math.abs(180 * i / Math.PI);//根据弧度转化为角度
//        Log.i(TAG, "touch: angle:" + angle);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                judgeQuadrantAndSetCurrentProgress(x, y, angle, true);
                return true;
            case MotionEvent.ACTION_MOVE:
                judgeQuadrantAndSetCurrentProgress(x, y, angle, false);
                return true;
            case MotionEvent.ACTION_UP:
                if (mOnTimeChangeListener != null) {
                    mOnTimeChangeListener.onChange(mCurrentTime, mSelectTickCount);
                }
                return true;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

下面是具体的根据角度计算当前刻度的方法。主要逻辑是判断当前触摸点坐标是在坐标轴的第几象限(view的坐标轴是左上角为原点,向右是x增加,正方向;向下是y增加,正方向),比如第一象限是触摸点x大于圆心x,y小于圆心y。

计算时根据当前角度算出在绘制范围内跨度,然后除以总跨度270。求出的角度是0-90范围。求出百分比,传入getSelectCount方法求出刻度,除了第三、第二象限的起点终点有特殊处理(增加了触摸范围)。比如第一象限的角度求出来是60度(touch方法里是求出绝对值,实际上是负数),所以360-60才是真实度数,然后减去135就是跨度。同理第三象限也是负数,所以也是特殊处理。

这里写图片描述
在计算出的刻度值与上一次不一致时才启动重新绘制。具体看下面代码注释。

    /**
     * 判断象限,并且计算当前百分比
     *
     * @param x     当前坐标x
     * @param y     当前坐标y
     * @param angle 角度
     */
    private void judgeQuadrantAndSetCurrentProgress(float x, float y, double angle, boolean
            isAnim) {
        double percent = 0;//百分比
        int selectCount = mSelectTickCount;
        if (x >= mCircleCenterX && y <= mCircleCenterY) {
            //第一象限
//            Log.i(TAG, "onTouchEvent: 第一象限");
            angle = 360 - angle;
            percent = (angle - 135) / 270;
            selectCount = getSelectCount(percent);
        } else if (x >= mCircleCenterX && y >= mCircleCenterY) {
            //第二象限
//            Log.i(TAG, "onTouchEvent: 第二象限");
            if (angle <= 55) {//加10度
                percent = (angle + 225) / 270;
                selectCount = getSelectCount(percent);
                if (angle > 45 - (mSinglPoint / 2)) {
                    selectCount = mTickMaxCount;
                }
            }
        } else if (x <= mCircleCenterX && y >= mCircleCenterY) {
            //第三象限
//            Log.i(TAG, "onTouchEvent: 第三象限");
            if (angle <= 65) {
                percent = (45 - angle) / 270;
                //由于第三象限的度数是逆时针递增,所以这里特殊处理,结果必须加1.
                // 比如45度,percent是0,但是此时格子应该是1格。
                selectCount = getSelectCount(percent) + 1;
                //下面代码处理,点击第一个附近时都可以选中第一个
                if (angle > 45 - (mSinglPoint / 2)) {
                    selectCount = 1;
                }
            } else if (angle > 65 && angle < 90) {
                if (mIsCanResetZero) {//如果允许点击第三象限的空白区域归零,
                    selectCount = 0;
                } else {
                    selectCount = 1;
                }
            }
        } else if (x <= mCircleCenterX && y <= mCircleCenterY) {
            //第四象限
//            Log.i(TAG, "onTouchEvent: 第四象限");
            percent = (angle + 45) / 270;
            selectCount = getSelectCount(percent);
        }
//        Log.i(TAG, "onTouchEvent: selectCount:" + selectCount);
        if (selectCount != mSelectTickCount) {
            //只有发生变化时,才重绘界面
            setSelectTickCount(selectCount, isAnim, true);
        }
    }

ondraw方法调用前,先初始化所需要的参数,比如圆的半径等。绘制流程按照上面所说进行。需要注意的是,这里是根据mAnimTickCount、mCenterText 的值进行绘制,如果是动画效果时,这个值是不断变化最终才变为当前值(一个根据线性差值器不断重绘的动画过程)。


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        initValues();
        int p;
        float start = 135f;
        //绘制选中刻度
        if (mAnimTickCount < 0) {
            mAnimTickCount = 0;  //避免初始时间不为0时,界面显示异常,所以过滤错误值
        } else if (mAnimTickCount > mTickMaxCount) {
            mAnimTickCount = mTickMaxCount;
        }

        p = mAnimTickCount;
        for (int i = 0; i < p; i++) {
            mCircleRingPaint.setColor(mSelectTickColor);
            canvas.drawArc(mRecf, start - mLineWidth, mLineWidth, false,
                    mCircleRingPaint); // 绘制间隔块
            start = (start + mSinglPoint);

        }
        //绘制全部刻度
        //剩余刻度的起点=start
        p = mTickMaxCount - p;
        for (int i = 0; i < p; i++) {
            mCircleRingPaint.setColor(mDefaultTickColor);
            canvas.drawArc(mRecf, start - mLineWidth, mLineWidth, false,
                    mCircleRingPaint); // 绘制间隔块
            start = (start + mSinglPoint);
        }

        //绘制
        Paint.FontMetricsInt fontMetrics = mCenterTextPaint.getFontMetricsInt();
        int baseline = (mHeight - getPaddingTop() / 2 - fontMetrics.bottom + fontMetrics.top) / 2 -
                fontMetrics.top;
        canvas.drawText(mCenterText, mCircleCenterX,
                baseline,
                mCenterTextPaint);

        float[] coordinatePoint = getCoordinatePoint(mCircleRadius, 45f + mSinglPoint);
//        Log.i(TAG, "onDraw: mCircleCenterX=" + mCircleCenterX);
//        Log.i(TAG, "onDraw: mCircleRadius=" + mCircleRadius);
//        Log.i(TAG, "onDraw:  coordinatePoint[1]=" + coordinatePoint[1] + "  coordinatePoint[0]=" +
//                coordinatePoint[0]);
        canvas.drawText(mBottomText, mCircleCenterX, coordinatePoint[1] + getPaddingTop(),
                mBottomTextPaint);


    }

    /**
     * 初始化各种view的参数
     */
    private void initValues() {
        mWidth = getWidth();//直径
        mHeight = getHeight();
        mCircleCenterX = mWidth / 2;//半径
        mSinglPoint = (float) 270 / (float) (mTickMaxCount - 1);
        Log.i(TAG, "initValues: mSinglPoint:" + mSinglPoint);
        mVerticalPadding = getPaddingTop() + getPaddingBottom();
        int padding = getPaddingTop() > getPaddingBottom() ? getPaddingTop() :
                getPaddingBottom();
        if (mHeight > mWidth) {
            mCircleRadius = mWidth / 2 - padding;
        } else {
            mCircleRadius = mHeight / 2 - padding;
        }
        mCircleRingRadius = mCircleRadius - mTickStrokeSize / 2; // 圆环的半径
        mCircleCenterY = mHeight / 2;

        mRecf.set(mCircleCenterX - mCircleRingRadius, mHeight / 2 - mCircleRadius,
                mCircleCenterX + mCircleRingRadius,
                mHeight / 2 + mCircleRadius);
    }

其它重要内部类:这里是动画类,主要控制每次动画状态下的绘制。这里做了优化,绘制时会根据上一次的刻度来进行,更自然的过渡到新的刻度值。

  public class ViewRefreshAnimation extends Animation {
        public ViewRefreshAnimation() {
        }

        protected void applyTransformation(float interpolatedTime, Transformation t) {

            super.applyTransformation(interpolatedTime, t);
            long mAnimTime = mCurrentTime;//动画当前的时间值
            int diffTick;//当前选中刻度与上一次的差值
            long diffTime;//动画当前的时间值上一次的差值
            if (interpolatedTime <= 1.0F) {
                Log.i(TAG, "applyTransformation: interpolatedTime:" + interpolatedTime + " " +
                        "mLastSelectTickCount:" + mLastSelectTickCount);
                if (mLastSelectTickCount < mSelectTickCount) {
                    //增加刻度与时间,从当前位置增加,不从起点
                    diffTick = mSelectTickCount - mLastSelectTickCount;
                    diffTime = mCurrentTime - mLastTime;
                    mAnimTickCount = mLastSelectTickCount + (int) (interpolatedTime * diffTick);
                    mAnimTime = mLastTime + (long) (interpolatedTime * diffTime);
                } else {//从当前位置减少刻度,减少时间
                    diffTick = mLastSelectTickCount - mSelectTickCount;
                    diffTime = mLastTime - mCurrentTime;
                    mAnimTickCount = mLastSelectTickCount - (int) (interpolatedTime * diffTick);
                    mAnimTime = mLastTime - (long) (interpolatedTime * diffTime);
                }
                Log.i(TAG, "applyTransformation: mAnimTickCount:" + mAnimTickCount);
            }
            mCenterText = formatTime(mAnimTime);
            postInvalidate();
        }
    }

5.使用方式

 compile 'com.tc.circletickview:library:0.1.1'

xml布局按照如下方式写,需要设置什么属性自行添加。


     <com.tc.library.CircleTickView
        android:id="@+id/crpv_tick"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:paddingTop="40dp"
        app:animDuration="500"
        app:bottomText="Set Time"
        app:isCanResetZero="true"
        app:maxTime="1200000"
        app:startTime="300000"
        app:tickMaxCount="30"
        />

界面代码示例:

mCtvTime.setSelectTickCount(1, false);
        mCurrentTime = mCtvTime.getCurrentTime();
        mCtvTime.setOnTimeChangeListener(new CircleTickView.OnTimeChangeListener() {
            @Override
            public void onChange(long time, int tickCount) {
                mCurrentTime = time;
                LogUtil.e(TAG, mCurrentTime + "  mCurrentTime");

                }
            }
        });

本文章对于基础的绘制方法介绍不是很详细,如有知识缺漏请移步其它文章或者其它大牛的博客学习。欢迎大家在github上下载源码学习或者fork后提交改进建议。如果觉得有帮助,点个star支持我一下。谢谢!有问题欢迎在博客下方留言。
github地址:https://github.com/389273716/CircleTickView

以上是关于自定义刻度盘View--详解的主要内容,如果未能解决你的问题,请参考以下文章

自定义刻度盘View--详解

android自定义View实现会议时间的占比效果

android自定义View实现会议时间的占比效果

Android自定义View之区块选择器

安卓自定义弧形刻度选择器

matlab中如何设置自定义刻度?