Android自定义View视图详解
Android自定义View - 一
前言
有多少小伙伴和我一样,一听到自定义View就感到头大的?看到View相关类个个都几万行代码,直接就放弃了?是否还在强行和产品解释为什么IOS能做而Andorid不能做?可见自定义View几乎是我们Android进阶的必经之路,也是Android开发的重中之重。
想不想以后让产品质问IOS问什么Andoird可以实现而他不可以?那我们就一起来学习,一层一层的揭开自定义View的神秘面纱!
View绘制流程
关于这一点,我相信大家早已滚瓜烂熟,不就是onMeasure() -> onLayout() -> onDraw()三部曲吗。可是他们之间是怎样一种联系呢,内部是如何调用的?
为了方便大家理解,我们先从view的requestLayout()方法入手。同学们都知道,当view调用了requestLayout(),就会重新测量和布局,那么它们具体的调用流程是什么样的呢?
viewGroup.addView()
当从ViewGroup添加一个view时,就会调用requestLayout()
public void addView(View child, int index, LayoutParams params) { if (DBG) { System.out.println(this + " addView"); } if (child == null) { throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); } // addViewInner() will call child.requestLayout() when setting the new LayoutParams // therefore, we call requestLayout() on ourselves before, so that the child's request // will be blocked at our level requestLayout(); invalidate(true); addViewInner(child, index, params, false); }
requestLayout()
来看一下View的requestLayout()方法:
public void requestLayout() { if (mMeasureCache != null) mMeasureCache.clear(); if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) { // Only trigger request-during-layout logic if this is the view requesting it, // not the views in its parent hierarchy ViewRootImpl viewRoot = getViewRootImpl(); if (viewRoot != null && viewRoot.isInLayout()) { if (!viewRoot.requestLayoutDuringLayout(this)) { return; } } mAttachInfo.mViewRequestingLayout = this; } mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; if (mParent != null && !mParent.isLayoutRequested()) { mParent.requestLayout(); } if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) { mAttachInfo.mViewRequestingLayout = null; } }
我们省略其它不重要的代码,看到倒数第6行的mParent.requestLayout();
。
mParent是谁呢?
public interface ViewParent { /**
* Called when something has changed which has invalidated the layout of a
* child of this view parent. This will schedule a layout pass of the view
* tree.
*/ public void requestLayout(); }
ViewParent有一个实现类:ViewRootImpl.java, 为什么就确定是这个实现类呢?同学不要着急,下一篇我们研究Activity中布局加载流程时就会知道啦! 我们继续:
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
追踪到scheduleTraversals
方法,里面有一个mTraversalRunnable,没错,就是通过这个任务来进行一系列神操作了!
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } } final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
在mTraversalRunnable
任务中,调用了doTraversal()方法:
void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } }
onMeasure
performTraversals()
,终于走到了重点,这个方法上千行代码,我们去其糟粕:
if (!mStopped || mReportNextDraw) { boolean focusChangedDueToTouchMode = ensureTouchModeLocally( (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0); if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || contentInsetsChanged || updatedConfiguration) { int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed! mWidth=" + mWidth + " measuredWidth=" + host.getMeasuredWidth() + " mHeight=" + mHeight + " measuredHeight=" + host.getMeasuredHeight() + " coveredInsetsChanged=" + contentInsetsChanged); // Ask host how big it wants to be performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // Implementation of weights from WindowManager.LayoutParams // We just grow the dimensions as needed and re-measure if // needs be int width = host.getMeasuredWidth(); int height = host.getMeasuredHeight(); boolean measureAgain = false; if (lp.horizontalWeight > 0.0f) { width += (int) ((mWidth - width) * lp.horizontalWeight); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); measureAgain = true; } if (lp.verticalWeight > 0.0f) { height += (int) ((mHeight - height) * lp.verticalWeight); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); measureAgain = true; } if (measureAgain) { if (DEBUG_LAYOUT) Log.v(mTag, "And hey let's measure once more: width=" + width + " height=" + height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } layoutRequested = true; } }
请小伙伴们仔细观察。**首先测量出了宽高,调用了一次performMeasure()
方法,然后判断了lp.horizontalWeight
和lp.verticalWeight
,如果大于0,那么就会再次调用;也就是说,LinearLayout如果设置了weight属性,那么就会调用两次这个方法。**这个方法不用多说了吧,最终会调用view的measure:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { if (mView == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }
补充一点课外小知识:有的面试官会问,RelativeLayout和LinearLayout怎么选择呢?
如果LinearLayout没有用到weiget属性,那么就使用LinearLayout, 否则使用RelativeLayout。
因为RelativeLayout可能会有一次绘制不完美,从而绘制两次的可能;
而LinearLayout如果没有weiget,那么只会绘制一次,但如果加了weiget,那么肯定会绘制两次!
onLayout
继续往下看
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw); boolean triggerGlobalLayoutListener = didLayout || mAttachInfo.mRecomputeGlobalAttributes; if (didLayout) { performLayout(lp, mWidth, mHeight); // 省略无关代码 }
这里面调用了performLayout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { mLayoutRequested = false; mScrollMayChange = true; mInLayout = true; final View host = mView; if (host == null) { return; } if (DEBUG_ORIENTATION || DEBUG_LAYOUT) { Log.v(mTag, "Laying out " + host + " to (" + host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")"); } Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout"); try { host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); mInLayout = false; int numViewsRequestingLayout = mLayoutRequesters.size(); if (numViewsRequestingLayout > 0) { // requestLayout() was called during layout. // If no layout-request flags are set on the requesting views, there is no problem. // If some requests are still pending, then we need to clear those flags and do // a full request/measure/layout pass to handle this situation. ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, false); if (validLayoutRequesters != null) { // Set this flag to indicate that any further requests are happening during // the second pass, which may result in posting those requests to the next // frame instead mHandlingLayoutInLayoutRequest = true; // Process fresh layout requests, then measure and layout int numValidRequests = validLayoutRequesters.size(); for (int i = 0; i < numValidRequests; ++i) { final View view = validLayoutRequesters.get(i); Log.w("View", "requestLayout() improperly called by " + view + " during layout: running second layout pass"); view.requestLayout(); } measureHierarchy(host, lp, mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight); mInLayout = true; host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); mHandlingLayoutInLayoutRequest = false; // Check the valid requests again, this time without checking/clearing the // layout flags, since requests happening during the second pass get noop'd validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true); if (validLayoutRequesters != null) { final ArrayList<View> finalRequesters = validLayoutRequesters; // Post second-pass requests to the next frame getRunQueue().post(new Runnable() { @Override public void run() { int numValidRequests = finalRequesters.size(); for (int i = 0; i < numValidRequests; ++i) { final View view = finalRequesters.get(i); Log.w("View", "requestLayout() improperly called by " + view + " during second layout pass: posting in next frame"); view.requestLayout(); } } }); } } } } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } mInLayout = false; }
这里就调用了host.layout()
方法,其中host就是当前的view,下面可以看到,调用完layout方法之后,还是有可能重新进行测量和布局的。
onDraw
我们先看主要流程;
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible; if (!cancelDraw && !newSurface) { if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).startChangingAnimations(); } mPendingTransitions.clear(); } performDraw(); } else { if (isViewVisible) { // Try again scheduleTraversals(); } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).endChangingAnimations(); } mPendingTransitions.clear(); } }
如果view是显示状态,那么调用performDraw()方法:
private void performDraw() { if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) { return; } else if (mView == null) { return; } // 省略不需要的东西。。。 try { boolean canUseAsync = draw(fullRedrawNeeded); if (usingAsyncReport && !canUseAsync) { mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null); usingAsyncReport = false; } } finally { mIsDrawing = false; Trace.traceEnd(Trace.TRACE_TAG_VIEW); } // 省略其它代码。。。。 } }
boolean canUseAsync = draw(fullRedrawNeeded);
,看看draw是怎么执行的:
private boolean draw(boolean fullRedrawNeeded) { Surface surface = mSurface; if (!surface.isValid()) { return false; } if (DEBUG_FPS) { trackFPS(); } boolean useAsyncReport = false; if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) { // If accessibility focus moved, always invalidate the root. boolean invalidateRoot = accessibilityFocusDirty || mInvalidateRootRequested; mInvalidateRootRequested = false; // Draw with hardware renderer. mIsAnimating = false; if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) { mHardwareYOffset = yOffset; mHardwareXOffset = xOffset; invalidateRoot = true; } if (invalidateRoot) { mAttachInfo.mThreadedRenderer.invalidateRoot(); } dirty.setEmpty(); // Stage the content drawn size now. It will be transferred to the renderer // shortly before the draw commands get send to the renderer. final boolean updated = updateContentDrawBounds(); if (mReportNextDraw) { // report next draw overrides setStopped() // This value is re-sync'd to the value of mStopped // in the handling of mReportNextDraw post-draw. mAttachInfo.mThreadedRenderer.setStopped(false); } if (updated) { requestDrawWindow(); } useAsyncReport = true; // draw(...) might invoke post-draw, which might register the next callback already. final FrameDrawingCallback callback = mNextRtFrameCallback; mNextRtFrameCallback = null; mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback); } else { // ......省略了较多的代码,不过不影响流程 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)) { return false; } } } if (animating) { mFullRedrawNeeded = true; scheduleTraversals(); } return useAsyncReport; }
看主要流程:drawSoftware方法:
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty, Rect surfaceInsets) { // Draw with software renderer. final Canvas canvas; int dirtyXOffset = xoff; int dirtyYOffset = yoff; if (surfaceInsets != null) { dirtyXOffset += surfaceInsets.left; dirtyYOffset += surfaceInsets.top; } try { dirty.offset(-dirtyXOffset, -dirtyYOffset); final int left = dirty.left; final int top = dirty.top; final int right = dirty.right; final int bottom = dirty.bottom; canvas = mSurface.lockCanvas(dirty); // The dirty rectangle can be modified by Surface.lockCanvas() //noinspection ConstantConditions if (left != dirty.left || top != dirty.top || right != dirty.right || bottom != dirty.bottom) { attachInfo.mIgnoreDirtyState = true; } canvas.setDensity(mDensity); } catch (Surface.OutOfResourcesException e) { handleOutOfResourcesException(e); return false; } catch (IllegalArgumentException e) { mLayoutRequested = true; // ask wm for a new surface next time. return false; } finally { dirty.offset(dirtyXOffset, dirtyYOffset); // Reset to the original value. } try { if (!canvas.isOpaque() || yoff != 0 || xoff != 0) { canvas.drawColor(0, PorterDuff.Mode.CLEAR); } dirty.setEmpty(); mIsAnimating = false; mView.mPrivateFlags |= View.PFLAG_DRAWN; try { canvas.translate(-xoff, -yoff); if (mTranslator != null) { mTranslator.translateCanvas(canvas); } canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0); attachInfo.mSetIgnoreDirtyState = false; mView.draw(canvas); drawAccessibilityFocusedDrawableIfNeeded(canvas); } finally { //............................................................ } } finally { // .................................... } return true; }
其实上面一些东西也比较重要,是负责view重绘相关的,这里以后我们在去深入。最后调用了mView.draw(canvas);
方法。
这里view绘制流程就走完了。
流程图
源码看完了,还是乱乱的?没关系,我们来看一下每个主要方法的流程图,这些都是从网上找的,非常清晰了,我就偷个懒——!
requestLayout()流程
performLayout()
invalidate()
上面这些流程图都是从网上找的,只为大家能够更清晰的了解View的绘制流程
自定义View基础
View介绍
View是ViewGroup的父类,但实际显示时,ViewGroup是父亲,里面显示的控件就是View。
View分类
自定义VIew都有哪几种呢?我们来整理一下
- 自定义View -》只需要重写onMeasure()和onDraw()
- 自定义ViewGroup -》 只需要重写onMeasure()和onLayout()
- 还有一种就很简单了,只继承系统提供的ViewGroup,里面根据需要创建系统的控件,并设置自己的一些属性即可,这种一半用于封装一些通用的View。
View坐标系
在Andorid中,坐标系区别去数学中的坐标系,是以屏幕左上角为原点:
向右为x轴增大方向
向下为y轴增大方向
View坐标描述
View的位置一般都是相对于父控件来描述的
- view.getTop() -> 当前view上边界到父View上边界的距离
- view.getLeft() -> 当前View左边界到父View左边界的距离
- view.getRight() -> 当前View右边界到父View左边界的距离
- view.getBottom() -> 当前View下边界到父View上边界的距离
同学们在自定义View时,right和bottom要尤为注意
View触摸事件获取位置
触摸事件一般通过MotionEvent对象来获取当前触摸位置。
- e.getX() -> 触摸点相对于当前View的X坐标
- e.getRawX() ->触摸点相对于父View的X坐标
- e.getY() -> 触摸点相对于当前View的Y坐标
- e.getRawY() -> 触摸点相对于父View的Y坐标
具体看上图
View的视图结构
-
PhoneWindow是Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口
-
DecorView是PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个FrameLayout
-
ViewRoot在Activtiy启动时创建,负责管理、布局、渲染窗口UI等
对于多个View的视图,结构为树形接口:最顶层是ViewGroup,ViewGroup下面有可能有多个ViewGroup或View,View为树的叶节点:
View构造函数
View一共有4个构造函数
// 通过java代码,直接new出来,调用第一个构造函数 public HeaderBar(Context context) { super(context); } // 在xml中使用控件会调用第二个构造函数 public HeaderBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } // 不会自动调用,通常在第二个构造函数里面主动调用 // 通常设置View的style属性时需要 public HeaderBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } // API21之后才出现的,一般用不到 public HeaderBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); }
自定义View时,前三个构造方法是必须的,每个构造方法都有不同的意义,上面注释已经说明,这也是面试中经常问道的一个小知识点。
View自定义属性
通常自定义View时都会跟随一些自定义的属性,从而更好的扩展你的View。
自定义的属性的步骤:
-
在attrs.xml中使用标签声明属性要使用的View和属性内容:
<!- name属性代表要在StarBar这个控件上使用 -> <declare-styleable name="HeaderBar"> <!- 定义我们需要扩展的属性和属性对应的类型 -> <attr name="isShowBack" format="boolean" /> <attr name="titleText" format="string" /> <attr name="rightText" format="string" /> </declare-styleable>
-
在layout中为响应的属性声明属性值
<com.kangf.test2.weigets.HeaderBar android:layout_width="match_parent" android:layout_height="wrap_content" app:isShowBack="true" app:rightText="@string/app_name" app:titleText="@string/app_name" />
-
在运行时获取属性值(一般在构造函数中获取)
private boolean isShowBack = true; private String titleText = ""; private String rightText = ""; private TextView rightView; private ImageView leftView; public HeaderBar(@NonNull Context context) { this(context, null); } public HeaderBar(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public HeaderBar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); } // 获取属性值 private void init(AttributeSet attrs) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.HeaderBar, 0, 0); isShowBack = a.getBoolean(R.styleable.HeaderBar_isShowBack, true); titleText = a.getString(R.styleable.HeaderBar_titleText); rightText = a.getString(R.styleable.HeaderBar_rightText); a.recycle(); // 将获取到的属性值应用到View initView(); }
-
将获取到的属性值应用到View
private void initView() { View.inflate(getContext(), R.layout.base_layout_header_bar, this); leftView = findViewById(R.id.mLeftIv); TextView titleView = findViewById(R.id.mTitleTv); rightView = findViewById(R.id.mRightTv); if (isShowBack) { leftView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (getContext() instanceof Activity) { ((Activity) getContext()).finish(); } } }); } titleView.setText(titleText); if (rightText == null || !rightText.isEmpty()) { rightView.setVisibility(View.VISIBLE); rightView.setText(rightText); } }
测量(MeasureSpec)
自定义View的测量和布局一般都是先对子View进行操作,测量完子View再测量当前的ViewGroup,但也不排除有其他情况(比如ViewPager是先测量的当前ViewGroup)。
测量模式
测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize是指在某种SpecMode下的参考尺寸,其中SpecMode 有如下三种
-
UNSPECIFIED 父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)
-
EXACTLY 父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。
-
AT_MOST 你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。
MeasureSpec介绍
通过将 SpecMode 和 SpecSize 打包成一个 int 值可以避免过多的对象内存分配,为了方便操作,其提供了打包/解包方法。
那么MeasueSpec的值到底是如何得来的呢?**MeasureSpec其实是一个32位的int值,其中高2位表示测量模式,低30位表示了测量出的具体大小。**子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里,我们来看一下源码:
/* 目标是将父控件的测量规格和child view的布局参数LayoutParams相结合,得到一个最可能符合条件的child view的测量规格。
* @param spec 父控件的测量规格
* @param padding 父控件里已经占用的大小
* @param childDimension child view布局LayoutParams里的尺寸
* @return child view 的测量规格
*/ 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) { // Parent has imposed an exact size on us //父控件的测量模式是精确模式,也就是有精确的尺寸了 case MeasureSpec.EXACTLY: //如果child的布局参数有固定值,比如"layout_width" = "100dp" //那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 如果child的布局参数是"match_parent",也就是想要占满父控件 // 而此时父控件是精确模式,也就是确定了自己的尺寸了,那child也能确定自己大小了 // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. //如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小, //比如TextView根据设置的字符串大小来决定自己的大小 //那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛, //所以测量模式就是AT_MOST,测量大小就是父控件的size resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us // 当父控件的测量模式 是 最大模式,也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size case MeasureSpec.AT_MOST: //同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求?? if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; // child想要和父控件一样大,但是父控件也不知道自己的大小,所以child也无法确定自己的大小 // 但child的尺寸上限也是父控件的尺寸上限 } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; // child想要根据自己的逻辑决定大小 } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 当未确定大小时,一般是由系统决定的,我们不用去管它 case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
上面注释应该已经非常清楚了,希望小伙伴们认真的理解一下,对我们以后的自定义View非常有帮助。
针对上表,这里再做一下具体的说明:
对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定对于不同的父容器和view本身不同的LayoutParams,view就可以有多种MeasureSpec。
- 当view采用固定宽高的时候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;
- 当view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么view也是最大模式并且其大小不会超过父容器的剩余空间;
- 当view的宽高是wrap_content时,不管父容器的模式是精准还是最大化,view的模式总是最大化并且大小不能超过父容器的剩余空间。
- Unspecifified模式,这个模式主要用于系统内部多次measure的情况下,一般来说,我们不需要关注此模式(这里注意自定义View放到ScrollView的情况 需要处理)。
总结
今天对View的理解就先到这里啦,本篇主要讲的是View的绘制流程和MeasureSpec的定义,都是比较基础的东西,这些东西理解了,我们就开始第二篇 ------ 自定义View之流式布局。
如果文章中有什么不对的,还请大佬们多多指正,感激不尽!
本文地址:https://blog.csdn.net/qq_22090073/article/details/107713174