在之前的电商APP中,要实现一个年化收益的效果图,废话不多说,先上个实际效果图来瞧瞧
效果很简单有木有,但是对于初学者而言,有时候就不知道从哪里下手,所以这里我将带大家去思考并实现效果。
思考:在我们做任何的自定义View的效果之前,我们都需要在脑子里面有一个明确的思路,千万别走一步想一步,这样很容易导致最后效果实现不了。所以,最好的办法就是先把所有要实现的步骤罗列出来,然后一步一步的进行实现。那么,下面我们就来看看实现上面的效果有哪些步骤。
步骤分析:
// 1. 画固定部分 (背景弧线(实线)+ 背景虚线(虚线)) // 1.1 画实线 // 1.1.1 画外弧和内弧的区域 // 1.1.2 画外弧 // 1.1.3 画内线 // 2. 画动态实现和虚线 // 3. 画圆 // 4. 画文字 // 5. 动画上面就是实现该效果的所有步骤,那么我们现在就一个一个的来实现:
1.画固定部分
// 1. 画固定部分 (背景弧线(实线)+ 背景虚线(虚线))
// 1.1 画实线
// 1.1.1 画外弧和内弧的区域
RectF rectF = new RectF(mArcDis, mArcDis, mScreenWidth - mArcDis, mScreenHeight - mArcDis);
RectF imaginaryLineRectF = new RectF(mImaginaryArcDis, mImaginaryArcDis, mScreenWidth - mImaginaryArcDis, mScreenHeight - mImaginaryArcDis);
// 1.1.2 画外弧
mFixationFullPaint.setColor(Color.GRAY);
canvas.drawArc(rectF, 160, 220, false, mFixationFullPaint);
// 1.1.3 画内线
mFixationImaginaryPaint.setPathEffect(new DashPathEffect(new float[]{12,12}, 0));
mFixationImaginaryPaint.setColor(Color.GRAY);
canvas.drawArc(imaginaryLineRectF, 160, 220, false, mFixationImaginaryPaint);
2.画动态实线和虚线
// 2. 画动态实现和虚线
if (mAnnualReturnNumber > 0) { // 只有当大于0的时候才需要画弧
mDynamicFullPaint.setColor(Color.WHITE);
canvas.drawArc(rectF, 160, 220 * mAnnualReturnNumber / 100, false, mDynamicFullPaint);
mDynamicImaginaryPaint.setPathEffect(new DashPathEffect(new float[]{12,12}, 0));
mDynamicImaginaryPaint.setColor(Color.WHITE);
canvas.drawArc(imaginaryLineRectF, 160, 220 * mAnnualReturnNumber / 100, false, mDynamicImaginaryPaint);
}
3.画圆
大家在画圆的时候需要注意,在我们使用Math.sin(a)和Math.cos(a)来确定圆的x和y的距离的时候,a代表的是弧度而非角度,而在实际的过程中,我们获取到的实际上是角度而非弧度,所以这里面就涉及到一个转换的问题:
//弧度和角度的转换公式: //1弧度=180/pai 度 //1度 = pai/180 弧度 //如:30度的余弦=Math.cos(30*Math.PI/180);那么在这里很多朋友就要问了,这个圆我要怎么去求呢?其实也很简单,大家都知道三角函数吧,不懂的或者忘记的记得再去回顾下,我们在画圆的时候是这样写的:
canvas.drawCircle(pointX, pointY, mCircleRadius, mPointPaint);
pointX代表圆心的x坐标,pointY代表圆心的y坐标,mCircleRadius代表圆的半径,mPointPaint代表的是画笔。这里的话我重点去讲解一下x和y的求法。
pointX = mScreenWidth/2 + mArcRadius * cos(a)
pointY = mScreenHeight/2 + mArcRadius * sin(a)
因为圆的轨迹是和外弧的轨迹一样的,所以我们只需要拿到外弧的中心位置和外弧的半径就可以通过三角函数来计算出圆的x和y。
4.画文字
画文字这里就很简单了
// 4. 画文字
String text = "年化收益" + (int) mAnnualReturnNumber + "(%)";
Rect rect = new Rect();
mTextPaint.getTextBounds(text, 0, text.length(), rect);
int mTextWidth = rect.width();
int mTextHeight = rect.height();
canvas.drawText(text, mScreenWidth / 2 - mTextWidth / 2, mScreenHeight / 2 - mTextHeight / 2, mTextPaint);
5.动画
上面的步骤仅仅只是实现了界面的绘制功能,但是那种动态效果是如何实现的呢?这里我们就要用到另外一个比较强大的东西:属性动画。个人觉得属性动画是android中实现各种炫酷效果的最重要的组成部分。好了,废话不多说,直接上代码。
首先我们需要写一个方法供外部使用,然后再去开启动画
// 开启属性动画
ValueAnimator animator = ObjectAnimator.ofFloat(0, annualReturnNumber);
animator.setDuration(800);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnnualReturnNumber = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
好了,这样一个简单的效果就实现了,下面我把所有的源码贴出来,需要的同学直接copy过去使用就好
public class AnnualReturnView extends View {
// 获取内容显示的宽度和高度
private int mScreenWidth;
private int mScreenHeight;
// 外边距
private float mMargin;
// 画固定线的画笔
private Paint mFixationFullPaint;
private Paint mFixationImaginaryPaint;
// 画动态线的画笔
private Paint mDynamicFullPaint;
private Paint mDynamicImaginaryPaint;
// 设置圆的画笔
private Paint mPointPaint;
// 设置文字的画笔
private Paint mTextPaint;
// 弧线的宽度
private float mArcWidth;
// 外弧的半径
private float mArcRadius;
// 圆的半径
private float mCircleRadius;
// 外弧距离父级边缘的距离
float mArcDis;
// 内弧距离父级边缘的距离
float mImaginaryArcDis;
// 当前的年化收益
private float mAnnualReturnNumber = 0;
public AnnualReturnView(Context context) {
this(context, null);
}
public AnnualReturnView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AnnualReturnView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mArcWidth = px2dp(2);
mCircleRadius = px2dp(4);
mMargin = px2dp(10);
mArcDis = mArcWidth + mMargin;
mImaginaryArcDis = mArcWidth + mMargin * 2;
mFixationFullPaint = getPaint();
mFixationImaginaryPaint = getPaint();
mDynamicFullPaint = getPaint();
mDynamicImaginaryPaint = getPaint();
// 设置圆的画笔
mPointPaint = new Paint();
mPointPaint.setAntiAlias(true);
mPointPaint.setDither(true);
mPointPaint.setColor(Color.RED);
// 设置文字的画笔
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setDither(true);
mTextPaint.setColor(Color.RED);
mTextPaint.setTextSize(px2dp(16));
}
// px 转换成 dp
private float px2dp(int size) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, getResources().getDisplayMetrics());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mScreenWidthValue = MeasureSpec.getSize(widthMeasureSpec);
int mScreenHeightValue = MeasureSpec.getSize(heightMeasureSpec);
mScreenWidth = Math.min(mScreenWidthValue, mScreenHeightValue);
mScreenHeight = Math.min(mScreenWidthValue, mScreenHeightValue);
mArcRadius = mScreenWidth / 2 - px2dp(10) + mArcWidth - mCircleRadius;
setMeasuredDimension(mScreenWidth, mScreenHeight);
}
// 1. 画固定部分 (背景弧线(实线)+ 背景虚线(虚线))
// 1.1 画实线
// 1.1.1 画外弧和内弧的区域
// 1.1.2 画外弧
// 1.1.3 画内线
// 2. 画动态实现和虚线
// 3. 画圆
// 4. 画文字
// 5. 动画
@Override
protected void onDraw(Canvas canvas) {
// 1. 画固定部分 (背景弧线(实线)+ 背景虚线(虚线))
// 1.1 画实线
// 1.1.1 画外弧和内弧的区域
RectF rectF = new RectF(mArcDis, mArcDis, mScreenWidth - mArcDis, mScreenHeight - mArcDis);
RectF imaginaryLineRectF = new RectF(mImaginaryArcDis, mImaginaryArcDis, mScreenWidth - mImaginaryArcDis, mScreenHeight - mImaginaryArcDis);
// 1.1.2 画外弧
mFixationFullPaint.setColor(Color.GRAY);
canvas.drawArc(rectF, 160, 220, false, mFixationFullPaint);
// 1.1.3 画内线
mFixationImaginaryPaint.setPathEffect(new DashPathEffect(new float[]{12,12}, 0));
mFixationImaginaryPaint.setColor(Color.GRAY);
canvas.drawArc(imaginaryLineRectF, 160, 220, false, mFixationImaginaryPaint);
// 2. 画动态实现和虚线
if (mAnnualReturnNumber > 0) { // 只有当大于0的时候才需要画弧
mDynamicFullPaint.setColor(Color.WHITE);
canvas.drawArc(rectF, 160, 220 * mAnnualReturnNumber / 100, false, mDynamicFullPaint);
mDynamicImaginaryPaint.setPathEffect(new DashPathEffect(new float[]{12,12}, 0));
mDynamicImaginaryPaint.setColor(Color.WHITE);
canvas.drawArc(imaginaryLineRectF, 160, 220 * mAnnualReturnNumber / 100, false, mDynamicImaginaryPaint);
}
// 3. 画圆
// 3.1 pointX = mScreenWidth/2 + mArcRadius * cos(a)
// pointY = mScreenHeight/2 + mArcRadius * sin(a)
// 3.2 如何求 a (a = 起始点和终点间的角度)
//弧度和角度的转换公式:
//1弧度=180/pai 度
//1度 = pai/180 弧度
//如:30度的余弦=Math.cos(30*Math.PI/180);
if (mAnnualReturnNumber > 0) { // 只有当大于0的时候才需要画圆
float a = (float) ((160 + 220 * mAnnualReturnNumber / 100) * Math.PI / 180);
float pointX = (float) (mScreenWidth / 2 + mArcRadius * Math.cos(a));
float pointY = (float) (mScreenHeight / 2 + mArcRadius * Math.sin(a));
canvas.drawCircle(pointX, pointY, mCircleRadius, mPointPaint);
}
// 4. 画文字
String text = "年化收益" + (int) mAnnualReturnNumber + "(%)";
Rect rect = new Rect();
mTextPaint.getTextBounds(text, 0, text.length(), rect);
int mTextWidth = rect.width();
int mTextHeight = rect.height();
canvas.drawText(text, mScreenWidth / 2 - mTextWidth / 2, mScreenHeight / 2 - mTextHeight / 2, mTextPaint);
}
// 设置画笔
public Paint getPaint() {
Paint mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setStrokeWidth(mArcWidth);
mPaint.setStyle(Paint.Style.STROKE);
return mPaint;
}
// 开始动态加载年化收益
public void startLoad(int annualReturnNumber) {
// 开启属性动画
ValueAnimator animator = ObjectAnimator.ofFloat(0, annualReturnNumber);
animator.setDuration(800);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnnualReturnNumber = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
}
}
最后一点需要注意的是,上面的代码我没有进行进一步的封装,有兴趣的同学可以在我的基础上进行再次封装,比如:起始角度和扫过的角度的封装,文字的封装等等。欢迎大家在下面留言