可以看到这个自定义控件结合了颜色渐变、动态绘制刻度、动态水球效果。接下来我们就来看看这个效果是如何一步一步实现的。
和很多自定义控件方式一样需要去基础某种 View 或者某种 ViewGroup 我这里选择的是 View,如下所示:
- public class HuaWeiView extends View{
- /**
- * 用来初始化画笔等
- * @paramcontext
- * @paramattrs
- */
- public HuaWeiView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);
- }/**
- * 用来测量限制view为正方形
- * @paramwidthMeasureSpec
- * @paramheightMeasureSpec
- */
- @Override
- protected void onMeasure(intwidthMeasureSpec,intheightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }/**
- * 实现各种绘制功能
- * @paramcanvas
- */
- @Override
- protected void onDraw(Canvas canvas) {super.onDraw(canvas);
- }
- }
其中构造方法用来布局中使用。 onMeasure() 方法用来测量和限定 view 大小 onDraw() 方法用来进行具体的绘制功能 如想详细了解请点击: 构造方法 onMeasure() MeasureSpec onDraw() 了解以上方法功能后,我们在来看看如何具体使用吧
只有确定了一个矩形才能够去画椭圆,如果这个矩形是正方形,椭圆也就随之变成了圆形。
- @Override
- protected void onMeasure(intwidthMeasureSpec,intheightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);intwidth=MeasureSpec.getSize(widthMeasureSpec);intheight = MeasureSpec.getSize(heightMeasureSpec);//以最小值为正方形的长len=Math.min(width,height);//设置测量高度和宽度(必须要调用,不然无效果)setMeasuredDimension(len,len);
- }
分别通过 MeasureSpec 取得用户设置的宽和高,然后取出最小值,设置给我们的 view,这样我们就做好了一个矩形 现在使用在布局中:
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="@color/colorPrimary"
- android:padding="20dp"
- tools:context="com.example.huaweiview.MainActivity">
- <com.example.huaweiview.HuaWeiView
- android:layout_gravity="center"
- android:background="@color/colorAccent"
- android:layout_width="200dp"
- android:layout_height="300dp"/>
- </LinearLayout>
父布局背景为蓝色背景,控件背景为粉色背景,而且设置的宽高不同,但是控件的显示效果还是一个正方形,而且以小值为准。我们的 onMeasure() 生效了 接下来就是如何在确定一个圆形区域了
绘制之前我们需要对 Android 中的坐标系有个了解 我们都知道手机屏幕左上角为坐标原点,往右为 X 正轴,往下为 Y 正轴。其实手机页面就是 activity 的展示界面,也是一个 View。那可不可以说所有的 View 在绘制图形的时候都有自己的这么一个坐标系呢(个人想法。。。) 也就是所每个 View 都有自己的一个坐标系,比如现在的自定义 View: 现在我们需要在我们自定义的 view 中绘制一个圆弧,那么这个圆弧的半径就是我们自定义 view 的长度的一半,即: radius=len/2; 那么圆心的坐标刚好是 (radius,radius)
- @Override
- protected void onDraw(Canvas canvas) {super.onDraw(canvas);//画圆弧的方法canvas.drawArc(oval, startAngle, sweepAngle, useCenter,paint);
- }
介绍一下绘制圆弧的方法:
初始化矩形
- @Override
- protected void onMeasure(intwidthMeasureSpec,intheightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);intwidth = MeasureSpec.getSize(widthMeasureSpec);intheight = MeasureSpec.getSize(heightMeasureSpec);//以最小值为正方形的长len = Math.min(width, height);//实例化矩形oval=newRectF(0,0,len,len);//设置测量高度和宽度(必须要调用,不然无效果)setMeasuredDimension(len, len);
- }
画矩形需要确定左上角和右下角的坐标(通过画板可以测试),通过上面的分析坐标原点就是我们 view 的左上角,右下角的坐标当然就是 len 了。
接下来就是初始化起始和经过角度
- private floatstartAngle=120;private floatsweepAngle=300;
需要搞清楚往下为 Y 轴正轴,刚好和上学时候学的相反,也就是说 90 度在下方,-90 度在上方
初始化画笔
- public HuaWeiView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);
- paint =newPaint();//设置画笔颜色paint.setColor(Color.WHITE);//设置画笔抗锯齿paint.setAntiAlias(true);//让画出的图形是空心的(不填充)paint.setStyle(Paint.Style.STROKE);
- }
- useCenter=false
到这里真不容易呀,然而发现只画个圆弧没用呀,我要的是刻度线呀,canvas 里面又没用给我们提供画刻度线的方法,这个时候就需要我们自己去写一个画刻度线的方法了。 通过观察图片我们可以看出,所有的线都是从圆弧上的点为起点向某个方向画一条直线,那么该如何确定这两个点呢,需要我们做两件事:
- @Override
- protected void onDraw(Canvas canvas) {super.onDraw(canvas);//画圆弧的方法canvas.drawArc(oval, startAngle, sweepAngle, useCenter,paint);//画刻度线的方法drawViewLine(canvas);
- }private void drawViewLine(Canvas canvas) {//先保存之前canvas的内容canvas.save();//移动canvas(X轴移动距离,Y轴移动距离)canvas.translate(radius,radius);//操作完成后恢复状态canvas.restore();
- }
我们自己写了一个绘制刻度线的方法并在 onDraw() 方法中调用。移动坐标系之前需要保存之前的 canvas 状态,然后 X 和 Y 轴分别移动圆弧半径的距离,如下图:
canvas.translate(radius,radius); 方法移动的是坐标系(通过实际效果和查资料所得) canvas.save() 和 canvas.restore() 要成对出现,就好像流用完要关闭一样。
只通过移动坐标系,仍然很难确定圆弧点上的坐标,和另外一点的坐标, 如果这两个点都在坐标轴上该多好呀,下面实现:
- private void drawViewLine(Canvascanvas) {//先保存之前canvas的内容
- canvas.save();//移动canvas(X轴移动距离,Y轴移动距离)
- canvas.translate(radius,radius);//旋转坐标系
- canvas.rotate(30);//操作完成后恢复状态
- canvas.restore();
- }
画刻度线的方法了增加了一个旋转 30 度的代码,旋转后的坐标系应该怎么样呢; 因为起始点和 90 度相差 30,旋转之后,起始点刚好落在了 Y 轴上,那么这个点的坐标就很好确定了吧,没错就是(0,radius); 如果我们在 Y 轴上在找一点不就可以画出一条刻度线了吗,那么它的坐标是多少呢?对,应该是 (0,radius-y),因为我们要往内部化刻度线,因此是减去一个值,赶快去试试吧,代码如下:
- private void drawViewLine(Canvascanvas) {//先保存之前canvas的内容
- canvas.save();//移动canvas(X轴移动距离,Y轴移动距离)
- canvas.translate(radius,radius);//旋转坐标系
- canvas.rotate(30);
- Paint linePatin=new Paint();//设置画笔颜色linePatin.setColor(Color.WHITE);//线宽linePatin.setStrokeWidth(2);//设置画笔抗锯齿linePatin.setAntiAlias(true);//画一条刻度线
- canvas.drawLine(0,radius,0,radius-40,linePatin);//操作完成后恢复状态
- canvas.restore();
- }
根据得到的两个点的坐标,画出来一条白线,如图:
当然这些点都是移动后的坐标系在旋转 30 度得到的,这里画好了一条线,如果画多条呢,还是刚才的思路每次都让它旋转一个小角度然后画条直线不就好了吗,那么旋转多少度呢,比如这里:总共扫过的角度 sweepAngle=300;需要 100 条刻度,那么每次需要旋转的角度 rotateAngle=sweepAngle/100,具体代码如下:
- private void drawViewLine(Canvascanvas) {//先保存之前canvas的内容
- canvas.save();//移动canvas(X轴移动距离,Y轴移动距离)
- canvas.translate(radius,radius);//旋转坐标系
- canvas.rotate(30);
- Paint linePatin=new Paint();//设置画笔颜色linePatin.setColor(Color.WHITE);//线宽linePatin.setStrokeWidth(2);//设置画笔抗锯齿linePatin.setAntiAlias(true);//确定每次旋转的角度
- floatrotateAngle=sweepAngle/99;for(inti=0;i<100;i++){//画一条刻度线
- canvas.drawLine(0,radius,0,radius-40,linePatin);canvas.rotate(rotateAngle);
- }//操作完成后恢复状态
- canvas.restore();
- }
100 个刻度,需要 101 次循环画线(请看你的手表),画完线就旋转。依次循环,如图
经过这么久的时间总于完成了刻度盘了,接下来就是去确定不同角度显示什么样的颜色,首选我们需要确定要绘制的范围 targetAngle:
- private void drawViewLine(Canvascanvas) {//先保存之前canvas的内容
- canvas.save();//移动canvas(X轴移动距离,Y轴移动距离)
- canvas.translate(radius,radius);//旋转坐标系
- canvas.rotate(30);
- Paint linePatin=new Paint();//设置画笔颜色linePatin.setColor(Color.WHITE);//线宽linePatin.setStrokeWidth(2);//设置画笔抗锯齿linePatin.setAntiAlias(true);//确定每次旋转的角度
- floatrotateAngle=sweepAngle/100;//绘制有色部分的画笔Paint targetLinePatin=new Paint();
- targetLinePatin.setColor(Color.GREEN);
- targetLinePatin.setStrokeWidth(2);
- targetLinePatin.setAntiAlias(true);//记录已经绘制过的有色部分范围
- floathasDraw=0;for(inti=0;i<=100;i++){if(hasDraw<=targetAngle&&targetAngle!=0){//需要绘制有色部分的时候
- //画一条刻度线
- canvas.drawLine(0,radius,0,radius-40,targetLinePatin);
- }else{//不需要绘制有色部分
- //画一条刻度线
- canvas.drawLine(0,radius,0,radius-40,linePatin);
- }//累计绘制过的部分hasDraw+=rotateAngle;//旋转
- canvas.rotate(rotateAngle);
- }//操作完成后恢复状态
- canvas.restore();
- }
我们需要不断的去记录绘制过的有效部分,之外的部分画白色。
需要计算出已经绘制过的角度占总角度(300)的比例
- for(inti=0;i<=100;i++){if(hasDraw<=targetAngle&&targetAngle!=0){//需要绘制有色部分的时候
- //计算已经绘制的比例
- float percent=hasDraw/sweepAngle;intred=255-(int) (255*percent);intgreen= (int) (255*percent);
- targetLinePatin.setARGB(255,red,green,0);//画一条刻度线
- canvas.drawLine(0,radius,0,radius-40,targetLinePatin);
- }else{//不需要绘制有色部分
- //画一条刻度线
- canvas.drawLine(0,radius,0,radius-40,linePatin);
- }
- hasDraw+=rotateAngle;canvas.rotate(rotateAngle);
- }
只是在绘制有色部分的时候,利用三元素来实现渐变。所占比例越低红色值越大,反正绿色值越大。
先想一下它的运动情况,分为前进状态和后退状态,如果正在运动(一次完整的后退和前进没用结束),就不能开始下次运动,需要两个参数,state 和 isRunning
- //判断是否在动
- private boolean isRunning;
- //判断是回退的状态还是前进状态
- private int state = 1;
- public void changeAngle(final float trueAngle) {
- if (isRunning) { //如果在动直接返回
- return;
- }
- final Timer timer = new Timer();
- timer.schedule(new TimerTask() {@Override public void run() {
- switch (state) {
- case 1:
- //后退状态
- isRunning = true;
- targetAngle -= 3;
- if (targetAngle <= 0) { //如果回退到0
- targetAngle = 0;
- //改为前进状态
- state = 2;
- }
- break;
- case 2:
- //前进状态
- targetAngle += 3;
- if (targetAngle >= trueAngle) { //如果增加到指定角度
- targetAngle = trueAngle;
- //改为后退状态
- state = 1;
- isRunning = false;
- //结束本次运动
- timer.cancel();
- }
- break;
- default:
- break;
- }
- //重新绘制(子线程中使用的方法)
- postInvalidate();
- }
- },
- 500, 30);
- }
利用时间任务,每个 30 毫秒去执行一次 run 方法,每次都重新绘制图片,然后在 activity 中调用此方法
- HuaWeiView hwv;@Override
- protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- hwv= (HuaWeiView) findViewById(R.id.hwv);
- hwv.setOnClickListener(newView.OnClickListener() {@Override
- public void onClick(View v) {//点击事件中,调用动的方法hwv.changeAngle(200);
- }
- });
- }
看到这里了,相信你对坐标系和角度动态变化,以及刻度盘的绘制有了个很好的认识,多多验证会有助于理解。
想想咱们的 view 中哪里用了渐变呢?对,在绘制有色部分的时候,如果我们能将颜色渐变的值不断的传到 activity 中该多好呀,下面就要用接口传值实现这一功能了:
- privateOnAngleColorListener onAngleColorListener;public void setOnAngleColorListener(OnAngleColorListener onAngleColorListener) {this.onAngleColorListener = onAngleColorListener;
- }public interfaceOnAngleColorListener{voidcolorListener(intred,intgreen);
- }
我们在自定义 View 中声明一个内部接口,并声明一个全局接口对象,提供一个 set 方法 接口内有个方法用来获取颜色值 接下来就是在合适的地方调用这个方法,那么哪里呢,就是我们绘制颜色刻度时调用:
- for(inti =0; i <=100; i++) {if(hasDraw <= targetAngle && targetAngle !=0) {//需要绘制有色部分的时候
- //计算已经绘制的比例
- float percent= hasDraw / sweepAngle;intred =255- (int) (255*percent);intgreen = (int) (255*percent);//实现接口回调,传递颜色值
- if(onAngleColorListener!=null){
- onAngleColorListener.colorListener(red,green);
- }
- targetLinePatin.setARGB(255, red, green,0);//画一条刻度线
- canvas.drawLine(0, radius,0, radius -40, targetLinePatin);
- }else{//不需要绘制有色部分
- //画一条刻度线
- canvas.drawLine(0, radius,0, radius -40, linePatin);
- }
我们在绘制的时候实现了接口回调,接下来去 activity 中实现接口
- public class MainActivity extends AppCompatActivity{HuaWeiView hwv;
- LinearLayout ll_parent;@Override
- protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- hwv= (HuaWeiView) findViewById(R.id.hwv);//实例父布局ll_parent= (LinearLayout) findViewById(R.id.ll_parent);
- hwv.setOnClickListener(newView.OnClickListener() {@Override
- public void onClick(View v) {//点击事件中,调用动的方法hwv.changeAngle(200);
- }
- });//设置角度颜色变化监听hwv.setOnAngleColorListener(newHuaWeiView.OnAngleColorListener() {@Override
- public void colorListener(intred,intgreen) {
- Color color=newColor();//通过Color对象将RGB值转为int类型
- intbackColor=color.argb(100,red,green,0);//父布局设置背景ll_parent.setBackgroundColor(backColor);
- }
- });
- }
- }
给父布局一个 id,然后实例化。给我们的自定义控件设置一个角度颜色变化监听,从而拿到回调中传过来的值,然后借助 Color 对象将 RGB 值转为 int 值,再设置给父布局背景,这里背景稍稍透明一些。效果图:
到了这里是不是感觉炫酷了不少呢,其实功能已经实现的差不多了,接下来就是去绘制里面的内容吧
当然不去绘制文字也是可以的,你可以直接在布局中添加 textview 等。好话不多说,先分析一下绘制的过程吧,在刻度盘的内部有一个小圆,然后这些文字就在小圆内,绘制小圆只需要让它的半径小点就 OK 了。
- /**
- * 绘制小圆和文本的方法,小圆颜色同样渐变
- * @paramcanvas
- */
- private void drawScoreText(Canvas canvas) {//先绘制一个小圆Paint smallPaint =newPaint();
- smallPaint.setARGB(100,red,green,0);// 画小圆指定圆心坐标,半径,画笔即可
- intsmallRadius=radius-60;
- canvas.drawCircle(radius, radius, radius -60, smallPaint);//绘制文本Paint textPaint=newPaint();//设置文本居中对齐textPaint.setTextAlign(Paint.Align.CENTER);
- textPaint.setColor(Color.WHITE);
- textPaint.setTextSize(smallRadius/2);//score需要通过计算得到canvas.drawText(""+score,radius,radius,textPaint);//绘制分,在分数的右上方textPaint.setTextSize(smallRadius/6);
- canvas.drawText("分",radius+smallRadius/2,radius-smallRadius/4,textPaint);//绘制点击优化在分数的下方textPaint.setTextSize(smallRadius/6);
- canvas.drawText("点击优化",radius,radius+smallRadius/2,textPaint);
- }
这里将之前渐变的 red 和 green 提为全局变量,先绘制一个小圆,画笔颜色渐变。然后绘制文字分数 score 需要通过计算的到
- //计算得到的分数score=(int)(targetAngle/sweepAngle*100);//重新绘制(子线程中使用的方法)postInvalidate();
在时间任务中,每次绘制之前计算得到分数,然后在右上方画一个固定值分,再在下方一个固定内容点击优化(这个时候的坐标已经回到最初的模样)
到此为止功能已经写的差不多了,还有一个水波加速球效果,下篇博客中写吧。 最后对于原理底层方面,我也有待学习,有错的地方欢迎指正,谢谢。项目已经上传到 github github 点击下载
来源: http://blog.csdn.net/android_hl/article/details/70455018