效果图
可以看到这个自定义控件结合了颜色渐变、动态绘制刻度、动态水球效果。接下来我们就来看看这个效果是如何一步一步实现的。
开始自定义控件
和很多自定义控件方式一样需要去基础某种View或者某种ViewGroup
我这里选择的是View,如下所示:
- public class HuaWeiView extends View {
- /**
- * 用来初始化画笔等
- * @param context
- * @param attrs
- */
- public HuaWeiView(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
- /**
- * 用来测量限制view为正方形
- * @param widthMeasureSpec
- * @param heightMeasureSpec
- */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- /**
- * 实现各种绘制功能
- * @param canvas
- */
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- }
- }
其中构造方法用来布局中使用。
onMeasure()方法用来测量和限定view大小
onDraw()方法用来进行具体的绘制功能
1.使用onMeasure()方法将View限制为一个正方形
只有确定了一个矩形才能够去画椭圆,如果这个矩形是正方形,椭圆也就随之变成了圆形。
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int width=MeasureSpec.getSize(widthMeasureSpec);
- int height = 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()生效了
接下来就是如何在确定一个圆形区域了
2.onDraw()绘制圆形区域
绘制之前我们需要对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);
- }
介绍一下绘制圆弧的方法:
- 参数一oval是一个RectF对象为一个矩形
- 参数二startAngle为圆弧的起始角度
- 参数三sweepAngle为圆弧的经过角度(扫过角度)
- 参数四useCenter为圆弧是一个boolean值,为true时画的是圆弧,为false时画的是割弧
- 参数五paint为一个画笔对象
也就是说只要确定了一个矩形,在确定他起始和经过的角度就能够画出一个圆弧(这点大家可以用画板测试)
下来就是初始化这些参数
初始化矩形
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- //以最小值为正方形的长
- len = Math.min(width, height);
- //实例化矩形
- oval=new RectF(0,0,len,len);
- //设置测量高度和宽度(必须要调用,不然无效果)
- setMeasuredDimension(len, len);
- }
画矩形需要确定左上角和右下角的坐标(通过画板可以测试),通过上面的分析坐标原点就是我们view的左上角,右下角的坐标当然就是len了。
接下来就是初始化起始和经过角度
- private float startAngle=120;
- private float sweepAngle=300;
需要搞清楚往下为Y轴正轴,刚好和上学时候学的相反,也就是说90度在下方,-90度在上方
初始化画笔
- public HuaWeiView(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- paint =new Paint();
- //设置画笔颜色
- 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(Canvas canvas) {
- //先保存之前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(Canvas canvas) {
- //先保存之前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(Canvas canvas) {
- //先保存之前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);
- //确定每次旋转的角度
- float rotateAngle=sweepAngle/99;
- for(int i=0;i<100;i++){
- //画一条刻度线
- canvas.drawLine(0,radius,0,radius-40,linePatin);
- canvas.rotate(rotateAngle);
- }
- //操作完成后恢复状态
- canvas.restore();
- }
100个刻度,需要101次循环画线(请看你的手表),画完线就旋转。依次循环,如图
经过这么久的时间总于完成了刻度盘了,接下来就是去确定不同角度显示什么样的颜色,首选我们需要确定要绘制的范围targetAngle:
绘制有色部分
- private void drawViewLine(Canvas canvas) {
- //先保存之前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);
- //确定每次旋转的角度
- float rotateAngle=sweepAngle/100;
- //绘制有色部分的画笔
- Paint targetLinePatin=new Paint();
- targetLinePatin.setColor(Color.GREEN);
- targetLinePatin.setStrokeWidth(2);
- targetLinePatin.setAntiAlias(true);
- //记录已经绘制过的有色部分范围
- float hasDraw=0;
- for(int i=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(int i=0;i<=100;i++){
- if(hasDraw<=targetAngle&&targetAngle!=0){//需要绘制有色部分的时候
- //计算已经绘制的比例
- float percent=hasDraw/sweepAngle;
- int red= 255-(int) (255*percent);
- int green= (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(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- //点击事件中,调用动的方法
- hwv.changeAngle(200);
- }
- });
- }
看到这里了,相信你对坐标系和角度动态变化,以及刻度盘的绘制有了个很好的认识,多多验证会有助于理解。
接下来要实现背景动态渐变
想想咱们的view中哪里用了渐变呢?对,在绘制有色部分的时候,如果我们能将颜色渐变的值不断的传到activity中该多好呀,下面就要用接口传值实现这一功能了:
- 首选在自定义View中声明一个内部接口:
- private OnAngleColorListener onAngleColorListener;
- public void setOnAngleColorListener(OnAngleColorListener onAngleColorListener) {
- this.onAngleColorListener = onAngleColorListener;
- }
- public interface OnAngleColorListener{
- void colorListener(int red,int green);
- }
我们在自定义View中声明一个内部接口,并声明一个全局接口对象,提供一个set方法
接口内有个方法用来获取颜色值
接下来就是在合适的地方调用这个方法,那么哪里呢,就是我们绘制颜色刻度时调用:
- for (int i = 0; i <= 100; i++) {
- if (hasDraw <= targetAngle && targetAngle != 0) {//需要绘制有色部分的时候
- //计算已经绘制的比例
- float percent = hasDraw / sweepAngle;
- int red = 255 - (int) (255 * percent);
- int green = (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(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- //点击事件中,调用动的方法
- hwv.changeAngle(200);
- }
- });
- //设置角度颜色变化监听
- hwv.setOnAngleColorListener(new HuaWeiView.OnAngleColorListener() {
- @Override
- public void colorListener(int red, int green) {
- Color color=new Color();
- //通过Color对象将RGB值转为int类型
- int backColor=color.argb(100,red,green,0);
- //父布局设置背景
- ll_parent.setBackgroundColor(backColor);
- }
- });
- }
- }
给父布局一个id,然后实例化。给我们的自定义控件设置一个角度颜色变化监听,从而拿到回调中传过来的值,然后借助Color对象将RGB值转为int值,再设置给父布局背景,这里背景稍稍透明一些。效果图:
到了这里是不是感觉炫酷了不少呢,其实功能已经实现的差不多了,接下来就是去绘制里面的内容吧
绘制文字
当然不去绘制文字也是可以的,你可以直接在布局中添加textview等。好话不多说,先分析一下绘制的过程吧,在刻度盘的内部有一个小圆,然后这些文字就在小圆内,绘制小圆只需要让它的半径小点就OK了。
- /**
- * 绘制小圆和文本的方法,小圆颜色同样渐变
- * @param canvas
- */
- private void drawScoreText(Canvas canvas) {
- //先绘制一个小圆
- Paint smallPaint = new Paint();
- smallPaint.setARGB(100,red,green,0);
- // 画小圆指定圆心坐标,半径,画笔即可
- int smallRadius=radius-60;
- canvas.drawCircle(radius, radius, radius - 60, smallPaint);
- //绘制文本
- Paint textPaint=new Paint();
- //设置文本居中对齐
- 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();
在时间任务中,每次绘制之前计算得到分数,然后在右上方画一个固定值分,再在下方一个固定内容点击优化(这个时候的坐标已经回到最初的模样)
到此为止功能已经写的差不多了,还有一个水波加速球效果,下篇博客中写吧。
本文作者:佚名
来源:51CTO