本篇是接上一篇seekbar的自定義view進階版。 本自定義view主要功能:android
效果圖以下。 git
1)首先老規矩仍是先分析有哪些繪製模塊,以及根據功能分析須要什麼配置參數。根據Gif圖,咱們從視圖效果看有刻度、時間提示、底部文本提示等元素。github
a.爲了繪製這個刻度,咱們確定是圍繞一個圓的邊進行繪製。也就是說咱們須要知道大圓半徑,以及圓心座標。本view的半徑是根據view的大小以及內邊距進行計算,而且圓心始終是自定義view控件的幾何中心。刻度繪製方式並不是採用熟知的畫布翻轉remote,而是經過刻度總數以及起始角度13五、終點角度45度(總跨度270度,這裏的角度是指繪製刻度的角度,0度爲水平方法向向右。下面有個圖解釋座標軸)來計算每格的跨度,每次drawArc畫刻度時不斷調整當前繪製的角度位置。canvas
b.時間提示根據當前選中刻度來調整,或者set方法的設置值。bash
c.底部文本提示的基準線爲45度或者135度的刻度的Y座標。app
先調用initValues計算當前view的圓心座標、半徑、設置繪製參數。繪製時,先繪製選中的刻度,而後以選中刻度的終點角度開始繪製未選中的刻度。利用圓心座標以及FontMetricsInt繪製居中的時間提示文本。最後根據getCoordinatePoint方法求出45度或者135度位置的刻度的y座標繪製底部文本。ide
下面方法爲如何處理觸摸事件。判斷當前觸摸事件,獲取當前觸摸點的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();
}
}
複製代碼
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…