美文网首页Android进阶之路Android知识Android技术知识
android基础-view的测量,布局,绘制

android基础-view的测量,布局,绘制

作者: return_toLife | 来源:发表于2019-02-27 18:56 被阅读12次

    知识点

    1. view的测量
    2. view的布局
    3. view的绘制

    android中的view显示方式主要就是测量出大小→决定在哪个位置→最后进行绘制


    一、view的测量

    view的测量是通过强大的MeasureSpec类帮助测量的,而关于该类起初我们只要了解它是一个32位的int值,其中高2位是用于标识当前view的测量模式,低30位就是用于记录view的大小。更多关于该类的知识可以查看官方文档MeasureSpec

    view的测量模式有三种:

    • EXACTLY : 就是当我们指定view的大小或者使用适配父控件的情况 例如:
      android:layout_width="100dp" /android:layout_width="match_parent"
    • AT_MOST : 该模式一般是控件适应自身内容大小,但不能超过父控件的大小 例如:
      android:layout_width="wrap_content"
    • UNSPECIFIED : 该模式标识不限制view的大小,要多大就多大,一般在自定义view的时候使用

    在view的测量中,系统默认的大小计算方法如下(API 27):

    /**
         * Utility to return a default size. Uses the supplied size if the
         * MeasureSpec imposed no constraints. Will get larger if allowed
         * by the MeasureSpec.
         *
         * @param size Default size for this view
         * @param measureSpec Constraints imposed by the parent
         * @return The size this view should be.
         */
        public static int getDefaultSize(int size, int measureSpec) {
            int result = size;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
    
            switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            }
            return result;
        }
    

    可以看出,默认的测量方式为,如果测量模式是UNSPECIFIED ,则采用系统默认大小,其余为measureSpec中所测量的大小,根据这个测量思路我们在自定义view的时候就可以采用自己的测量方式

    举个例子:

     @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int resultWidth;
            int resultHeight;
    
            int specWMode = MeasureSpec.getMode(widthMeasureSpec);
            int specWSize = MeasureSpec.getSize(widthMeasureSpec);
             resultWidth=myMeasure(specWMode,specWSize, Dp2PxUtil.dip2px(mContext,200));
            
            int specHMode = MeasureSpec.getMode(heightMeasureSpec);
            int specHSize = MeasureSpec.getSize(heightMeasureSpec);
    
                   resultHeight=myMeasure(specHMode,specHSize,Dp2PxUtil.dip2px(mContext,300));
            setMeasuredDimension(resultWidth,resultHeight);
        }
    
        /**
         * 
         * @param specMode 测量模式
         * @param specSize 测量大小
         * @param result  在非精确测量模式中用来约束的大小
         * @return
         */
        private  int myMeasure(int specMode,int specSize,int result){
            if(specMode==MeasureSpec.EXACTLY){
                result=specSize;
            }else if(specMode==MeasureSpec.AT_MOST){
                result=Math.min(specSize,result);
            }else {
                
            }
            return result;
        }
    

    测试结果:

    结果 备注
    指定大小为100dp*100dp.png 设置自定义view的宽高为100dp*100dp
    填充父布局.png 设置自定义view的宽高为match_parent
    自适应.png 设置自定义view的宽高为wrap_content(实际根据我们的测量方法设置的是宽高为200dp*300dp)
    不重写.png 如果没有重写自定义view中的onMeasure方法,并且设置宽高为wrap_content的时候,也是填充父布局,具体原因可以查看系统测量默认大小的源码

    二、view的布局

    与view布局相关主要就有两个方法:
    layout(int l, int t, int r, int b) :
    onLayout(boolean changed, int left, int top, int right, int bottom) :

    2.1 onLayout
    该方法在自定义view中一般不需用重写,其常用viewgroup通知子view进行布局Layout的时候使用

    2.2 layout
    view中源码如下(API 27)

       public void layout(int l, int t, int r, int b) {
            if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
                onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
    
            int oldL = mLeft;
            int oldT = mTop;
            int oldB = mBottom;
            int oldR = mRight;
    
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
    
                if (shouldDrawRoundScrollbar()) {
                    if(mRoundScrollbarRenderer == null) {
                        mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                    }
                } else {
                    mRoundScrollbarRenderer = null;
                }
    
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnLayoutChangeListeners != null) {
                    ArrayList<OnLayoutChangeListener> listenersCopy =
                            (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                    int numListeners = listenersCopy.size();
                    for (int i = 0; i < numListeners; ++i) {
                        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                    }
                }
            }
    
            mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
            mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    
            if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
                mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
                notifyEnterOrExitForAutoFillIfNeeded(true);
            }
        }
    

    我们不必要知道该方法的全部流程以及作用,主要看下面这行代码:
    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    这行代码涉及了三个方法isLayoutModeOptical,setOpticalFrame,setFrame。isLayoutModeOptical看注释说是判断是否使用光学边界,这个我们也可以不管,主要看setOpticalFrame,setFrame这两个方法
    我们先看setOpticalFrame:

     private boolean setOpticalFrame(int left, int top, int right, int bottom) {
            Insets parentInsets = mParent instanceof View ?
                    ((View) mParent).getOpticalInsets() : Insets.NONE;
            Insets childInsets = getOpticalInsets();
            return setFrame(
                    left   + parentInsets.left - childInsets.left,
                    top    + parentInsets.top  - childInsets.top,
                    right  + parentInsets.left + childInsets.right,
                    bottom + parentInsets.top  + childInsets.bottom);
        }
    

    可以看到最终return的方法也是setFrame,那么我们就往这方法里面看看

    protected boolean setFrame(int left, int top, int right, int bottom) {
            boolean changed = false;
    
            if (DBG) {
                Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                        + right + "," + bottom + ")");
            }
    
            if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
                changed = true;
    
                // Remember our drawn bit
                int drawn = mPrivateFlags & PFLAG_DRAWN;
    
                int oldWidth = mRight - mLeft;
                int oldHeight = mBottom - mTop;
                int newWidth = right - left;
                int newHeight = bottom - top;
                boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
    
                // Invalidate our old position
                invalidate(sizeChanged);
    
                mLeft = left;
                mTop = top;
                mRight = right;
                mBottom = bottom;
                mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    
                mPrivateFlags |= PFLAG_HAS_BOUNDS;
    
    
                if (sizeChanged) {
                    sizeChange(newWidth, newHeight, oldWidth, oldHeight);
                }
    
                if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                    // If we are visible, force the DRAWN bit to on so that
                    // this invalidate will go through (at least to our parent).
                    // This is because someone may have invalidated this view
                    // before this call to setFrame came in, thereby clearing
                    // the DRAWN bit.
                    mPrivateFlags |= PFLAG_DRAWN;
                    invalidate(sizeChanged);
                    // parent display list may need to be recreated based on a change in the bounds
                    // of any child
                    invalidateParentCaches();
                }
    
                // Reset drawn bit to original value (invalidate turns it off)
                mPrivateFlags |= drawn;
    
                mBackgroundSizeChanged = true;
                mDefaultFocusHighlightSizeChanged = true;
                if (mForegroundInfo != null) {
                    mForegroundInfo.mBoundsChanged = true;
                }
    
                notifySubtreeAccessibilityStateChangedIfNeeded();
            }
            return changed;
        }
    

    这个方法我们也不用全部理解,主要看到它进行了当前位置参数和传进来的位置参数是否相等,如果不相等就进行重新绘制,看到这里,我们就知道了layout()方法如果传进去的位置和之前的位置参数不一样就会重新绘制该view,那么在viewgroup需要定位子view位置的时候,我们就可以调用每个子view的layout方法来重新给子view设置位置

    三、view的绘制

    当有了view的大小以及view的位置信息之后,我们就可以在屏幕上绘制该view了, view的绘制比较简单,我们可以直接重写onDraw方法

     @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
        }
    

    该方法会传递一个Canvas(画布)过来,我们只需要创建一个Paint(画笔)之类的在该画布上进行绘制就可以了

    总结

    view的测量,布局,绘制是基础中比较重要的,因为后续的一些复杂的特效,动画都可以由自定义view去实现,理解清楚其基本的绘制流程对后续开发会很有帮助

    参考文章

    《Android群英传》

    相关文章

      网友评论

        本文标题:android基础-view的测量,布局,绘制

        本文链接:https://www.haomeiwen.com/subject/vqunyqtx.html