一、前言
Android提供了一个复杂且强大的组件化模型,帮我我们根据布局类View和ViewGroup来构建界面。Button、TestView、EdiText,LinearLayout、FrameLayout、RelativeLayout 等,然而在开发过程,一些系统通过的控件不能满足我们的要求,因此需要我自定义视图组件。
二、自定义视图组件的方式
- 完全自定义视图组件,继承View或者ViewGoup来完成
- 复合控件,结合现有的视图组件组合为满足我们需求的控件
- 修改现有的View类型,比如继承自ImageView,TextView,EditText,根据要求重写相应的方法来完成
自定义视图组件用上面的三种方式基本满足我们要求,其中实现过程的复杂度由我们具体的功能来定,但一些基本的流程不变,接下来通过一个绘制水杯的实例来了解自定义View的流程。
三、自定义View的流程
3.1 创建自定义视图类
public class WaterView extends View {
public WaterView(Context context) {
super(context);
}
public WaterView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public WaterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
3.2 定义自定义属性
如果需要自定义属性,需要向项目中添加 资源。这些资源通常放在res/values/attrs.xml文件中。如下所示:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="WaterView">
<attr name="color" format="color"></attr>
</declare-styleable>
</resources>
定义好属性后,接下来我们可以像内置属性一样在布局XML文件中使用他们。唯一的区别是自定义属性属于另一个命名空间。他们不属于 http://schemas.android.com/apk/res/android 命名空间,而是属于http://schemas.android.com/apk/res/[your package name]。如下所示:
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.water.view.demo.WaterView
android:id="@+id/waterView"
android:layout_width="100dp"
android:layout_height="200dp"
app:color="@color/colorPrimaryDark" />
</LinearLayout>
这里为了避免反复使用冗长的命名空间URI,我们使用xmlns指令。此指令将别名app分配给命名空间http://schemas.android.com/apk/res-auto,改别名可以为任何别名,只要同一个文件不相同即可。
3.3 获取自定义属性
通过XML创建布局视图时,XML标记的所有属性都会从资源包读取,并作为AttributeSet传递到视图的构造函数。
Android资源编译器做了大量的工作,context.getTheme().obtainStyledAttributes()获得个TypeArray数组,其中包含已解除引用并设置了样式的值。对应res目录中的各个资源,生成的R.java定义一个由属性ID组成的数组,同时定义一组常量,用于定义改数组中各属性的索引。我们可以按照下面的方法从TypeArray中读取属性值。
public WaterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WaterView, 0, 0);
try {
color = typedArray.getColor(R.styleable.WaterView_color, DEFAULT_COLOR);
} finally {
typedArray.recycle();
}
}
Note:TypedArray 对象是共享资源,必须在使用后回收。
3.4 处理布局大小
为了正确绘制自定义视图,我们需要知道他有多大。View提供多种测量方法,如果想要更精细地控制视图布局参数,需要重写onMeasure()方法,如果不需要对视图大小进行控制,只需要重写onSizeChanged()方法。
系统会在首次为您的视图分配大小时调用 onSizeChanged(),如果视图大小由于任何原因而改变,系统会再次调用该方法。可在该方法中计算位置、尺寸以及其他与视图大小相关的任何值,而不是在每次绘制时都重新计算。如下所示,我们可以在下面的方法中获取视图的高宽以及计算水位的结束Y坐标
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWidth = w;
viewHeight = h;
waterEndYpx = viewHeight - waterPx;
}
onMeasure()可以更精细的控制布局参数。该方法参数是View.MeasureSpec值,用于告诉父视图希望子视图有多大,以及该大小是硬性最大值还是建议值。在实际开发中,我们可以根据以下代码得到specMode和specSize
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
这里的specMode有三个值:
- MeasureSpec.UNSPECIFIED:The parent has not imposed any constraint
on the child. It can be whatever size it wants;父视图没有对子视图添加任何约束,子视图可以任意大小。 - MeasureSpec.EXACTLY:The parent has determined an exact size
for the child. The child is going to be given those bounds regardless
of how big it wants to be;父视图决定子视图的确切大小,子视图被限定在给定的边界里,忽略本身想要的大小。 - MeasureSpec.AT_MOST:The child can be as large as it wants up
to the specified size.;子最大视图可以达到指定的大小。
我们按照下面的代码模式,分别设置WaterView以及父视图layout_width来探讨onMeasue中获取到的specMode和specSize。这里的specMode和specSize按照下面的方式回去
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.water.view.demo.WaterView
android:id="@+id/waterView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:color="@color/colorPrimaryDark" />
</LinearLayout>
</LinearLayout>
下图只展示更改layout_width的变化,layout_height变化和layout_width类似:
父视图-layout_width | WaterView-layout_width | WaterView-specMode | WaterView-specSize |
wrap_content | wrap_content | MeasureSpec.AT_MOST | 1080px |
match_parent | MeasureSpec.AT_MOST | 1080px | |
100dp | MeasureSpec.EXACTLY | 300px | |
match_content | wrap_content | MeasureSpec.AT_MOST | 1080px |
match_parent | MeasureSpec.EXACTLY | 1080px | |
100dp | MeasureSpec.EXACTLY | 300px | |
50dp | wrap_content | MeasureSpec.AT_MOST | 150px |
match_parent | MeasureSpec.EXACTLY | 150px | |
100dp | MeasureSpec.EXACTLY | 300px |
那么在实际的开发中我们拿到specMode和specSize做什么用呢?更多是为setMeasuredDimension()使用,如果我们view的大小超过specSize。我们就可以根据specMode和specSize来进行裁剪、滚动、换行等操作,从而计算View的精确大小。
3.5 控制子View位置
对子View的位置进行控制主要是针对ViewGrop视图,如果添加了多个子View,那么我就需要功能以及测量的大小对子View的位置进行确定。这里需要重写onLayout方法,计算好子view的left、top、right、bottom后,调用view.layout()确定子view的位置。
3.6 自定义绘制
自定义视图最重要的部分是外观。绘制自定义视图可能很简单,也可能横复杂,具体取决于具体的需求。
要实现自定义视图的外观效果,我们需要重写onDraw()方法,利用Canvas进行绘制。
android.graphics 框架将绘制分为两个方面:
- 需要绘制什么,有Canvas处理。
- 如何绘制,由Paint处理。
例如,Canvas提供了drawLine()绘制线的方法,那么就需要Paint对象设置颜色,最后绘制出一条指定颜色的线条。
如果视图非常频繁地重新绘制,那么onDraw()会不停的执行,因此我们就不能再onDraw()里面做一些对象的创建,需要对象的创建提前到类的构造函数中,这样可以避免内存抖动带来的界面卡顿问题。我们可以按照以下方式进行对象的构建。
public class WaterView extends View {
private Paint paintWater;
private RectF rectF;
public WaterView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init()
}
......
private void init() {
paintWater = new Paint();
paintWater.setColor(color);
paintWater.setAntiAlias(true);
rectF = new RectF();
}
}
最后在onDraw()中完成绘制
@Override
protected void onDraw(Canvas canvas) {
rectF.set(0,viewHeight/2,viewWidth,viewHeight);
canvas.drawRect(rectF,paintWater);
}