利用清明假期好好的学习了一番自定义View,稍有心得,今天来记录一下。
工作中经常想要实现一些View效果,但是有没有现成的,那么久需要我们来自定义一个了。自定义View大致上有以下三种应用情况:
- 在现有的控件上,做些个性化的处理(继承自ImageView等 )
- 现有的控件不满足于我们的需求,需要自己去创造(继承自View)
- 讲几个控件组合到一起,新生成一个(继承自ViewGroup)
这里这要记录一下第二种情况。
首先要自定义一个类CircleImageProgressBar,使之继承自View,同时实现其构造方法:
public CircleImageProgressBar(Context context) {
this(context, null);//调用两个参数的构造方法
}
public CircleImageProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);//调用三个参数的构造方法
}
public CircleImageProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);//调用四个参数的构造方法
}
public CircleImageProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
在这里为了方便,我们其他的三个构造方法都使用this关键字调用到四个参数的构造方法。然后我们需要在在res/values/ 下建立一个attrs.xml , 在里面定义我们自定义View的属性和声明我们的整个样式。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleImageProgressBar">
<attr name="circleColor" format="color"/>
<attr name="radius" format="dimension"/>
<attr name="strokeWidth" format="dimension"/>
<attr name="progressColor" format="color"/>
<attr name="progressWidth" format="dimension"/>
</declare-styleable>
在这里定义了以后,就可以在布局文件中使用这些参数。接下来我们在自定义View的构造方法中做初始化:
public class CircleImageProgressBar extends View {
/** 画笔 */
private Paint circlePaint;
/** 圆的颜色 */
private float circleColor;
/** 圆的半径 */
private float radius;
/** 圆的宽度 */
private float strockWidth;
/** 进度的颜色,要与圆的颜色区分 */
private float progressColor;
/** 进度的画笔 */
private Paint progressPaint;
/** 弧线 */
private RectF rectF;
/** 当前进度 */
private float currentProgress;
//文字画笔
private Paint textPaint;
//为了密度转换,需要一个context
private Context context;
//先写构造函数,在这里统一使用四个参数的
.......
public CircleImageProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.context=context;
//初始化属性
initAttrs(context, attrs);
//创建并初始化画笔,因为Android要求尽量不要在布局(onLayout)和绘制(onDraw)阶段初始化对象,因为这些方法会被频繁调用,损耗性能。所以在此处就提前初始化了
initVariables();
}
//在attrs.xml中定义好属性后,这些就会传入到自定义View的构造方法的AttributeSet类型的参数中。
//我们可以通过context.obtainStyledAttributes()方法返回一个TypedArray对象。然后用TypedArray对象获取自定义View的属性的值。
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleImageProgressBar, 0, 0);
radius = typedArray.getDimension(R.styleable.CircleImageProgressBar_radius, 200);//获取半径,默认值为100
circleColor = typedArray.getColor(R.styleable.CircleImageProgressBar_circleColor, Color.RED);//获取圆环的颜色,默认为红色
strockWidth = typedArray.getDimension(R.styleable.CircleImageProgressBar_strokeWidth, 20);//获取圆环的宽度,默认为20
progressColor=typedArray.getColor(R.styleable.CircleImageProgressBar_progressColor,Color.RED);
typedArray.recycle();//TypedArray对象是共享的资源,所以在获取完值之后必须要调用recycle()方法来回收。
}
private void initVariables() {
circlePaint = new Paint();
circlePaint.setAntiAlias(true);//画笔去掉锯齿
circlePaint.setColor((int) circleColor);//设置为红色
circlePaint.setStyle(Paint.Style.STROKE);//设置样式,有实心,和空心之分
circlePaint.setStrokeWidth(strockWidth);//设置圆的线条宽度
rectF=new RectF();
progressPaint=new Paint();
progressPaint.setAntiAlias(true);
progressPaint.setColor((int) progressColor);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeWidth(strockWidth);
textPaint=new Paint();
textPaint.setAntiAlias(true);
textPaint.setColor((int) progressColor);
textPaint.setStyle(Paint.Style.STROKE);
textPaint.setTextSize(DensityUtils.sp2px(context, 22));
}
}
当这些初始化完成以后,我们就可以写onDraw()方法了,这个是自定义View 的一个非常重要的方法,自定义View的绘制要在这里完成:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int minimun=Math.min(getWidth()/2,getHeight()/2);//获取屏幕宽度和高度的最小值
radius=radius>=minimun?minimun:radius;//获取半径
//四个参数,分别为圆心的x,圆心的y,半径,画笔
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius-strockWidth/2, circlePaint);
//获取进度圆的矩形区域,此时getWidth才有值。
rectF.top=getHeight()/2-radius+strockWidth/2;
rectF.left=getWidth()/2-radius+strockWidth/2;
rectF.right=getWidth()/2+radius-strockWidth/2;
rectF.bottom=getHeight()/2+radius-strockWidth/2;
//动态圆的总进度
int totalProgress=100;
//本质其实是画一个圆弧型的矩形
//RectF oval:就是椭圆的矩形区域,如果我们设定一个正方向区域,那么绘制的就是一个圆的弧线,否则即为椭圆的弧线。
//float startAngle:起始的角度,0度角对应钟表的3点钟方向,所以起始位置为-90度。
//float sweepAngle:就是前进的角度
//boolean useCenter:true就是绘制扇形,false仅仅绘制弧线
//Paint paint:就是画笔了。
canvas.drawArc(rectF,-90,currentProgress/totalProgress*360,false,progressPaint);
//这里还要画一个文字,来显示进度
String text=currentProgress+"%";
float textWidth=textPaint.measureText(text,0,text.length());
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float textHeight=Math.abs(fontMetrics.ascent)-fontMetrics.descent;
canvas.drawText(currentProgress+"%",(getWidth()-textWidth)/2,(getHeight()+textHeight)/2,textPaint);
}
通过onDraw()方法我们就把需要的视图基本都画出来了,但显然不够生动,进度是死的,下面我们就通过view 的postInvalidate()方法更新进度:
在view中:
public void updateProgress(int currentProgress){
this.currentProgress=currentProgress;
postInvalidate();
}
然后在activity的onResume()方法中书写:
@Override
protected void onResume() {
super.onResume();
new Thread(new Runnable() {
@Override
public void run() {
int progress=0;
while (progress<=100){
circleImageProgressBar.updateProgress(progress);
try{
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}
}
}).start();
}
接下来再看一下自定义view的onMeasure()方法:
MeasureSpe类把测量模式和大小组合到一个32位的int型的数值中,其中高2位表示模式,低30位表示大小而在计算中使用位运算的原因是为了提高并优化效率。
- UNSPECIFIED:要多大给多大,一般不关心这个模式。
- EXACTLY:即精确值模式,当控件的layout_width属性或layout_height属性指定为具体数值时,例如android:layout_width="100dp",或者指定为match_parent属性时,系统使用的是EXACTLY 模式。
- AT_MOST:即最大值模式,当控件的layout_width属性或layout_height属性指定为warp_content时,控件大小一般随着控件的子控件或者内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。
下面我们重写onMeasure()方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height ;
if (widthMode == MeasureSpec.EXACTLY)
{
width = widthSize;
} else
{
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
float textWidth = mBounds.width();
int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
width = desired;
}
if (heightMode == MeasureSpec.EXACTLY)
{
height = heightSize;
} else
{
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
float textHeight = mBounds.height();
int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
height = desired;
}
setMeasuredDimension(width, height);
}
这样我们的自定义view就基本写完了,为了更好的体验,我还想扩展一下,就是当我们点击的时候,将进度 重置为 0 。这里有两个方法可以实现,performClick()和onTouchEvent();这两个方法都可以实现监听,但是当实现了该View的onTouchEvent触摸监听,那么无论触摸监听的返回值是什么,performClick()都无法执行了,一个解决方案是在onTouchEvent()中调用performClick()方法:
@Override
public boolean performClick() {
currentProgress=0;
postInvalidate();
return super.performClick();
}
//有触摸监听的时候performClick()会失效,所以要在触摸监听的方法中调用persormClick()方法
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction()==MotionEvent.ACTION_DOWN){
Toast.makeText(context, "按下", Toast.LENGTH_SHORT).show();
return true;
}else if (event.getAction()==MotionEvent.ACTION_MOVE){
Toast.makeText(context, "移动", Toast.LENGTH_SHORT).show();
return true;
}else if (event.getAction()==MotionEvent.ACTION_UP){
Toast.makeText(context, "松开", Toast.LENGTH_SHORT).show();
performClick();
return true;
}
return super.onTouchEvent(event);
}
欧拉,关于自定义view 的记录就写这么多吧,各位看官有什么好的建议可以告诉我,谢谢!