简介
本篇是接上一篇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;
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布局按照如下方式写,需要设置什么属性自行添加。
界面代码示例:
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