文章目录
学习框架
基础
view 绘制流程
方法 | 说明 | 相关方法 |
---|---|---|
onMeasure() | 对当前View的尺寸进行测量 | |
onDraw() | 把当前这个View绘制出来 |
View 坐标系
(图片来自:https://www.jianshu.com/p/705a6cb6bfee)
- 可以通过getWidth()和getHeight()方法用来获取View的宽度和高度
- 通过如下方法可以获取View到其父控件的距离。
getTop();获取View到其父布局顶边的距离。
getLeft();获取View到其父布局左边的距离。
getBottom();获取View到其父布局底边的距离。
getRight();获取View到其父布局右边的距离。
构造函数
- 常用的是第一个和第二个,前者在java里new的时候用到,后者在xml布局文件中自动调用。
public class TestView extends View {
/**
* 在java代码里new的时候会用到
* @param context
*/
public TestView(Context context) {
super(context);
}
/**
* 在xml布局文件中使用时自动调用
* @param context
*/
public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 不会自动调用,如果有默认style时,在第二个构造函数中调用
* @param context
* @param attrs
* @param defStyleAttr
*/
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 只有在API版本>21时才会用到
* 不会自动调用,如果有默认style时,在第二个构造函数中调用
* @param context
* @param attrs
* @param defStyleAttr
* @param defStyleRes
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
View 自定义属性
参考:https://www.jianshu.com/p/705a6cb6bfee
基本流程
- 首先我们需要在
res/values/styles.xml
文件(如果没有请自己新建)里面声明一个我们自定义的属性:
<resources>
...
<declare-styleable name="MyView">
<!--宽高比-->
<attr name="scaleWH" format="float"/>
</declare-styleable>
</resources>
- 然后在布局文件用上我们的自定义的属性
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
xmlns:hc="http://schemas.android.com/apk/res-auto"
...>
<com.labwork.video.views.MyView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
hc:scaleWH="0.75"
android:id="@+id/show_view"/>
</LinearLayout>
注意:需要在根标签(LinearLayout)里面设定命名空间,命名空间名称可以随便取,比如hc,命名空间后面取得值是固定的:"http://schemas.android.com/apk/res-auto"
- 最后就是在我们的自定义的View里面把我们自定义的属性的值取出来
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
...
attrsInit(context, attrs);
}
private void attrsInit(Context context,AttributeSet attrs) {
//第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
//即属性集合的标签,在R文件中名称为R.styleable+name
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
//第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
//第二个参数为,如果没有设置这个属性,则设置的默认的值
scaleWH = a.getFloat(R.styleable.MyView_scaleWH, 0.75f);
//最后记得将TypedArray对象回收
a.recycle();
}
复杂属性
- 枚举类型
定义:
<declare-styleable name="名称">
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>
使用:
<com.labwork.video.views.MyView
hc:scaleWH="0.75"
hc:orientation="vertical"
...>
- flag类型, 可以位运算
<declare-styleable name="名称">
<attr name="gravity">
<flag name="top" value="0x01" />
<flag name="bottom" value="0x02" />
<flag name="left" value="0x04" />
<flag name="right" value="0x08" />
<flag name="center_vertical" value="0x16" />
...
</attr>
</declare-styleable>
类似使用:
<TextView android:gravity="bottom|left"/>
- 组合属性
<declare-styleable name = "名称">
<attr name = "background" format = "reference|color" />
</declare-styleable>
类似使用:
<ImageView
android:background = "@drawable/图片ID" />
或者:
<ImageView
android:background = "#00FF00" />
onMeasure()
以下内容转载自 https://www.cnblogs.com/yishujun/p/5560838.html。
推荐阅读:https://blog.csdn.net/a396901990/article/details/36475213?utm_source=tuicool&utm_medium=referral
示意图
onMeasure 作用
(1)一般情况重写onMeasure()方法作用是为了自定义View尺寸的规则,如果你的自定义View的尺寸是根据父控件行为一致,就不需要重写onMeasure()方法
(2)如果不重写onMeasure方法,那么自定义view的尺寸默认就和父控件一样大小,当然也可以在布局文件里面写死宽高,而重写该方法可以根据自己的需求设置自定义view大小
- 原型:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
widthMeasureSpec,heightMeasureSpec 这两这参数都是父视图传给子视图的,包含宽高信息和测量模式。这两个参数都是32位,前两位代表着测量模式,后30位代表尺寸数据,三种测量模式的含义如下。
模式 | 意义 | 对应 |
---|---|---|
EXACTLY | 精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size | match_parent |
AT_MOST | 最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值 | wrap_content |
UNSPECIFIED | 无限制,View对尺寸没有任何限制,View设置为多大就应当为多大 | 一般系统内部使用 |
例:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
switch (heightMode){
case MeasureSpec.EXACTLY:
// math_parent
setMeasuredDimension(width, height);
break;
case MeasureSpec.AT_MOST:
// wrap_content
height = (int)(width/scaleWH);
setMeasuredDimension(width, height);
break;
}
}
- 我们根据父视图传给子视图的数据定制自己需要的尺寸,然后用setMeasuredDimension()方法设置子视图的尺寸。
onLayout()
onDraw()
draw流程也就是的View绘制到屏幕上的过程,
- 如果需要,绘制背景、边缘、阴影等效果。
- 有过有必要,保存当前canvas。
- 绘制View的内容。
- 绘制子View。
- 当需要重新绘制时,使用方法invalidate()或postInvalidate(),使onDraw()被自动调用从而重绘视图。invalidate()在UI线程中使用,postInvalidate()在子线程中使用。
例:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mCanvas = canvas;
width = canvas.getWidth(); height = canvas.getHeight();
logcat("width: "+canvas.getWidth()+" height: "+canvas.getHeight());
dataInit();
mCanvas.drawCircle(points[index].x,points[index].y,10,mPaint);
自定义ViewGroup
- viewGroup要考虑自身位置大小的同时,还需要考虑子view 的位置大小。
下面的案例来自博客:https://blog.csdn.net/huachao1001/article/details/51577291
- 目的:将子View按从上到下垂直顺序一个挨着一个摆放,即模仿实现LinearLayout的垂直布局。
- 首先重写onMeasure,实现测量子View大小以及设定ViewGroup的大小:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//将所有的子View进行测量,这会触发每个子View的onMeasure函数
//注意要与measureChild区分,measureChild是对单个view进行测量
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childCount = getChildCount();
if (childCount == 0) {//如果没有子View,当前ViewGroup没有存在的意义,不用占用空间
setMeasuredDimension(0, 0);
} else {
//如果宽高都是包裹内容
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//我们将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度
int height = getTotleHeight();
int width = getMaxChildWidth();
setMeasuredDimension(width, height);
} else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹内容
//宽度设置为ViewGroup自己的测量宽度,高度设置为所有子View的高度总和
setMeasuredDimension(widthSize, getTotleHeight());
} else if (widthMode == MeasureSpec.AT_MOST) {//如果只有宽度是包裹内容
//宽度设置为子View中宽度最大的值,高度设置为ViewGroup自己的测量值
setMeasuredDimension(getMaxChildWidth(), heightSize);
}
}
}
/***
* 获取子View中宽度最大的值
*/
private int getMaxChildWidth() {
int childCount = getChildCount();
int maxWidth = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getMeasuredWidth() > maxWidth)
maxWidth = childView.getMeasuredWidth();
}
return maxWidth;
}
/***
* 将所有子View的高度相加
**/
private int getTotleHeight() {
int childCount = getChildCount();
int height = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
height += childView.getMeasuredHeight();
}
return height;
}
- 然后考虑如何摆放子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;
}
}
- 这样就定义完成了,下面测试一下这个ViewGroup的效果
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.hc.studyview.MyViewGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff9900">
<Button
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="btn" />
<Button
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="btn" />
<Button
android:layout_width="50dp"
android:layout_height="wrap_content"
android:text="btn" />
</com.hc.studyview.MyViewGroup>
</LinearLayout>
自定义组合控件
和自定义View/ViewGroup基本相似,甚至我觉得本质上是一个东西。
主要区别在于,自动逸组合控件的时候一般需要编写布局文件并在自定义类中加载它,而自定义view一般直接通过属性去控制效果,不需要为一个view单独编写布局文件
另外一个主要的区别在于继承的基类不同。可能是View也可能是ViewGroup
- 首先编写布局文件,例:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/info"
android:text="0"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/folder"
android:hint="folder"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/name"
android:hint="name"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/switch_file"
android:text="switch"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
- 然后实现自定义类,主要在初始化的时候需要加载布局文件
public class FileGroupView extends LinearLayout {
private TextView info;
private EditText folderEdit;
private EditText name;
private Button switchButton;
...
public FileGroupView(Context context) {initView(); ...}
public FileGroupView(Context context, @Nullable AttributeSet attrs) {initView(); ...}
public FileGroupView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {...initView();}
private void initView(){
LayoutInflater.from(mContext).inflate(R.layout.file_group, this, true);
info = findViewById(R.id.info);
folderEdit = findViewById(R.id.folder);
name = findViewById(R.id.name);
switchButton = findViewById(R.id.switch_file);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
...
}