有时候我们需要一些特殊的效果或者功能,而系统控件无法满足我们的需求,这时候就需要自己定义一个控件。
自定义view流程
继承View
要自定义View首先需要需要继承View或者其子类,如果需要实现的效果比较复杂,通常需要继承View,有时候我们需要的是系统的控件再加上一些特殊的效果则可以继承View的子类(如TextView)
如果是要自己设计一种布局或者要组合其他控件,这时候就需要继承ViewGroup或者LinearLayout、FrameLayout等系统自带的布局
重写构造方法
自定义View至少需要重写两个构造方法
- 在java代码中直接创建控件所用的构造方法
public CustomView(Context context) {
this(context, null);
}
- 在xml文件中定义View所用的构造函数
public CustomView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
}
自定义xml中的属性
首先需要新建res/values/custom_view_attrs.xml,并在里面声明如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomView">
<attr name="custom_width" format="dimension" />
</declare-styleable>
</resources>
然后就可以在xml布局文件中声明了
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cn.lkllkllkl.customview.MainActivity">
<cn.lkllkllkl.customview.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:custom_width="100dp"/>
</LinearLayout>
接着我们就可以在自定义View的构造方法中将该属性的值取出使用了
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
float customWidth = 0;
TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
customWidth = typeArray.getDimension(R.styleable.CustomView_custom_width, 100);
// 需要记得回收
typeArray.recycle();
}
onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
MeasureSpec
这里要明白onMeasure的两个参数首先需要明白MeasureSpec这个类。
MeasureSpec通过将SpecMode(模式)和SpecSize(具体尺寸)打包成一个int(即widthMeasureSpec、heightMeasureSpec),前2位表示SpecMode,后30位表示尺寸。
SpecMode有三类
UNSPECIFIED: 父容器对View的大小没有限制,一般我们自定义View用的较少
EXACTLY: 在xml文件中设置具体大小为多少dp,或者设置为match_parent则SpecMode为EXACTLY,此模式表示则直接使用SpecSize作为自定义View的尺寸
AT_MOST: 对应xml文件中的wrap_content,表示设置的尺寸大小不能超过SpecSize
通常我们需要view支持wrap_content的时候才需要重写onMeasure,如果不重写则wrap_content效果和match_parent一样。一般使用如下代码就够了
@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);
// dp转换成px, 事先定义的默认宽高单位为dp
int defaultWidth = dp2px(getContext(), DEFAULT_WIDTH);
int defaultHeight = dp2px(getContext(), DEFAULT_HEIGHT);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
widthSize = Math.min(defaultWidth, widthSize);
heightSize = Math.min(defaultHeight, heightSize);
} else if (widthMode == MeasureSpec.AT_MOST) {
widthSize = Math.min(defaultWidth, widthSize);
} else if (heightMode == MeasureSpec.AT_MOST){
heightSize = Math.min(defaultHeight, heightSize);
}
setMeasuredDimension(widthSize, heightSize);
}
一般为了适配不同屏幕需要使用dp为单位,而setMeasuredDimension是使用px为单位的,所以上面的代码将我们设置的默认宽高转换成px,转换方法如下
public static int dp2px(Context context, float dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dp, context.getResources().getDisplayMetrics());
}
onLayout方法
一般自定义ViewGroup才需要重写该方法对子View进行排放
onDraw方法
通过onDraw方法我们可以将view绘制到屏幕上,如果要让view支持padding属性则需要在onDraw中做处理
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/* mPaint为成员变量,画笔Paint的对象,
* 在构造函数中进行初始化,可以设置一些在canvas上绘制的通用属性
*/
mPaint.setColor(Color.RED);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//在View所占区域绘制一条对角线
canvas.drawLine(paddingLeft, paddingTop,
getWidth() - paddingRight, getHeight() - paddingBottom, mPaint);
}
注意事项
onMeasure、onLayout、onDraw三个方法可能会对此调用,特别是onDraw方法,所以最好不要在这些方法中创建对象避免频繁分配内存造成内存抖动,或者做一些耗时操作导致跳帧
View中有提供post系列方法,所以不需要在View中使用Handler
View中若有创建线程或者动画需要及时停止,View#onDetachedFromWindow就是一个很好的时机
View中若有滑动嵌套的情形,需要处理好滑动冲突
reference
《Android开发艺术探索》