欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android自定义View视图详解

程序员文章站 2022-03-23 08:05:27
Android自定义View - 一前言View绘制流程viewGroup.addView()requestLayout()onMeasureonLayoutonDraw流程图requestLayout()流程performLayout()invalidate()自定义View基础View介绍View分类View坐标系View坐标描述View触摸事件获取位置View的视图结构View构造函数View自定义属性测量(MeasureSpec)测量模式MeasureSpec介绍总结前言有多少小伙伴和我一样,一...



前言

有多少小伙伴和我一样,一听到自定义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.horizontalWeightlp.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绘制流程就走完了。

流程图

源码看完了,还是乱乱的?没关系,我们来看一下每个主要方法的流程图,这些都是从网上找的,非常清晰了,我就偷个懒——!

Android自定义View视图详解

Android自定义View视图详解

requestLayout()流程

Android自定义View视图详解

performLayout()

Android自定义View视图详解

invalidate()

Android自定义View视图详解

上面这些流程图都是从网上找的,只为大家能够更清晰的了解View的绘制流程

自定义View基础

View介绍

View是ViewGroup的父类,但实际显示时,ViewGroup是父亲,里面显示的控件就是View。

View分类

自定义VIew都有哪几种呢?我们来整理一下

  • 自定义View -》只需要重写onMeasure()和onDraw()
  • 自定义ViewGroup -》 只需要重写onMeasure()和onLayout()
  • 还有一种就很简单了,只继承系统提供的ViewGroup,里面根据需要创建系统的控件,并设置自己的一些属性即可,这种一半用于封装一些通用的View。

View坐标系

在Andorid中,坐标系区别去数学中的坐标系,是以屏幕左上角为原点:

向右为x轴增大方向

向下为y轴增大方向

Android自定义View视图详解

View坐标描述

View的位置一般都是相对于父控件来描述的

  • view.getTop() -> 当前view上边界到父View上边界的距离
  • view.getLeft() -> 当前View左边界到父View左边界的距离
  • view.getRight() -> 当前View右边界到父View边界的距离
  • view.getBottom() -> 当前View下边界到父View边界的距离

同学们在自定义View时,right和bottom要尤为注意

Android自定义View视图详解

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等

    Android自定义View视图详解

对于多个View的视图,结构为树形接口:最顶层是ViewGroup,ViewGroup下面有可能有多个ViewGroup或View,View为树的叶节点:

Android自定义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,但具体是多少,得看你自己的实现。

Android自定义View视图详解

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非常有帮助。

Android自定义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