文章目录
前言
在开发中,View视图具有非常重要的作用,它是直接呈现给使用者的,因此向用户展示精美高效的View视图很有意义。Android系统提供了丰富的视图组件,如TextView、ImageView、Button等,还提供了RelativeLayout、LinearLayout、FrameLayout等组合组件,使用这些组件搭配能实现良好的视图效果。但是,有时候我们需要实现更加个性化和有特点的视觉效果,使用系统提供的组件就比较难满足这种需求了,此时自定义View视图便派上用场了,本文将主要分析继承View类实现自定义View视图的流程,去创建符合特定需求的自定义View视图。
基本实现流程:
1、创建自定义类并继承View,设置自定义View的属性
2、测量自定义View——重写onMeasure()
3、绘制自定义View——重写onDraw()
4、与用户进行交互——响应用户事件
5、优化自定义View
以下是具体流程
一、创建View
1、创建继承View的类
自定义的View是继承于View,当然如要自定义的View拥有某些Android已经提供的控件功能,可直接继承于已经提供的控件。
创建一个类,并继承View,本示例创建一个名为CustomView的类,
2、重写包含Context和AttributeSet参数的构造方法
重写其构造方法,为了在XML布局中通过attrs.xml中使用自定义View的属性,至少需要提供一个参数包含Context和AttributeSet的构造方法
public class CustomView extends View {
public CustomView(Context context) {
super(context, null);
init(context, null, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
}
在XML布局中使用自定义属性,需要提供命名空间,命名空间的格式如:xmlns:[别名]="schemas.android.com/apk/res/[pa… name],
一种常用的命名空间:xmlns:app="schemas.android.com/apk/res-aut…
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainActivity"
android:orientation="vertical">
<com.coolweather.uicustomviews.CustomView
android:id="@+id/custom_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
此时虽然能正常引用自定义View,但不包含任何自定义属性
3、在attrs.xml中定义自定义属性——新建attrs.xml文件
为了像系统提供的组件那样,可以在XML布局中设置视图组件的属性,自定义属性通常写在res/values/attrs.xml文件中。声明自定义属性,都属于styleable,一般styleable的name和自定义控件的类名一样。
在res/values路径下新建一个attrs.xml文件,并在其中编辑属性名和格式,常用的格式有string:字符串,boolean:布尔值,color:颜色值, dimension:尺寸值,enum:枚举值,flags:位,float:浮点值,fraction:百分数,integer整数值,reference:引用资源ID。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomView">
<attr name="customColor" format="color|reference" />
<attr name="customText" format="string|reference" />
<attr name="customSize" format="dimension|reference" />
</declare-styleable>
</resources>
4、获取attr.xml中自定义View的属性——Context的obtainStyledAttributes()方法
在attrs.xml布局中设置属性值后,接着便是在自定义的View中获取这些属性值:
- 当在xml中创建一个view时,所有在xml中声明的属性都会被传入到view的构造方法中的AttributeSet类型的参数当中。
- 通过调用Context的obtainStyledAttributes()方法返回一个TypedArray对象。然后直接用TypedArray对象获取自定义属性的值。如调用typedArray.getString(R.styleable.CustomView_textContent)获得字符串。
- 因为TypedArray对象是共享的资源,所以在调用之后要调用typedArray.recycle()回收资源
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
// 初始化画笔、颜色、文本等属性
// 获取自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.CustomView, defStyleAttr, 0);
customColor = typedArray.getColor(R.styleable.CustomView_customColor, Color.WHITE);
customText = typedArray.getString(R.styleable.CustomView_customText);
customDimension = typedArray.getDimension(R.styleable.CustomView_customSize, 30);
typedArray.recycle();
}
CustomView定义的字段如下:
private String customText;
private int customColor;
private float customSize;
private float textY;
private float centerX;
private float centerY;
private float maxCircleRadius;
5、提供自定义View属性的getter()和setter()方法
自定义View的属性不仅可以在XML布局中设置,还应提供getter和setter方法,以便在代码中更改属性。
在调用setter方法更改属性时,View的外观发生变化时需要调用invalidate()方法使当前的视图失效,进而触发onDraw()方法重绘视图,如果View的大小和形状发生了变化,则需要调用requestLayout()请求重新布局,需要注意的是invalidate()方法要在UI线程中调用,在非UI线程中调用postInvalidate()
public void setTextContent(String textContent) {
this.customText = textContent;
//外观发生变化时,在UI线程中调用
invalidate();
//大小和形状发生了变化调用,非必要不调用,以提高性能
requestLayout();
}
public String getCustomText() {
return customText;
}
二、测量自定义View——重写onMeasure()
一个View在展示时是有宽和高,测量View就是为了能够让自定义的控件能够根据各种不同的情况以合适的宽高去展示。测量就必须要提到onMeasure方法。onMeasure方法是一个view确定宽高的地方。
重写onMeasure的固定伪代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = measure(widthMeasureSpec, true);
int measureHeight = measure(heightMeasureSpec, false);
setMeasuredDimension(measureWidth, measureHeight);
}
计算出height和width之后在onMeasure中要调用setMeasuredDimension()方法,否则会出现运行时异常。
通常,可以使用MeasureSpec
类的getSize()
和getMode()
方法来获取宽度和高度的建议值以及测量模式。,调用setMeasuredDimension()方法将计算出的宽高传入,示例如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int measuredWidth = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
int measuredHeight = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
switch (widthMode) {
case MeasureSpec.EXACTLY:
measuredWidth = widthSize;
break;
case MeasureSpec.AT_MOST:
// 计算宽度的最大值,通常基于内容和内边距
measuredWidth = Math.min(widthMeasureSpec, widthSize);
break;
case MeasureSpec.UNSPECIFIED:
// 宽度可以是任意值,通常基于内容和内边距
measuredWidth = widthMeasureSpec;
break;
}
switch (heightMode) {
case MeasureSpec.EXACTLY:
measuredHeight = heightSize;
break;
case MeasureSpec.AT_MOST:
// 计算高度的最大值,通常基于内容和内边距
measuredHeight = Math.min(heightMeasureSpec, heightSize);
break;
case MeasureSpec.UNSPECIFIED:
// 高度可以是任意值,通常基于内容和内边距
measuredHeight = heightMeasureSpec;
break;
}
setMeasuredDimension(measuredWidth, measuredHeight);
}
三、绘制自定义View——重写onDraw()
-
自定义控件被创建并且测量代码写好后,接下来就调用onDraw()来绘制View
- onDraw方法包含一个Canvas叫做画布的参数,onDraw()简单来说就两点:Canvas决定要去画什么;Paint决定怎么画
- Canvas提供了画线方法,Paint就来决定线的颜色。Canvas提供了画矩形,Paint又可以决定让矩形是空心还是实心。
-
在onDraw方法中开始绘制之前
- 画笔Paint对象信息要初始化完毕。因为View的重新绘制是比较频繁的,可能多次调用onDraw,所以初始化的代码不应该放在onDraw方法里。(重要)
-
Paint画笔
- 在绘图过程中起到了极其重要的作用,画笔主要保存颜色,样式等绘制信息,指定如何绘制文本和图形,画笔对象有很多设置方法,大体上可以分为两类,一类与图形绘制相关,一类与文本绘制相关。
-
Canvas画布
- 当调整好画笔之后,需要绘制到画布上,这就得用Canvas类。Canvas画布可以绘制任何东西。还需要设置一些关于画布的属性,比如,画布的颜色、尺寸等。
-
常见绘制操作有哪些
- drawRect(),drawRoundRect():绘制矩形。Rect的参数为int类型,而RectF的参数类型为float类型,从这一点上来看,RectF的精度更高一些,但是他们都是通过四个坐标参数来确定一个矩形的区域
- drawOval(),drawCircle(),drawArc():绘制椭圆,圆,以及圆弧
- drawText():绘制文本
- drawLine():绘制线段
- drawPoint():绘制点
- drawBitmap():绘制图片
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();// 此处Paint应在初始化阶段创建
paint.setColor(customColor);
paint.setTextSize(customSize);
canvas.drawText(customText, 100, textY + 100, paint);
}
四、与用户进行交互——响应用户点击事件
某些情况自定义控件不仅只是展示漂亮的样式,还需要支持用户点击,拖动操作。自定义控件就需要做用户交互这一步。
1、响应用户手势操作——重写onTouchEvent()
View还会经常与使用者进行交互,因此还需要响应和处理用户的手势操作,一般来说,需要重写onTouchEvent(android.view.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、对外提供回调接口
自定义View还应对外提供回调接口,以传递一些事件和数据,方便调用方处理相应的逻辑,常见的操作是在View内定义一些接口,在接口内部定义一些事件,并对外提供回调接口的方法,示例如下:
private OnCustomViewClickListener onCustomViewClickListener;
public interface OnCustomViewClickListener {
void onCustomViewClick();
}
public void setOnCustomViewClickListener(OnCustomViewClickListener onCustomViewClickListener) {
this.onCustomViewClickListener = onCustomViewClickListener;
}
3、添加用户点击事件响应事件逻辑
在onTouchEvent()中添加对应动作后执行的方法
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
onCustomViewClickListener.onCustomViewClick();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
4、Activity中实际测试
CustomView customView = (CustomView) findViewById(R.id.custom_view);
customView.setOnCustomViewClickListener(new CustomView.OnCustomViewClickListener() {
@Override
public void onCustomViewClick() {
Toast.makeText(MainActivity.this, "clicked the view", Toast.LENGTH_SHORT).show();
}
});
五、优化自定义View
在上述步骤结束之后,其实一个较为完善的自定义控件已经出来。接下来需要确保自定义控件运行得流畅,官方说法是:为了避免控件体验迟缓,确保动画尽可能保持每秒60帧效果。
官网给出的优化建议:
1、避免不必要的代码
2、在onDraw()方法中不应该有会导致垃圾回收的代码
3、尽可能少让onDraw()方法调用,大多数onDraw()方法调用都伴随调用invalidate(),所以不是必须,不要调用invalidate()方法。
(可选)当视图的大小发生变化——重写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;
}
(可选)添加动画效果(高阶用法)
为了让自定义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);
}
});
总结
自定义View很有实用意义,在系统组件不能实现需求时,我们可以通过自定义View来达到目的。本文分析了实现自定义View的流程,包括自定义View属性、提供属性的getter和setter方法、重写onMeasure()、重写onSizeChanged()、初始化画笔Paint、重写onDraw()、响应用户手势操作、添加动画效果、对外提供回调接口。根据实际的需要,这些环节可能不需要都实现,或者增加别的环节。