自定義刻度盤View 詳解

簡介

本篇是接上一篇seekbar的自定義view進階版。 本自定義view主要功能:android

  1. 可自定義起始時間以及最大時間,設置總格數,每格均分時間差。
  2. 可自定義界面顏色字體大小,文本提示。
  3. 單擊觸摸可觸發刻度以及時間的變更動畫效果,動畫效果更天然,從上一次位置開始變動。觸摸範圍爲大圓內到圓心距離大於1/2半徑距離的座標範圍。觸摸事件爲action_move時不會觸發動畫。
  4. 提供禁用觸摸操做,以便特殊需求。
  5. 提供是否清零設置(開啓後設置時間等周邊位置可清零),默認是0格,0格表明的是你設置的初始時間值。
  6. 提供適用於自動倒計時模式下的方法,以便更好更新view的顯示。
  7. 提供時間以及刻度變化的監聽。

效果圖以下。 git

這裏寫圖片描述

1.主要思路

1)首先老規矩仍是先分析有哪些繪製模塊,以及根據功能分析須要什麼配置參數。根據Gif圖,咱們從視圖效果看有刻度、時間提示、底部文本提示等元素。github

  • a.爲了繪製這個刻度,咱們確定是圍繞一個圓的邊進行繪製。也就是說咱們須要知道大圓半徑,以及圓心座標。本view的半徑是根據view的大小以及內邊距進行計算,而且圓心始終是自定義view控件的幾何中心。刻度繪製方式並不是採用熟知的畫布翻轉remote,而是經過刻度總數以及起始角度13五、終點角度45度(總跨度270度,這裏的角度是指繪製刻度的角度,0度爲水平方法向向右。下面有個圖解釋座標軸)來計算每格的跨度,每次drawArc畫刻度時不斷調整當前繪製的角度位置。canvas

  • b.時間提示根據當前選中刻度來調整,或者set方法的設置值。bash

  • c.底部文本提示的基準線爲45度或者135度的刻度的Y座標。app

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座標繪製底部文本。ide

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。post

計算時根據當前角度算出在繪製範圍內跨度,而後除以總跨度270。求出的角度是0-90範圍。求出百分比,傳入getSelectCount方法求出刻度,除了第3、第二象限的起點終點有特殊處理(增長了觸摸範圍)。好比第一象限的角度求出來是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地址:github.com/389273716/C…

相關文章
相關標籤/搜索