Android 自定义View流程解析
1.简介
在开发中,View视图具有非常重要的作用,它是直接呈现给使用者的,因此向用户展示精美高效的View视图很有意义。Android系统提供了丰富的视图组件,如TextView、ImageView、Button等,还提供了RelativeLayout、LinearLayout、FrameLayout等组合组件,使用这些组件搭配能实现良好的视图效果。但是,有时候我们需要实现更加个性化和有特点的视觉效果,使用系统提供的组件就比较难满足这种需求了,此时自定义View视图便派上用场了,本文将主要分析继承View类实现自定义View视图的流程,去创建符合特定需求的自定义View视图。
2.自定义View流程
2.1 创建类并继承View
创建一个类,并继承View,本示例创建一个名为CustomView的类,需要实现其构造方法,为了在XML布局中使用自定义View的属性,至少需要提供一个参数包含Context和AttributeSet的构造方法,如下所示:
public class CustomView extends View {
public CustomView(Context context) {
this(context,null);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
}
2.2 提供自定义属性
为了像系统提供的组件那样,可以在XML布局中设置视图组件的属性,需要提供自定义View的属性设置,在res/values路径下新建一个attrs.xml文件,并在其中编辑属性名和格式,常用的格式有string:字符串,boolean:布尔值,color:颜色值, dimension:尺寸值,enum:枚举值,flags:位,float:浮点值,fraction:百分数,integer整数值,reference:引用资源ID。示例如下:
<resources>
<declare-styleable name="CustomView">
<attr name="textContent" format="string|reference" />
<attr name="textSize" format="dimension|reference" />
<attr name="textColor" format="color|reference" />
<attr name="circleColor" format="color|reference" />
</declare-styleable>
</resources>
在XML布局中使用自定义属性,需要提供命名空间,命名空间的格式如:xmlns:[别名]="http://schemas.android.com/apk/res/[pacakge name],还有一种常用的命名空间:xmlns:app="http://schemas.android.com/apk/res-auto",示例如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context=".MainActivity">
<com.android.viewdemo.CustomView
android:id="@+id/cv_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:textContent="Android"
app:textSize="50sp"
app:textColor="@color/teal_200"
app:circleColor="@color/purple_500"/>
</RelativeLayout>
在XML布局中设置属性值后,接着便是在自定义的View中获取这些属性值,调用context.obtainStyledAttributes()返回TypedArray数组,TypedArray调用相应的方法获取属性值,如调用typedArray.getString(R.styleable.CustomView_textContent)获得字符串,TypedArray对象在调用之后要调用typedArray.recycle()回收资源,示例如下:
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView, 0, 0);
try {
textContent = typedArray.getString(R.styleable.CustomView_textContent);
textSize = typedArray.getDimensionPixelSize(R.styleable.CustomView_textSize, 50);
textColor = typedArray.getColor(R.styleable.CustomView_textColor, 0);
circleColor = typedArray.getColor(R.styleable.CustomView_circleColor, 0);
} finally {
typedArray.recycle();
}
2.3 提供属性的getter和setter方法
自定义View的属性不仅可以在XML布局中设置,还应提供getter和setter方法,以便在代码中更改属性,在调用setter方法更改属性时,View的外观发生变化时需要调用invalidate()方法使当前的视图失效,进而触发onDraw()方法重绘视图,如果View的大小和形状发生了变化,则需要调用requestLayout()请求重新布局,需要注意的是invalidate()方法要在UI线程中调用,在非UI线程中调用postInvalidate(),示例如下:
public void setTextContent(String textContent) {
this.textContent = textContent;
//外观发生变化时,在UI线程中调用
invalidate();
//大小和形状发生了变化调用,非必要不调用,以提高性能
requestLayout();
}
2.4 重写onMeasure()方法
此方法主要是用来控制View的大小,让父视图知道View希望的大小,在方法内计算得出希望View显示的大小后,调用setMeasuredDimension()方法将计算出的宽高传入,示例如下;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height = 0;
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
if (modeWidth == MeasureSpec.EXACTLY) {
width = sizeWidth;
} else if (modeWidth == MeasureSpec.AT_MOST) {
width = Math.min(defaultWidth, sizeWidth);
} else {
width = defaultWidth;
}
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
if (modeHeight == MeasureSpec.EXACTLY) {
height = sizeHeight;
} else if (modeHeight == MeasureSpec.AT_MOST) {
height = Math.min(defaultHeight, sizeHeight);
} else {
height = defaultHeight;
}
setMeasuredDimension(width, height);
}
2.5 重写onSizeChanged()方法
当视图的大小发生变化时,onSizeChanged()方法会被调用,onSizeChanged()方法会携带4个参数,分别是新的宽度、新的高度、旧的宽度、旧的高度,这对正确地绘制View至关重要,绘制需要的位置和尺寸等参数需要在此方法内进行计算,示例如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
textY = (float) h / 2;
centerX = (float) w / 2;
centerY = (float) h / 2;
maxCircleRadius = (float) (w - 20) / 2;
}
2.6 初始化画笔Paint
绘制View需要用到画布Canvas和画笔Paint,Canvas负责处理绘制什么,如点、线、圆、矩形等,Paint负责处理如何绘制,如绘制的颜色、是否填充、透明度等,画布Canvas可以在重写onDraw()方法后获取,而画笔Paint则需要在初始化阶段新建一个或多个Paint对象,示例如下:
paintText = new Paint();
paintText.setAntiAlias(true);
paintText.setTextSize(textSize);
paintText.setColor(textColor);
paintText.setStyle(Paint.Style.FILL);
paintCircle = new Paint();
paintCircle.setAntiAlias(true);
paintCircle.setColor(circleColor);
paintCircle.setStyle(Paint.Style.STROKE);
paintCircle.setStrokeWidth(10);
2.7 重写onDraw()方法绘制View
绘制View是重要的一环,它将可见的界面呈现给使用者,重写onDraw()方法后,它将提供一个画布Canvas,它将和画笔Paint一起执行绘制,Canvas提供了丰富的绘制方法,如drawLine()绘制线段、drawText()绘制文本、drawPoint()绘制点、drawRect()绘制矩形等,传入计算好的参数和画笔,便可绘制出相应的图形,示例如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(textContent, 30, textY + 30, paintText);
canvas.drawCircle(centerX, centerY, circleRadius, paintCircle);
}
2.8 响应用户手势操作
View还会经常与使用者进行交互,因此还需要响应和处理用户的手势操作,一般来说,需要重写onTouchEvent(MotionEvent event),在此方法内处理手势操作,常见的手势操作有按下、滑动、抬起等,在此方法内加上业务逻辑,示例如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
此外,还可以借助GestureDetector类实现更多的手势检测,如双击、长按、滚动等。
2.9 添加动画效果
为了让自定义View更有吸引力和自然,还需要添加一些动画效果,这时候使用属性动画修改View的属性,可以产生动画效果,示例如下:
ObjectAnimator textAlpha = ObjectAnimator.ofInt(this, "textAlpha", 255, 50);
textAlpha.setDuration(2000);
textAlpha.setRepeatCount(ValueAnimator.INFINITE);
textAlpha.setRepeatMode(ValueAnimator.RESTART);
textAlpha.start();
textAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int animatedValue = (int) animation.getAnimatedValue();
setTextAlpha(animatedValue);
}
});
ObjectAnimator circle = ObjectAnimator.ofFloat(this, "circleRadius", 0.0f, maxCircleRadius);
circle.setDuration(2000);
circle.setRepeatCount(ValueAnimator.INFINITE);
circle.setRepeatMode(ValueAnimator.RESTART);
circle.start();
circle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
setCircleRadius(animatedValue);
}
});
2.10 对外提供回调接口
自定义View还应对外提供回调接口,以传递一些事件和数据,方便调用方处理相应的逻辑,常见的操作是在View内定义一些接口,在接口内部定义一些事件,并对外提供回调接口的方法,示例如下:
public interface OnCircleAnimationStartListener {
void onCircleAnimationStart();
}
public void setOnCircleAnimationStartListener(OnCircleAnimationStartListener onCircleAnimationStartListener) {
this.onCircleAnimationStartListener = onCircleAnimationStartListener;
}
cv_view.setOnCircleAnimationStartListener(new CustomView.OnCircleAnimationStartListener() {
@Override
public void onCircleAnimationStart() {
}
});
3.总结
自定义View很有实用意义,在系统组件不能实现需求时,我们可以通过自定义View来达到目的。本文分析了实现自定义View的流程,包括自定义View属性、提供属性的getter和setter方法、重写onMeasure()、重写onSizeChanged()、初始化画笔Paint、重写onDraw()、响应用户手势操作、添加动画效果、对外提供回调接口。根据实际的需要,这些环节可能不需要都实现,或者增加别的环节。