一.概述
通过本篇文章的学习,你将学会:
1.自定义View的流程
2.自定义View分类
二.自定义View的流程
自定义View主要是用来实现Android系统自带View无法实现的控件,需要我们自己自定义得到我们需要的效果。一般情况下,我们自定义View的步骤总体上是:
- 自定义属性
- 自定义构造方法
- 重写onMeasure方法测量宽高
- 重写onDraw方法进行绘制
- 重写onLayout方法负责布局的调整(继承ViewGroup用到)
- 重写其他方法诸如onTouchEvent()检测用户手指操作
自定义属性
我们通常将自定义属性定义在/values/attr.xml文件中(attr.xml文件需要自己创建)。然后创建一个属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--name为声明的"属性集合"名,可以随便取,但是最好是设置为跟我们的View一样的名称-->
<declare-styleable name="CustomeView">
<!--声明我们的属性,名称为default_size,取值类型为尺寸类型(dp,px等)-->
<attr name="default_size" format="dimension" />
</declare-styleable>
</resources>
其中format表示的是数据类型,常见的format有:
- string:字符串类型
- integer:整数类型
- dimension:尺寸,后面必须跟dp、dip、px、sp等单位
- Boolean:布尔值
- color:颜色,必须是“#”符号开头
使用:在XML布局文件中使用自定义的属性时,我们需要先定义一个namespace,Android中默认的namespace是android:
xmlns:android="http://schemas.android.com/apk/res/android"
我们自定义的属性不在这个命名空间下,因此我们需要添加一个命名空间。这个namespace可以随便取,比如:
xmlns:gyq="http://schemas.android.com/apk/res-auto"
但是注意把最后的“res/android”改成了“res-auto”,这样我们就能在XML文件中使用自定义的属性。
自定义构造方法
我们继承View或者已有View之后至少需要重写一个构造函数,我们需要重写含AttributeSet 的构造函数
CustomeView(Context context, AttributeSet attrs)去获取我们的自定义属性
private int default_size;
public CustomeView(Context context, AttributeSet attrs) {
super(context, attrs);
//第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.CustomeView);
//第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
//第二个参数为,如果没有设置这个属性,则设置的默认的值
default_size=ta.getDimensionPixelSize(R.styleable.CustomeView_default_size,100);
//最后记得将TypedArray对象回收
ta.recycle();
}
注意:在TypedArray使用结束后,需要调用recycle()方法回收它。不管有没有使用自定义属性,都会默认调用两个参数的构造方法,“使用了自定义属性就会默认调用三个参数的构造方法”的说法是错误的。
重写onMeasure方法测量宽高
onMeasure()方法中主要负责测量,决定控件本身或其子控件所占的宽高。我们可以通过onMeasure()方法提供的参数widthMeasureSpec和heightMeasureSpec来分别获取控件宽度和高度的测量模式和测量值(测量 = 测量模式 + 测量值)。widthMeasureSpec和heightMeasureSpec是int值,int占用的32bit中的前面两个bit用于表示测量模式,后面30个bit用于表示测量值。这里的的测量值并不是最终我们的View的尺寸大小,而是父View提供的参考大小。我们需要结合测量模式获取大小,测量模式有:
- EXACTLY:当宽高值设置为具体值时使用,如100dp、match_parent等,此时取出的size是精确的尺寸
- AT_MOST:当宽高值设置为wrap_content时使用,此时取出的size是控件最大可获得的尺寸
- UNSPECIFIED:当没有指定宽高值时使用(很少见)
也就是说,在EXACTLY模式,宽高就用得到的测量值,在AT_MOST模式,测量值是最大尺寸,可以通过计算childView.getMeasuredHeight()获取实际宽高。
onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int resultWidth, resultHeight;
resultWidth = default_size;//默认尺寸
resultHeight = default_size;//默认尺寸
//宽
int width = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
Log.i("lvv","width:"+width);//720
if (widthMode == MeasureSpec.EXACTLY) {
resultWidth = width;
} else if (widthMode == MeasureSpec.AT_MOST) {
resultWidth = width > default_size ? default_size : width;//取小于width的值
}
//高
int height = MeasureSpec.getSize(heightMeasureSpec);
int heighMode = MeasureSpec.getMode(heightMeasureSpec);
Log.i("lvv","height:"+height);//1048
if (heighMode == MeasureSpec.EXACTLY) {
resultHeight = height;
}else if (heighMode == MeasureSpec.AT_MOST) {
resultHeight = height > default_size ? default_size : height;//取小于height的值
}
//正方形,宽高取更小值
if (resultHeight > resultWidth) {
resultHeight = resultWidth;
} else {
resultWidth = resultHeight;
}
setMeasuredDimension(resultWidth, resultHeight);
}
布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gyq="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.tinkertest.tinkertest.view.CustomeView
android:layout_width="200dp"
android:layout_height="wrap_content"
gyq:default_size="100dp"
android:background="#f00"/>
</LinearLayout>
image.png
如果布局改为:
<com.tinkertest.tinkertest.view.CustomeView
android:layout_width="200dp"
android:layout_height="match_parent"
gyq:default_size="100dp"
android:background="#f00"/>
image.png
解释:第一种情况如果高用的是wrap_content,那么高的模式就是MeasureSpec.AT_MOST,则高会选择默认的高度100dp,这个时候的layout_width在大于100dp的时候不管是多大,正方形宽高都是100dp。第二中情况高用的是match_parent,那么高模式就是MeasureSpec.EXACTLY,是精确尺寸,使用测量高度,这个时候的layout_width决定了正方形的宽高。
重写onDraw方法进行绘制
onDraw()方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自己绘制我们的自定义控件的显示效果。在onDraw()方法中使用最多的两个类:Paint和Canvas。
Paint
用于设置如何绘制几何图形、文字和位图的样式和颜色信息,用于辅助Canvas绘制
- 文本绘制
setTextAlign(Path.Align a):设置绘制的文本的对齐方式;
setTextScaleX(float s):设置文本在X轴的缩放比例,可以实现文字的拉伸效果;
setTextSize(float s):设置字号;
setTextSkewX(float s):设置斜体文字,s是文字倾斜度;
setTypeFace(TypeFace tf):设置字体风格,包括粗体、斜体等;
setUnderlineText(boolean b):设置绘制的文本是否带有下划线效果;
setStrikeThruText(boolean b):设置绘制的文本是否带有删除线效果;
setSubpixelText(boolean b):如果设置为true则有助于文本在LCD屏幕上显示效果; - 图形绘制
setArgb(int a, int r, int g, int b):设置绘制的颜色,a表示透明度,r、g、b表示颜色值;
setAlpha(int a):设置绘制的图形的透明度;
setColor(int color):设置绘制的颜色;
setAntiAlias(boolean a):设置是否使用抗锯齿功能,抗锯齿功能会消耗较大资源,绘制图形的速度会减慢;
setDither(boolean b):设置是否使用图像抖动处理,会使图像颜色更加平滑饱满,更加清晰;
setFileterBitmap(Boolean b):设置是否在动画中滤掉Bitmap的优化,可以加快显示速度;
setMaskFilter(MaskFilter mf):设置MaskFilter来实现滤镜的效果;
setPathEffect(PathEffect pe):设置绘制的路径的效果;
setShader(Shader s):设置Shader绘制各种渐变效果;
setStyle(Paint.Style s):设置画笔的样式:FILL实心;STROKE空心;FILL_OR_STROKE同时实心与空心;
setStrokeWidth(float w):当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度;
Canvas
Canvas即画布,可以使用Paint画笔对象绘制什么东西
drawArc():绘制圆弧;
drawBitmap():绘制Bitmap图像;
drawCircle():绘制圆圈;
drawLine():绘制线条;
drawOval():绘制椭圆;
drawPath():绘制Path路径;
drawPicture():绘制Picture图片;
drawRect():绘制矩形;
drawRoundRect():绘制圆角矩形;
drawText():绘制文本;
drawVertices():绘制顶点。
使用如下的onDraw方法绘制一个绿色的圆形或者椭圆
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我们已经将宽高设置相等了
//圆心的横坐标为当前的View的左边起始位置+半径
int centerX = getLeft() + r;
//圆心的纵坐标为当前的View的顶部起始位置+半径
int centerY = getTop() + r;
Paint paint = new Paint();
paint.setColor(Color.GREEN);
//绘制圆
canvas.drawCircle(centerX, centerY, r, paint);
//绘制椭圆
// canvas.drawOval(0,0,getMeasuredWidth(),getMeasuredHeight()/2,paint);
}
效果图:
圆形
椭圆
重写onLayout方法负责布局的调整
onLayout()方法负责布局,是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。在其中常用的方法有:
- getChildCount():获取子View的数量
- getChildAt(i):获取第i个子View
- getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距
- child.getMeasuredWidth/Height():获取onMeasure()方法中测量的子View的宽度和高度值
- child.layout(l, t, r, b):设置子View布局的上下左右边的坐标
使用如下的onlayout对自定义ViewGroup的子View进行布局:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
//记录当前的高度位置
int curHeight = t;
//将子View逐个摆放
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
int height = child.getMeasuredHeight();
int width = child.getMeasuredWidth();
//摆放子View,参数分别是子View矩形区域的左、上、右、下边
child.layout(l, curHeight, l + width, curHeight + height);
curHeight += height;
}
}
child.layout()方法中可以看出是将子View进行垂直布局,实际上达到LinearLayout的效果。
重写其他方法
其他的方法比如onTouchEvent():用来监测用户手指操作。我们通过方法中MotionEvent参数对象的getAction()方法来实时获取用户的手势,有UP、DOWN和MOVE三个枚举值,分别表示用于手指抬起、按下和滑动的动作。每当用户有操作时,就会回掉onTouchEvent()方法。同时这个也往往结合GestureDetector手势动作探器类,如下面的继承ListView并实现OnTouchListener,OnGestureListener触摸和手势动作接口去实现侧滑条目出现删除按钮。
// 手势动作探测器
private GestureDetector mGestureDetector;
// 在构造函数创建手势监听器对象
mGestureDetector = new GestureDetector(getContext(), this);
// 在构造函数监听onTouch事件
setOnTouchListener(this);
// 触摸监听事件
@Override
public boolean onTouch(View v, MotionEvent event) {
if (isDeleteShown) {
hideDelete();
return false;
} else {
return mGestureDetector.onTouchEvent(event);
}
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
// 如果当前删除按钮没有显示出来,并且x方向滑动的速度大于y方向的滑动速度
if (!isDeleteShown && Math.abs(velocityX) > Math.abs(velocityY)) {
mDeleteBtn = LayoutInflater.from(getContext()).inflate(
R.layout.delete_btn, null);
mDeleteBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mItemLayout.removeView(mDeleteBtn);
mDeleteBtn = null;
isDeleteShown = false;
//对外接口去删除当前条目的数据
mOnDeleteListener.onDelete(mSelectedItem);
}
});
//添加删除按钮的布局
mItemLayout = (ViewGroup) getChildAt(mSelected - getFirstVisiblePosition());
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
params.addRule(RelativeLayout.CENTER_VERTICAL);
mItemLayout.addView(mDeleteBtn, params);
isDeleteShown = true;
}
return false;
}
效果图:
image.png
三.自定义View的分类
看到有点文章上将其分为三种,组合控件、自绘控件和继承控件。不过,通过文章第二部分的讲解,我觉得严格意义上来说就分为自绘控件和继承控件两种。
- 自绘控件的内容都是自己绘制出来的,在View的onDraw方法中完成绘制。
- 继承控件就是继承已有的控件,创建新控件,保留继承的父控件的特性,并且还可以引入新特性。
基本上自定义View就是这些内容,不过,这只是刚刚入门,以后会碰到更多的新花样可能需要我们通过自定义View去实现。
四.总结
以上就是关于自定义View的知识点,如有不足或者错误的地方请指正。不管怎样,代码不只是需要多看,更需要通过自己动手去写去熟悉才能有更深的印象,更好更全面的了解。
网友评论