这次高仿的是QQ运动的周报界面的网图。这个控件刚开始的时候以为代码量不大,没想到一路下来界面代码在加上动画代码还是蛮多的。好了老规矩先上图:
效果还是和qq的才不多吧。 1. 首先我把各个变量都贴出来以便在后续中你们可以更好理解代码的意思:
//屏幕的宽度
private int mScreemWidth;
//屏幕的高度
private int mScreemHight;
//圆的线
private Paint mCirclePaint;
//圆区域的颜色
private Paint mCirclePaintColor;
//虚线
private Paint mLineCircle;
//圆点
private Paint mCircleHoldPaint;
//画字体
private Paint mCenterCircle;
//最外的圆的透明度
private int mCircleAlpha1=0;
//中间的圆的透明度
private int mCircleAlpha2=0;
//最内的圆的透明度
private int mCircleAlpha3=0;
//好友排名
private int mFriendDranking=0;
//达标天数
private int mStandardDay=0;
//平均步数
private int mAverageCount=0;
//好友排名的X轴坐标
private float mFriendDrankingX=0;
//好友排名的Y轴坐标
private float mFriendDrankingY=0;
//平均步数的X轴坐标
private float mStandardDayX=0;
//平均步数的Y轴坐标
private float mStandardDayY=0;
//达标天数的X轴坐标
private float mAverageCountX=0;
//达标天数的Y轴坐标
private float mAverageCountY=0;
//临时的View的半径
private int tempCircleRadius=0;
//View的半径
private int circleRadius=0;
//每个圆圈的间隔
private float marginCircleSize=0;
//圆的颜色
private int circleColor=0;
//朋友区域的颜色
private int friendColor;
//平均步数区域的颜色
private int averageColor;
//达标天数区域的颜色
private int standardColor;
//总步数
private String allStep;
//好友排名
private String firendDrank;
//达标天数
private String standarDay;
//平均步数
private String averageCount;
//波浪动画的数值
private int waveData=-30;
//中间文字翻转动画的数值
private float centerData=0;
//画波浪的看门狗
private boolean waveWatchDag=false;
//画虚线的看门狗
private boolean lineWatchDag=false;
//各点解释的看门狗
private boolean expainWatchDag=false;
//中心圆的内容的看门狗
private boolean centerWatchDag=false;
//解释的字符串
private String averageCountTxt="平均步数";
private String friendDrankTxt="好友排名";
private String standarDayTxt="达标天数";
private String theyCount="本周总步数";
private String tip="步";
2.有点多了,其次就是测量View的大小的onMeasure():
@Override protected void onMeasure(
int widthMeasureSpec, int heightMeasureSpec) {
int widthModel=MeasureSpec.getMode(widthMeasureSpec);
int heightModel=MeasureSpec.getMode(heightMeasureSpec);
int measureWidth=MeasureSpec.getSize(widthMeasureSpec);
int measureHeight=MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if(widthModel==MeasureSpec.EXACTLY){
width=measureWidth;
}else{
width=getPaddingLeft()+getPaddingRight()+measureWidth;
}
if(heightModel==MeasureSpec.EXACTLY){
height=measureHeight;
}else{
height=(getPaddingLeft()+getPaddingRight()+measureHeight)/2;
}
setMeasuredDimension(width,height);
loadAnimator();
}
3.这里当设置大小为wrap_content的时候,View的宽度的话是用屏幕的的宽,而View的高的话是屏幕的高度的一半。当View的大小生成之后会调用onSizeChange()方法,具体操作如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mScreemWidth=w;
mScreemHight=h;
//得出最大的圆的半径
if(mScreemWidth>mScreemHight){
circleRadius=Float.valueOf((w/3.4)+"").intValue();
}else{
circleRadius=Float.valueOf((h/3.4)+"").intValue();
}
if(tempCircleRadius!=0&&tempCircleRadius<=circleRadius){
circleRadius=tempCircleRadius;
}
//得出每个圆的间隔
marginCircleSize=circleRadius/6;
}
当View的宽度大于View的高度时,最外边的圆的半径就是w/3.4,反之当View的高度大于View的宽度时,最外边的圆的半径就是h/3.4,而每个圆的间隔就是圆半径的六分之一。4.接着就是最重要的onDraw()方法了,代码如下:
@Override protected void onDraw(Canvas canvas) {
canvas.translate(getWidth()/2,getHeight()/2-(circleRadius/6));
canvas.save();
//画出三条圆圈
drawCircle(canvas);
//画出波浪图形
drawWaves(canvas);
//画虚线
drawDottedLine(canvas);
//画点
drawCircleHold(canvas);
//画解释的内容
drawExpain(canvas);
//画中心圆的内容
centerCircleContent(canvas);
}
首先把canvas的原点移到X轴为宽度的一半,Y轴为高度的一半再减去六分之一的半径,保存canvas的状态。接着就一个个说这里面的每一个方法:
//画出三条圆圈
public void drawCircle(Canvas canvas){
//画出最大的圆
mCirclePaint.setAlpha(mCircleAlpha1);
canvas.drawCircle(0,0,circleRadius,mCirclePaint);
//画出第二大的圆
mCirclePaint.setAlpha(mCircleAlpha2);
canvas.drawCircle(0,0,circleRadius-marginCircleSize,mCirclePaint);
mCirclePaintColor.setColor(Color.parseColor("#F1FCFE"));
mCirclePaintColor.setAlpha(mCircleAlpha2);
canvas.drawCircle(0,0,circleRadius-marginCircleSize-2,mCirclePaintColor);
//画出第三大的圆
mCirclePaint.setAlpha(mCircleAlpha3);
canvas.drawCircle(0,0,circleRadius-marginCircleSize*2,mCirclePaint);
mCirclePaintColor.setColor(Color.parseColor("#E7F9FE"));
mCirclePaintColor.setAlpha(mCircleAlpha3);
canvas.drawCircle(0,0,circleRadius-marginCircleSize*2-2,mCirclePaintColor);
}
这个方法是比较简单的,就是画出三个圆圈,每个圆圈的间隔就是前面所初始化的marginCircleSize,圆圈的圆心就是canvas的原点,之前我们移动过原点了。第二和第三个圆圈里面还配有圆的背景,效果如下图:
接着就是画出波浪图形的方法 drawWaves(canvas)代码如下:
//画出波浪图形
public void drawWaves(Canvas canvas){
if(!waveWatchDag){
return ;
}
canvas.rotate(waveData);
float inCircleRadius=circleRadius-marginCircleSize*3;
//算出最上面的点
float topPointX=0;
float topPointY=-inCircleRadius;
//算出左下角的点
float leftBottpmPointX=-(float)Math.sqrt(Math.pow(inCircleRadius,2)-Math.pow(inCircleRadius/2,2));
float leftBottomPointY=inCircleRadius/2;
//算出右小角的点
float rightBottomPointX=-leftBottpmPointX;
float rightBottomPointY=inCircleRadius/2;
//得到好友排名半径
float mFriendDrankingData=circleValue(mFriendDranking);
//得到达标天数半径
float mStandarDayData=circleValue(mStandardDay);
//得到平均步数半径
float mAverageCountData=circleValue(mAverageCount);
/*画好友排名*/
//得出左上角的圆的坐标
float[] mFriendDrankingPoint=calculatePoint(mFriendDrankingData);
//好友排名的X轴坐标
mFriendDrankingX=-mFriendDrankingPoint[0];
//好友排名的Y轴坐标
mFriendDrankingY=-mFriendDrankingPoint[1];
//画出还有排名的波浪线
Path mFriendDrankingPath=new Path();
mFriendDrankingPath.moveTo(leftBottpmPointX,leftBottomPointY);
mFriendDrankingPath.lineTo(mFriendDrankingX-6,mFriendDrankingY-6);
mFriendDrankingPath.lineTo(topPointX,topPointY);
mFriendDrankingPath.lineTo(topPointX+10,topPointY+10);
mCirclePaintColor.setPathEffect(new CornerPathEffect(20));
mCirclePaintColor.setColor(friendColor);
canvas.drawPath(mFriendDrankingPath,mCirclePaintColor);
/*画达标天数*/
//得出右上角的圆的坐标
float[] mStandarDayPoint=calculatePoint(mStandarDayData);
//达标天数的X轴坐标
mStandardDayX=mStandarDayPoint[0];
//达标天数的Y轴坐标
mStandardDayY=-mStandarDayPoint[1];
//画出还有达标天数的波浪线
Path mStandarDayPath=new Path();
mStandarDayPath.moveTo(topPointX,topPointY);
mStandarDayPath.lineTo(mStandardDayX+6,mStandardDayY-6);
mStandarDayPath.lineTo(rightBottomPointX,rightBottomPointY);
mStandarDayPath.lineTo(rightBottomPointX-10,rightBottomPointY+10);
mCirclePaintColor.setColor(standardColor);
canvas.drawPath(mStandarDayPath,mCirclePaintColor);
/*平均步数*/
//平均步数的X轴坐标
mAverageCountX=0;
//平均步数的Y轴坐标
mAverageCountY=mAverageCountData;
//画出还有平均步数的波浪线
Path mAverageCountPath=new Path();
mAverageCountPath.moveTo(rightBottomPointX,rightBottomPointY);
mAverageCountPath.lineTo(topPointX,mAverageCountData+8);
mAverageCountPath.lineTo(leftBottpmPointX,leftBottomPointY);
mAverageCountPath.lineTo(leftBottpmPointX+10,leftBottomPointY+10);
mCirclePaintColor.setColor(averageColor);
canvas.drawPath(mAverageCountPath,mCirclePaintColor);
//最里面的圆 mCirclePaintColor.setColor(Color.WHITE);
canvas.drawCircle(0,0,circleRadius-marginCircleSize*3,mCirclePaintColor);
}
这方法里最核心的就是数学计算了,整个View有3个波浪区域,各占一个圆的三分之一,所以第一步就是计算出这个圆的左下角,右小角和正上角的三个点,如图
的蓝色点所示。具体代码见注释。在通过circleValue算出波浪线的半径:
//算出弧线区域的半径
public float circleValue(int mDataDranking){
if(mDataDranking==1){
return circleRadius-marginCircleSize*2;
}else if(mDataDranking==2){
return circleRadius-marginCircleSize;
}else if(mDataDranking==3){
return circleRadius;
}else{
return circleRadius-marginCircleSize*2;
} }
然后通过calculatePoint()方法来各个波浪区域对应的顶点,代码如下:
//算出右上角或左上角的坐标
public float[] calculatePoint(float radius){
float[] result=new float[2];
float pointY=radius/2;
float pointX=(float)Math.sqrt(Math.pow(radius,2)-Math.pow(pointY,2));
result[0]=pointX;
result[1]=pointY;
return result;
}
最后转化为形象的图就是:
接着用Path把各个区域的点连起来就是形成区域,不过现在还是尖角,要把它变成原角就要用mCirclePaintColor.setPathEffect(new CornerPathEffect(20));方法,这样各个边的连接处都可以转换成圆角,可是因为是圆角所以到不到圆圈的边,这时候你要对你的顶点进行微调,所以我再顶点都进行了减6或者加6的操作。至于我这个6是怎么得出来的,我用的等比例的数学方法来求出来的,到时有优化我可以把我的方法用代码表示出来。至此,重要的就说完了,剩下的只是用canvas和path和paint画出来就是了。效果如下:
接着就是画虚线的方法了drawDottedLine(canvas)代码如下:
//画圆点和虚线
public void drawDottedLine(Canvas canvas){
if(!lineWatchDag){
return;
}
for(int i=0;i<3;i++){
canvas.rotate(120);
if(i==0){
//画好友排名的虚线
mLineCircle.setTextSize(18);
mLineCircle.setColor(friendColor);
drawDottedLine(canvas,judgeDotte(mFriendDranking));
}else if(i==1){
//画达标天数的虚线
mLineCircle.setColor(standardColor);
drawDottedLine(canvas,judgeDotte(mStandardDay));
}else if(i==2){
//画平均步数的虚线
mLineCircle.setColor(averageColor);
drawDottedLine(canvas,judgeDotte(mAverageCount));
} }
canvas.restore();
}
//判断虚线
public List<Float> judgeDotte(int value){ List<Float> temp=new ArrayList<>();
if(value==1){
//当为1时,波浪顶点到第三个圆
temp.add(circleRadius-marginCircleSize*2);
temp.add((float)circleRadius);
temp.add(circleRadius-marginCircleSize*3);
}else if(value==2){
//当为2时,波浪顶点到第二个圆
temp.add(circleRadius-marginCircleSize);
temp.add((float)circleRadius);
temp.add(circleRadius-marginCircleSize*3);
}else if(value==3){
//当为3时,波浪顶点到第一个圆
temp.add(circleRadius-marginCircleSize*3);
temp.add((float)circleRadius);
}
return temp;
}
//画虚线
public void drawDottedLine(Canvas canvas,List<Float> data){
if(data.size()==2){
/*当数值是最大的是时候也就是3*/
mLineCircle.setColor(Color.WHITE);
Path path=new Path();
path.moveTo(0,data.get(0));
path.lineTo(0,data.get(1));
canvas.drawPath(path,mLineCircle);
return ;
}else{
/*当数值在1和2的时候*/
//画出数值外的虚线
Path pathOut=new Path();
pathOut.moveTo(0,data.get(0));
pathOut.lineTo(0,data.get(1));
mLineCircle.setPathEffect(new DashPathEffect(new float[]{7,5,7,5},5));
canvas.drawPath(pathOut,mLineCircle);
//画出数值内的虚线
Path pathIn=new Path();
pathIn.moveTo(0,data.get(1));
pathIn.lineTo(0,data.get(2));
mLineCircle.setColor(Color.WHITE);
canvas.drawPath(pathIn,mLineCircle);
}
}
首先canvas通过每次旋转120度来画出每一条波浪线,通过judgeDotte()方法得出波浪线三个点对应的Y轴的坐标,假如judgeDotte返回的个数是两个的话那就是证明顶点在最外面的圆,假如是3个的话就画出顶点之外和顶点之内的线就可以了,代码注释已经很详细了,效果图如下:
接着是画虚线上的圆点,drawCircleHold(Canvas canvas)代码如下:
//画虚线上的圆点
public void drawCircleHold(Canvas canvas){
if(!lineWatchDag){
return;
}
float[] yuan1=calculatePoint(circleRadius-marginCircleSize*2);
float[] yuan2=calculatePoint(circleRadius-marginCircleSize);
float[] yuan3=calculatePoint(circleRadius);
//画好友排名的圆点
drawCircleHoldImpl(-yuan1[0],-yuan1[1],-yuan2[0],-yuan2[1], -yuan3[0],-yuan3[1],mFriendDranking,canvas,friendColor);
//画达标天数的圆点
drawCircleHoldImpl(yuan1[0],-yuan1[1],yuan2[0],-yuan2[1], yuan3[0],-yuan3[1],mStandardDay,canvas,standardColor);
//画平均步数的圆点 drawCircleHoldImpl(0,circleRadius-marginCircleSize*2,0,circleRadius-marginCircleSize, 0,circleRadius,mAverageCount,canvas,averageColor);
expainWatchDag=true;
}
//画圆的具体的方法
public void drawCircleHoldImpl(float mCirlce1X,float mCircle1Y,float mCirlce2X,float mCircle2Y, float mCirlce3X,float mCircle3Y,int action,Canvas canvas,int color){
mCircleHoldPaint.setColor(color);
if(action==1){
//当数值为3时画所有圆圈 canvas.drawCircle(mCirlce1X,mCircle1Y,8,mCircleHoldPaint);
canvas.drawCircle(mCirlce2X,mCircle2Y,8,mCircleHoldPaint);
}else if(action==2){
//当数值为2时画中间的圆圈
canvas.drawCircle(mCirlce2X,mCircle2Y,8,mCircleHoldPaint);
}
//画一定要画的圆圈和圆点
canvas.drawCircle(mCirlce3X,mCircle3Y,8,mCircleHoldPaint);
mCircleHoldPaint.setColor(Color.WHITE);
canvas.drawCircle(mCirlce1X,mCircle1Y,6,mCircleHoldPaint);
canvas.drawCircle(mCirlce2X,mCircle2Y,6,mCircleHoldPaint);
canvas.drawCircle(mCirlce3X,mCircle3Y,6,mCircleHoldPaint);
}
这里同样注释也是很详细的,整个思路就是通过calculatePoint()算出三个圆点的坐标,在通过传进去的数值来要画多少个圆圈,而原点是不管数值多少都要画的。效果图如下:
接着就是画解释的内容drawExpain(Canvas canvas)代码如下:
//画解释的内容
public void drawExpain(Canvas canvas){
if(!expainWatchDag){
return ;
}
//间隔 int margin=circleRadius/5;
//画平均步数和对应的数值
Rect txtRect=new Rect();
mCenterCircle.setColor(Color.BLACK);
mCenterCircle.setTextSize(circleRadius/6);
mCenterCircle.setTypeface(Typeface.SANS_SERIF);
canvas.drawText(averageCount,0,circleRadius+margin,mCenterCircle);
mCenterCircle.setColor(friendColor); mCenterCircle.setTextSize(circleRadius/10);
mCenterCircle.getTextBounds(averageCountTxt,0,averageCountTxt.length(),txtRect);
canvas.drawText(averageCountTxt,0,circleRadius+margin+(txtRect.bottom- txtRect.top),mCenterCircle);
//画好友排名和对应的数值
mCenterCircle.setColor(Color.BLACK);
mCenterCircle.setTextSize(circleRadius/6);
canvas.drawText(firendDrank,-circleRadius,-(circleRadius- marginCircleSize),mCenterCircle);
mCenterCircle.setColor(friendColor);
mCenterCircle.setTextSize(circleRadius/10);
mCenterCircle.getTextBounds(friendDrankTxt,0,friendDrankTxt.length(),txtRect);
canvas.drawText(friendDrankTxt,-circleRadius,-(circleRadius-marginCircleSize)+(txtRect.bottom-txtRect.top),mCenterCircle);
//画达标天数和对应的数值
mCenterCircle.setColor(Color.BLACK);
mCenterCircle.setTextSize(circleRadius/6);
canvas.drawText(standarDay,circleRadius,-(circleRadius- marginCircleSize),mCenterCircle);
mCenterCircle.setColor(friendColor);
mCenterCircle.setTextSize(circleRadius/10);
mCenterCircle.getTextBounds(friendDrankTxt,0,friendDrankTxt.length(),txtRect);
canvas.drawText(standarDayTxt,circleRadius,-(circleRadius-marginCircleSize)+(txtRect.bottom-txtRect.top),mCenterCircle); centerWatchDag=true;
}
看起来代码有点多,其实是最简单的,就是确定好友排名的坐标(-circleRadius,-(circleRadius-marginCircleSize)),int margin=circleRadius/5,平均步数的坐标(0,circleRadius+margin),达标天数的坐标(circleRadius,-(circleRadius-marginCircleSize))来进行drawText的操作而已,没什么可以说的,Rect是得出字体大小的,具体看上面代码。效果如下图:
最后就是画中心圆的内容的centerCircleContent(canvas)了,代码如下:
//画中心圆的内容
public void centerCircleContent(Canvas canvas){
if(!centerWatchDag){
return ;
}
//画出颜色渐变的圆圈
canvas.rotate(140);
float centerSize=circleRadius-marginCircleSize*3-(circleRadius/20);
mCenterCircle.setShader(new SweepGradient(0,0,new int[]{ friendColor,friendColor,standardColor,averageColor},null));
canvas.drawCircle(0,0,centerSize,mCenterCircle);
canvas.rotate(-140);
//画出运动的总步数
mCenterCircle.setShader(null);
mCenterCircle.setColor(friendColor);
mCenterCircle.setTextSize(circleRadius/4);
mCenterCircle.setTextAlign(Paint.Align.CENTER);
Rect numRect=new Rect();
mCenterCircle.getTextBounds(allStep,0,allStep.length(),numRect);
Camera camera=new Camera(); camera.rotateY(centerData);
camera.applyToCanvas(canvas);
canvas.drawText(allStep,0,(numRect.bottom-numRect.top)/2,mCenterCircle);
//画出总运动步数右边的字
Rect tipRect=new Rect();
mCenterCircle.setTextSize(circleRadius/12);
mCenterCircle.getTextBounds(tip,0,tip.length(),tipRect);
canvas.drawText(tip,(numRect.right-numRect.left)/2+(tipRect.right-tipRect.left)/2+5 ,(numRect.bottom-numRect.top)/2-3,mCenterCircle);
//画出总运动步数下面的提示
Rect theyRect=new Rect();
mCenterCircle.getTextBounds(theyCount,0,theyCount.length(),theyRect);
float marginBottom=circleRadius/12;
mCenterCircle.setTextSize(circleRadius/11);
canvas.drawText(theyCount,0,marginBottom+(numRect.bottom-numRect.top)/2 +(theyRect.bottom-theyRect.top)/2,mCenterCircle);
}
中心圆的内容里实现的大概思路画解释的内容的思路都差不多,我觉得值得讲的就是这个Camera类了,这里的Camera类可不是相机里的Camera类,他可以实现Camera的旋转缩放的功能,是一个十分强大的类,而camera.rotateY(centerData)
就是设置Y轴旋转的效果的关键代码。其次就是用mCenterCircle.setShader(new SweepGradient(0,0,new int[]{ friendColor,friendColor,standardColor,averageColor},null));
来实现圆圈颜色的渐变功能的关键代码,里面还可以实现更多效果,这就需要小伙伴们用外的时间学了。最后效果如下:
至此整个绘画就结束了,接着就是动画效果,代码如下:
//启动动画的方法
public void loadAnimator(){
final ValueAnimator alphaAmimator3=ValueAnimator.ofInt(0,225);
final ValueAnimator alphaAmimator2=ValueAnimator.ofInt(0,225);
final ValueAnimator wavesAminator=ValueAnimator.ofInt(-30,0);
final ValueAnimator centerAnimator=ValueAnimator.ofFloat(0,360);
ValueAnimator alphaAmimator1=ValueAnimator.ofInt(0,225);
centerAnimator.setDuration(1000);
centerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
centerData=(float)animation.getAnimatedValue(); postInvalidate();
}
});
wavesAminator.setDuration(1000);
wavesAminator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
waveData=(int)animation.getAnimatedValue();
waveWatchDag=true;
if(waveData==0&&lineWatchDag==false){
lineWatchDag=true; centerAnimator.start();
}
postInvalidate();
} });
alphaAmimator3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override
public void onAnimationUpdate(ValueAnimator animation) {
mCircleAlpha3=(int)animation.getAnimatedValue();
postInvalidate();
if(mCircleAlpha3==225){
wavesAminator.start();
}
}
});
alphaAmimator3.setDuration(250);
alphaAmimator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override
public void onAnimationUpdate(ValueAnimator animation) {
mCircleAlpha2=(int)animation.getAnimatedValue();
postInvalidate();
if(mCircleAlpha2==225){
alphaAmimator3.start();
}
} });
alphaAmimator2.setDuration(250);
alphaAmimator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override
public void onAnimationUpdate(ValueAnimator animation) {
mCircleAlpha1=(int)animation.getAnimatedValue(); postInvalidate();
if(mCircleAlpha1==225){
alphaAmimator2.start();
}
}
});
alphaAmimator1.setDuration(250);
alphaAmimator1.start();
}
其实就是通过ValueAnimator不断的生成状态量然后调用postInvalidate()不断的刷新View即可实现。最后要想更详细的了解整个流程请看源码吧。
奉上源码。如果对你有帮助就请给我给星星或喜欢吧