前言:
这是一个,既可以用做时间轴,也可以用做垂直进度条的自定义控件。(没有用系统的Progress控件,进度是自己用Canvas画的)。
先看Gif效果图:
通过接口,来定义自定义控件的功能。阅读代码的时候,看接口就行。具体功能,再到自定义控件的源码找。
/**
* @Author 李岳锋
* @CreateTime 2018/1/26
* @Description 垂直进度条样式的功能定义
* 当需要给VerticalProgress增加新功能时,要在这里定义方法
**/
public interface IVerticalProgress {
/**
* 初始化视图
*
* @param context 上下文
* @param attrs 属性
*/
void initView(Context context, @Nullable AttributeSet attrs);
/**
* 设置当前所在的点(位置)
*
* @param currentPoint 该值必须大于0。并且小于或等于最大的点数,不然会抛出运行时异常。
* 详见{{@link #setMaxPointCount(int)}}.
*/
void setCurrentPoint(int currentPoint);
/**
* 设置最大圆点数
*
* @param maxPointCount 最大圆点数。
*/
void setMaxPointCount(int maxPointCount);
/**
* 计算分割线的大小
* @param extraLength 额外添加的长度。该值大于0时,分割线的长度增加。小于0时,分割线的长度减少。为0时,不做处理。
*/
void countDividerLength(int extraLength);
/**
* 获取当前点的位置
*/
int getCurrentPoint();
/**
* 获取最大圆点数
*/
int getMaxPointCount();
/**
* 计算点的位置
*/
void countPointPositions();
/**
* 画直线,多条水平线组成一条垂直线,来实现渐变色
*/
void drawVerticalLine(Canvas canvas);
/**
* 画圆点
*/
void drawPoints(Canvas canvas);
/**
* 绘制最后一个点,需要加光晕效果,圆点一样,但半径是3倍
*/
void drawLastPoint(Canvas canvas);
/**
* 获取圆的中心点的起始x坐标
*/
float getDrawingStartCircleX();
/**
* 获取圆的中心点的起始y坐标
*/
float getDrawingNextCircleY();
/**
* 获取下个要绘制圆点的中心点的Y坐标
*
* @param cy 根据cy,计算出下个要绘制点的Y坐标
* @return 计算出下个要绘制点的Y坐标
*/
float getDrawingNextCircleY(float cy);
/**
* @param drawingPoint 当前正在绘制的点的下标,比如,第x个。
* @return 是否达到该点
*/
boolean isSearched(int drawingPoint);
/**
* @param drawingY 正在绘制的Y坐标
* @return 是否达到该点
*/
boolean isSearched(float drawingY);
/**
* @param radio 当前的渐变比率
* @return 获取渐变色
*/
int getGradientColor(float radio);
/**
* 获取分割线的长度
*/
float getDividerLength();
/**
* 获取点的坐标
*/
float[][] getPointPositions();
}
这是源码部分:
/**
* @Author 李岳锋
* @CreateTime 2018/1/25
* @Description 垂直进度条样式.
**/
public class VerticalProgress extends View implements IVerticalProgress {
// 创建画笔
private Context mContext;
private Paint mPaint; // 画笔
private Paint mVerticalPaint; // 垂直线的画笔
private Paint mHalationPaint; // 光晕画笔
private int mHalationColor =Color.parseColor("#FFE052"); // 光晕的颜色
private int mBackGroundColor = Color.parseColor("#f2f2f2"); // 进度条背景颜色
protected float mRadius; // 圆的半径
private float mDividerLength; // 圆的间距
private int mMaxPointCount = 0; // 圆点数量
private int mCurrentPoint = 1; // 当前到达的位置
private int mMeasuredHeight; // 控件高
private int mMeasuredWidth; // 控件宽
private float mVerticalLineWidth; // 直线宽度
// 圆点的坐标
protected float mPointPositions[][];
// 透明度
private int mAlpha = 255;
// 光晕
private int mHalation = 255; //透明度
private float mHalationWidthTimes = 3.0f;// 光晕相对于圆点的大小,倍数。
// 起始颜色的RGB
private int mRedStart;
private int mGreenStart;
private int mBlueStart;
// 终止颜色的RGB
private int mRedEnd;
private int mGreenEnd;
private int mBlueEnd;
// 过度颜色差
private int mRedFlag = 1;
private int mGreenFlag = 1;
private int mBlueFlag = 1;
// 每一帧,光晕的透明度的变化值,该值越大,透明度变淡的速度越慢。
// 该值必须在这个范围 mReduceHalationAlpha < 255 && mReduceHalationAlpha > 0
private float mReduceHalationAlpha = 16.0f;
// 动画效果是否已准备好
private boolean mIsAnimationPrepared = false;
// 是否停止动画
private boolean isForceStopAnimation = false;
// 当前的帧
private int mFrameCount = 1;
// 最大帧
private int mMaxFrameCount = 29;
// 最小帧
private int mMinFrameCount = 1;
public VerticalProgress(Context context) {
super(context);
initView(context, null);
}
public VerticalProgress(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context, attrs);
}
public VerticalProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
mMeasuredWidth = getMeasuredWidth();
// 半径长 == 控件宽度的一半,再除以光晕的倍数
if(mRadius == 0) {
mRadius = (mMeasuredWidth / 2 / mHalationWidthTimes);
}
mMeasuredHeight = getMeasuredHeight() - (int) mRadius * 2;
// 计算分割线的长度
countDividerLength(0);
}
@Override
public void countDividerLength(int extraLength){
mDividerLength = (mMeasuredHeight - mRadius * 2 * mMaxPointCount - mRadius * mHalationWidthTimes) / (mMaxPointCount - 1) + extraLength;
}
@Override
public void initView(Context context, @Nullable AttributeSet attrs) {
mContext = context;
mPaint = new Paint();
mHalationPaint = new Paint();
mVerticalPaint = new Paint();
mPaint.setAntiAlias(true);
mVerticalPaint.setAntiAlias(true);
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VerticalProgress);
mRadius = typedArray.getDimension(R.styleable.VerticalProgress_point_radius,0f);
mBackGroundColor = typedArray.getColor(R.styleable.VerticalProgress_background_color, mBackGroundColor);
mMaxPointCount = typedArray.getInt(R.styleable.VerticalProgress_point_count, mMaxPointCount);
mCurrentPoint = typedArray.getInt(R.styleable.VerticalProgress_current_point, mMaxPointCount);
int startColor = typedArray.getColor(R.styleable.VerticalProgress_start_color, mBackGroundColor);
int endColor = typedArray.getColor(R.styleable.VerticalProgress_end_color, mBackGroundColor);
mRedStart = Color.red(startColor);
mGreenStart = Color.green(startColor);
mBlueStart = Color.blue(startColor);
mRedEnd = Color.red(endColor);
mGreenEnd = Color.green(endColor);
mBlueEnd = Color.blue(endColor);
typedArray.recycle();
}
if (mMaxPointCount == 0) {
throw new RuntimeException("必须设置app:point_count属性,并且值要大于0");
}
// 初始化圆点坐标数组
mPointPositions = new float[mMaxPointCount][2];
// 开始动画
mAnimateThread.start();
// 直线宽
mVerticalLineWidth =px2dp(mContext,3);
}
@Override
public void setCurrentPoint(int currentPoint) {
if (currentPoint == 0) {
throw new RuntimeException("currentPoint必须大于0");
}
if (currentPoint > mMaxPointCount) {
throw new RuntimeException("currentPoint不能大于mMaxPointCount");
}
mCurrentPoint = currentPoint;
invalidate();
}
@Override
public void setMaxPointCount(int maxPointCount) {
if (maxPointCount < mCurrentPoint) {
throw new RuntimeException("maxPointCount不能小于mCurrentPoint");
}
mMaxPointCount = maxPointCount;
invalidate();
}
@Override
public int getCurrentPoint() {
return mCurrentPoint;
}
@Override
public int getMaxPointCount() {
return mMaxPointCount;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 计算点的位置
countPointPositions();
// 画直线
drawVerticalLine(canvas);
// 画圆点
drawPoints(canvas);
}
@Override
public void countPointPositions() {
// 当前正在绘制的点
int drawingPoint = 0;
for (float cy = getDrawingNextCircleY(); cy < mMeasuredHeight && drawingPoint < mMaxPointCount;
cy = getDrawingNextCircleY(cy), ++drawingPoint) {
// 保存当前绘制点的坐标
mPointPositions[drawingPoint][0] = getDrawingStartCircleX();
mPointPositions[drawingPoint][1] = cy;
}
}
@Override
public void drawPoints(Canvas canvas) {
// 分段的颜色比率
float radio;
// 当前的比率
float currentRadio = 0;
// 计算分段颜色比率
if (mCurrentPoint == 1) {
radio = 0;
} else {
radio = 1.0f / (mCurrentPoint - 1);
}
// 画圆
for (int drawingPoint = 1; drawingPoint <= mMaxPointCount; ++drawingPoint, currentRadio += radio) {
// 已到达的
if (isSearched(drawingPoint)) {
if (drawingPoint == mCurrentPoint) {
// 最后一个点,额外处理
drawLastPoint(canvas);
} else {
// 不是最后一个点,设置渐变色
mPaint.setColor(getGradientColor(currentRadio));
}
} else {
// 未到达的,设置背景色
mPaint.setColor(mBackGroundColor);
}
canvas.drawCircle(mPointPositions[drawingPoint - 1][0], mPointPositions[drawingPoint - 1][1], mRadius, mPaint);
}
}
@Override
public void drawVerticalLine(Canvas canvas) {
if(mPointPositions == null) {
return;
}
// 直线的位置
float startY = getDrawingNextCircleY();
float stopY = startY + 1;
// 直线的宽度
float startX = (int) (getDrawingStartCircleX() - mVerticalLineWidth / 2);
float stopX = (int) (getDrawingStartCircleX() + mVerticalLineWidth / 2);
// 绘制的颜色
int drawingColor;
// 直线长度
float lineHeight = mPointPositions[mMaxPointCount-1][1] - mRadius;
// 渐变线的长度
float gradientLineHeight = mPointPositions[mCurrentPoint - 1][1] - mRadius;
// 画一条直线
while (startY <= lineHeight) {
if (isSearched(startY)) {
// 绘制已到达的部分的渐变色
if (startY == mRadius * mHalationWidthTimes) {
drawingColor = getGradientColor(0);
} else {
drawingColor = getGradientColor(startY / gradientLineHeight);
}
} else {
// 绘制未到达部分的渐变色
drawingColor = mBackGroundColor;
}
// 设置颜色
mVerticalPaint.setColor(drawingColor);
// 画线
canvas.drawLine(startX, startY, stopX, stopY, mVerticalPaint);
// 不断的增长
++startY;
++stopY;
}
}
@Override
public boolean isSearched(int drawingPoint) {
return (drawingPoint <= mCurrentPoint);
}
@Override
public boolean isSearched(float drawingY) {
if (mCurrentPoint == 1) {
return false;
}
return (drawingY <= (mPointPositions[mCurrentPoint - 1][1]));
}
@Override
public void drawLastPoint(Canvas canvas) {
float mLastPointX = mPointPositions[mCurrentPoint - 1][0];
float mLastPointY = mPointPositions[mCurrentPoint - 1][1];
if (mLastPointX == 0 && mLastPointY == 0) {
return;
}
// 光晕
mHalationPaint.setColor(mHalationColor);
mHalationPaint.setAlpha(mHalation);
if (mFrameCount == 1 || mFrameCount > 16) {
canvas.drawCircle(mLastPointX, mLastPointY, mRadius, mHalationPaint);
} else {
float radius = (mFrameCount / 16.0f) * (mRadius * mHalationWidthTimes);
canvas.drawCircle(mLastPointX, mLastPointY, radius, mHalationPaint);
}
// 最后一个圆点
mPaint.setColor(getGradientColor(1));
canvas.drawCircle(mLastPointX, mLastPointY, mRadius, mPaint);
// 可以开始动画效果
mIsAnimationPrepared = true;
}
@Override
public float getDrawingStartCircleX() {
return mRadius * mHalationWidthTimes;
}
@Override
public float getDrawingNextCircleY() {
return mRadius * mHalationWidthTimes;
}
@Override
public float getDrawingNextCircleY(float cy) {
return cy + mDividerLength + mRadius * 2;
}
@Override
public int getGradientColor(float radio) {
if (radio == 0) {
return Color.argb(mAlpha, mRedStart, mGreenStart, mBlueStart);
} else if (radio >= 1) {
return Color.argb(mAlpha, mRedEnd, mGreenEnd, mBlueEnd);
}
int red = (int) (mRedStart + ((mRedEnd - mRedStart) * radio + mRedFlag));
int green = (int) (mGreenStart + ((mGreenEnd - mGreenStart) * radio + mGreenFlag));
int blue = (int) (mBlueStart + ((mBlueEnd - mBlueStart) * radio + mBlueFlag));
return Color.argb(mAlpha, red, green, blue);
}
@Override
public float getDividerLength() {
return mDividerLength;
}
@Override
public float[][] getPointPositions() {
return mPointPositions;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// 停止动画
isForceStopAnimation = true;
}
/**
* 动画线程
*/
private Thread mAnimateThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
if (isForceStopAnimation) {
break;
}
if (mIsAnimationPrepared) {
mRefreshUI.sendEmptyMessage(0);
}
Thread.sleep(35);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
});
/**
* 刷新界面
*/
private Handler mRefreshUI = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (mReduceHalationAlpha < 255 && mReduceHalationAlpha > 0) {
mHalation -= 255 / mReduceHalationAlpha;
}
invalidate();
// 达到最大帧,重置
if (mFrameCount == mMaxFrameCount) {
mHalation = 255;
mFrameCount = mMinFrameCount;
} else {
++mFrameCount;
}
return false;
}
});
/**
* px转换成dp
*/
private int px2dp(Context context,float pxValue){
float scale=context.getResources().getDisplayMetrics().density;
return (int)(pxValue/scale+0.5f);
}
}
这是自定义控件属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="VerticalProgress">
<!-- 默认的背景颜色 -->
<attr name="background_color" format="color" />
<!-- 圆点的数量 -->
<attr name="point_count" format="integer" />
<!-- 当前到达的位置 -->
<attr name="current_point" format="integer" />
<!-- 起始颜色 -->
<attr name="start_color" format="color" />
<!-- 结束颜色 -->
<attr name="end_color" format="color" />
<!-- 圆点的半径 -->
<attr name="point_radius" format="dimension"/>
</declare-styleable>
</resources>
这是使用示例:
<com.lyf.okmvp.widget.verticalprogress.VerticalProgress
android:id="@+id/verticalProgress"
android:layout_width="30dp"
android:layout_height="match_parent"
android:layout_marginBottom="25dp"
android:layout_marginStart="10dp"
android:layout_marginTop="20dp"
android:visibility="visible"
app:background_color="@color/colorF2F2F2"
app:current_point="1"
app:end_color="@color/color_vertical_progress_end"
app:point_count="7"
app:point_radius="5dp"
app:start_color="@color/color_vertical_progress_start" />
这是静态示例图,支付宝跟我的对比:
这是我的:
这是支付宝的:
本文到此为止… Thank you
附上可以跑的代码(0 warnings, 0 errors):
https://github.com/SuperBeagleDog/OkMVP
注意,代码位于这个包下:
com.lyf.okmvp.widget.verticalprogress