效果图
能够看到这个自己定义控件结合了颜色渐变、动态绘制刻度、动态水球效果。接下来我们就来看看这个效果是怎样一步一步实现的。
開始自己定义控件
和非常多自己定义控件方式一样须要去基础某种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()方法用来进行详细的绘制功能
如想详细了解请点击:
构造方法
onMeasure()
MeasureSpec
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()生效了
接下来就是怎样在确定一个圆形区域了
2onDraw()绘制圆形区域
绘制之前我们须要对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
到这里真不easy呀,然而发现仅仅画个圆弧没用呀,我要的是刻度线呀。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();
在时间任务中。每次绘制之前计算得到分数。然后在右上方画一个固定值分。再在下方一个固定内容点击优化(这个时候的坐标已经回到最初的模样)
到此为止功能已经写的几乎相同了。另一个水波加速球效果,下篇博客中写吧。
最后对于原理底层方面,我也有待学习,有错的地方欢迎指正,谢谢。
项目已经上传到github
github点击下载
最后的最后。个人淘宝店(抱歉。请见谅)。。霓裳雅阁