Android_开发Day29自己绘制控件
目的:
在Android中很多时候系统的控件是不能满足需要的,组合方式定义控件又非常繁琐,因此此时需要自己画一个控件,才能满足需要
技术:
<1> 绘制控件时的步骤:
1.创建一个类并找一个类来继承
2.重写里面的三个构造方法
3.在onDraw(Canvas canvas)方法里绘制你的控件
<2> onDraw:
onDraw方法是系统调用的方法,在界面被创建之前改方法被系统调用,然后在里面用户可以自定义要画的东西,然后再由系统统一渲染到屏幕上,方法里面的一个参数canvas是系统提供的画布,在该画布上画控件,什么时候需要画控件,就是在系统的控件没法满足你要的控件的形状,特征和功能时就需要自己画控件,你画好的控件将由GPU渲染到屏幕上显示出来。缺点:如果频繁绘制那么占用内存可能会吃紧,因此能够使用系统的就不要自己画,原则。系统提供了一些基本的形状,下面看一看canvas里的一些基本方法:
方法 | 用法 |
---|---|
drawLine() | 参数是其实点坐标和终止点坐标最后加上一支画笔 |
drawCircle() | 参数是圆点的坐标和半径和一支画笔 |
drawText() | 参数是一个文本加要显示文本的x于y和一支画笔 |
drawBitmap() | 参数是一张bitmap和一只画笔 |
drawColor() | 参数就是一个颜色 |
drawOval() | 画一个椭圆,参数是左右上下的距离和一支画笔 |
drawArc() | 画一个扇形,参数是上下左右起始角度和终止角度和是否使用中心,最后还有一支画笔 |
<3> onMeasure方法:
加载控件时系统会调用此方法,其中的参数widthMeasureSpec是代表父控件预测的该控件的最大宽度,相对应的heightMeasureSpec是代表父控件预测的该控件的最大高度,一般在自己需要自定义控件的大小时可以重写该方法,如果不需要那父视图说你这个控件是多大那就是多大,调用关系如图:
<4> onSizeChanged方法:
控件测量完毕,或者控件尺寸改变了都会调用此方法,该方法一般作为一个过度,许多要控件定型后才能使用的代码可以在这里写。没 什么好讲的。
<5> 改变自定义控件的某个属性并及时展现出来的方法:
方法有多种,这里展示三种:
第一种:简单粗暴,点一下改变一下刷新一下
方法:1.讲要改变的属性弄成一个成员变量,并设置set,get方法
2.在onTouchEvent方法里面去set该属性
3.调用invalidate()方法,刷新并重绘控件
第二种:用一个子线程来每隔一定的时间改变一次,推荐使用Timer
方法:1.讲要改变的属性弄成一个成员变量,并设置set,get方法
2.new 一个Timer,调用schedule方法,参数传一个TimerTask接口对象,实现里面的run方法,run方法的内容就是set该属
性,第二个参数是多少秒后执行该run方法,传0,第三个参数是要隔多少秒后再次执行该方法,这个可以根据实际需要来
定。
3.调用invalidate()方法,刷新并重绘控件
缺点:上面的方法很多情况下会出错,因为我们在子线程里面去改变UI线程(主线程)的东西,系统很多时候不认账的,小弟怎么能动大哥的动西呢,但也不排除有些时候是可以的,具体是什么时候不清楚,可能和你写的代码的实际情况有关,可以自行百度,当然如果系统没报错,那你就尽情的用吧。若要改进可以用一个Handler,每当你改变完后通知主线程刷新就行了,小弟动不得大哥的东西,我通知你,让你自己动总行了吧。Handler用法可以参考以下代码:
//用handler来接收消息
final Handler handler = new Handler(){
public void handleMessage(Message msg) {
headPicture.invalidate();
}
};
//显示剩余时间
new Timer().schedule(new TimerTask() {
@Override
public void run() {
headPicture.setAchieve(getDATE(startDate));
handler.sendEmptyMessage(1);
}
},0, 1000);
第三种:还记得前面说Android中的动画时讲了几种动画,其中有一个ValueAnimator没用到么,因为它只能得到动画过程中的每一个中间值,却不会去做动画,说到这里是不是就发现什么了,我只要得到动画过程中的每一个值,然后手动set一下那个值,然后刷新一下不就行了吗,没错就是这样就可以做自定义控件的动画了。
方法:1.创建一个ValueAnimator的对象,然后ValueAnimator.ofInt/ValueAnimator.ofFloat你要的值是什么类型的就of什么,然后参
数传开始时的值和结束时的值就行,当然你要传中间值也没问题,从左往右依次传就行了。
2.添加监听器,调用addUpdateListener(),参数new一个ValueAnimator.AnimatorUpdateListener()接口,实现里面的
onAnimationUpdate方法,该方法会传一个ValueAnimator的对象过来,传过来时调用里面的getAnimatedValue()就得到了
这一时刻对应得值,赋值给要改变得属性即可,当然别忘刷新哦
下面是演示代码:
ValueAnimator va = ValueAnimator.ofInt(0, 360);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//改变属性值
outer = valueAnimator.getAnimatedValue()
//刷新
invalidate();
}
});
va.start();
<6> 画笔Paint的设置:
画笔Paint的设置来看一下里面的一些常用的方法:
方法 | 描述 |
---|---|
setStrokeWidth | 设置画笔的粗细,参数是一个整型变量 |
setColor | 设置画笔的颜色,参数就是颜色值 |
setTextSize | 设置文字的大小,在画文字的时候生效 |
setStyle | 设置画笔的样式,是实心还是空心,参数是:STROKE(空心),FILL(实心) |
setAntiAlias | 设置画笔是否抗锯齿,true还是false |
setAlpha | 设置alpha值,也就是透明度 |
setStrokeCap | 设置画笔的落笔的笔锋 |
<7> 文字的画法以及位置的确定:
画文字可以使用canvas里面的方法drawText(),该方法的参数是文字的文本以及文字的x和y,和一只画笔,其中画笔决定了文字大小,x和y决定了文字的位置,文本决定文字内容,但是系统是怎么确定文字的位置的,就是用到了文字的基准线,如图:
图中的基准线是紧贴文字底部的那条线,有点像英语书写时的基准线,所有字母都写在该线的上面,这样就能很好的对齐。
同时还有一些距离如上图中的,Ascent就是文字的整个高度,当然如果要得到文字的宽度就可以调用paint里面的measureText()方法来计算文字的宽度,返回值即宽度,参数就是你的文字字符串,如果要得到Ascent需要先创建一个FontMetricsInt对象,才能得到,因此计算文字的坐标时算的其实是基准线的坐标。
<8> 贝塞尔曲线的使用:
要画贝塞尔曲线用canvas已经不行了,因为canvas只提供了一些基础图形的方法,复杂的图案还需要用到Path,所谓的Path也就是一个路径,其实你画的任何图形都是一个路径,只要我们跟着这个路径画就能画出图形,Path的基本方法:
方法 | 用法 |
---|---|
moveTo | 参数是x,y就是你落笔的点在哪里,默认0,0 |
lineTo | 连线的终点,参数也是x,y |
cubicTo | 画贝塞尔曲线,参数是三个点,起点终点和拉伸点 |
quadTo | 也是画贝塞尔曲线,参数相比上一个少一个点,也就是起点,可以通过moveTo方法来设定 |
rXXXXTo | 也就是上面的方法名前加一个r就代表坐标的计算方式换成了相对坐标,相对于上一次的坐标的相对坐标 |
如何将Path画出来,可以通过canvas里面的drawPath()方法来画出路径,参数是Path和Paint
<9> ViewGroup:
ViewGroup是一个抽象类,继承于它时必须要实现里面的方法onLayout(),该方法是布局的时候必要的方法,系统要得到ViewGroup的大小时会先去测量子控件的大小会调用onMeasure()方法,然后再调用onLayout()弄清楚子控件的布局。onLayout方法有四个参数,从左往右依次是:left,top,right,bottom,见名知其意就是上下左右的距离。
技术如何使用:
做一个水波流动效果的进图条,水波流动效果可以通过贝塞尔曲线来做,进度值的改变可以用上面的三种方法中的一种来做,文字的提示百分比需要自己手动画上去,然后外边用一个⚪来将整个控件包起来,形成一个整体。参考代码如下:
类⭕和文字:
package com.example.waveloadingbyself;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
public class CircleView extends View {
Paint circlePaint;
Paint textPaint;
//-----------------------------------------------------------------//
private int circleColor = Color.BLACK;
private int circleWidth = 10;
private int textColor = Color.BLACK;
private int textSize = 50;
private int centerYSpace;
//-----------------------------------------------------------------//
private double progress;
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
circlePaint.setColor(circleColor);
circlePaint.setStrokeWidth(circleWidth);
circlePaint.setStyle(Paint.Style.STROKE);
//-----------------------------------------------------------------//
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(textColor);
textPaint.setTextSize(textSize);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//确定半径
int radius = Math.min(getWidth(), getHeight()/2 - circleWidth);
//画⚪
canvas.drawCircle(getPivotX(), getPivotY(), radius, circlePaint);
//-----------------------------------------------------------------//
//计算文本宽度
System.out.println("CircleProgress:--"+progress);
String text = (int) (progress*100)+"%";
System.out.println("text:--"+text);
int width = (int) textPaint.measureText(text);
//获取文字的fontMetrics
Paint.FontMetricsInt fm = textPaint.getFontMetricsInt();
//画文字
canvas.drawText(text, getPivotX()-width/2, getPivotY()+(-fm.ascent)/2+centerYSpace, textPaint);
}
public void setCircleColor(int circleColor) {
this.circleColor = circleColor;
circlePaint.setColor(circleColor);
}
public void setCircleWidth(int circleWidth) {
this.circleWidth = circleWidth;
circlePaint.setStrokeWidth(circleWidth);
}
public void setTextColor(int textColor) {
this.textColor = textColor;
textPaint.setColor(textColor);
}
public void setTextSize(int textSize) {
this.textSize = textSize;
textPaint.setTextSize(textSize);
}
public double getProgress() {
return progress;
}
public void setProgress(double progress) {
this.progress = progress;
//System.out.println(progress);
invalidate();
}
public void setCenterYSpace(int centerYSpace) {
this.centerYSpace = centerYSpace;
}
}
类波浪:
package com.example.waveloadingbyself;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;
public class WaveView extends View {
private Paint paint;
private Path path;
private ValueAnimator va;
float density = getResources().getDisplayMetrics().density;
private int waveLength = (int) (100*density);
private int waveCrest = (int) (50*density);
private int speed;
private int lineColor = Color.BLACK;//线条颜色
private int lineSize = 10;//线条粗细
private int centerYSpace;
public WaveView(Context context) {
super(context);
init();
}
public WaveView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(lineColor);
paint.setStrokeWidth(lineSize);
paint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
startWave();//开始动画
}
public void startWave(){
va = ValueAnimator.ofInt(0, waveLength);
va.setDuration(500);
va.setRepeatCount(ValueAnimator.INFINITE);
va.setRepeatMode(ValueAnimator.RESTART);
va.setInterpolator(new LinearInterpolator());
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
speed = (int) valueAnimator.getAnimatedValue();
invalidate();//刷新
}
});
va.start();
}
public void stopWave(){
if (va != null) {
va.cancel();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path = new Path();
//计算周期,完整的波
int count = getWidth() / waveLength;
path.moveTo(-waveLength, getHeight() / 2);
//获取中心线
int centerY = (int) getPivotY();
//确定曲线的路径
for (int start = -waveLength+speed; start < getWidth(); start += waveLength) {
path.cubicTo(start, centerY+centerYSpace, start+waveLength/4, centerY-waveCrest+centerYSpace, start+waveLength/2, centerY+centerYSpace);
path.cubicTo(start+waveLength/2, centerY+centerYSpace, start+waveLength*3/4, centerY+waveCrest+centerYSpace, start+waveLength, centerY+centerYSpace);
}
canvas.drawPath(path, paint);
}
/**
* 父容器会按照自己的规则给出一个方案
* 子View通过MeasureSpec.getMode .getSize获取相应的值
* getMode:
* Unspecified 无限制的 父容器没有对控件进行约束 肌肤不可能
* At_Most 不能超过最大值 一般就是包裹内容
* Exactly 确定的值 (200dp)
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
switch (mode){
case MeasureSpec.UNSPECIFIED:
System.out.println("UNSPECIFIED:"+width);
break;
case MeasureSpec.AT_MOST:
System.out.println("AT_MOST:"+width);
break;
default:
System.out.println("EXACTLY:"+width);
break;
}
}
public void setWaveLength(int waveLength) {
this.waveLength = waveLength;
}
public void setWaveCrest(int waveCrest) {
this.waveCrest = waveCrest;
}
public void setLineColor(int lineColor) {
this.lineColor = lineColor;
paint.setColor(lineColor);
}
public void setLineSize(int lineSize) {
this.lineSize = lineSize;
paint.setStrokeWidth(lineSize);
}
public void setCenterYSpace(int centerYSpace) {
this.centerYSpace = centerYSpace;
}
}
组合类上述控件类:
package com.example.waveloadingbyself;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.ViewGroup;
public class WaveLodging extends ViewGroup {
private double process;
public WaveLodging(Context context) {
super(context);
}
public WaveLodging(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
WaveView waveView = new WaveView(getContext());
waveView.layout(0, 0, getWidth(), getHeight());
waveView.setLineSize(20);
waveView.setLineColor(Color.BLUE);
waveView.setCenterYSpace((int) (getHeight() / 2 - process *getHeight()));
addView(waveView);
CircleView circleView = new CircleView(getContext());
circleView.setCircleColor(Color.RED);
circleView.setCircleWidth(30);
circleView.layout(0, 0, getWidth(), getHeight());
circleView.setProgress(process);
addView(circleView);
CircleView circleView1 = new CircleView(getContext());
circleView1.setCircleWidth(180);
circleView1.setCircleColor(0xFFF8F8F9);
circleView1.setProgress(process);
circleView1.layout(-250, -250, getWidth()+250, getHeight()+250);
addView(circleView1);
}
public void setProcess(double process) {
this.process = process;
}
}
MainActivity代码:
package com.example.waveloadingbyself;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WaveLodging waveLodging = findViewById(R.id.lView);
waveLodging.setProcess(0.33);
}
}
xml里面的代码:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.waveloadingbyself.WaveLodging
android:id="@+id/lView"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_centerInParent="true" />
</RelativeLayout>
实际使用效果:
总结:
自定义控件的方式,最好的肯定是自己画的,但效率最高的还是系统的,因此当系统控件能用时一定要用系统的,不能用时才要自己定义,因此那些自己定义的好的控件可以封装起来保存好了,以便以后要用的时候随时都能拿出来用。