@TryLoveCatch
2022-05-07T10:12:55.000000Z
字数 24043
阅读 1829
Android知识体系
这篇文章,主要介绍一下自定义View和自定义ViewGroup中的onMeasure用法,下面进入正题。
我们举个例子:
一个简单的自定义View
public class ImgView extends View {
private Bitmap mBitmap;// 位图对象
public ImgView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
// 绘制位图
canvas.drawBitmap(mBitmap, 0, 0, null);
}
@Override
protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* 设置位图
*
* @param bitmap
* 位图对象
*/
public void setBitmap(Bitmap bitmap) {
this.mBitmap = bitmap;
}
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.test.views.ImgView
android:id="@+id/main_pv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</LinearLayout>
这个例子运行后,我们会发现,无论ImgView的width、height是match_parent,还是wrap_content,button和textview都不会显示在屏幕上。这个就跟view的测量机制有关系了。
@Override
protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
如果自定义一个View,onMeasure方法默认就是上面这种写法,直接将参数传给父类view,此方法中的两个参数是谁传过来的呢?答案,都是父布局传过来,这个例子中是由LinearLayout传过来的,那么LinearLayout里面的又是谁传进来的呢?这里我们就需要知道根布局这个概念了。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
setContentView是我们会在自己写的Activity里面调用的,而在Activity源码里面
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initActionBar();
}
getWindow()会返回一个继承Window的PhoneWindow(TV的话,会是TVWindow),然后执行它的setContentView(),而如下所示,PhoneWindow是在Activity的attach()中通过makeNewWindow生成的
final void attach(Context context, ActivityThread aThread,
// 此处省去一些代码……
mWindow = PolicyManager.makeNewWindow(this);
mWindow.setCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
// 此处省去巨量代码……
}
PolicyManager通过反射得到了Policy,如下
public final class PolicyManager {
private static final String POLICY_IMPL_CLASS_NAME =
"com.android.internal.policy.impl.Policy";
private static final IPolicy sPolicy;
static {
try {
Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
sPolicy = (IPolicy)policyClass.newInstance();
} catch (ClassNotFoundException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
} catch (InstantiationException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
}
}
// 省去构造方法……
public static Window makeNewWindow(Context context) {
return sPolicy.makeNewWindow(context);
}
// 省去无关代码……
}
在Policy中的makeNewWindow()直接返回了一个PhoneWindow
public Window makeNewWindow(Context context) {
return new PhoneWindow(context);
}
下面就说到了,上面提的setContentView()
public class PhoneWindow extends Window implements MenuBuilder.Callback {
// 省去代码……
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
// 省去代码……
}
这个源码,我们可以看出来,就是将我们自己传进来的layoutId,添加到mContentParent下面,而这个mContentParent是在installDecor()里面赋值的,首先会初始化成员变量DecorView类的mDecor,然后调用generateLayout(),传入DecorView,来得到mContentParent。
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
// 省省省……
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// 省省省……
}
// 省省省……
}
protected ViewGroup generateLayout(DecorView decor) {
// 省去巨量代码……
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
// 省去一些代码……
}
在generateLayout()中,会根据不同的Style类型来选择不同的布局文件,然后会add进DecorView中,然后调用findViewById()从DecorView里面得到mContentParent,这个才是真正的根布局,一般情况下,根布局都是FrameLayout来担任,我们可以用xml的最顶层viewGroup调用getParent(),返回的就是FrameLayout对象,其id是android:id="@android:id/content"。
所以,一个Activity,里面是PhoneWindow,然后是DecorView,这个DecorView继承自FrameLayout,最后,才是我们自己setContentView的布局
接下来,View就显示出来了么?我们熟知的 onMeasure、onLayout 和 onDraw都还没有调用呢?看起来,我们需要一个时机来触发 View 的操作。
这个就得说下垂直同步机制(VSYNC):
VSYNC 就是一种同步机制,以某种固定的频率进行同步,当其他组件收到这个同步信号时,就执行相应的操作。设想一下,如果没有这个同步机制,各个模块又怎能知道在哪个时候去执行自己的工作了? 这里可以初步地将 VSYNC 当做闹钟,每间隔固定时间,就响一次,其他组件听到闹铃后,就开始干活了。这个间隔的时间,与屏幕刷新频率有关,例如大多数 Android 设备的刷新频率是 60 FPS(Frame per second),一秒钟刷新60次,因而间隔时间就是 1000 / 60 = 16.667 ms。这个时间,大家是不是很熟悉了?看过太多性能优化的文章,都说每一帧的绘制时间不要超过 16 ms,其背后的原因就是这个。绘制每一帧对应的 View,这个步骤发生在 UI 线程上,所以也不要在 UI 线程上进行耗时的操作,否则就可能在 16 ms内,无法完成界面更新操作了。
长话短说,总结一下,当 Choreographer 接收到 VSYNC 信号后,ViewRootImpl 调用 scheduleTraversals 方法,通知 View 进行相应的渲染,其后 ViewRootImpl 将 View 添加或更新到 Window 上去,并会执行TraversalRunnable 的doTraversal(),会调用到 performTraversals(),这是一个非常长的方法,里面就会提到我们熟悉的Measure、Layout 和 Draw。
下面会用到MeasureSpec,我们提前介绍下
MeasureSpec 是什么,这里是利用了位运算,将一个 int 类型包含了两种信息,分别是 size 和 mode。java 的 int 类型,可以表示 32 位数字,最高的两位数字用来表示 mode,其余的部分用来表示 size。对于手机屏幕而言,size 一般都有限,不用担心需要 31 位数字来表达 size 的情况。
MeasureSpec 分别有三种 mode,分别是 UNSPECIFIED, EXACTLY 和 AT_MOST.
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
UNSPECIFIED: 标明自身对 View 大小没有任何限制,需要子 View 的信息来帮助协定
EXACTLY: View 已经确定自身的大小
AT_MOST: 父 View 已经限定了最大大小,具体 View 能不能超过这个限制,得看不同 View 的实现情况。
而对于 Size 而言,就是具体的数值大小了。Android 提供了生成 MeasureSpec 的静态方法。
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
private void performTraversals() {
// ………省略宇宙尘埃数量那么多的代码………
if (!mStopped) {
// ……省略一些代码
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// ……省省省
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// ………省略人体细胞数量那么多的代码………
}
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
从上面代码可以看出来,DecorView的大小,取决于Window的大小和WindowManager.LayoutParams,接下来看看 windowSize, rootDimension 是怎么赋值的
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
其中lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定,mWidth和mHeight表示当前窗口的大小,其值由performTraversals中一系列逻辑计算确定。
所以就是,window 的 LayoutParams 就是 MATCH_PARENT, 这样 DecorView 的大小就是 Window 的大小,也就是必定是全屏的。
接着看最开始代码中 performMeasure 的实现。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
代码终于执行到measure()了
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
}
我们可以看到measure()是final方法,因为这个measure是view的重要流程,所以不能复写,代码中调用了onMeasure(),这个方法中,一定要指定MeasureSpec,不然就不知道view的大小了。
默认的onMeasure实现
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
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;
}
getSuggestedMinimumWidth(),如果背景为空,那么我们直接返回mMinWidth最小宽度,否则,就在mMinWidth和背景最小宽度之间取一个最大值,getSuggestedMinimumHeight类同,mMinWidth和mMinHeight我没记错的话应该都是100px。
getDefaultSize()中,当模式为AT_MOST和EXACTLY时均会返回解算出的测量尺寸,也就是说,上面我们说的PhoneWindow、DecorView么从它们那里获取到的测量规格层层传递到我们的自定义View中,这也解释了文章一开始那个例子,为什么我们的View在默认情况下不管是math_parent还是warp_content都能占满父容器的剩余空间。
所以,我们可以自己重写onMeasure实现自己的测量逻辑,修改我们ImgView中的onMeasure():
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置测量尺寸
setMeasuredDimension(250, 250);
}
简单粗暴的直接设置长宽都为250px,当然,这样不好,用Android官方的话来说就是太过“专政”,因为它完全摒弃了父容器的意愿,完全由自己决定了大小。
这个时候就需要上面说的MeasureSpec了,根据它的mode来判断,父容器的意图。再次修改我们的onMeasure():
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 声明一个临时变量来存储计算出的测量值
int resultWidth = 0;
// 获取宽度测量规格中的mode
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
// 获取宽度测量规格中的size
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
/*
* 如果爹心里有数
*/
if (modeWidth == MeasureSpec.EXACTLY) {
// 那么儿子也不要让爹难做就取爹给的大小吧
resultWidth = sizeWidth;
}
/*
* 如果爹心里没数
*/
else {
// 那么儿子可要自己看看自己需要多大了
resultWidth = mBitmap.getWidth();
/*
* 如果爹给儿子的是一个限制值
*/
if (modeWidth == MeasureSpec.AT_MOST) {
// 那么儿子自己的需求就要跟爹的限制比比看谁小要谁
resultWidth = Math.min(resultWidth, sizeWidth);
}
}
// 设置测量尺寸
setMeasuredDimension(resultWidth, resultHeight);
}
我们从父容器传来的MeasureSpec中分离出了mode和size,size只是一个期望值,我们需要根据mode来计算最终的size,如果父容器对子元素没有一个确切的大小,那么我们就需要尝试去计算我们自定义View的大小,而这部分大小,更多的是由我们,也就是开发者,去根据实际情况计算的,这里我们模拟的是一个显示图片的控件,那么控件的实际大小就应该跟我们的图片一致,即使我们可以做出一定的决定,也必须要考虑父容器的限制值,当mode为AT_MOST时,size则是父容器给予我们的一个最大值,我们控件的大小就不应该超过这个值。
如上所说,控件的实际大小需要根据我们的实际需求去计算,这里我更改一下xml为我们的ImgView加一个内边距值:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.ImgView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</LinearLayout>
这时候运行,我们会发现,图片毫无内边距效果,因为我们的宽高,并没有考虑内边距的大小,所以予以修改:
resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
但是,你运行之后,会发现,左右可以了,但是上下没有边距,这个是因为我们drawBitmap的时候没有考虑边距:
@Override
protected void onDraw(Canvas canvas) {
// 绘制位图
canvas.drawBitmap(mBitmap, getPaddingLeft(), getPaddingTop(), null);
}
运行一下,完美,哈哈哈。
一句话,外边距轮不到view来算,Andorid将其封装在LayoutParams内交由父容器统一处理。
ViewGroup继承自View,没有重写onMeasure(),只是提供了几个方法供子类实现,但是在在子类里面(例如LinearLayout)都重写了onMeasure(),并调用了父类提供的这几个方法。
measureChildren
measureChildren 对所有可见 view 调用 measureChild 方法。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChildWithMargins
measureChildWithMargins要求 child 进行 Measure,在绘制的时候将自己的 padding 和 margins 考虑进去。经过 Measure 过后,可以得到对应的 LayoutParams 这个方法要求 child 必须得是 MarginLayoutParams,大部分的容器 View (LinearLayout、FrameLayout等等)都是这个 MarginLayoutParams。
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChild
measureChild 这个方法与前面这个是相对的,不同之处在于只考虑自身的 padding,不考虑 margin。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 获取子元素的布局参数
final LayoutParams lp = child.getLayoutParams();
/*
* 将父容器的测量规格已经上下和左右的边距还有子元素本身的布局参数传入getChildMeasureSpec方法计算最终测量规格
*/
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 调用子元素的measure传入计算好的测量规格
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
getChildMeasureSpec,非常重要的方法
getChildMeasureSpec 这个是上述方法都会调用的方法,分别有三个参数,父view的measureSpec, 父view 的 padding(或者加上margin), 子 view 期望的大小(不一定会实现)。 方法根据特定的规则来返回对应的 MeasureSpec,这是非常重要的一个方法。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 获取父容器的测量模式和尺寸大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 这个尺寸应该减去内边距的值
int size = Math.max(0, specSize - padding);
// 声明临时变量存值
int resultSize = 0;
int resultMode = 0;
/*
* 根据模式判断
*/
switch (specMode) {
case MeasureSpec.EXACTLY: // 父容器尺寸大小是一个确定的值
/*
* 根据子元素的布局参数判断
*/
if (childDimension >= 0) { //如果childDimension是一个具体的值
// 那么就将该值作为结果
resultSize = childDimension;
// 而这个值也是被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT
// 那么就将父容器的大小作为结果
resultSize = size;
// 因为父容器的大小是被确定的所以子元素大小也是可以被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT
// 那么就将父容器的大小作为结果
resultSize = size;
// 但是子元素的大小包裹了其内容后不能超过父容器
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST: // 父容器尺寸大小拥有一个限制值
/*
* 根据子元素的布局参数判断
*/
if (childDimension >= 0) { //如果childDimension是一个具体的值
// 那么就将该值作为结果
resultSize = childDimension;
// 而这个值也是被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT
// 那么就将父容器的大小作为结果
resultSize = size;
// 因为父容器的大小是受到限制值的限制所以子元素的大小也应该受到父容器的限制
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT
// 那么就将父容器的大小作为结果
resultSize = size;
// 但是子元素的大小包裹了其内容后不能超过父容器
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED: // 父容器尺寸大小未受限制
/*
* 根据子元素的布局参数判断
*/
if (childDimension >= 0) { //如果childDimension是一个具体的值
// 那么就将该值作为结果
resultSize = childDimension;
// 而这个值也是被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT
// 因为父容器的大小不受限制而对子元素来说也可以是任意大小所以不指定也不限制子元素的大小
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT
// 因为父容器的大小不受限制而对子元素来说也可以是任意大小所以不指定也不限制子元素的大小
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
// 返回封装后的测量规格
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面的代码,更直观点就如下图:
通过上面这几个方法,我们知道,父View会根据自身的measureSpec和子View的LayoutParams来确定子View期望的measureSpec,并传到子View的measure()
我们自定义view的时候,最外层是一个LinearLayout,我们现在来自己实现一个简单的LinearLayout。
public class CustomLayout extends ViewGroup {
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 那么对子元素进行测量
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 声明一个临时变量存储高度倍增值
int mutilHeight = 0;
// 那么遍历子元素并对其进行定位布局
for (int i = 0; i < getChildCount(); i++) {
// 获取一个子元素
View child = getChildAt(i);
// 通知子元素进行布局
child.layout(0, mutilHeight, child.getMeasuredWidth(),
child.getMeasuredHeight() + mutilHeight);
// 改变高度倍增值
mutilHeight += child.getMeasuredHeight();
}
}
}
}
<com.test.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.test.views.ImgView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="50dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</com.test.CustomLayout>
ViewGroup中的onLayout方法是一个抽象方法,这意味着你在继承时必须实现,onLayout的目的,是为了确定子元素在父容器中的位置,那么这个步骤,理应该由父容器来决定,而不是子元素。
可以看到,我们通过一个mutilHeight来存储高度倍增值,每一次子元素布局完后,将当前mutilHeight与当前子元素的高度相加,并在下一个子元素布局时,在高度上加上mutilHeight。
关于layout()的四个参数,如下图所示:
给自定义的ViewGroup增加一个padding,如下
<com.test.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="60dp"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.test.views.ImgView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="50dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</com.test.CustomLayout>
这个时候,运行,会发现padding把我们的子View给吃了,导致子View显示不全,那么,我们在对子元素进行定位时,应该进一步考虑到父容器内边距的影响。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 获取父容器内边距
int parentPaddingLeft = getPaddingLeft();
int parentPaddingTop = getPaddingTop();
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 声明一个临时变量存储高度倍增值
int mutilHeight = 0;
// 那么遍历子元素并对其进行定位布局
for (int i = 0; i < getChildCount(); i++) {
// 获取一个子元素
View child = getChildAt(i);
// 通知子元素进行布局
// 此时考虑父容器内边距的影响
child.layout(parentPaddingLeft, mutilHeight + parentPaddingTop,
child.getMeasuredWidth() + parentPaddingLeft,
child.getMeasuredHeight() + mutilHeight + parentPaddingTop);
// 改变高度倍增值
mutilHeight += child.getMeasuredHeight();
}
}
}
既然内边距已经可以了,我们继续试试外边距,给自定义ViewGroup增加margin,如下:
<com.test.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="30dp"
android:padding="60dp"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.test.views.ImgView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="50dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</com.test.CustomLayout>
我们运行,发现没什么问题,但是,如果我们给我们的子View配置外边距的时候,问题就出现了,不管margin设置多大,都不起任何效果。我们在上面也说过,子View的margin应该由父容器来处理,但是我们在CustomLayout里面没有做任何处理,所以我们需要修改我们的CustomLayout。
我们知道,Margin是封装在LayoutParams中,而我们在ViewGroup中介绍过一个方法measureChildWithMargins(),当时我们说过:
measureChildWithMargins要求 child 进行 Measure,在绘制的时候将自己的 padding 和 margins 考虑进去。经过 Measure 过后,可以得到对应的 LayoutParams 这个方法要求 child 必须得是 MarginLayoutParams,大部分的容器 View (LinearLayout、FrameLayout等等)都是这个 MarginLayoutParams。
也就是说,LayoutParams必须是MarginLayoutParams,大部分容器,LinearLayout、RelativeLayout、FrameLayout等等都继承了MarginLayoutParams,所以我们需要在CustomLayout里面也使用这个:
public class CustomLayout extends ViewGroup {
// 省略部分代码…………
public static class CustomLayoutParams extends MarginLayoutParams {
public CustomLayoutParams(MarginLayoutParams source) {
super(source);
}
public CustomLayoutParams(android.view.ViewGroup.LayoutParams source) {
super(source);
}
public CustomLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public CustomLayoutParams(int width, int height) {
super(width, height);
}
}
/**
* 生成默认的布局参数
*/
@Override
protected CustomLayoutParams generateDefaultLayoutParams() {
return new CustomLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
}
/**
* 生成布局参数
* 将布局参数包装成我们的
*/
@Override
protected android.view.ViewGroup.LayoutParams
generateLayoutParams(android.view.ViewGroup.LayoutParams p) {
return new CustomLayoutParams(p);
}
/**
* 生成布局参数
* 从属性配置中生成我们的布局参数
*/
@Override
public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomLayoutParams(getContext(), attrs);
}
/**
* 检查当前布局参数是否是我们定义的类型这在code声明布局参数时常常用到
*/
@Override
protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
return p instanceof CustomLayoutParams;
}
// 省略部分代码…………
}
下面,说一下这几个方法
generateDefaultLayoutParams
generateDefaultLayoutParams,生成默认布局参数,那么肯定是在没有设置的时候才会调用,在那里调用的呢?
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException(
"generateDefaultLayoutParams() cannot return null"
);
}
}
addView(child, index, params);
}
这个也解释了,我们平常在addview的时候,即使没有设置LayoutParams,依然可以显示,就是因为如果没有,会调用父容器的generateDefaultLayoutParams生成默认的LayoutParams。
generateLayoutParams
有两个重载方法,一个是在代码中主动new LayoutParams()时调用,一个是在xml里面设置的
接下来,修改改我们的onMeasure(),考虑margin
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 声明临时变量存储父容器的期望值
int parentDesireWidth = 0;
int parentDesireHeight = 0;
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 那么遍历子元素并对其进行测量
for (int i = 0; i < getChildCount(); i++) {
// 获取子元素
View child = getChildAt(i);
// 获取子元素的布局参数
CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();
// 测量子元素并考虑外边距
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 计算父容器的期望值
parentDesireWidth += child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin;
parentDesireHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
}
// 考虑父容器的内边距
parentDesireWidth += getPaddingLeft() + getPaddingRight();
parentDesireHeight += getPaddingTop() + getPaddingBottom();
// 尝试比较建议最小值和期望值的大小并取大值
parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());
parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());
}
// 设置最终测量值
setMeasuredDimension(
resolveSize(parentDesireWidth, widthMeasureSpec),
resolveSize(parentDesireHeight, heightMeasureSpec)
);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 获取父容器内边距
int parentPaddingLeft = getPaddingLeft();
int parentPaddingTop = getPaddingTop();
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 声明一个临时变量存储高度倍增值
int mutilHeight = 0;
// 那么遍历子元素并对其进行定位布局
for (int i = 0; i < getChildCount(); i++) {
// 获取一个子元素
View child = getChildAt(i);
CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();
// 通知子元素进行布局
// 此时考虑父容器内边距和子元素外边距的影响
child.layout(parentPaddingLeft + clp.leftMargin,
mutilHeight + parentPaddingTop + clp.topMargin,
child.getMeasuredWidth() + parentPaddingLeft + clp.leftMargin,
child.getMeasuredHeight() + mutilHeight + parentPaddingTop + clp.topMargin
);
// 改变高度倍增值
mutilHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
}
}
}
1. padding,都是自己来确定。自定义View,在onMeasure()和onDraw()来考虑padding;自定义ViewGroup,在onMeasure()(measureChild方法)和onLayout()时需要考虑使用padding。
2. margin,因为是在LayoutParams里面定义,所以都是父容器来确定。子View在自身的LayoutParams(必须为MarginLayoutParams)重设置margin,父ViewGroup在onMeasure()(measureChildWithMargins方法)和onLayout()中考虑使用margin。
3. 基于1和2,我们可以确定,子View或子ViewGroup的getMeasuredWidth()是包含自身的padding,但是不包含自身的margin的。
4. 一个Activity,里面是PhoneWindow,然后是DecorView,这个DecorView继承自FrameLayout,最后,才是我们自己setContentView的布局,window 的 LayoutParams 是 MATCH_PARENT, 这样 DecorView 的大小就是 Window 的大小,也就是必定是全屏的,然后把生成的MeasureSpec传递给我们的布局。
参考: 自定义控件其实很简单7/12
Window、PhoneWindow、DecorView和android.R.id.content
Android View 全解析(一) -- 窗口管理系统
Android View 全解析(二) -- 窗口管理系统