Android之自定義View的死亡三部曲之(Layout)


前言

  • 大家好!本次我們將繼續(xù)學(xué)習(xí)Android之自定義View的死亡三部曲中的第二部:排兵布陣

  • 我們在上一篇Android之自定義View的死亡三部曲之(Measure)中分析了死亡三部曲的第一部,也是三部中最復(fù)雜的一步:View的測量芒划,想知道View的測量相關(guān)知識可以點(diǎn)進(jìn)去查看哦!

  • 通過第一部View的測量衙吩,我們就能拿到View的三圍數(shù)據(jù)了(View的寬高)望抽。

  • 那么接下來我們要做的當(dāng)然就是對測量好的View進(jìn)行布局了。

  • Ok塞耕,說干就干,這次嘴瓤,我們同樣是從ViewRootImpl的performTraversals方法開始扫外,還記得我們的performTraversals方法體內(nèi)部都有哪些內(nèi)容么莉钙?我們再粘貼一下代碼吧。

      private void performTraversals() {
          ...
            if (!mStopped) {
          //1筛谚、獲取頂層布局的childWidthMeasureSpec
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  
          //2磁玉、獲取頂層布局的childHeightMeasureSpec
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            //3、測量開始測量
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);       
            }
          } 
    
          if (didLayout) {
          //4驾讲、執(zhí)行布局方法
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);
            ...
          }
          if (!cancelDraw && !newSurface) {
           ...
          //5蚊伞、開始繪制了哦
                performDraw();
            }
          } 
        ...
      }
    
  • 我們上次分析測量是以performMeasure為入口進(jìn)行分析的,那么本次分析到布局吮铭,當(dāng)然是從performLayout作為起點(diǎn)了时迫。

  • Ok,那么我們就直接看performLayout方法體內(nèi)部的源碼吧

      private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
      int desiredWindowHeight) {
          mLayoutRequested = false;
          mScrollMayChange = true;
          mInLayout = true;
    
          final View host = mView;
          if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
                Log.v(TAG, "Laying out " + host + " to (" +
                        host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
          }
    
          Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
          try {
          //1谓晌、調(diào)用了host.layout
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); 
            mInLayout = false;
    
          .....
          } finally {
              Trace.traceEnd(Trace.TRACE_TAG_VIEW);
          }
          mInLayout = false;
      }
    
  • 我們可以看到掠拳,在1處,直接調(diào)用了host.layout進(jìn)行布局扎谎,而host是什么東東呢碳想?其實host就是我們的DecorView,還記得我們之前分析View的誕生之謎的時候毁靶,在創(chuàng)建ViewRootImpl時胧奔,直接把DecorView賦值給mView了。

  • 那么也就是說其實是調(diào)用了DecorView的layout方法预吆。我們再看下其傳遞的參數(shù)分別是0龙填,0,host.getMeasuredWidth()拐叉,host.getMeasuredHeight()

  • 而這四個參數(shù)按順利所代碼的含義分別是left岩遗,top,right凤瘦,bottom宿礁,也就是左、上蔬芥、右梆靖、下

  • left、top當(dāng)然是0了笔诵,為什么呢返吻?難道你想手機(jī)屏幕顯示一個畫面是,左邊和頂部不是剛好貼合的么乎婿?顯然不會希望這樣测僵,簡直丑死啦。

  • 寬就是我們DecorView測量后的寬度谢翎,高就是DecorView測量后的高度


  • Ok捍靠,所有的控件當(dāng)時都是繼承自View了沐旨,那么我們看下View的layout方法

      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;
            //1、isLayoutModeOptical(mParent)判斷是傳統(tǒng)模式還是視覺模式剂公,不懂的小伙伴可以百度一下哦
            //然后對不同模式分別調(diào)用對象的方法希俩,作用是設(shè)置View的四個點(diǎn)
            boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //2、直接調(diào)用onLayout方法進(jìn)行布局
            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 listenersCopy =
                      (ArrayList)li.mOnLayoutChangeListeners.clone();
              int numListeners = listenersCopy.size();
              for (int i = 0; i < numListeners; ++i) {
                  //3纲辽、如果設(shè)置了OnLayoutChangeListener,在layout之后就會回調(diào)告訴你了哦
                  listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
              }
          }
          }
    
          mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
          mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
      }
    
  • 在1中針對不同的layoutMode調(diào)用了不同的方法璃搜,我們來看下一班的layoutMode模式下調(diào)用setFrame方法時拖吼,內(nèi)部做了什么操作呢,


    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 + ")");
        }
        //1这吻、如果有一個值發(fā)生了改變吊档,那么就需要重新調(diào)用onLayout方法了,后面會分析到
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            //2唾糯、保存舊的寬和高
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            //計算新的寬和高
            int newWidth = right - left;
            int newHeight = bottom - top;
            //3怠硼、判斷寬高是否有分生變化
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            //Invalidate our old position
            //4、如果大小變化了移怯,在已繪制了的情況下就請求重新繪制
            invalidate(sizeChanged);

            //5香璃、存儲新的值
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;

            if (sizeChanged) {
                //6、大小變化時進(jìn)行處理
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
             }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                 //7舟误、如果此時View是可見狀態(tài)下葡秒,立即執(zhí)行繪制操作
                  invalidate(sizeChanged);

                }

             mPrivateFlags |= drawn;

            mBackgroundSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
              }

            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
         return changed;
    }
  • 可以看到changed的值只與四個點(diǎn)是否發(fā)生了變化有關(guān)。
  • 同時嵌溢,我們還發(fā)現(xiàn)眯牧,如果你想獲得某個view的top、left赖草、right学少、bottom的值,在layout之后就可以拿到了秧骑。
  • 而從View.layout方法的2位置處我們知道版确,在執(zhí)行了setFrame之后調(diào)用的是onLayout方法,所以也就是說腿堤,我們可以在onLayout方法中獲得四個位置點(diǎn)的值
  • View類的成員變量mLeft阀坏、mRight、mTop和mBottom分別用來描述當(dāng)前視圖的左右上下四條邊與其父視圖的左右上下四條邊的距離笆檀,如果它們的值與參數(shù)left忌堂、right、top和bottom的值不相等酗洒,那么就說明當(dāng)前視圖的大小或者位置發(fā)生變化了士修。這時候View類的成員函數(shù)setFrame就會將參數(shù)left枷遂、right、top和bottom的值分別記錄在成員變量mLeft棋嘲、mRight酒唉、mTop和mBottom中。

  • 然后我們很開心的點(diǎn)開了View.onLayout方法沸移,發(fā)現(xiàn)痪伦,居然是空的!~~空的雹锣!
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
  • 沒錯网沾,就是空的,一般該方法是用來確認(rèn)childView的位置的蕊爵,比如FrameLayout會調(diào)用onLayout方法告知childView辉哥,你可以可以開始布局了哦。然后childView就會調(diào)用自身的layout方法完成自身的布局工作攒射,如果childView中還包含有childView醋旦,就會一直調(diào)用下去。

  • 我們先來梳理下流程:
    1会放、performTraversals內(nèi)部調(diào)用performLayout開始執(zhí)行布局工作
    2饲齐、performLayout內(nèi)部會調(diào)用layout開始進(jìn)行布局
    3、layout中會調(diào)用setFrame確定mTop鸦概,mLeft箩张,mRight,mBottom的值以及判斷是個點(diǎn)的值是否發(fā)生了變化
    4窗市、最后調(diào)用onLayout方法通知下面的childView進(jìn)行布局操作
  • ok先慷,那么我們就分析下FrameLayout的onLayout方法

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
  • 從上面可以看到內(nèi)部只是調(diào)用了layoutChildren方法,layoutChildren才是具體的實現(xiàn)
  • 我們繼續(xù)看下layoutChildren里面的代碼:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    //1咨察、獲得子view的熟練
    final int count = getChildCount();
    //2论熙、獲得父view左面位置,getPaddingLeftWithForeground獲得的是對應(yīng)的內(nèi)邊距
    final int parentLeft = getPaddingLeftWithForeground();
    //3摄狱、獲得父view右邊位置
    final int parentRight = right - left - getPaddingRightWithForeground();
    //4脓诡、獲得父view頂部位置
    final int parentTop = getPaddingTopWithForeground();
    //4、獲得父view底部位置
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    for (int i = 0; i < count; i++) {
        //5媒役、遍歷子view
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;

            int gravity = lp.gravity;
            if (gravity == -1) {
                gravity = DEFAULT_CHILD_GRAVITY;
            }

            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
             //6祝谚、針對不同的水平方向Gravity做處理
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }
            //6、針對不同的垂直方向Gravity做處理
            switch (verticalGravity) {
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                case Gravity.CENTER_VERTICAL:
                      childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                    lp.topMargin - lp.bottomMargin;
                    break;
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            }
            //7酣衷、調(diào)用child的layout方法交惯,對child進(jìn)行布局,前面我們分析了
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
}
}
  • 知識點(diǎn)梳理:
    1、獲取父View的內(nèi)邊距padding的值
    2席爽、遍歷子View意荤,處理子View的layout_gravity屬性、根據(jù)View測量后的寬和高只锻、父View的padding值玖像、來確定子View的布局參數(shù),
    3齐饮、調(diào)用child.layout方法捐寥,對子View進(jìn)行布局

對childView進(jìn)行布局

  • 從上面的分析我們的可以知道,如果子view屬于FrameLayout這種布局類的View祖驱,里面就會重復(fù)上面流程上真,如果不是,最終就會調(diào)用到View.onLayout,而這個方法是一個空的實現(xiàn)羹膳,所以我們在自定義View時,需要重新onLayout實現(xiàn)布局的操作

總結(jié):

  • 布局流程主要的操作就是確定View的四個點(diǎn)的數(shù)值根竿,相對于之前的測量陵像,是不是要簡單一些呢?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寇壳,一起剝皮案震驚了整個濱河市醒颖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌壳炎,老刑警劉巖泞歉,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異匿辩,居然都是意外死亡腰耙,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門铲球,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挺庞,“玉大人,你說我怎么就攤上這事稼病⊙∏龋” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵然走,是天一觀的道長援制。 經(jīng)常有香客問我,道長芍瑞,這世上最難降的妖魔是什么晨仑? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上寻歧,老公的妹妹穿的比我還像新娘掌栅。我一直安慰自己,他們只是感情好码泛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布猾封。 她就那樣靜靜地躺著,像睡著了一般噪珊。 火紅的嫁衣襯著肌膚如雪晌缘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天痢站,我揣著相機(jī)與錄音磷箕,去河邊找鬼。 笑死阵难,一個胖子當(dāng)著我的面吹牛岳枷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播呜叫,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼空繁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了朱庆?” 一聲冷哼從身側(cè)響起盛泡,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎娱颊,沒想到半個月后傲诵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡箱硕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年拴竹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颅痊。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡殖熟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出斑响,到底是詐尸還是另有隱情菱属,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布舰罚,位于F島的核電站纽门,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏营罢。R本人自食惡果不足惜赏陵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一饼齿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蝙搔,春花似錦缕溉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至勤晚,卻和暖如春枉层,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赐写。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工鸟蜡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人挺邀。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓揉忘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親端铛。 傳聞我的和親對象是個殘疾皇子癌淮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容