GVKun编程网logo

Android – 是否可以“锁定”相机的预览帧率?(android自定义相机预览视图)

3

在这篇文章中,我们将为您详细介绍Android–是否可以“锁定”相机的预览帧率?的内容,并且讨论关于android自定义相机预览视图的相关问题。此外,我们还会涉及一些关于AndroidView的绘制流

在这篇文章中,我们将为您详细介绍Android – 是否可以“锁定”相机的预览帧率?的内容,并且讨论关于android自定义相机预览视图的相关问题。此外,我们还会涉及一些关于Android View 的绘制流程之 Layout 和 Draw 过程详解 (二) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 的绘制流程之 Measure 过程详解 (一)、Android View 的绘制流程之 Measure 过程详解 (一) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 绘制流程之 DecorView 与 ViewRootImp、Android View 绘制流程之 DecorView 与 ViewRootImpl Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 的测量流程详解、Android 自定义 View 详解 Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解的知识,以帮助您更全面地了解这个主题。

本文目录一览:

Android – 是否可以“锁定”相机的预览帧率?(android自定义相机预览视图)

Android – 是否可以“锁定”相机的预览帧率?(android自定义相机预览视图)

我正在尝试创建一个视频录制应用程序,以24 FPS录制视频.
我正在使用以下代码尝试将FPS锁定为24:
Camera.Parameters params = mCamera.getParameters();
params.setPreviewFrameRate(24);
params.setPreviewFpsRange(24000,24000);

以及与MediaRecorder一起使用的以下CamcorderProfile:

CamcorderProfile ccp = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
ccp.videoFrameRate = 24;

不幸的是,它仅在视频在低光照条件下拍摄时才有效,但是当我在有光线时出门时,视频开始以30 FPS录制.

是否可以在光天化日之前将帧速率锁定为24fps?

提前致谢!

解决方法

public List<Integer> getSupportedPreviewFrameRates ()

检查此列表.我想你不能设置那里没有列出的帧率.这可能是因为Android OS中使用的编解码器修改.

Android View 的绘制流程之 Layout 和 Draw 过程详解 (二) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 的绘制流程之 Measure 过程详解 (一)

Android View 的绘制流程之 Layout 和 Draw 过程详解 (二) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 的绘制流程之 Measure 过程详解 (一)

View 的绘制系列文章:

  • Android View 绘制流程之 DecorView 与 ViewRootImpl

  • Android View 的绘制流程之 Measure 过程详解 (一)

  • Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)

  • Android View 的事件分发原理解析

  • Android 自定义 View 详解

 

在上一篇 Android View 的绘制流程之 Measure 过程详解 (一),已经详细的分析了 DecorView 和其子 View 的测量过程,接下去就要开始讲  layout 和 draw 流程。下面开始进入分析:

DecorView Layout 阶段

在 ViewRootImpl 中,调用 performlayout 方法来确定 DecorView 在屏幕中的位置,下面看下具体的代码逻辑:

// ViewRootImpl 
private void performlayout(WindowManager.LayoutParams lp,int desiredWindowWidth, desiredWindowHeight) { mLayoutRequested = false; mScrollMayChange = true; mInLayout = ; 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 = ; 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,1)">); if (validLayoutRequesters != ) { 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 = ; 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,host.getMeasuredHeight()); mHandlingLayoutInLayoutRequest = 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,1)">); ) { final ArrayList<View> finalRequesters = validLayoutRequesters; Post second-pass requests to the next frame getRunQueue().post(new Runnable() { @Override public void run() { finalRequesters.size(); i) { 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 = ; }

当在 layout 绘制过程中,收到了关于重新 layout 的请求,会先判断这些请求里面哪些是有效的,如果是有效的,那么就必须先处理,清除 layout-request flags (View.PFLAG_FORCE_LAYOUT)这些标记,再做一次彻底的重绘工作:重新测量,layout。

那么对于 DecorView 来说,调用 layout 方法,就是对它自身进行布局,注意到传递的参数分别是 0,0, host.getMeasuredWidth,host.getMeasuredHeigh,它们分别代表了一个  View 的上下左右四个位置,显然,DecorView 的左上位置为 0,然后宽高为它的测量宽高,下面来看 layout 的具体代码:

 ViewGroup 
final void layout(int l,1)">int t,1)">int r,1)"> b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != ) {
                mTransition.layoutChange(this);
            }
            super.layout(l,t,r,b);
        } else {
             record the fact that we noop''d it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = ;
        }
    }

 由于 ViewGroup 的 layout 方法是 final 类型,子类不能重写,这里调用了父类的  View#layout 方法,下面看看该方法是如何操作的:

// View   
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,b) : setFrame(l,b);      // 发生了改变,或者是需要重新 layout,那么就会进入 onLayout 逻辑 if (changed || (mPrivateFlags & PFLAG_LAYOUT_required) == PFLAG_LAYOUT_required) {
// 开始 onLayout(changed,l,b);
if (shouldDrawRoundScrollbar()) { if(mRoundScrollbarRenderer == ) { mRoundScrollbarRenderer = new RoundScrollbarRenderer(); } } { mRoundScrollbarRenderer = ; }        // 清除请求 layout 的标记 mPrivateFlags &= ~PFLAG_LAYOUT_required; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutchangelisteners != ) { ArrayList<OnLayoutchangelistener> listenerscopy = (ArrayList<OnLayoutchangelistener>)li.mOnLayoutchangelisteners.clone(); int numListeners = listenerscopy.size(); int i = 0; i < numListeners; ++i) { listenerscopy.get(i).onLayoutChange(boolean wasLayoutValid = isLayoutValid(); mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; if (!wasLayoutValid && isFocused()) { mPrivateFlags &= ~PFLAG_WANTS_FOCUS; (canTakeFocus()) { We have a robust focus,so parents should no longer be wanting focus. clearParentsWantFocus(); } else if (getViewRootImpl() == getViewRootImpl().isInLayout()) { This is a weird case. Most-likely the user,rather than ViewRootImpl,called layout. In this case,there''s no guarantee that parent layouts will be evaluated and thus the safest action is to clear focus here. clearFocusInternal(null,1)">/* propagate */ true,1)"> refocus ); clearParentsWantFocus(); } if (!hasParentWantsFocus()) { original requestFocus was likely on this view directly,so just clear focus clearFocusInternal( otherwise,we let parents handle re-assigning focus during their layout passes. } if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) { mPrivateFlags &= ~PFLAG_WANTS_FOCUS; View focused = findFocus(); if (focused != Try to restore focus as close as possible to our starting focus. if (!restoreDefaultFocus() && !hasParentWantsFocus()) { Give up and clear focus once we''ve reached the top-most parent which wants focus. focused.clearFocusInternal(); } } } if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) { mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT; notifyEnterOrExitForAutoFillIfNeeded(); } }

 调用了 setFrame 方法,并把四个位置信息传递进去,这个方法用于确定 View 的四个顶点的位置,看下具体的代码:

protected boolean setFrame(int left,1)">int top,1)">int right,1)"> bottom) {
        boolean changed =  (DBG) {
            Log.d(VIEW_LOG_TAG,1)">this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }
     // 有一个不一样说明发生了改变
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = ;

             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;

       // size 改变的回调
             (sizeChanged) {
                sizeChange(newWidth,newHeight,oldWidth,oldHeight);
            }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView !=  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 = ;
            mDefaultFocusHighlightSizeChanged = if (mForegroundInfo != ) {
                mForegroundInfo.mBoundsChanged = ;
            }

            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
         changed;
    }
这里我们看到它对 mLeft、mTop、mRight、mBottom 这四个值进行了重新赋值,对于每一个View,包括 ViewGroup 来说,以上四个值保存了 Viwe 的位置信息,所以这四个值是最终宽高,也即是说,如果要得到 View 的位置信息,那么就应该在 layout 方法完成后调用 getLeft()、getTop() 等方法来取得最终宽高,如果是在此之前调用相应的方法,只能得到 0 的结果。当初始化完毕后,ViewGroup 的布局流程也就完成了。

赋值后,前后对比大小,如果发生了改变,就会调用了 sizeChange()方法,最终会回调 onSizeChanged,标明 view 的尺寸发生了变化,第一次 laout 的时候也会调用。在自定义view 的时候,可以在这里获取控件的宽和高度。

  void sizeChange(int newWidth,1)">int newHeight,1)">int oldWidth,1)"> oldHeight) {
        onSizeChanged(newWidth,oldHeight);
      ......
}

子 View Layout 流程

当 DecorView 确定好了自己的位置之后,开始调用 onLayout 来确定子 view 的位置。对于 onLayout 方法,View 和 ViewGroup 类是空实现,接下来看 FrameLayout 的实现:

 FrameLayout 
void onLayout(boolean changed,1)"> bottom) {
        layoutChildren(left,top,right,bottom,1)">false  no force left gravity */);
    }

 可以看到,该方法调用了 layoutChildren 来确定子 view 的位置。

 FrameLaout   
void layoutChildren(int bottom,1)">boolean forceLeftGravity) { int count = getChildCount();      // 获取 DecoverView 剩余的空间范围 int parentLeft = getPaddingLeftWithForeground(); int parentRight = right - left - getPaddingRightWithForeground(); int parentTop = getPaddingTopWithForeground(); int parentBottom = bottom - top - getPaddingBottomWithForeground(); int i = 0; i < count; i++final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); int width = child.getMeasuredWidth(); int height = child.getMeasuredHeight(); childLeft; childTop;
          // DEFAULT_CHILD_GraviTY = Gravity.TOP | Gravity.START 默认是在左上角
int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GraviTY;
                }

                int layoutDirection = getLayoutDirection();
                int absoluteGravity = Gravity.getAbsoluteGravity(gravity,layoutDirection);
                int verticalGravity = gravity & Gravity.VERTICAL_GraviTY_MASK;
          // 确定左边起始点
                switch (absoluteGravity & Gravity.HORIZONTAL_GraviTY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                     Gravity.RIGHT:
                        forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            ;
                        }
                     Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }
          // 确定上边起始点
                switch (verticalGravity) {
                     Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                         Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                         Gravity.BottOM:
                        childTop = parentBottom - height -:
                        childTop = parentTop + lp.topMargin;
                }

                child.layout(childLeft,childTop,childLeft + width,childTop + height);
            }
        }
    }

先梳理一下以上逻辑:

  • 首先先获取父容器的 padding 值,得到 DecorView 的可用于显示的空间范围。

  • 然后遍历其每一个子 View,根据子 View 的 layout_gravity 属性、子 View 的测量宽高、父容器的 padding 值、来确定子 View 的左上角的坐标位置

  • 然后调用 child.layout 方法,参数是左上角坐标和自身宽高结合起来的,这样就可以确定子 View 的位置。

最终调用 layout 是 View#layout,前面已经分析过,就不在分析了。

可以看到子 View 的布局流程也很简单,如果子 View 是一个 ViewGroup,那么就会重复以上步骤,如果是一个 View,那么会直接调用 View#layout 方法,根据以上分析,在该方法内部会设置 view 的四个布局参数,接着调用 onLayout 方法 :

 bottom) {

}

此方法是一个空方法,也就是说需要子类去实现此方法,不同的 View 实现方式不同,这里就不分析了。

layout阶段的基本思想也是由根View开始,递归地完成整个控件树的布局(layout)工作。 

对于宽高的获取这里在总结下:

 

 

 这里需要注意一下,在非一般情况下,也就是通过人为设置,重写View的layout()强行设置,这种情况下,测量的值与最终的值是不一样的。

Layout 整体的流程图

上面分别从 View 和 ViewGroup 的角度讲解了布局流程,这里再以流程图的形式归纳一下整个 Layout 过程,便于加深记忆:

 

DecorView Draw 流程

Draw 的入口也是在 ViewRootImpl 中,执行 ViewRootImpl#performTraversals 中会执行 ViewRootIml#performDraw:

 performDraw() {
...
fullRedrawNeeded,它的作用是判断是否需要重新绘制全部视图
draw(fullRedrawNeeded);
...
}

然后会执行到 ViewRootImpl#draw:

void draw( fullRedrawNeeded) {
 ...
 获取mDirty,该值表示需要重绘的区域
 final Rect dirty = mDirty;
 if (mSurfaceHolder != ) {
   The app owns the surface,we won''t draw.
  dirty.setEmpty();
   (animating) {
   if (mScroller != ) {
    mScroller.abortAnimation();
   }
   disposeResizeBuffer();
  }
  ;
 }

 如果fullRedrawNeeded为真,则把dirty区域置为整个屏幕,表示整个视图都需要绘制
 第一次绘制流程,需要绘制所有视图
  (fullRedrawNeeded) {
  mAttachInfo.mIgnoreDirtyState = ;
  dirty.set(0,(int) (mWidth * appScale + 0.5f),1)">int) (mHeight * appScale + 0.5f));
 }
 ...
 drawSoftware(surface,mAttachInfo,xOffset,yOffset,scalingrequired,dirty)) {
    ;
  }
}

 接着会执行到 ViewRootIml#drawSoftware,然后在 ViewRootIml#drawSoftware 会执行到 mView.draw(canvas)。

boolean drawSoftware(Surface surface,AttachInfo attachInfo,1)">int xoff,1)"> yoff,1)"> scalingrequired,Rect dirty) {
 final Canvas canvas;
  锁定canvas区域,由dirty区域决定
  这个canvas就是我们想在上面绘制东西的画布
  canvas = mSurface.lockCanvas(dirty);
  ...
 画布支持位图的密度,和手机分辨率相关
  canvas.setDensity(mDensity);
 ...
   if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0yoff);
   ...
   正式开始绘制
   mView.draw(canvas);
  ...
 提交需要绘制的东西
  surface.unlockCanvasAndPost(canvas);
}

 mView.draw(canvas) 开始真正的绘制。此处 mView 就是 DecorView,先看 DecorView 中 Draw 的方法:

   draw(Canvas canvas) {
        .draw(canvas);

        if (mMenuBackground != ) {
            mMenuBackground.draw(canvas);
        }
    }

 里面会调用 View#draw:

    int privateFlags = mPrivateFlags;
        boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary,save the canvas'' layers to prepare for fading
         *      3. Draw view''s content
         *      4. Draw children
         *      5. If necessary,draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

         Step 1,draw the background,if needed
         saveCount;

        绘制背景
        dirtyOpaque) {
            drawBackground(canvas);
        }

         如果可以跳过2和5步
        int viewFlags = mViewFlags;
      判断是否有绘制衰退边缘的标示
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
      如果没有绘制衰退边缘只需要3,4,6步
        if (!verticalEdges && !horizontalEdges) {
             Step 3,draw the content
            dirtyOpaque) onDraw(canvas);

             Step 4,draw the children
            dispatchDraw(canvas);

             Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getoverlayView().dispatchDraw(canvas);
            }

             Step 6,draw decorations (foreground,scrollbars)
            onDrawForeground(canvas);

             we''re done...
            ;
        }

        
         * Here we do the full fledged routine...
         * (this is an uncommon case where speed matters less,* this is why we repeat some of the tests that have been
         * done above)
         boolean drawTop = boolean drawBottom = boolean drawLeft = boolean drawRight = float topFadeStrength = 0.0ffloat bottomFadeStrength = 0.0ffloat leftFadeStrength = 0.0ffloat rightFadeStrength = 0.0f Step 2,save the canvas'' layers
        int paddingLeft = mPaddingLeft;

        boolean offsetrequired = isPaddingOffsetrequired();
         (offsetrequired) {
            paddingLeft += getLeftPaddingOffset();
        }

        int left = mScrollX + paddingLeft;
        int right = left + mRight - mLeft - mPaddingRight -int top = mScrollY + getFadetop(offsetrequired);
        int bottom = top + getFadeHeight(offsetrequired);

         (offsetrequired) {
            right += getRightPaddingOffset();
            bottom += getBottomPaddingOffset();
        }

        final ScrollabilityCache scrollabilityCache = mScrollCache;
        float fadeHeight = scrollabilityCache.fadingEdgeLength;
        int length = () fadeHeight;

         clip the fade length if top and bottom fades overlap
         overlapping fades produce odd-looking artifacts
        if (verticalEdges && (top + length > bottom - length)) {
            length = (bottom - top) / 2 also clip horizontal fades if necessary
        if (horizontalEdges && (left + length > right - length)) {
            length = (right - left) / 2 (verticalEdges) {
            topFadeStrength = Math.max(0.0f,Math.min(1.0f;
            bottomFadeStrength = Math.max(0.0f,getBottomFadingEdgeStrength()));
            drawBottom = bottomFadeStrength * fadeHeight > 1.0f (horizontalEdges) {
            leftFadeStrength = Math.max(0.0f,getLeftFadingEdgeStrength()));
            drawLeft = leftFadeStrength * fadeHeight > 1.0f;
            rightFadeStrength = Math.max(0.0f,getRightFadingEdgeStrength()));
            drawRight = rightFadeStrength * fadeHeight > 1.0f;
        }

        saveCount = canvas.getSaveCount();

        int solidColor = getSolidColor();
        if (solidColor == 0int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

             (drawTop) {
                canvas.saveLayer(left,top + length,flags);
            }

             (drawBottom) {
                canvas.saveLayer(left,bottom - length,1)"> (drawLeft) {
                canvas.saveLayer(left,left + length,1)"> (drawRight) {
                canvas.saveLayer(right - length,flags);
            }
        }  {
            scrollabilityCache.setFadeColor(solidColor);
        }

        dirtyOpaque) onDraw(canvas);

                dispatchDraw(canvas);

         Step 5,draw the fade effect and restore layers
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;

         (drawTop) {
            matrix.setScale(1,fadeHeight * topFadeStrength);
            matrix.postTranslate(left,top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left,top + length,p);
        }

         (drawBottom) {
            matrix.setScale(1,1)"> bottomFadeStrength);
            matrix.postRotate(180);
            matrix.postTranslate(left,bottom);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left,bottom - (drawLeft) {
            matrix.setScale(1,1)"> leftFadeStrength);
            matrix.postRotate(-90 (drawRight) {
            matrix.setScale(1,1)"> rightFadeStrength);
            matrix.postRotate(90);
            matrix.postTranslate(right,top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(right -etoCount(saveCount);

         Overlay is part of the content and draws beneath Foreground
        mOverlay.isEmpty()) {
            mOverlay.getoverlayView().dispatchDraw(canvas);
        }

                onDrawForeground(canvas);
    }

可以看到,draw过程比较复杂,但是逻辑十分清晰,而官方注释也清楚地说明了每一步的做法。我们首先来看一开始的标记位 dirtyOpaque,该标记位的作用是判断当前 View 是否是透明的,如果 View 是透明的,那么根据下面的逻辑可以看出,将不会执行一些步骤,比如绘制背景、绘制内容等。这样很容易理解,因为一个 View 既然是透明的,那就没必要绘制它了。接着是绘制流程的六个步骤,这里先小结这六个步骤分别是什么,然后再展开来讲。绘制流程的六个步骤:

  1. 对 View 的背景进行绘制

  2. 保存当前的图层信息(可跳过)

  3. 绘制 View 的内容

  4. 对 View 的子 View 进行绘制(如果有子 View )

  5. 绘制 View 的褪色的边缘,类似于阴影效果(可跳过)

  6. 绘制 View 的装饰(例如:滚动条)

其中第2步和第5步是可以跳过的,我们这里不做分析,我们重点来分析其它步骤。

ViewGroup子类默认情况下就是不执行 onDraw 方法的,在 ViewGroup 源码中的 initViewGroup() 方法中设置了一个标记,源码如下:

 initViewGroup() {
         ViewGroup doesn''t draw by default
        debugDraw()) {
            setFlags(WILL_NOT_DRAW,DRAW_MASK);
        }
        ......
}

看第二行注释也知道,ViewGroup 默认情况下是不会 draw 的。第四行调用 setFlags 方法设置标记 WILL_NOT_DRAW,在回到 View 中 draw 方法看第2行代码:

1   mPrivateFlags;
2  boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
3      (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);

 

setFlags 方法就是对 View中mPrivateFlags 值进行相应改变,我们设置标记 WILL_NOT_DRAW 那么 dirtyOpaque 得到的值就为 true,从而 if (!dirtyOpaque) 不成立,也就不会执行onDraw 方法。

1. 绘制背景

View#drawBackground

     drawBackground(Canvas canvas) {
       获取背景的Drawable,没有就不需要绘制
        final Drawable background = mBackground;
        if (background == ;
        }
       确定背景Drawable边界
        setBackgroundBounds();
        ...

       如果有偏移量先偏移画布再将drawable绘制上去
        int scrollX = mScrollX;
        int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        }  {
            canvas.translate(scrollX,scrollY);
            此处会执行各种Drawable对应的draw方法
            background.draw(canvas);
            把画布的原点移回去,drawable在屏幕上的位置不动
            canvas.translate(-scrollX,1)">scrollY);
        }
    }

3. 绘制 View 的内容

先跳过第 2 步,是因为不是所有的 View 都需绘制褪色边缘。DecorView#onDraw:

    onDraw(Canvas c) {
        .onDraw(c);

        mBackgroundFallback.draw(
   onDraw(Canvas canvas) {
}

onDraw 是空实现,需要子 View 自己去绘制。对于DecorView 一般也没啥内容,除了需要背景颜色等,所以本身并需要绘制啥。

4. 绘制子View

DecorView 绘制完成后,开始绘制子 View,所以 ViewGroup 的绘制需要绘制子 View,直接看看 ViewGroup#dispatchDraw:

 dispatchDraw(Canvas canvas) {
       boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
       int childrenCount = mChildrenCount;
       final View[] children = mChildren;
        mGroupFlags;

ViewGroup是否有设置子View入场动画,如果有绑定到View
 启动动画控制器
      ...

指定修改区域
       int clipSaveCount = 0;
       boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
    // 不让子view绘制在pandding里面,也就是去除padding
(clipToPadding) { clipSaveCount = canvas.save(); canvas.clipRect(mScrollX + mPaddingLeft,mScrollY + mPaddingTop,mScrollX + mRight - mLeft - mPaddingRight,mScrollY + mBottom - mTop - mPaddingBottom); } ... int i = 0; i < childrenCount; i++) { 先取mTransientViews中的View,mTransientViews中的View通过addTransientView添加,它们只是容器渲染的一个item while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) { final View transientChild = mTransientViews.get(transientIndex); if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE || transientChild.getAnimation() != ) { more |= drawChild(canvas,transientChild,drawingTime); } transientIndex++; if (transientIndex >= transientCount) { transientIndex = -1; } } int childindex = customOrder ? getChildDrawingOrder(childrenCount,i) : i; final View child = (preorderedList == ) ? children[childindex] : preorderedList.get(childindex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != ) { more |=dispatchDraw 的流程是先启动第一次加到布局中的动画,然后确定绘制区域,遍历绘制 View,遍历 View 的时候优先绘制渲染的 mTransientViews,绘制 View 调用到ViewGroup#drawChild:

boolean drawChild(Canvas canvas,View child,1)">long drawingTime) {
        View中有两个draw方法
        这个多参数的draw用于view绘制自身内容
        return child.draw(canvas,drawingTime);
    }

 View#draw(canvas,this,drawingTime)

boolean draw(Canvas canvas,ViewGroup parent,1)"> drawingTime) {
 
       boolean drawingWithRenderNode = mAttachInfo != null
                && mAttachInfo.mHardwareAccelerated
                && hardwareAcceleratedCanvas;
      ...

     主要判断是否有绘制缓存,如果有,直接使用缓存,如果没有,调用 draw(canvas)方法
        drawingWithDrawingCache) {
             (drawingWithRenderNode) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                ((displayListCanvas) canvas).draWrenderNode(renderNode);
            }  {
                 Fast path for layouts with no backgrounds
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    dispatchDraw(canvas);
                }  {
                    draw(canvas);
                }
            }
        } if (cache != PFLAG_DIRTY_MASK;
            if (layerType == LAYER_TYPE_NONE) {
                 no layer paint,use temporary paint to draw bitmap
                Paint cachePaint = parent.mCachePaint;
                if (cachePaint == ) {
                    cachePaint =  Paint();
                    cachePaint.setDither();
                    parent.mCachePaint = cachePaint;
                }
                cachePaint.setAlpha((int) (alpha * 255));
                canvas.drawBitmap(cache,0.0f,0.0f use layer paint to draw the bitmap,merging the two alphas,but also restore
                int layerPaintAlpha = mLayerPaint.getAlpha();
                mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
                canvas.drawBitmap(cache,mLayerPaint);
                mLayerPaint.setAlpha(layerPaintAlpha);
            }
      }
}

首先判断是否已经有缓存,即之前是否已经绘制过一次了,如果没有,则会调用 draw(canvas) 方法,开始正常的绘制,即上面所说的六个步骤,否则利用缓存来显示。

这一步也可以归纳为 ViewGroup 绘制过程,它对子 View 进行了绘制,而子 View 又会调用自身的 draw 方法来绘制自身,这样不断遍历子 View 及子 View 的不断对自身的绘制,从而使得 View 树完成绘制。

对于自定义 View ,如果需要绘制东西的话,直接重新 onDraw 就可以了。

6. 绘制装饰

     onDrawForeground(Canvas canvas) {
     绘制滑动指示
        onDrawScrollIndicators(canvas);
     绘制ScrollBar
        onDrawScrollBars(canvas);
     获取前景色的Drawable,绘制到canvas上
        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : if (foreground !=  (mForegroundInfo.mBoundsChanged) {
                mForegroundInfo.mBoundsChanged = ;
                final Rect selfBounds = mForegroundInfo.mSelfBounds;
                final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
                 (mForegroundInfo.mInsidePadding) {
                    selfBounds.set(0,0 {
                    selfBounds.set(getPaddingLeft(),getPaddingTop(),getWidth() - getPaddingRight(),getHeight() - getPaddingBottom());
                }
                int ld = getLayoutDirection();
                Gravity.apply(mForegroundInfo.mGravity,foreground.getIntrinsicWidth(),foreground.getIntrinsicHeight(),selfBounds,overlayBounds,ld);
                foreground.setBounds(overlayBounds);
            }
            foreground.draw(canvas);
        }
    }

 2和5.绘制View的褪色边缘

当 horizontalEdges 或者 verticalEdges 有一个 true 的时候,表示需要绘制 View 的褪色边缘:

     boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;

这时候先计算出是否需要绘制上下左右的褪色边缘和它的参数,然后保存视图层:

         mPaddingLeft;
         getLeftPaddingOffset();
        }
         getFadeHeight(offsetrequired);
         getBottomPaddingOffset();
        }
        ) fadeHeight;
        ;
        }
        saveCount = canvas.getSaveCount();
         Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
             {
            scrollabilityCache.setFadeColor(solidColor);
        }

绘制褪色边缘,恢复视图层 :

         scrollabilityCache.shader;
        etoCount(saveCount);

所谓的绘制装饰,就是指 View 除了背景、内容、子 View 的其余部分,例如滚动条等。

最后附上 View 的 draw 流程:

 

 

到此,View 的绘制流程就讲完了,下一篇会讲自定义 View。

小结:

  • view 不停找 parent 可以一直找到 DecorView,按理说 DecorView 是顶点了,但是 DecorView 还有个虚拟父 view,ViewRootImpl。 ViewRootImpl 不是一个 View 或者ViewGroup,他有个成员 mView 是 DecorView,所有的操作从 ViewRootImpl 开始自上而下分发

  • view 的 invalidate 不会导致 ViewRootImpl 的 invalidate 被调用,而是递归调用父 view的invalidateChildInParent,直到 ViewRootImpl 的 invalidateChildInParent,然后触发peformTraversals,会导致当前 view 被重绘,由于 mLayoutRequested 为 false,不会导致 onMeasure 和 onLayout 被调用,而 OnDraw 会被调用

  • 一个 view 的 invalidate 会导致本身 PFLAG_INVALIDATED 置 1,导致本身以及父族 viewgroup 的 PFLAG_DRAWING_CACHE_VALID 置 0

  • requestLayout 会直接递归调用父窗口的 requestLayout,直到 ViewRootImpl,然后触发 peformTraversals,由于 mLayoutRequested 为 true,会导致 onMeasure 和onLayout 被调用。不一定会触发 OnDraw

  • requestLayout 触发 onDraw 可能是因为在在 layout 过程中发现 l,b 和以前不一样,那就会触发一次 invalidate,所以触发了onDraw,也可能是因为别的原因导致 mDirty 非空(比如在跑动画)

  • requestLayout 会导致自己以及父族 view 的 PFLAG_FORCE_LAYOUT 和 PFLAG_INVALIDATED 标志被设置。

  • 一般来说,只要刷新的时候就调用 invalidate,需要重新 measure 就调用 requestLayout,后面再跟个 invalidate(为了保证重绘),

 

总结

以上是小编为你收集整理的Android View 的绘制流程之 Layout 和 Draw 过程详解 (二) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 的绘制流程之 Measure 过程详解 (一)全部内容。

如果觉得小编网站内容还不错,欢迎将小编网站推荐给好友。

Android View 的绘制流程之 Measure 过程详解 (一) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 绘制流程之 DecorView 与 ViewRootImp

Android View 的绘制流程之 Measure 过程详解 (一) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 绘制流程之 DecorView 与 ViewRootImp

View 的绘制系列文章:

  • Android View 绘制流程之 DecorView 与 ViewRootImpl

  • Android View 的绘制流程之 Measure 过程详解 (一)

  • Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)

  • Android View 的事件分发原理解析

  • Android 自定义 View 详解

 

概述

上一篇 Android View 绘制流程之 DecorView 与 ViewRootImpl 分析了在调用 setContentView 之后,DecorView 是如何与 activity 关联在一起的,最后讲到了 ViewRootImpl 开始绘制的逻辑。本文接着上篇,继续往下讲,开始分析 view 的绘制流程。

上文说到了调用 performTraversals 进行绘制,由于 performTraversals 方法比较长,看一个简化版:

// ViewRootImpl 类
private void performTraversals() {
     这个方法代码非常多,但是重点就是执行这三个方法
     执行测量
    performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
     执行布局(ViewGroup)中才会有
    performlayout(lp,mWidth,mHeight);
     执行绘制
    performDraw();
}

其流程具体如下:

 View的整个绘制流程可以分为以下三个阶段:

  • measure: 判断是否需要重新计算 View 的大小,需要的话则计算;

  • layout: 判断是否需要重新计算 View 的位置,需要的话则计算;

  • draw: 判断是否需要重新绘制 View,需要的话则重绘制。

MeasureSpec

在介绍绘制前,先了解下 MeasureSpec。MeasureSpec 封装了父布局传递给子布局的布局要求,它通过一个 32 位 int 类型的值来表示,该值包含了两种信息,高两位表示的是 SpecMode(测量模式),低 30 位表示的是 Specsize(测量的具体大小)。下面通过注释的方式来分析来类:

/**  
 * 三种SpecMode: 
 * 1.UNSPECIFIED 
 * 父 ViewGroup 没有对子View施加任何约束,子 view 可以是任意大小。这种情况比较少见,主要用于系统内部多次measure的情形,
* 用到的一般都是可以滚动的容器中的子view,比如ListView、GridView、RecyclerView中某些情况下的子view就是这种模式。
* 一般来说,我们不需要关注此模式。  * 2.EXACTLY   * 该 view 必须使用父 ViewGroup 给其指定的尺寸。对应 match_parent 或者具体数值(比如30dp)  * 3.AT_MOST   * 该 View 最大可以取父ViewGroup给其指定的尺寸。对应wrap_content  *    * MeasureSpec使用了二进制去减少对象的分配。   
*/   public class MeasureSpec {            进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和第二高位也就是32和31位做标志位)           private static final int MODE_SHIFT = 30;            运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)            (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)           int MODE_MASK  = 0x3 << MODE_SHIFT;            0向左进位30,就是00 00000000000(00后跟30个0)           int UNSPECIFIED = 0 << MODE_SHIFT;            1向左进位30,就是01 00000000000(01后跟30个0)           int EXACTLY     = 1 << 2向左进位30,就是10 00000000000(10后跟30个0)           int AT_MOST     = 2 <<           * 根据提供的size和mode得到一个详细的测量结果                       第一个return:          measureSpec = size + mode;   (注意:二进制的加法,不是十进制的加法!)            这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值            例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100                      第二个return:          size &; ~MODE_MASK就是取size 的后30位,mode &amp; MODE_MASK就是取mode的前两位,最后执行或运算,得出来的数字,前面2位包含代表mode,后面30位代表size         int makeMeasureSpec(int size, int mode) {               if (sUsebrokenMakeMeasureSpec) {                 return size + mode;             } else {                 return (size & ~MODE_MASK) | (mode &  MODE_MASK);             }         }                      * 获得SpecMode           mode = measureSpec &amp; MODE_MASK;            MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。            例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值           int getMode( measureSpec) {               return (measureSpec & MODE_MASK);           }                      * 获得Specsize            size = measureSpec &  ~MODE_MASK;            原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size           int getSize(return (measureSpec &  ~MODE_MASK);           }   }  

顺便提下 MATCH_PARENT 和 WRAP_CONTENT 这两个代表的值,分别是 -1 和 -2。

       
         * Special value for the height or width requested by a View.
         * MATCH_PARENT means that the view wants to be as big as its parent,* minus the parent''s padding,if any. Introduced in API Level 8.
         */
        public static final int MATCH_PARENT = -1;

        
         * Special value for the height or width requested by a View.
         * WRAP_CONTENT means that the view wants to be just large enough to fit
         * its own internal content,taking its own padding into account.
         int WRAP_CONTENT = -2;

Decorview 尺寸的确定

在 performTraversals 中,首先是要确定 DecorView 的尺寸。只有当 DecorView 尺寸确定了,其子 View 才可以知道自己能有多大。具体是如何去确定的,可以看下面的代码:

Activity窗口的宽度和高度
 desiredWindowWidth;
 desiredWindowHeight;
...
用来保存窗口宽度和高度,来自于全局变量mWinFrame,这个mWinFrame保存了窗口最新尺寸
Rect frame = mWinFrame;
构造方法里mFirst赋值为true,意思是第一次执行遍历吗    
 (mFirst) {
    是否需要重绘
    mFullRedrawNeeded = true;
    是否需要重新确定Layout
    mLayoutRequested = ;
    
    // 这里又包含两种情况:是否包括状态栏
    
    判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度
     (shouldUsedisplaySize(lp)) {
         NOTE -- system code,won''t try to do compat mode.
        Point size = new Point();
        mdisplay.getRealSize(size);
        desiredWindowWidth = size.x;
        desiredWindowHeight = size.y;
    }  {
        宽度和高度为整个屏幕的值
        Configuration config = mContext.getResources().getConfiguration();
        desiredWindowWidth = dipToPx(config.screenWidthDp);
        desiredWindowHeight = dipToPx(config.screenHeightDp);
    }
    ...
 else{
    
        // 这是window的长和宽改变了的情况,需要对改变的进行数据记录
    
        如果不是第一次进来这个方法,它的当前宽度和高度就从之前的mWinFrame获取
        desiredWindowWidth = frame.width();
        desiredWindowHeight = frame.height();
        
         * mWidth和mHeight是由WindowManagerService服务计算出的窗口大小,
         * 如果这次测量的窗口大小与这两个值不同,说明WMS单方面改变了窗口的尺寸
         if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
            if (DEBUG_ORIENTATION) Log.v(mTag,"View " + host + " resized to: " + frame);
            需要进行完整的重绘以适应新的窗口尺寸
            mFullRedrawNeeded = ;
            需要对控件树进行重新布局
            mLayoutRequested = window窗口大小改变
            windowSizeMayChange = ;
        }
 }
    ...
    // 进行预测量
     (layoutRequested){
        ...
         (mFirst) {
             视图窗口当前是否处于触摸模式。
            mAttachInfo.mInTouchMode = !mAddedTouchMode;
            确保这个Window的触摸模式已经被设置
            ensuretouchModeLocally(mAddedTouchMode);
        }  {
            六个if语句,判断insects值和上一次比有什么变化,不同的话就改变insetsChanged
            insects值包括了一些屏幕需要预留的区域、记录一些被遮挡的区域等信息
            if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
                    insetsChanged = ;
            }
            ...
            
          //  这里有一种情况,我们在写dialog时,会手动添加布局,当设定宽高为Wrap_content时,会把屏幕的宽高进行赋值,给出尽量长的宽度
            
            
             * 如果当前窗口的根布局的width或height被指定为 WRAP_CONTENT 时,
             * 比如Dialog,那我们还是给它尽量大的长宽,这里是将屏幕长宽赋值给它
             */
            if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                    || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                windowSizeMayChange = ;
                判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度
                 (shouldUsedisplaySize(lp)) {
                     Point();
                    mdisplay.getRealSize(size);
                    desiredWindowWidth = size.x;
                    desiredWindowHeight = size.y;
                }  {
                    Configuration config = res.getConfiguration();
                    desiredWindowWidth = dipToPx(config.screenWidthDp);
                    desiredWindowHeight = dipToPx(config.screenHeightDp);
                }
            }
        }
    }
}

这里主要是分两步走:

  1. 如果是第一次测量,那么根据是否有状态栏,来确定是直接使用屏幕的高度,还是真正的显示区高度。

  2. 如果不是第一次,那么从 mWinFrame 获取,并和之前保存的长宽高进行比较,不相等的话就需要重新测量确定高度。

当确定了 DecorView 的具体尺寸之后,然后就会调用 measureHierarchy 来确定其 MeasureSpec :

  Ask host how big it wants to be
  windowSizeMayChange |= measureHierarchy(host,lp,res,desiredWindowWidth,desiredWindowHeight); 

其中 host 就是 DecorView,lp 是 wm 在添加时候传给 DecorView 的,最后两个就是刚刚确定显示宽高 ,看下方法的具体逻辑 :

    boolean measureHierarchy(final View host,final WindowManager.LayoutParams lp,final Resources res,1)">int desiredWindowWidth,1)"> desiredWindowHeight) {
         childWidthMeasureSpec;
         childHeightMeasureSpec;
        boolean windowSizeMayChange = false;boolean goodMeasure = ;
     // 说明是 dialog
ViewGroup.LayoutParams.WRAP_CONTENT) { On large screens,we don''t want to allow dialogs to just stretch to fill the entire width of the screen to display one line of text. First try doing the layout at a smaller size to see if it will fit. final displayMetrics packageMetrics = res.getdisplayMetrics(); res.getValue(com.android.internal.R.dimen.config_prefDialogWidth,mTmpValue,1)">); int baseSize = 0       // 获取一个基本的尺寸 if (mTmpValue.type == TypedValue.TYPE_DIMENSION) { baseSize = ()mTmpValue.getDimension(packageMetrics); } if (DEBUG_DIALOG) Log.v(mTag,"Window " + mView + ": baseSize=" + baseSize + ",desiredWindowWidth=" + desiredWindowWidth);
       // 如果大于基本尺寸
if (baseSize != 0 && desiredWindowWidth > baseSize) { childWidthMeasureSpec = getRootMeasureSpec(baseSize,lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height); performMeasure(childWidthMeasureSpec,childHeightMeasureSpec); host.getMeasuredHeight() + ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec) + " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));
          // 判断测量是否准确
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { goodMeasure = ; } { Didn''t fit in that size... try expanding a bit. baseSize = (baseSize+desiredWindowWidth)/2; baseSize); childWidthMeasureSpec = performMeasure(childWidthMeasureSpec,childHeightMeasureSpec); ); ) { ); goodMeasure = ; } } } }      // 这里就是一般 DecorView 会走的逻辑 goodMeasure) { childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width); childHeightMeasureSpec = performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
       // 与之前的尺寸进行对比,看看是否相等,不想等,说明尺寸可能发生了变化
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = ; } } return windowSizeMayChange; }

上面主要主要做的就是来确定父 View 的 MeasureSpec。但是分了两种不同类型:

  • 如果宽是 WRAP_CONTENT 类型,说明这是 dialog,会有一些针对 dialog 的处理,最终会调用 performMeasure 进行测量;

  • 对于一般 Activity 的尺寸,会调用  getRootMeasureSpec MeasureSpec 。

下面看下 DecorView MeasureSpec 的计算方法:

    int getRootMeasureSpec(int windowSize,1)"> rootDimension) {
         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;
         ViewGroup.LayoutParams.WRAP_CONTENT:
             Window can resize. Set max size for root view.
            measureSpec =default:
             Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension,1)">;
        }
        return measureSpec;
    }

 该方法主要是根据 View 的 MeasureSpec 是根据宽高的参数来划分的。

  • MATCH_PARENT :精确模式,大小就是窗口的大小;

  • WRAP_CONTENT :最大模式,大小不定,但是不能超过窗口的大小;

  • 固定大小:精确模式,大小就是指定的具体宽高,比如100dp。

对于 DecorView 来说就是走第一个 case,到这里 DecorView 的 MeasureSpec 就确定了,从 MeasureSpec 可以得出 DecorView 的宽高的约束信息。

获取子 view 的 MeasureSpec

当父 ViewGroup 对子 View 进行测量时,会调用 View 类的 measure 方法,这是一个 final 方法,无法被重写。ViewGroup 会传入自己的 widthMeasureSpec 和  heightMeasureSpec,分别表示父 View 对子 View 的宽度和高度的一些限制条件。尤其是当 ViewGroup 是 WRAP_CONTENT 的时候,需要优先测量子 View,只有子 View 宽高确定,ViewGroup 才能确定自己到底需要多大的宽高。

当 DecorView 的 MeasureSpec 确定以后,ViewRootImpl 内部会调用 performMeasure 方法:

  void performMeasure(int childWidthMeasureSpec,1)"> childHeightMeasureSpec) {
        if (mView == null) {
            ;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW,"measure");
        try {
            mView.measure(childWidthMeasureSpec,childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

该方法传入的是对 DecorView 的 MeasureSpec,其中 mView 就是 DecorView 的实例,接下来看 measure() 的具体逻辑:


 * 调用这个方法来算出一个View应该为多大。参数为父View对其宽高的约束信息。
 * 实际的测量工作在onMeasure()方法中进行
 */
void measure(int widthMeasureSpec,1)"> heightMeasureSpec) {
  ......
  // Suppress sign extension for the low bytes
   long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
   if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT标记,则强制重新布局
   比如调用View.requestLayout()会在mPrivateFlags中加入此标记
  boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
  boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
      || heightMeasureSpec != mOldHeightMeasureSpec;
  boolean isspecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
      && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
  boolean matchesSpecsize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
      && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
  boolean needsLayout = specChanged
      && (sAlwaysRemeasureExactly || !isspecExactly || !matchesSpecsize);

   需要重新布局  
  if (forceLayout || needsLayout) {
    // first clears the measured dimension flag 标记为未测量状态
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
  // 对阿拉伯语、希伯来语等从右到左书写、布局的语言进行特殊处理
resolveRtlPropertiesIfNeeded();
 先尝试从缓从中获取,若forceLayout为true或是缓存中不存在或是
     忽略缓存,则调用onMeasure()重新进行测量工作
    int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
       measure ourselves,this should set the measured dimension flag back
      onMeasure(widthMeasureSpec,heightMeasureSpec);
      . . .
    }  {
       缓存命中,直接从缓存中取值即可,不必再测量
      long value = mMeasureCache.valueAt(cacheIndex);
       Casting a long to int drops the high 32 bits,no mask needed
      setMeasuredDimensionRaw((int) (value >> 32),() value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
   // 如果自定义的View重写了onMeasure方法,但是没有调用setMeasuredDimension()方法就会在这里抛出错误;
   // 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()");
   } 
      //到了这里,View已经测量完了并且将测量的结果保存在View的mMeasuredWidth和mMeasuredHeight中,将标志位置为可以layout的状态

    mPrivateFlags |= PFLAG_LAYOUT_required;

  }
  mOldWidthMeasureSpec = widthMeasureSpec;
  mOldHeightMeasureSpec = heightMeasureSpec;
// 保存到缓存中 mMeasureCache.put(key,((
long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); suppress sign extension }

 这里要注意的是,这是一个 final 方法,不能被继承。这个方法只在 View 类里面。总结一下 measure() 都干了什么事:

  • 调用 View.measure()方法时 View 并不是立即就去测量,而是先判断一下要不要进行测量操作,如果没必要,那么 View 就不需要重新测量了,避免浪费时间资源

  • 如果需要测量,在测量之前,会先判断是否存在缓存,存在直接从缓存中获取就可以了,再调用一下 setMeasuredDimensionRaw 方法,将从缓存中读到的测量结果保存到成员变量 mMeasuredWidth 和 mMeasuredHeight 中。

  • 如果不能从 mMeasureCache 中读到缓存过的测量结果,调用 onMeasure() 方法去完成实际的测量工作,并且将尺寸限制条件 widthMeasureSpec 和 heightMeasureSpec 传递给 onMeasure() 方法。关于 onMeasure() 方法,会在下面详细介绍。

  • 将结果保存到 mMeasuredWidth 和 mMeasuredHeight 这两个成员变量中,同时缓存到成员变量 mMeasureCache 中,以便下次执行 measure() 方法时能够从其中读取缓存值。

  • 需要说明的是,View 有一个成员变量 mPrivateFlags,用以保存 View 的各种状态位,在测量开始前,会将其设置为未测量状态,在测量完成后会将其设置为已测量状态。

DecorView 是 FrameLayout 子类,这时候应该去看 FrameLayout 中的 onMeasure() 方法,代码具体如下:

 protected void onMeasure( heightMeasureSpec) {
     // 获取子view的个数
int count = getChildCount(); boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; mMatchParentChildren.clear(); int maxHeight = 0int maxWidth = 0int childState = 0for (int i = 0; i < count; i++final View child = getChildAt(i);
       // mMeasureAllChildren 默认为FALSE,表示是否全部子 view 都要测量,子view不为GONE就要测量
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 测量子view measureChildWithMargins(child,widthMeasureSpec,
0,heightMeasureSpec,0);
// 获取子view的布局参数
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 记录子view的最大宽度和高度 maxWidth
= Math.max(maxWidth,child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); maxHeight = Math.max(maxHeight,child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = combineMeasuredStates(childState,child.getMeasuredState());
// 记录所有跟父布局有着相同宽或高的子view
(measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } } Account for padding too 子view的最大宽高计算出来后,还要加上父View自身的padding maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground(); maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground(); Check against our minimum height and width maxHeight = Check against our foreground''s minimum height and width final Drawable drawable = getForeground(); if (drawable != ) { maxHeight =     // 确定父 view 的宽高 setMeasuredDimension(resolveSizeAndState(maxWidth,childState),resolveSizeAndState(maxHeight,childState << MEASURED_HEIGHT_STATE_SHIFT)); count = mMatchParentChildren.size(); if (count > 1) { mMatchParentChildren.get(i); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); childWidthMeasureSpec;
          // 如果子view的宽是MATCH_PARENT,那么宽度 = 父view的宽 - 父Padding - 子Margin
LayoutParams.MATCH_PARENT) { int width = Math.max(0,getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width,MeasureSpec.EXACTLY); } { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin,lp.width); } childHeightMeasureSpec;
          // 同理
if (lp.height ==int height = Math.max(0 getPaddingBottomWithForeground() - lp.topMargin - lp.bottomMargin); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( height,1)"> { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin,lp.height); } child.measure(childWidthMeasureSpec,childHeightMeasureSpec); } } }

FrameLayout 是 ViewGroup 的子类,后者有一个 View[] 类型的成员变量 mChildren,代表了其子 View 集合。通过 getChildAt(i) 能获取指定索引处的子 View,通过 getChildCount() 可以获得子 View 的总数。

在上面的源码中,首先调用 measureChildWithMargins() 方法对所有子 View 进行了一遍测量,并计算出所有子 View 的最大宽度和最大高度。而后将得到的最大高度和宽度加上padding,这里的 padding 包括了父 View 的 padding 和前景区域的 padding。然后会检查是否设置了最小宽高,并与其比较,将两者中较大的设为最终的最大宽高。最后,若设置了前景图像,我们还要检查前景图像的最小宽高。

经过了以上一系列步骤后,我们就得到了 maxHeight 和 maxWidth 的最终值,表示当前容器 View 用这个尺寸就能够正常显示其所有子 View(同时考虑了 padding 和 margin )。而后我们需要调用 resolveSizeAndState() 方法来结合传来的 MeasureSpec 来获取最终的测量宽高,并保存到 mMeasuredWidth 与 mMeasuredHeight 成员变量中。

如果存在一些子 View 的宽或高是 MATCH_PARENT,那么需要等父 View 的尺寸计算出来后,重新计算这些子 View 的 MeasureSpec,再来测量这些子 view 的宽高。

这里提醒我们在自定义 View 的时候需要考虑你的子 View 是不是和你自定义的 View 的大小是一样,如果一样,就需要等自定义 View 的大小确定了,再重新测量一遍。

下面看看 measureChildWithMargins() 方法具体逻辑:


     * Ask one of the children of this view to measure itself,taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     *  parentWidthMeasureSpec The width requirements for this view
     *  widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     *  parentHeightMeasureSpec The height requirements for this view
     *  heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
     measureChildWithMargins(View child,1)">int parentWidthMeasureSpec,1)"> widthUsed,1)">int parentHeightMeasureSpec,1)"> heightUsed) {
         (MarginLayoutParams) child.getLayoutParams();

        int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        +int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed,lp.height);

        child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
    }

 该方法主要是获取子 View 的 MeasureSpec,然后调用 child.measure() 来完成子 View 的测量。下面看看子 View 获取 MeasureSpec 的具体逻辑:

 int getChildMeasureSpec(int spec,1)">int padding,1)"> childDimension) {
// 父 view 的 mode 和 size int specMode = MeasureSpec.getMode(spec); int specsize = MeasureSpec.getSize(spec);      // 去掉 padding int size = Math.max(0,specsize - padding); int resultSize = 0int resultMode = 0 (specMode) { Parent has imposed an exact size on us MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { Child wants to be our size. So be it. resultSize = size; resultMode = LayoutParams.WRAP_CONTENT) { Child wants to determine its own size. It can''t be bigger than us. resultSize = MeasureSpec.AT_MOST; } Parent has imposed a maximum size on us MeasureSpec.AT_MOST: Child wants a specific size... so be it resultSize = Child wants to be our size,but our size is not fixed. Constrain child to not be bigger than us. resultSize = MeasureSpec.AT_MOST; } Parent asked to see how big we want to be MeasureSpec.UNSPECIFIED: Child wants a specific size... let him have it resultSize = Child wants to be our size... find out how big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } Child wants to determine its own size.... find out how big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 MeasureSpec.UNSPECIFIED; } noinspection ResourceType MeasureSpec.makeMeasureSpec(resultSize,resultMode); }

 方法清楚展示了普通 View 的 MeasureSpec 的创建规则,每个 View 的 MeasureSpec 状态量由其直接父 View 的 MeasureSpec 和 View 自身的属性 LayoutParams (LayoutParams 有宽高尺寸值等信息)共同决定。

从上面的代码可以知道,返回 View 的 MeasureSpec 大致可以分为一下机制情况:
  • 子 View 为具体的宽/高,那么 View 的 MeasureSpec 都为 LayoutParams 中大小。

  • 子 View 为 match_parent,父元素为精度模式(EXACTLY),那么 View 的 MeasureSpec 也是精准模式他的大小不会超过父容器的剩余空间。

  • 子 View 为 wrap_content,不管父元素是精准模式还是最大化模式(AT_MOST),View 的 MeasureSpec 总是为最大化模式并且大小不超过父容器的剩余空间。

  • 父容器为 UNSPECIFIED 模式主要用于系统多次 Measure 的情形,一般我们不需要关心。

总结为下表:

 View.measure()  代码逻辑前面已经分析过了,最终会调用 onMeasuere 方法,下面看下 View.onMeasuere() 的代码:

 protected int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
    }

上面方法中调用了 方法中调用了 setMeasuredDimension()方法,setMeasuredDimension()又调用了 getDefaultSize() 方法。getDefaultSize() 又调用了getSuggestedMinimumWidth()和 getSuggestedMinimumHeight(),那反向研究一下,先看下 getSuggestedMinimumWidth() 方法  (getSuggestedMinimumHeight() 原理 getSuggestedMinimumWidth() 跟一样)。 

  getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
    }

 源码很简单,如果 View 没有背景,就直接返回 View 本身的最小宽度 mMinWidth;如果给 View 设置了背景,就取 View 本身的最小宽度 mMinWidth 和背景的最小宽度的最大值.

那么 mMinWidth 是哪里来的?搜索下源码就可以知道,View 的最小宽度 mMinWidth 可以有两种方式进行设置:

  • 第一种是在 View 的构造方法中进行赋值的,View 通过读取 XML 文件中View设置的 minWidth 属性来为 mMinWidth 赋值
 R.styleable.View_minWidth:
     mMinWidth = a.getDimensionPixelSize(attr,1)">);
     break;
  •  第二种是在调用 View 的 setMinimumWidth 方法为 mMinWidth 赋值
void setMinimumWidth( minWidth) {
    mMinWidth = minWidth;
    requestLayout();
}

 下面看下 getDefaultSize() 的代码逻辑:

    int getDefaultSize(int size,1)"> measureSpec) {
        int result = size;
         MeasureSpec.getMode(measureSpec);
         MeasureSpec.getSize(measureSpec);

         MeasureSpec.UNSPECIFIED:
            result = size;
             MeasureSpec.AT_MOST:
         MeasureSpec.EXACTLY:
            result = specsize;
             result;
    }

从注释可以看出,getDefaultSize()这个测量方法并没有适配 wrap_content 这一种布局模式,只是简单地将 wrap_content 跟 match_parent 等同起来。

到了这里,我们要注意一个问题:

getDefaultSize()方法中 wrap_content 和 match_parent 属性的效果是一样的,而该方法是 View 的 onMeasure()中默认调用的,也就是说,对于一个直接继承自 View 的自定义 View 来说,它的 wrap_content 和 match_parent 属性是一样的效果,因此如果要实现自定义 View 的 wrap_content,则要重写 onMeasure() 方法,对 wrap_content 属性进行处理。

如何处理呢?也很简单,代码如下所示:

protected void onMeasure(int widthMeasureSpec,1)"> heightMeasureSpec){
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  取得父ViewGroup指定的宽高测量模式和尺寸
  int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
  int widthSpecsize = MeasureSpec.getSize(widthMeasureSpec);
  int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
  int heightSpecsize = MeasureSpec.getSize(heightMeasureSpec);
  if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
    如果宽高都是AT_MOST的话,即都是wrap_content布局模式,就用View自己想要的宽高值
        setMeasuredDimension(mWidth, mHeight);
  }else if (widthSpecMode ==如果只有宽度都是AT_MOST的话,即只有宽度是wrap_content布局模式,宽度就用View自己想要的宽度值,高度就用父ViewGroup指定的高度值
pecsize);
  }if (heightSpecMode ==如果只有高度都是AT_MOST的话,即只有高度是wrap_content布局模式,高度就用View自己想要的宽度值,宽度就用父ViewGroup指定的高度值
        setMeasuredDimension(widthSpecsize, mHeight);
  }
}

 在上面的代码中,我们要给 View 指定一个默认的内部宽/高(mWidth 和 mHeight),并在 wrap_content 时设置此宽/高即可。最后将在将宽高设置到 View 上:

    // View    
    protected void setMeasuredDimension(int measuredWidth,1)"> measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getopticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
 
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

    void setMeasuredDimensionRaw( measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

这里就是把测量完的宽高值赋值给 mMeasuredWidthmMeasuredHeight 这两个 View 的属性,然后将标志位置为已测量状态。

子 View 测量完成以后,会计算 childState,看下 combineMeasuredStates 方法 :

int combineMeasuredStates(int curState,1)"> newState) {
        return curState | newState;
    }

 当前 curState 为 0, newState 是调用 child.getMeasuredState() 方法得到的,来看下这个方法的具体逻辑:

 
     * Return only the state bits of {@link #getMeasuredWidthAndState()}
     * and { #getMeasuredHeightAndState()},combined into one integer.
     * The width component is in the regular bits { #MEASURED_STATE_MASK}
     * and the height component is at the shifted bits
     * { #MEASURED_HEIGHT_STATE_SHIFT}>>{ #MEASURED_STATE_MASK}.
      getMeasuredState() {
        return (mMeasuredWidth&MEASURED_STATE_MASK)
                | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
                        & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
    }

 该方法返回一个 int 值,该值同时包含宽度的 state 以及高度的 state 信息,不包含任何的尺寸信息。

  • MEASURED_STATE_MASK 的值为 0xff000000,其高字节的 8 位全部为 1,低字节的 24 位全部为 0。

  • MEASURED_HEIGHT_STATE_SHIFT 值为 16。

  • MEASURED_STATE_MASK 与 mMeasuredWidth 做与操作之后就取出了存储在宽度首字节中的 state 信息,过滤掉低位三个字节的尺寸信息。

  • 由于 int 有四个字节,首字节已经存了宽度的 state 信息,那么高度的 state 信息就不能存在首位字节。MEASURED_STATE_MASK 向右移 16 位,变成了 0x0000ff00,这个值与高度值 mMeasuredHeight 做与操作就取出了 mMeasuredHeight 第三个字节中的信息。而 mMeasuredHeight 的 state 信息是存在首字节中,所以也得对mMeasuredHeight 向右移相同的位置,这样就把 state 信息移到了第三个字节中。

  • 最后,将得到的宽度 state 与高度 state 按位或操作,这样就拼接成一个 int 值,该值首个字节存储宽度的 state 信息,第三个字节存储高度的 state 信息。

这些都得到之后,就可以开始去计算父 View 的尺寸了:

        // 确定父 View 的宽高
        setMeasuredDimension(resolveSizeAndState(maxWidth,1)">resolveSizeAndState(maxHeight,childState << MEASURED_HEIGHT_STATE_SHIFT));

 下面开始看 resolveSizeAndState 具体逻辑:

 View 的静态方法
int resolveSizeAndState(int measureSpec,1)"> childMeasuredState) {
         MeasureSpec.getSize(measureSpec);
         result;
         (specMode) {
             MeasureSpec.AT_MOST:
                if (specsize < size) {
                    result = specsize | MEASURED_STATE_TOO_SMALL;
                }  {
                    result = size;
                }
                 MeasureSpec.EXACTLY:
                result = specsize;
                :
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

这个方法的代码结构跟前文提到的 getDefaultSize()方法很相似,主要的区别在于 specMode 为 AT_MOST 的情况。我们当时说 getDefaultSize() 方法是没有适配wrap_content 这种情况,而这个 resolveSizeAndState() 方法是已经适配了 wrap_content 的布局方式,那具体怎么实现 AT_MOST 测量逻辑的呢?有两种情况:

  • 当父 ViewGroup 指定的最大尺寸比 View 想要的尺寸还要小时,会给这个父 ViewGroup  的指定的最大值 specsize 加入一个尺寸太小的标志  MEASURED_STATE_TOO_SMALL,然后将这个带有标志的尺寸返回,父 ViewGroup 通过该标志就可以知道分配给 View 的空间太小了,在窗口协商测量的时候会根据这个标志位来做窗口大小的决策。

  • 当父 ViewGroup 指定的最大尺寸比没有比 View 想要的尺寸小时(相等或者 View 想要的尺寸更小),直接取 View 想要的尺寸,然后返回该尺寸。

getDefaultSize() 方法只是 onMeasure() 方法中获取最终尺寸的默认实现,其返回的信息比 resolveSizeAndState() 要少,那么什么时候才会调用 resolveSizeAndState() 方法呢? 主要有两种情况:

  • Android 中的大部分 ViewGroup 类都调用了 resolveSizeAndState() 方法,比如 LinearLayout 在测量过程中会调用 resolveSizeAndState() 方法而非 getDefaultSize()方法。

  • 我们自己在实现自定义的 View 或 ViewGroup 时,我们可以重写 onMeasure() 方法,并在该方法内调用 resolveSizeAndState() 方法。

到此,终于把 View 测量过程讲完了。

下一篇开始讲 View 的 layout 和 draw 过程。

 

参考文章

Android源码完全解析——View的Measure过程

View的绘制流程 

总结

以上是小编为你收集整理的Android View 的绘制流程之 Measure 过程详解 (一) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 绘制流程之 DecorView 与 ViewRootImp全部内容。

如果觉得小编网站内容还不错,欢迎将小编网站推荐给好友。

Android View 绘制流程之 DecorView 与 ViewRootImpl Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 的测量流程详解

Android View 绘制流程之 DecorView 与 ViewRootImpl Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 的测量流程详解

一年多以前,曾经以为自己对 View 的添加显示逻辑已经有所了解了,事后发现也只是懂了些皮毛而已。经过一年多的实战,Android 和 Java 基础都有了提升,是时候该去看看 DecorView 的添加显示。

View 的绘制系列文章:

  • Android View 绘制流程之 DecorView 与 ViewRootImpl

  • Android View 的绘制流程之 Measure 过程详解 (一)

  • Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)

  • Android View 的事件分发原理解析

  • Android 自定义 View 详解

概论

Android 中 Activity 是作为应用程序的载体存在,代表着一个完整的用户界面,提供了一个窗口来绘制各种视图,当 Activity 启动时,我们会通过 setContentView 方法来设置一个内容视图,这个内容视图就是用户看到的界面。那么 View 和 activity 是如何关联在一起的呢 ?

 上图是 View 和 Activity 之间的关系。先解释图中一些类的作用以及相关关系:

  • Activity : 对于每一个 activity 都会有拥有一个 PhoneWindow。

  • PhoneWindow :该类继承于 Window 类,是 Window 类的具体实现,即我们可以通过该类具体去绘制窗口。并且,该类内部包含了一个 DecorView 对象,该 DectorView 对象是所有应用窗口的根 View。
  • DecorView 是一个应用窗口的根容器,它本质上是一个 FrameLayout。DecorView 有唯一一个子 View,它是一个垂直 LinearLayout,包含两个子元素,一个是 TitleView( ActionBar 的容器),另一个是 ContentView(窗口内容的容器)。

  • ContentView :是一个 FrameLayout(android.R.id.content),我们平常用的 setContentView 就是设置它的子 View 。

  • WindowManager : 是一个接口,里面常用的方法有:添加View,更新View和删除View。主要是用来管理 Window 的。WindowManager 具体的实现类是WindowManagerImpl。最终,WindowManagerImpl 会将业务交给 WindowManagerGlobal 来处理。
  • WindowManagerService (WMS) : 负责管理各 app 窗口的创建,更新,删除, 显示顺序。运行在 system_server 进程。

ViewRootImpl :拥有 DecorView 的实例,通过该实例来控制 DecorView 绘制。ViewRootImpl 的一个内部类 W,实现了 IWindow 接口,IWindow 接口是供 WMS 使用的,WSM 通过调用 IWindow 一些方法,通过 Binder 通信的方式,最后执行到了 W 中对应的方法中。同样的,ViewRootImpl 通过 IWindowSession 来调用 WMS 的 Session 一些方法。Session 类继承自 IWindowSession.Stub,每一个应用进程都有一个唯一的 Session 对象与 WMS 通信。

DecorView 的创建 

先从 Mainactivity 中的代码看起,首先是调用了 setContentView;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

该方法是父类 AppCompatActivity 的方法,最终会调用 AppCompatDelegateImpl 的 setContentView 方法:

// AppCompatDelegateImpl  
public void setContentView(int resId) { this.ensureSubDecor(); ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290); contentParent.removeAllViews(); LayoutInflater.from(.mContext).inflate(resId,contentParent); .mOriginalWindowCallback.onContentChanged(); }

ensureSubDecor 从字面理解就是创建 subDecorView,这个是根据主题来创建的,下文也会讲到。创建完以后,从中获取 contentParent,再将从 activity 传入的 id xml 布局添加到里面。不过大家注意的是,在添加之前先调用 removeAllViews() 方法,确保没有其他子 View 的干扰。

    private  ensureSubDecor() {
        if (!.mSubDecorInstalled) {
            this.mSubDecor = .createSubDecor(); 
            ......
        }
        ......
    }        

 最终会调用 createSubDecor() ,来看看里面的具体代码逻辑:

 private ViewGroup createSubDecor() {
        // 1、获取主题参数,进行一些设置,包括标题,actionbar 等 
        TypedArray a = .mContext.obtainStyledAttributes(styleable.AppCompatTheme);
        a.hasValue(styleable.AppCompatTheme_windowActionBar)) {
            a.recycle();
            throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        } else {
            if (a.getBoolean(styleable.AppCompatTheme_windowNoTitle,false)) {
                this.requestwindowFeature(1);
            } else if (a.getBoolean(styleable.AppCompatTheme_windowActionBar,1)">this.requestwindowFeature(108);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionBarOverlay,1)">this.requestwindowFeature(109if (a.getBoolean(styleable.AppCompatTheme_windowActionModeOverlay,1)">this.requestwindowFeature(10this.mIsFloating = a.getBoolean(styleable.AppCompatTheme_android_windowIsFloating,1)">);
            a.recycle();
             2、确保优先初始化 DecorView
            .mWindow.getDecorView();
            LayoutInflater inflater = LayoutInflater.from(.mContext);
            ViewGroup subDecor = null;
             3、根据不同的设置来对 subDecor 进行初始化
            .mWindowNoTitle) {
                if (.mIsFloating) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_dialog_title_material,(ViewGroup));
                    this.mHasActionBar = this.mOverlayActionBar = ;
                } .mHasActionBar) {
                    TypedValue outValue = new TypedValue();
                    this.mContext.getTheme().resolveAttribute(attr.actionBarTheme,outValue,1)">true);
                    Object themedContext;
                    if (outValue.resourceId != 0) {
                        themedContext = new ContextThemeWrapper(.mContext,outValue.resourceId);
                    }  {
                        themedContext = .mContext;
                    }

                    subDecor = (ViewGroup)LayoutInflater.from((Context)themedContext).inflate(layout.abc_screen_toolbar,1)">this.mDecorContentParent = (DecorContentParent)subDecor.findViewById(id.decor_content_parent);
                    this.mDecorContentParent.setwindowCallback(.getwindowCallback());
                    .mOverlayActionBar) {
                        this.mDecorContentParent.initFeature(109);
                    }

                    .mFeatureProgress) {
                        this.mDecorContentParent.initFeature(2.mFeatureIndeterminateProgress) {
                        this.mDecorContentParent.initFeature(5);
                    }
                }
            }  {
                .mOverlayActionMode) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple_overlay_action_mode,1)">);
                }  {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple,1)">);
                }

                if (VERSION.SDK_INT >= 21) {
                    ViewCompat.setonApplyWindowInsetsListener(subDecor, OnApplyWindowInsetsListener() {
                        public WindowInsetsCompat onApplyWindowInsets(View v,WindowInsetsCompat insets) {
                            int top = insets.getSystemWindowInsetTop();
                            int newTop = AppCompatDelegateImpl..updateStatusGuard(top);
                            if (top != newTop) {
                                insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(),newTop,insets.getSystemWindowInsetRight(),insets.getSystemWindowInsetBottom());
                            }

                            return ViewCompat.onApplyWindowInsets(v,insets);
                        }
                    });
                }  {
                    ((FitwindowsViewGroup)subDecor).setonFitSystemWindowsListener( OnFitSystemWindowsListener() {
                         onFitSystemWindows(Rect insets) {
                            insets.top = AppCompatDelegateImpl..updateStatusGuard(insets.top);
                        }
                    });
                }
            }

            if (subDecor == ) {
                new IllegalArgumentException("AppCompat does not support the current theme features: { windowActionBar: " + this.mHasActionBar + ",windowActionBarOverlay: " + this.mOverlayActionBar + ",android:windowIsFloating: " + this.mIsFloating + ",windowActionModeOverlay: " + this.mOverlayActionMode + ",windowNoTitle: " + this.mWindowNoTitle + " }"this.mDecorContentParent == ) {
                    this.mTitleView = (TextView)subDecor.findViewById(id.title);
                }

                ViewUtils.makeOptionalFitsSystemWindows(subDecor);
                ContentFrameLayout contentView = (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content);
                ViewGroup windowContentView = (ViewGroup)this.mWindow.findViewById(16908290);
                if (windowContentView != while(windowContentView.getChildCount() > 0) {
                        View child = windowContentView.getChildAt(0);
                        windowContentView.removeViewAt(0);
                        contentView.addView(child);
                    }

                    windowContentView.setId(-1);
                    contentView.setId(16908290if (windowContentView instanceof FrameLayout) {
                        ((FrameLayout)windowContentView).setForeground((Drawable));
                    }
                }
                 将 subDecor 添加到 DecorView 中
                .mWindow.setContentView(subDecor);
                contentView.setAttachListener( OnAttachListener() {
                     onAttachedFromWindow() {
                    }

                     onDetachedFromWindow() {
                        AppCompatDelegateImpl..dismisspopups();
                    }
                });
                 subDecor;
            }
        }
    }
                    

上面的代码总结来说就是在做一件事,就是创建 subDecor。摊开来说具体如下:

1、根据用户选择的主题来设置一些显示特性,包括标题,actionbar 等。

2、根据不同特性来初始化 subDecor;对 subDecor 内部的子 View 进行初始化。

3、最后添加到 DecorView中。

添加的具体代码如下:此处是通过调用 

  // AppCompatDelegateImpl
  this.mWindow.getDecorView();

   // phoneWindow 
   public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }
 

void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            // 生成 DecorView
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            // 这样 DecorView 就持有了window
            mDecor.setwindow(this);
        }
      ......
}


   protected DecorView generateDecor(int featureId) {
        // System process doesn''t have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context,so we don''t cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext,getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context,featureId,this,getAttributes());
   }

到此,DecorView 的创建就讲完了。可是我们似乎并没有看到 DecorView 是被添加的,什么时候对用户可见的。

 WindowManager

View 创建完以后,那 Decorview 是怎么添加到屏幕中去的呢?当然是 WindowManager 呢,那么是如何将 View 传到 WindowManager 中呢。

看 ActivityThread 中的 handleResumeActivity 方法:

// ActivityThread
public void handleResumeActivity(IBinder token,1)">boolean finalStateRequest,1)">boolean isForward,String reason) { ...... final int forwardBit = isForward ? WindowManager.LayoutParams.soFT_INPUT_IS_FORWARD_NAVIGATION : 0; If the window hasn''t yet been added to the window manager,1)"> and this guy didn''t finish itself or start another activity,1)"> then go ahead and add the window. boolean willBeVisible = !a.mStartedActivity; willBeVisible) { try { willBeVisible = ActivityManager.getService().willActivityBeVisible( a.getActivityToken()); } catch (remoteexception e) { throw e.rethrowFromSystemServer(); } } if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getwindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getwindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; ...... if (a.mVisibleFromClient) { a.mWindowAdded) { a.mWindowAdded = ; wm.addView(decor,l); } { The activity will get a callback for this {@link LayoutParams} change earlier. However,at that time the decor will not be set (this is set in this method),so no action will be taken. This call ensures the callback occurs with the decor set. a.onWindowAttributesChanged(l); } } If the window has already been added,but during resume we started another activity,then don''t yet make the window visible. } if (localLOGV) Slog.v(TAG,"Launch " + r + " mStartedActivity set"); r.hideForNow = ; } Get rid of anything left hanging around. cleanUpPendingRemoveWindows(r,1)">false /* force */); The window is Now visible if it has been added,we are not simply finishing,and we are not starting another activity. if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != ) { performConfigurationChangedForActivity(r,r.newConfig); (DEBUG_CONfigURATION) { Slog.v(TAG,"Resuming activity " + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig); } r.newConfig = ; } isForward); WindowManager.LayoutParams l = r.window.getAttributes(); ((l.softInputMode & WindowManager.LayoutParams.soFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.soFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; (r.activity.mVisibleFromClient) { ViewManager wm = a.getwindowManager(); View decor = r.window.getDecorView(); wm.updateViewLayout(decor,l); } } r.activity.mVisibleFromServer = ; mNumVisibleActivities++ (r.activity.mVisibleFromClient) {            这里也会调用addview r.activity.makeVisible(); } } r.nextIdle = mNewActivities; mNewActivities = r; r); Looper.myQueue().addIdleHandler( Idler()); }

上面的代码主要做了以下几件事:

1、获取到 DecorView,设置不可见,然后通过 wm.addView(decor,l) 将 view 添加到 WindowManager;

2、在某些情况下,比如此时点击了输入框调起了键盘,就会调用 wm.updateViewLayout(decor,l) 来更新 View 的布局。

3、这些做完以后,会调用 activity 的  makeVisible ,让视图可见。如果此时 DecorView 没有添加到 WindowManager,那么会添加。 

// Activity
makeVisible() { mWindowAdded) { ViewManager wm = getwindowManager(); wm.addView(mDecor,getwindow().getAttributes()); mWindowAdded = ; } mDecor.setVisibility(View.VISIBLE); }

 接下来,看下 addview 的逻辑。 WindowManager 的实现类是 WindowManagerImpl,而它则是通过 WindowManagerGlobal 代理实现 addView 的,我们看下 addView 的方法:

 WindowManagerGlobal  
  addView(View view,ViewGroup.LayoutParams params,display display,Window parentwindow) {
            ......
    
            root =  ViewRootImpl(view.getContext(),display);
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            do this last because it fires off messages to start doing things
             {
                root.setView(view,wparams,panelParentView);
            }  (RuntimeException e) {
                 BadTokenException or InvaliddisplayException,clean up.
                if (index >= 0) {
                    removeViewLocked(index,1)">);
                }
                 e;
            } 
}

在这里,实例化了 ViewRootImpl 。同时调用 ViewRootImpl 的 setView 方法来持有了 DecorView。此外这里还保存了 DecorView ,Params,以及 ViewRootImpl 的实例。

现在我们终于知道为啥 View 是在 OnResume 的时候可见的呢。

 ViewRootImpl

实际上,View 的绘制是由 ViewRootImpl 来负责的。每个应用程序窗口的 DecorView 都有一个与之关联的 ViewRootImpl 对象,这种关联关系是由 WindowManager 来维护的。

先看 ViewRootImpl 的 setView 方法,该方法很长,我们将一些不重要的点注释掉:

   /**
     * We have one child
     */
     setView(View view,WindowManager.LayoutParams attrs,View panelParentView) {
        synchronized () {
            if (mView == ) {
                mView = view;
                ......
               
                mAdded = ;
                int res;  = WindowManagerImpl.ADD_OKAY; */

                 Schedule the first layout -before- adding to the window
                 manager,to make sure we do the relayout before receiving
                 any other events from the system.

                requestLayout();
                ......
            }
        }
    }

这里先将 mView 保存了 DecorView 的实例,然后调用 requestLayout() 方法,以完成应用程序用户界面的初次布局。

  requestLayout() {
        mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = ;
            scheduleTraversals();
        }
    }

因为是 UI 绘制,所以一定要确保是在主线程进行的,checkThread 主要是做一个校验。接着调用 scheduleTraversals 开始计划绘制了。

 scheduleTraversals() {
        mTraversalScheduled) {
            mTraversalScheduled = ;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL,mTraversalRunnable,1)">);
            mUnbufferedInputdispatch) {
                scheduleConsumeBatchedinput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

这里主要关注两点:

mTraversalBarrier : Handler 的同步屏障。它的作用是可以拦截 Looper 对同步消息的获取和分发,加入同步屏障之后,Looper 只会获取和处理异步消息,如果没有异步消息那么就会进入阻塞状态。也就是说,对 View 绘制渲染的处理操作可以优先处理(设置为异步消息)。

mChoreographer: 编舞者。统一动画、输入和绘制时机。也是这章需要重点分析的内容。

mTraversalRunnable :TraversalRunnable 的实例,是一个Runnable,最终肯定会调用其 run 方法:

class TraversalRunnable implements Runnable {
        @Override
         run() {
            doTraversal();
        }
    }

doTraversal,如其名,开始绘制了,该方法内部最终会调用 performTraversals 进行绘制。

   doTraversal() {
         (mTraversalScheduled) {
            mTraversalScheduled = ;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

             (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

             (mProfile) {
                Debug.stopMethodTracing();
                mProfile = ;
            }
        }
    }

到此,DecorView 与 activity 之间的绑定关系就讲完了,下一章,将会介绍 performTraversals 所做的事情,也就是 View 绘制流程。 

附上一张流程图:

 

到此,DecorView 与 ViewRootImpl 之间的关系就讲的很清楚了。

点击查看下一篇 Android View 的测量流程详解

总结

以上是小编为你收集整理的Android View 绘制流程之 DecorView 与 ViewRootImpl Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 的测量流程详解全部内容。

如果觉得小编网站内容还不错,欢迎将小编网站推荐给好友。

Android 自定义 View 详解 Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解

Android 自定义 View 详解 Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解

View 的绘制系列文章:

  • Android View 绘制流程之 DecorView 与 ViewRootImpl

  • Android View 的绘制流程之 Measure 过程详解 (一)

  • Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)

  • Android View 的事件分发原理解析

  • Android 自定义 View 详解

对于 Android 开发者来说,原生控件往往无法满足要求,需要开发者自定义一些控件,因此,需要去了解自定义 view 的实现原理。这样即使碰到需要自定义控件的时候,也可以游刃有余。

基础知识

自定义 View 分类

自定义 View 的实现方式有以下几种:

类型 定义 自定义组合控件 多个控件组合成为一个新的控件,方便多处复用 继承系统 View 控件 继承自TextView等系统控件,在系统控件的基础功能上进行扩展 继承 View 不复用系统控件逻辑,继承View进行功能定义 继承系统 ViewGroup 继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展 继承 View ViewGroup 不复用系统控件逻辑,继承ViewGroup进行功能定义

从上到下越来越难,需要的了解的知识也是越来越多的。

构造函数

当我们在自定义 View 的时候,构造函数都是不可缺少,需要对构造函数进行重写,构造函数有多个,至少要重写其中一个才行。例如我们新建 MyTextView:

   
public class MyTextView extends View {
  /** * 在java代码里new的时候会用到 * @param context */ public MyTextView(Context context) { super(context); } /** * 在xml布局文件中使用时自动调用 * MyTextView(Context context,@Nullable AttributeSet attrs) { (context,attrs); } * 不会自动调用,如果有默认style时,在第二个构造函数中调用 * context * attrs * defStyleAttr public MyTextView(Context context,@Nullable AttributeSet attrs,int defStyleAttr) { * 只有在API版本>21时才会用到 * 不会自动调用,如果有默认style时,在第二个构造函数中调用 * defStyleAttr * defStyleRes */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) int defStyleAttr,1)"> defStyleRes) { 函数的作用,都已经再代码里面写出来了。

自定义属性

写过布局的同学都知道,系统控件的属性在 xml 中都是以 android 开头的。对于自定义 View,也可以自定义属性,在 xml 中使用。

Android 自定义属性可分为以下几步:

  1. 自定义一个 View

  2. 编写 values/attrs.xml,在其中编写 styleable 和 item 等标签元素

  3. 在布局文件中 View 使用自定义的属性(注意 namespace)

  4. 在 View 的构造方法中通过 TypedArray 获取

e.g  还是以上面的 MyTextView 做演示:

首先我在 activity_main.xml 中引入了 MyTextView:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height
    tools:context=".MainActivity">

    com.example.myapplication.MyTextView
        android:layout_width="100dp"
        android:layout_height="200dp"
        app:testAttr="520"
        app:text="helloWorld" />

</android.support.constraint.ConstraintLayout>

然后我在 values/attrs.xml 中添加自定义属性:

resources>
    declare-styleable name="test">
        attr ="text" format="string" />
        ="testAttr"="integer" />
    declare-styleable>
>

记得在构造函数里面说过,xml 布局会调用第二个构造函数,因此在这个构造函数里面获取属性和解析:

    context.obtainStyledAttributes(attrs,R.styleable.test);
        int textAttr = ta.getInteger(R.styleable.test_testAttr,-1);
        String text = ta.getString(R.styleable.test_text);
        Log.d(TAG," text = " + text + ",textAttr = " + textAttr);
     // toast 显示获取的属性值 Toast.makeText(context,text
+ " " + textAttr,Toast.LENGTH_LONG).show(); ta.recycle(); }

注意当你在引用自定义属性的时候,记得加上 name 前缀,否则会引用不到。

这里本想截图 log 的,奈何就是不显示,就搞成 toast 了。

当然,你还可以自定义很多其他属性,包括 color,string, integer,boolean,flag,甚至是混合等。

自定义组合控件

自定义组合控件就是将多个控件组合成为一个新的控件,主要解决多次重复使用同一类型的布局。如我们顶部的 HeaderView 以及 dailog 等,我们都可以把他们组合成一个新的控件。

我们通过一个自定义 MyView1 实例来了解自定义组合控件的用法。

xml 布局 

merge ="wrap_content"="wrap_content">
    
    TextView
        android:id="@+id/Feed_item_com_cont_title"
        android:layout_width
        android:ellipsize="end"
        android:includeFontPadding="false"
        android:maxLines="2"
        android:text="title" />

    ="@+id/Feed_item_com_cont_desc"
        android:layout_below="@id/Feed_item_com_cont_title"="desc" merge>

 自定义 View 代码 :

package com.example.myapplication;

import android.content.Context;
 android.util.AttributeSet;
 android.view.LayoutInflater;
 android.view.View;
 android.widget.RelativeLayout;
 android.widget.TextView;

class MyView1 extends RelativeLayout {

     标题 private TextView mTitle;
     描述  TextView mDesc;

     MyView1(Context context) {
        this(context,1)">null);
    }

     MyView1(Context context,AttributeSet attrs) {
        public MyView1(Context context,AttributeSet attrs,defStyleAttr);
        initView(context);
    }

    
     * 初使化界面视图
     *
     *  context 上下文环境
     protected void initView(Context context) {
        View rootView = LayoutInflater.from(getContext()).inflate(R.layout.my_view1,1)">this);

        mDesc = rootView.findViewById(R.id.Feed_item_com_cont_desc);
        mTitle = rootView.findViewById(R.id.Feed_item_com_cont_title);
    }
}

在布局当中引用该控件 

LinearLayout 
    android:orientation="vertical"="@+id/text"
        android:clickable="true"
        android:enabled
        android:focusable="trsfnjsfksjfnjsdfjksdhfjksdjkfhdsfsdddddddddddddddddddddddddd" ="@+id/myview"com.example.myapplication.MyView1
        ="wrap_content" LinearLayout>

最终效果如下图所示 :

 

继承系统控件

继承系统的控件可以分为继承 View子类(如 TextView 等)和继承 ViewGroup 子类(如 LinearLayout 等),根据业务需求的不同,实现的方式也会有比较大的差异。这里介绍一个比较简单的,继承自View的实现方式。

业务需求:为文字设置背景,并在布局中间添加一条横线。

因为这种实现方式会复用系统的逻辑,大多数情况下我们希望复用系统的 onMeaseur 和 onLayout 流程,所以我们只需要重写 onDraw 方法 。实现非常简单,话不多说,直接上代码。

 android.graphics.Canvas;
 android.graphics.LinearGradient;
 android.graphics.Shader;
 android.text.TextPaint;
 android.widget.TextView;


import static android.support.v4.content.ContextCompat.getColor;


 * 包含分割线的textView
 * 文字左右两边有一条渐变的分割线
 * 样式如下:
 * ———————— 文字 ————————
 */
class DividingLineTextView  TextView {
     线性渐变  LinearGradient mLinearGradient;
     textPaint  TextPaint mPaint;
     文字 private String mText = "";
     屏幕宽度 private  mScreenWidth;
     开始颜色  mStartColor;
     结束颜色  mEndColor;
     字体大小  mTextSize;


    
     * 构造函数
     public DividingLineTextView(Context context,1)"> defStyle) {
         getResources().getDimensionPixelSize(R.dimen.text_size);
        mScreenWidth = getCalculateWidth(getContext());
        mStartColor = getColor(getContext(),R.color.colorAccent);
        mEndColor =new LinearGradient(0,mScreenWidth,1)">,new []{mStartColor,mEndColor,mStartColor},1)">float[]{0,0.5fnew TextPaint();
    }

     DividingLineTextView(Context context,1)"> DividingLineTextView(Context context) {
        );
    }

    @Override
     onDraw(Canvas canvas) {
        .onDraw(canvas);
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(mTextSize);
        int len = getTextLength(mText,mPaint);
        // 文字绘制起始坐标
        int sx = mScreenWidth / 2 - len / 2;
         文字绘制结束坐标
        int ex = mScreenWidth / 2 + len / 2int height = getMeasuredHeight();
        mPaint.setShader(mLinearGradient);
         绘制左边分界线,从左边开始:左边距15dp, 右边距距离文字15dp
        canvas.drawLine(mTextSize,height / 2,sx - mTextSize,height / 2 绘制右边分界线,从文字右边开始:左边距距离文字15dp,右边距15dp
        canvas.drawLine(ex + mTextSize,mScreenWidth - mTextSize,mPaint);
    }

    
     * 返回指定文字的宽度,单位px
     *
     *  str   要测量的文字
     *  paint 绘制此文字的画笔
     * @return 返回文字的宽度,单位px
      getTextLength(String str,TextPaint paint) {
        return () paint.measureText(str);
    }

    
     * 更新文字
     *
     *  text 文字
      update(String text) {
        mText = text;
        setText(mText);
         刷新重绘
        requestLayout();
    }


    
     * 获取需要计算的宽度,取屏幕高宽较小值,
     *
     *  context context
     *  屏幕宽度值
     static  getCalculateWidth(Context context) {
         context.getResources().getdisplayMetrics().heightPixels;
         动态屏幕宽度,在折叠屏手机上宽度在分屏时会发生变化
        int Width = context.getResources().getdisplayMetrics().widthPixels;

        return Math.min(Width,height);
    }
}

对于 View 的绘制还需要对 Paint()canvas 以及 Path 的使用有所了解,不清楚的可以稍微了解一下。 

看下布局里面的引用:

xml 布局 

>

   // ...... 跟前面一样忽视
    com.example.myapplication.DividingLineTextView
        ="@+id/divide"
        android:gravity="center" >

 

activty 里面代码如下 :
   onCreate(Bundle savedInstanceState) {
        .onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DividingLineTextView te = findViewById(R.id.divide);
        te.update("DividingLineTextView");
  }

这里通过 update() 对来重新绘制,确保边线在文字的两边。视觉效果如下:

 

直接继承View

直接继承 View 会比上一种实现方复杂一些,这种方法的使用情景下,完全不需要复用系统控件的逻辑,除了要重写 onDraw 外还需要对 onMeasure 方法进行重写。

我们用自定义 View 来绘制一个正方形。

首先定义构造方法,以及做一些初始化操作

ublic class RectView  View{
    定义画笔
    private Paint mPaint =  Paint();

    
     * 实现构造方法
     *  RectView(Context context) {
        (context);
        init();
    }

     RectView(Context context,attrs);
        init();
    }

    public RectView(Context context,defStyleAttr);
        init();
    }

     init() {
        mPaint.setColor(Color.BLUE);

    }

}

 重写 draw 方法,绘制正方形,注意对 padding 属性进行设置:


     * 重写draw方法
     *  canvas
     
    @Override
    .onDraw(canvas);
        获取各个编剧的padding值
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        获取绘制的View的宽度
        int width = getWidth()-paddingLeft-paddingRight;
        获取绘制的View的高度
        int height = getHeight()-paddingTop-paddingBottom;
        绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)
        canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,mPaint);
    }

在 View 的源码当中并没有对 AT_MOST 和 EXACTLY 两个模式做出区分,也就是说 View 在 wrap_content 和 match_parent 两个模式下是完全相同的,都会是 match_parent,显然这与我们平时用的 View 不同,所以我们要重写 onMeasure 方法。

    
     * 重写onMeasure方法
     *
     *  widthMeasureSpec
     *  heightMeasureSpec
     void onMeasure(int widthMeasureSpec,1)"> heightMeasureSpec) {
        .onMeasure(widthMeasureSpec,heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        处理wrap_contentde情况
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300,300);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize,300);
        }
    }

 最终效果如图所示:

可以发现,我们设置的是 wrap_content,但是最后还是有尺寸的。

整个过程大致如下,直接继承 View 时需要有几点注意:

  1. 在 onDraw 当中对 padding 属性进行处理。

  2. 在 onMeasure 过程中对 wrap_content 属性进行处理。

  3. 至少要有一个构造方法。

继承ViewGroup

自定义 ViewGroup 的过程相对复杂一些,因为除了要对自身的大小和位置进行测量之外,还需要对子 View 的测量参数负责。

需求实例

实现一个类似于 Viewpager 的可左右滑动的布局。

布局文件:

com.example.myapplication.MyHorizonView
        
        android:background="@color/colorAccent"="400dp">

        ListView
            ="@+id/list1"
            android:layout_width
            android:layout_height
            android:background="@color/colorAccent" />

        ="@+id/list2"="@color/colorPrimary" ="@+id/list3"="@color/colorPrimaryDark" com.example.myapplication.MyHorizonView="1dp"="2dp"com.example.myapplication.RectView
        />


>

一个 ViewGroup 里面放入 3 个 ListView,注意 ViewGroup 设置的宽是 wrap_conten,在测量的时候,会对 wrap_content 设置成与父 View 的大小一致,具体实现逻辑可看后面的代码。

代码比较多,我们结合注释分析。

class MyHorizonView  ViewGroup {

    final String TAG = "HorizontaiView"private List<View> mMatchedChildrenList = new ArrayList<>();


     MyHorizonView(Context context) {
         MyHorizonView(Context context,AttributeSet attributes) {
        public MyHorizonView(Context context,AttributeSet attributes,attributes,defStyleAttr);
    }

    @Override
    void onLayout(boolean changed,1)">int l,1)">int t,1)">int r,1)"> b) {
        int childCount = getChildCount();
        int left = 0;
        View child;
        for (int i = 0; i < childCount; i++) {
            child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                int childWidth = child.getMeasuredWidth();
                 因为是水平滑动的,所以以宽度来适配
                child.layout(left,left + childWidth,child.getMeasuredHeight());
                left += childWidth;
            }
        }
    }

    @Override
    int widthSpecMode =int widthSpecsize =int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecsize = 如果不是确定的的值,说明是 AT_MOST,与父 View 同宽高
        final boolean measureMatchParentChildren = heightSpecMode != MeasureSpec.EXACTLY ||
                widthSpecMode != MeasureSpec.EXACTLY;
         getChildCount();
        View child;
        final LayoutParams layoutParams = child.getLayoutParams();
                measureChild(child,widthMeasureSpec,heightMeasureSpec);
                if (measureMatchParentChildren) {
                     需要先计算出父 View 的高度来再来测量子 view
                    if (layoutParams.width == LayoutParams.MATCH_PARENT
                            || layoutParams.height == LayoutParams.MATCH_PARENT) {
                        mMatchedChildrenList.add(child);
                    }
                }
            }
        }

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
             如果宽高都是AT_MOST的话,即都是wrap_content布局模式,就用View自己想要的宽高值
            setMeasuredDimension(getMeasuredWidth(),getMeasuredHeight());
        } if (widthSpecMode == 如果只有宽度都是AT_MOST的话,即只有宽度是wrap_content布局模式,宽度就用View自己想要的宽度值,高度就用父ViewGroup指定的高度值
pecsize);
        } if (heightSpecMode == 如果只有高度都是AT_MOST的话,即只有高度是wrap_content布局模式,高度就用View自己想要的宽度值,宽度就用父ViewGroup指定的高度值
            setMeasuredDimension(widthSpecsize,getMeasuredHeight());
        }

        int i = 0; i < mMatchedChildrenList.size(); i++) {
            View matchChild =if (matchChild.getVisibility() != matchChild.getLayoutParams();
                 计算子 View 宽的 MeasureSpec
                 childWidthMeasureSpec;
                 LayoutParams.MATCH_PARENT) {
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(),MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,layoutParams.width);
                }
                 计算子 View 高的 MeasureSpec
                 childHeightMeasureSpec;
                if (layoutParams.height == LayoutParams.MATCH_PARENT) {
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(),1)"> {
                    childHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec,layoutParams.height);
                }
                 根据 MeasureSpec 计算自己的宽高
                matchChild.measure(childWidthMeasureSpec,childHeightMeasureSpec);
            }
        }
    }
}

这里我们只是重写了两个绘制过程中的重要的方法:onMeasure 和 onLayout 方法。

对于 onMeasure 方法具体逻辑如下:

  1. super.onMeasure 会先计算自定义 view 的大小;

  2. 调用 measureChild 对 子 View 进行测量;
  3. 自定义 view 设置的宽高参数不是 MeasureSpec.EXACTLY 的话,对于子 View 是 match_parent 需要额外处理,同时也需要对 MeasureSpec.AT_MOST 情况进行额外处理。

  4.  当自定义view 的大小确定后,在对子 View 是 match_parent 重新测量;

上述的测量过程的代码也是参考 FrameLayout 源码的,具体可以参看文章:

对于 onLayout 方法,因为是水平滑动的,所以要根据宽度来进行layout。

到这里我们的 View 布局就已经基本结束了。但是要实现 Viewpager 的效果,还需要添加对事件的处理。事件的处理流程之前我们有分析过,在制作自定义 View 的时候也是会经常用到的,不了解的可以参考文章 Android Touch事件分发超详细解析。

  init(Context context) {
        mScroller =  Scroller(context);
        mTracker = VeLocityTracker.obtain();
    }

    
     * 因为我们定义的是ViewGroup,从onInterceptTouchEvent开始。
     * 重写onInterceptTouchEvent,对横向滑动事件进行拦截
     *
     *  event
     * @return
     boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = falseint x = () event.getX();
        int y = () event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;必须不能拦截,否则后续的ACTION_MOME和ACTION_UP事件都会拦截。
                break;
             MotionEvent.ACTION_MOVE:
                intercepted = Math.abs(x - mLastX) > Math.abs(y - mLastY);
                ;
        }
        Log.d(TAG,"onInterceptTouchEvent: intercepted " + intercepted);
        mLastX = x;
        mLastY = y;
        return intercepted ? intercepted : .onInterceptHoverEvent(event);
    }

    
     * 当ViewGroup拦截下用户的横向滑动事件以后,后续的Touch事件将交付给`onTouchEvent`进行处理。
     * 重写onTouchEvent方法
      onTouchEvent(MotionEvent event) {
        mTracker.addMovement(event);
         MotionEvent.ACTION_DOWN:
                 MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                Log.d(TAG,"onTouchEvent: deltaX " + deltaX);

                 scrollBy 方法将对我们当前 View 的位置进行偏移
                scrollBy(-deltaX,1)">);
                 MotionEvent.ACTION_UP:
                Log.d(TAG,"onTouchEvent: " + getScrollX());
                 getScrollX()为在X轴方向发生的便宜,mChildWidth * currentIndex表示当前View在滑动开始之前的X坐标
                 distance存储的就是此次滑动的距离
                int distance = getScrollX() - mChildWidth * mCurrentIndex;
                当本次滑动距离>View宽度的1/2时,切换View
                if (Math.abs(distance) > mChildWidth / 2) {
                    if (distance > 0) {
                        mCurrentIndex++;
                    }  {
                        mCurrentIndex--;
                    }
                }  {
                    获取X轴加速度,units为单位,默认为像素,这里为每秒1000个像素点
                    mTracker.computeCurrentVeLocity(1000);
                    float xV = mTracker.getXVeLocity();
                    当X轴加速度>50时,也就是产生了快速滑动,也会切换View
                    if (Math.abs(xV) > 50) {
                        if (xV < 0) {
                            mCurrentIndex++;
                        }  {
                            mCurrentIndex--;
                        }
                    }
                }

                对currentIndex做出限制其范围为【0,getChildCount() - 1】
                mCurrentIndex = mCurrentIndex < 0 ? 0 : mCurrentIndex > getChildCount() - 1 ? getChildCount() - 1 : mCurrentIndex;
                滑动到下一个View
                smoothScrollTo(mCurrentIndex * mChildWidth,1)">);
                mTracker.clear();

                ;
        }

        Log.d(TAG,"onTouchEvent: ");
        mLastX =return .onTouchEvent(event);
    }

    @Override
     dispatchTouchEvent(MotionEvent ev) {
        .dispatchTouchEvent(ev);
    }

    void smoothScrollTo(int destX,1)"> destY) {
         startScroll方法将产生一系列偏移量,从(getScrollX(),getScrollY()),destX - getScrollX()和destY - getScrollY()为移动的距离
        mScroller.startScroll(getScrollX(),getScrollY(),destX - getScrollX(),destY - getScrollY(),1000);
         invalidate方法会重绘View,也就是调用View的onDraw方法,而onDraw又会调用computeScroll()方法
        invalidate();
    }

     重写computeScroll方法
    @Override
     computeScroll() {
        .computeScroll();
         当scroller.computeScrollOffset()=true时表示滑动没有结束
         (mScroller.computeScrollOffset()) {
             调用scrollTo方法进行滑动,滑动到scroller当中计算到的滑动位置
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
             没有滑动结束,继续刷新View
            postInvalidate();
        }
    }

具体效果如下图所示:


对于 Scroller 的用法总结如下:

  1. 调用 Scroller 的 startScroll() 方法来进行一些滚动的初始化设置,然后迫使 View 进行绘制 (调用 View 的 invalidate() 或 postInvalidate() 就可以重新绘制 View);

  2. 绘制 View 的时候 drawchild 方法会调用 computeScroll() 方法,重写 computeScroll(),通过 Scroller 的 computeScrollOffset() 方法来判断滚动有没有结束;

  3. scrollTo() 方法虽然会重新绘制 View,但还是要调用下 invalidate() 或者 postInvalidate() 来触发界面重绘,重新绘制 View 又触发 computeScroll();

  4. 如此往复进入一个循环阶段,即可达到平滑滚动的效果;

也许有人会问,干嘛还要调用来调用去最后在调用 scrollTo() 方法,还不如直接调用 scrollTo() 方法来实现滚动,其实直接调用是可以,只不过 scrollTo() 是瞬间滚动的,给人的用户体验不太好,所以 Android 提供了 Scroller 类实现平滑滚动的效果。

为了方面大家理解,我画了一个简单的调用示意图:

 

 

到此,自定义 view 的方法就讲完了。希望对大家有用。

参考文献:

1、Android自定义View全解

总结

以上是小编为你收集整理的Android 自定义 View 详解 Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解全部内容。

如果觉得小编网站内容还不错,欢迎将小编网站推荐给好友。

关于Android – 是否可以“锁定”相机的预览帧率?android自定义相机预览视图的问题我们已经讲解完毕,感谢您的阅读,如果还想了解更多关于Android View 的绘制流程之 Layout 和 Draw 过程详解 (二) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 的绘制流程之 Measure 过程详解 (一)、Android View 的绘制流程之 Measure 过程详解 (一) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 绘制流程之 DecorView 与 ViewRootImp、Android View 绘制流程之 DecorView 与 ViewRootImpl Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 的测量流程详解、Android 自定义 View 详解 Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解等相关内容,可以在本站寻找。

本文标签: