知识点
- view的测量
- view的布局
- view的绘制
android中的view显示方式主要就是测量出大小→决定在哪个位置→最后进行绘制
一、view的测量
view的测量是通过强大的MeasureSpec类帮助测量的,而关于该类起初我们只要了解它是一个32位的int值,其中高2位是用于标识当前view的测量模式,低30位就是用于记录view的大小。更多关于该类的知识可以查看官方文档MeasureSpec
view的测量模式有三种:
- EXACTLY : 就是当我们指定view的大小或者使用适配父控件的情况 例如:
android:layout_width="100dp" /android:layout_width="match_parent"
- AT_MOST : 该模式一般是控件适应自身内容大小,但不能超过父控件的大小 例如:
android:layout_width="wrap_content"
- UNSPECIFIED : 该模式标识不限制view的大小,要多大就多大,一般在自定义view的时候使用
在view的测量中,系统默认的大小计算方法如下(API 27):
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以看出,默认的测量方式为,如果测量模式是UNSPECIFIED ,则采用系统默认大小,其余为measureSpec中所测量的大小,根据这个测量思路我们在自定义view的时候就可以采用自己的测量方式
举个例子:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int resultWidth;
int resultHeight;
int specWMode = MeasureSpec.getMode(widthMeasureSpec);
int specWSize = MeasureSpec.getSize(widthMeasureSpec);
resultWidth=myMeasure(specWMode,specWSize, Dp2PxUtil.dip2px(mContext,200));
int specHMode = MeasureSpec.getMode(heightMeasureSpec);
int specHSize = MeasureSpec.getSize(heightMeasureSpec);
resultHeight=myMeasure(specHMode,specHSize,Dp2PxUtil.dip2px(mContext,300));
setMeasuredDimension(resultWidth,resultHeight);
}
/**
*
* @param specMode 测量模式
* @param specSize 测量大小
* @param result 在非精确测量模式中用来约束的大小
* @return
*/
private int myMeasure(int specMode,int specSize,int result){
if(specMode==MeasureSpec.EXACTLY){
result=specSize;
}else if(specMode==MeasureSpec.AT_MOST){
result=Math.min(specSize,result);
}else {
}
return result;
}
测试结果:
结果 | 备注 |
---|---|
指定大小为100dp*100dp.png | 设置自定义view的宽高为100dp*100dp |
填充父布局.png | 设置自定义view的宽高为match_parent |
自适应.png | 设置自定义view的宽高为wrap_content(实际根据我们的测量方法设置的是宽高为200dp*300dp) |
不重写.png | 如果没有重写自定义view中的onMeasure方法,并且设置宽高为wrap_content的时候,也是填充父布局,具体原因可以查看系统测量默认大小的源码 |
二、view的布局
与view布局相关主要就有两个方法:
layout(int l, int t, int r, int b) :
onLayout(boolean changed, int left, int top, int right, int bottom) :
2.1 onLayout
该方法在自定义view中一般不需用重写,其常用viewgroup通知子view进行布局Layout的时候使用
2.2 layout
view中源码如下(API 27)
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
我们不必要知道该方法的全部流程以及作用,主要看下面这行代码:
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
这行代码涉及了三个方法isLayoutModeOptical,setOpticalFrame,setFrame。isLayoutModeOptical看注释说是判断是否使用光学边界,这个我们也可以不管,主要看setOpticalFrame,setFrame这两个方法
我们先看setOpticalFrame:
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
可以看到最终return的方法也是setFrame,那么我们就往这方法里面看看
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d("View", this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
// If we are visible, force the DRAWN bit to on so that
// this invalidate will go through (at least to our parent).
// This is because someone may have invalidated this view
// before this call to setFrame came in, thereby clearing
// the DRAWN bit.
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
// parent display list may need to be recreated based on a change in the bounds
// of any child
invalidateParentCaches();
}
// Reset drawn bit to original value (invalidate turns it off)
mPrivateFlags |= drawn;
mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
这个方法我们也不用全部理解,主要看到它进行了当前位置参数和传进来的位置参数是否相等,如果不相等就进行重新绘制,看到这里,我们就知道了layout()方法如果传进去的位置和之前的位置参数不一样就会重新绘制该view,那么在viewgroup需要定位子view位置的时候,我们就可以调用每个子view的layout方法来重新给子view设置位置
三、view的绘制
当有了view的大小以及view的位置信息之后,我们就可以在屏幕上绘制该view了, view的绘制比较简单,我们可以直接重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
该方法会传递一个Canvas(画布)过来,我们只需要创建一个Paint(画笔)之类的在该画布上进行绘制就可以了
总结
view的测量,布局,绘制是基础中比较重要的,因为后续的一些复杂的特效,动画都可以由自定义view去实现,理解清楚其基本的绘制流程对后续开发会很有帮助
参考文章
《Android群英传》
网友评论