編輯:關於Android編程
本篇是接上一篇seekbar的自定義view進階版。
本自定義view主要功能:
效果圖如下。
1)首先老規矩還是先分析有哪些繪制模塊,以及根據功能分析需要什麼配置參數。根據Gif圖,我們從視圖效果看有刻度、時間提示、底部文本提示等元素。
a.為了繪制這個刻度,我們肯定是圍繞一個圓的邊進行繪制。也就是說我們需要知道大圓半徑,以及圓心坐標。本view的半徑是根據view的大小以及內邊距進行計算,並且圓心始終是自定義view控件的幾何中心。刻度繪制方式並非采用熟知的畫布翻轉remote,而是通過刻度總數以及起始角度135、終點角度45度(總跨度270度,這裡的角度是指繪制刻度的角度,0度為水平方法向向右。下面有個圖解釋坐標軸)來計算每格的跨度,每次drawArc畫刻度時不斷調整當前繪制的角度位置。
b.時間提示根據當前選中刻度來調整,或者set方法的設置值。
c.底部文本提示的基准線為45度或者135度的刻度的Y坐標。
先調用initValues計算當前view的圓心坐標、半徑、設置繪制參數。繪制時,先繪制選中的刻度,然後以選中刻度的終點角度開始繪制未選中的刻度。利用圓心坐標以及FontMetricsInt繪制居中的時間提示文本。最後根據getCoordinatePoint方法求出45度或者135度位置的刻度的y坐標繪制底部文本。
下面方法為如何處理觸摸事件。判斷當前觸摸事件,獲取當前觸摸點的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();
}
}
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
一、數據存儲選項:Data Storage ——Storage Options【重點】 1、Shared Preferences Stor
經過學習,我們知道Volley的架構如下:從架構上我們可以看到,volley有設置緩存機制,當找不到數據緩存或數據緩存過期時,才會聯網獲取新的數據。Volley 本身有緩
廢話不多說,先看效果圖: package com.example.circlemenuofbottom.anim;import android.v
android actionbar這個導航欄,相信大家愛已經不陌生了。自從android 3.0以上就有了這個導航欄功能。在郭大神博客有詳細介紹actionbar功能。我