本文介绍了 Android 自定义 View 的基本步骤
LayoutInflater分析
功能:加载布局的
Activity中调用setContentView()方法也是调用LayoutInflater()完成加载布局的
使用步骤
-获取LayoutInflater的实例
法1:
LayoutInflater layoutInflater = LayoutInflater.from(context);
法2:
LayoutInflater layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-使用inflate()方法加载布局
layoutInflater.inflate(resourceId, root);
参数1:需要加载的布局Id
参数2:是指给该布局的外部再嵌套一层父布局,如果不需要就直接传null。这样就成功成功创建了一个布局的实例,之后再将它添加到指定的位置就可以显示出来了。
举例
定义一个button_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="button">
</Button>
MainActivity中
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LayoutInflater layoutInflater = LayoutInflater.from(this);
mainLayout = (LinearLayout) findViewById(R.id.activity_main);
View buttonLayout = layoutInflater.inflate(R.layout.bytton_layout, null);
mainLayout.addView(buttonLayout);
}
此时即可将button_layout.xml
加载进来
测试若想调整button_layout.xml
的大小,修改android:layout_width
是没有效果的,必须将button_layout.xml 放入一个layout中,跳转layout的大小来。
而setContentView()方法中的子控件可以调整大小是因为,Android会自动在布局文件的最外层再嵌套一个FrameLayout,所以layout_width和layout_height属性才会有效果。
分析源码
View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot):pull解析xml
->View temp = createViewFromTag(name, attrs); 根据节点名来创建View对象的
->createView() 创建view实例并且返回
->rInflate()循环遍历这个根布局下的子元素
-> rInflate()递归调用,查找这个View下的子元素,每次递归完成后则将这个View添加到父布局当中
View的绘制过程
onMeasure()
功能:测量视图的大小的
View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示:
-EXACTLY
表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
-AT_MOST
表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
-UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
widthMeasureSpec和heightMeasureSpec两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小
measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架的。然后在第9行调用了onMeasure()方法,这里才是真正去测量并设置View大小的地方,默认会调用getDefaultSize()方法来获取视图的大小,
onMeasure()方法是可以重写的,也就是说,如果你不想使用系统默认的测量方式,可以按照自己的意愿进行定制,比如:
public class MyView extends View {
......
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(200, 200);
}
}
这样的话就把View默认的测量流程覆盖掉了,不管在布局文件中定义MyView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200。
需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。
由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。
onLayout()
ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程
View中的onLayout()方法就是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。
onDraw()
ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作。draw()方法内部的绘制过程总共可以分为六步,其中第二步和第五步在一般情况下很少用到,因此这里我们只分析简化后的绘制过程。代码如下所示:
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
}
视图重绘
调用视图的setVisibility()、setEnabled()、setSelected()等方法时都会导致视图重绘,而如果我们想要手动地强制让视图进行重绘,可以调用invalidate()方法来实现
-> invalidate()
->skipInvalidate()判断是否许重绘
-> invalidateChild()
-> invalidateChildInParent()
-> invalidateChild()
-> scheduleTraversals()
-> sendEmptyMessage(DO_TRAVERSAL);
handler 接受
- > performTraversals() [onmeasure的入口]
不断地获取当前布局的父布局,并调用它的invalidateChildInParent()方法,当循环到最外层的根布局后,就会调用ViewRoot的invalidateChildInParent()方法了,仅仅是去调用了invalidateChild()方法而