Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義刻度盤View--詳解

自定義刻度盤View--詳解

編輯:關於Android編程

簡介

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

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

效果圖如下。
這裡寫圖片描述

1.主要思路

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

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

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

c.底部文本提示的基准線為45度或者135度的刻度的Y坐標。

2.重要方法描述

onTouchEvent:負責處理觸摸事件,並且觸發重繪界面的代碼。 onDraw:繪制界面元素,繪制邏輯按照上面分析。 init:初始化自定義屬性以及創建paint等 initValues:計算大圓半徑、圓心坐標等 judgeQuadrantAndSetCurrentProgress(float x, float y, double angle, boolean isAnim):onTouchEvent裡如果處於action_down或者action_move,觸發本方法。這裡是計算當前觸摸坐標,求弧度然後求出角度(求弧度、角度公式請看代碼),根據角度以及觸摸坐標判斷處於第幾象限,然後計算當前所在位置的選中刻度為多少。 getSelectCount(double percent):根據當前計算出的進度百分比獲取四捨五入的刻度值 getCoordinatePoint(int radius, float cirAngle):獲取當前角度所在的y坐標 formatTime(long mss) :格式化當前時間 autoCountDown(long time, boolean isLockTouch) :倒計時自動刷新方法 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;
            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 <= 65) {//加10度
                percent = (angle + 225) / 270;
                selectCount = getSelectCount(percent);
                if (angle > 45) {
                    selectCount = mTickMaxCount;
                }
            } else {
                if (mIsCanResetZero) {//如果允許點擊第二象限的空白區域歸零,
                    selectCount = 0;
                } else {
                    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) {
                    selectCount = 1;
                }
            } else {
                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, "judgeQuadrantAndSetCurrentProgress: selectCount:" + selectCount);
        if (selectCount != mSelectTickCount) {
            //只有發生變化時,才重繪界面
            setSelectTickCount(selectCount, isAnim);
        }
    }

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布局按照如下方式寫,需要設置什麼屬性自行添加。

     

界面代碼示例:

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

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved