本来想找一个ios的菊花加载图旋转一下搞定这个的,但是发现使用图片后由于图片的不对称导致动画时有抖动现象,不能忍,于是搜了下自定义菊花图,额,然后就没有然后了,都不是想要的,于是开始了苦逼的自定义View...
先来张效果图:
再来张动画效果图:
看图说话:
1、12条颜色线,带圆角
2、动画起来后12个颜色跟着变化
需要解决的问题:
1、12个颜色,也太多了吧!!!!我可不想吸这么多次,有毒的!(嗯,我为吸管代言)
于是愉快的决定了只是用两种颜色,即开始颜色和结束颜色,其余使用渐变获取。(额,好吧,我是懒了!)
2、动画
什么,动画是问题吗?一个旋转搞定了!嗯,你大佬,你来,反正我做不来!!
由于旋转动画是转动的,难以实现颜色跟着变化的效果,所以我使用了ValueAnimator,
嗯,是的,你猜对了,既然使用了ValueAnimator,就要调用 invalidate(); 或 postInvalidate(); 来实现重绘。
嗯,看起来还是挺简单的,来试试吧:
先定义一下属性:
/**
* 线圆角及宽度
*/
private int mLineBold;
/**
* 线条开始颜色 默认白色
*/
private int mStartColor = Color.parseColor("#FFFFFF");
/**
* 线条结束颜色 默认灰色
*/
private int mEndColor = Color.parseColor("#9B9B9B");
/**
* view的宽度 高度
*/
private int mWidth;
/**
* view的高度
*/
private int mHeight;
/**
* 线条长度
*/
private int mLineLength;
/**
* 线条个数 默认12条
*/
private int mLineCount = 12;
/**
* 背景画笔
*/
private Paint mBgPaint;
/**
* 渐变颜色
*/
private int[] mColors;
/**
* 动画是否已开启
*/
private boolean isAnimationStart;
/**
* 开始index
*/
private int mStartIndex;
/**
* 动画
*/
private ValueAnimator mValueAnimator;
>>>>>>>>>>>>>>> 然后继续 >>>>>>>>>>>>>>>>>
1.第一步:
自定义属性:
// 由于考虑到后续可能使用更多数量的线,所以索性加了个线个数的属性
2.第二步,初始化:
public ChrysanthemumView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
loadAttrs(context, attrs);
initPaint();
initColor();
}
/**
* 加载自定义的属性
*/
private void loadAttrs(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ChrysanthemumView);
mStartColor = array.getColor(R.styleable.ChrysanthemumView_startColor, mStartColor);
mEndColor = array.getColor(R.styleable.ChrysanthemumView_endColor, mEndColor);
mLineCount = array.getInt(R.styleable.ChrysanthemumView_lineCount, mLineCount);
//TypedArray回收
array.recycle();
}
/**
* 初始化画笔
*/
private void initPaint() {
mBgPaint = new Paint();
//使得画笔更加圆滑
mBgPaint.setAntiAlias(true);
mBgPaint.setStrokeJoin(Paint.Join.ROUND);
mBgPaint.setStrokeCap(Paint.Cap.ROUND);
}
3.第三步,解决颜色的问题:
/**
* 初始化颜色
*/
private void initColor() {
// 渐变色计算类
ArgbEvaluator argbEvaluator = new ArgbEvaluator();
// 初始化对应空间
mColors = new int[mLineCount];
// 获取对应的线颜色 此处由于是白色起头 黑色结尾所以需要反过来计算 即线的数量到0的数量递减 对应的ValueAnimator 是从0到线的数量-1递增
for (int i = mLineCount; i > 0; i--) {
float alpha = (float) i / mLineCount;
mColors[mLineCount - i] = (int) argbEvaluator.evaluate(alpha, mStartColor, mEndColor);
}
}
4.第四步,解决尺寸的问题:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取view的宽度 默认40dp
mWidth = getViewSize(dp2px(getContext(), 40), widthMeasureSpec);
// 获取view的高度 默认40dp
mHeight = getViewSize(dp2px(getContext(), 40), heightMeasureSpec);
// 使宽高保持一致
mHeight = mWidth = Math.min(mWidth, mHeight);
// 获取线的长度
mLineLength = mWidth / 6;
// 获取线圆角及宽度
mLineBold = mWidth / mLineCount;
// 设置线的圆角及宽度
mBgPaint.setStrokeWidth(mLineBold);
setMeasuredDimension(mWidth,mHeight);
}
/**
* 测量模式 表示意思
* UNSPECIFIED 父容器没有对当前View有任何限制,当前View可以任意取尺寸
* EXACTLY 当前的尺寸就是当前View应该取的尺寸
* AT_MOST 当前尺寸是当前View能取的最大尺寸
*
* @param defaultSize 默认大小
* @param measureSpec 包含测量模式和宽高信息
* @return 返回View的宽高大小
*/
private int getViewSize(int defaultSize, int measureSpec) {
int viewSize = defaultSize;
//获取测量模式
int mode = MeasureSpec.getMode(measureSpec);
//获取大小
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED:
//如果没有指定大小,就设置为默认大小
viewSize = defaultSize;
break;
case MeasureSpec.AT_MOST:
//如果测量模式是最大取值为size
//我们将大小取最大值,你也可以取其他值
viewSize = size;
break;
case MeasureSpec.EXACTLY:
//如果是固定的大小,那就不要去改变它
viewSize = size;
break;
default:
}
return viewSize;
}
5.第五步,绘制:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 获取半径
int r = mWidth / 2;
// 绘制前先旋转一个角度,使最顶上开始位置颜色与开始颜色匹配
canvas.rotate(360f / mLineCount, r, r);
for (int i = 0; i < mLineCount; i++) {
// 获取颜色下标
int index = (mStartIndex + i) % mLineCount;
// 设置颜色
mBgPaint.setColor(mColors[index]);
// 绘制线条 mLineBold >> 1 == mLineBold / 2 使居中显示
canvas.drawLine(r, mLineBold >> 1, r, (mLineBold >> 1) + mLineLength, mBgPaint);
// 旋转角度
canvas.rotate(360f / mLineCount, r, r);
}
}
6.第六步,动起来!!!!:
/**
* 开始动画
*
* @param duration 动画时间
*/
public void startAnimation(int duration) {
Log.d(TAG, "startAnimation: " + mStartIndex);
if (mValueAnimator == null) {
mValueAnimator = ValueAnimator.ofInt(mLineCount, 0);
mValueAnimator.setDuration(duration);
mValueAnimator.setTarget(0);
mValueAnimator.setRepeatCount(-1);
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 此处会回调3次 需要去除后面的两次回调
if (mStartIndex != (int) animation.getAnimatedValue()) {
mStartIndex = (int) animation.getAnimatedValue();
Log.d(TAG, "onAnimationUpdate: " + mStartIndex);
invalidate();
}
}
});
}
mValueAnimator.start();
isAnimationStart = true;
}
完成了!!!
额,少了东西了。
/**
* 结束动画
*/
public void stopAnimation() {
Log.d(TAG, "stopAnimation: " + mStartIndex);
if (mValueAnimator != null) {
mValueAnimator.cancel();
isAnimationStart = false;
}
}
不行,还不够!
/**
* 防止内存溢出 未结束动画并退出页面时,需使用此函数,或手动释放此view
*/
public void detachView() {
if (mValueAnimator != null) {
mValueAnimator.cancel();
mValueAnimator = null;
isAnimationStart = false;
}
}
所有源码:
/**
* @author : Kingsley
* info: 菊花图
* @date : 2019/4/30 10:12
*/
public class ChrysanthemumView extends View {
private static final String TAG = "ChrysanthemumView";
/**
* 线圆角及宽度
*/
private int mLineBold;
/**
* 线条开始颜色 默认白色
*/
private int mStartColor = Color.parseColor("#FFFFFF");
/**
* 线条结束颜色 默认灰色
*/
private int mEndColor = Color.parseColor("#9B9B9B");
/**
* view的宽度 高度
*/
private int mWidth;
/**
* view的高度
*/
private int mHeight;
/**
* 线条长度
*/
private int mLineLength;
/**
* 线条个数 默认12条
*/
private int mLineCount = 12;
/**
* 背景画笔
*/
private Paint mBgPaint;
/**
* 渐变颜色
*/
private int[] mColors;
/**
* 动画是否已开启
*/
private boolean isAnimationStart;
/**
* 开始index
*/
private int mStartIndex;
/**
* 动画
*/
private ValueAnimator mValueAnimator;
public ChrysanthemumView(Context context) {
this(context, null);
}
public ChrysanthemumView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ChrysanthemumView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
loadAttrs(context, attrs);
initPaint();
initColor();
}
/**
* 初始化颜色
*/
private void initColor() {
// 渐变色计算类
ArgbEvaluator argbEvaluator = new ArgbEvaluator();
// 初始化对应空间
mColors = new int[mLineCount];
// 获取对应的线颜色 此处由于是白色起头 黑色结尾所以需要反过来计算 即线的数量到0的数量递减 对应的ValueAnimator 是从0到线的数量-1递增
for (int i = mLineCount; i > 0; i--) {
float alpha = (float) i / mLineCount;
mColors[mLineCount - i] = (int) argbEvaluator.evaluate(alpha, mStartColor, mEndColor);
}
}
/**
* 加载自定义的属性
*/
private void loadAttrs(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ChrysanthemumView);
mStartColor = array.getColor(R.styleable.ChrysanthemumView_startColor, mStartColor);
mEndColor = array.getColor(R.styleable.ChrysanthemumView_endColor, mEndColor);
mLineCount = array.getInt(R.styleable.ChrysanthemumView_lineCount, mLineCount);
//TypedArray回收
array.recycle();
}
/**
* 初始化画笔
*/
private void initPaint() {
mBgPaint = new Paint();
//使得画笔更加圆滑
mBgPaint.setAntiAlias(true);
mBgPaint.setStrokeJoin(Paint.Join.ROUND);
mBgPaint.setStrokeCap(Paint.Cap.ROUND);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取view的宽度 默认40dp
mWidth = getViewSize(dp2px(getContext(), 40), widthMeasureSpec);
// 获取view的高度 默认40dp
mHeight = getViewSize(dp2px(getContext(), 40), heightMeasureSpec);
// 使宽高保持一致
mHeight = mWidth = Math.min(mWidth, mHeight);
// 获取线的长度
mLineLength = mWidth / 6;
// 获取线圆角及宽度
mLineBold = mWidth / mLineCount;
// 设置线的圆角及宽度
mBgPaint.setStrokeWidth(mLineBold);
setMeasuredDimension(mWidth,mHeight);
}
/**
* 测量模式 表示意思
* UNSPECIFIED 父容器没有对当前View有任何限制,当前View可以任意取尺寸
* EXACTLY 当前的尺寸就是当前View应该取的尺寸
* AT_MOST 当前尺寸是当前View能取的最大尺寸
*
* @param defaultSize 默认大小
* @param measureSpec 包含测量模式和宽高信息
* @return 返回View的宽高大小
*/
private int getViewSize(int defaultSize, int measureSpec) {
int viewSize = defaultSize;
//获取测量模式
int mode = MeasureSpec.getMode(measureSpec);
//获取大小
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED:
//如果没有指定大小,就设置为默认大小
viewSize = defaultSize;
break;
case MeasureSpec.AT_MOST:
//如果测量模式是最大取值为size
//我们将大小取最大值,你也可以取其他值
viewSize = size;
break;
case MeasureSpec.EXACTLY:
//如果是固定的大小,那就不要去改变它
viewSize = size;
break;
default:
}
return viewSize;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 获取半径
int r = mWidth / 2;
// 绘制前先旋转一个角度,使最顶上开始位置颜色与开始颜色匹配
canvas.rotate(360f / mLineCount, r, r);
for (int i = 0; i < mLineCount; i++) {
// 获取颜色下标
int index = (mStartIndex + i) % mLineCount;
// 设置颜色
mBgPaint.setColor(mColors[index]);
// 绘制线条 mLineBold >> 1 == mLineBold / 2 使居中显示
canvas.drawLine(r, mLineBold >> 1, r, (mLineBold >> 1) + mLineLength, mBgPaint);
// 旋转角度
canvas.rotate(360f / mLineCount, r, r);
}
}
/**
* 开始动画
*
* @param duration 动画时间
*/
public void startAnimation(int duration) {
Log.d(TAG, "startAnimation: " + mStartIndex);
if (mValueAnimator == null) {
mValueAnimator = ValueAnimator.ofInt(mLineCount, 0);
mValueAnimator.setDuration(duration);
mValueAnimator.setTarget(0);
mValueAnimator.setRepeatCount(-1);
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 此处会回调3次 需要去除后面的两次回调
if (mStartIndex != (int) animation.getAnimatedValue()) {
mStartIndex = (int) animation.getAnimatedValue();
Log.d(TAG, "onAnimationUpdate: " + mStartIndex);
invalidate();
}
}
});
}
mValueAnimator.start();
isAnimationStart = true;
}
/**
* 开始动画 时间为1800毫秒一次
*/
public void startAnimation() {
startAnimation(1800);
}
/**
* 结束动画
*/
public void stopAnimation() {
Log.d(TAG, "stopAnimation: " + mStartIndex);
if (mValueAnimator != null) {
mValueAnimator.cancel();
isAnimationStart = false;
}
}
/**
* 是否在动画中
*
* @return 是为 true 否则 false
*/
public boolean isAnimationStart() {
return isAnimationStart;
}
/**
* dp转px
*
* @param context context
* @param dpVal dpVal
* @return px
*/
public static int dp2px(Context context, float dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dpVal, context.getResources().getDisplayMetrics());
}
/**
* 防止内存溢出 未结束动画并退出页面时,需使用此函数,或手动释放此view
*/
public void detachView() {
if (mValueAnimator != null) {
mValueAnimator.cancel();
mValueAnimator = null;
isAnimationStart = false;
}
}
}