自定義ViewGroup—回顧

自定ViewGroup要比自定義View要複雜一點,由於自定義ViewGroup不只測量自身還要測量子元素和及重寫onLayout()來一次排列子View。下面這篇文章是關於自定義ViewGroup的一些基本知識,這些主要內容來自《android開發藝術探索》,在文章最後又這本書的網上版本。java

目錄

  • ViewGroup的measure過程
  • onMeasure()函數
  • onLayout()函數
  • 對Padding和Margin的處理
  • 在Activity中獲取View的寬高

ViewGroup的measure過程

ViewGroup是一個抽象類,他沒有重寫View的onMeasure()方法。所以並無定義具體的測量過程,具體的測量過程交給了他的子類來完成,好比:LinearLayoutRelativeLayout等。ViewGroup提供了一個measureChildren的方法來測量子View:android

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            //測量子元素
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}
複製代碼

下面爲measureChild()的源碼:bash

protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    //獲取子元素寬度MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    //獲取子元素高度MeasureSpec        
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    //子元素進行測量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼

measureChild()方法會先獲取子View的LayoutParams參數,而後再經過getChildMeasureSpec()獲取子View的寬高MeasureSpec,最後將獲取到的MeasureSpec傳遞給view的()方法進行測量。具體的執行過程我在《View的繪製流程》這篇文章中介紹過,這裏就不在多少說了。ide

onMeasure()方法

由於ViewGroup沒有重寫View的onMeasure方法,咱們在自定義的時候集成了ViewGruppo成了View的子類,所以要寫本身佈局的測量過則。那我上篇文章《自定義ViewGroup—FlowLayout》中的部分代碼爲例:函數

@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   ...
                          //##1
    //循環遍歷子View
    for (int i = 0; i < getChildCount(); i++) {
        View childView = getChildAt(i);

        //先測量子View
        measureChild(childView, widthMeasureSpec, widthMeasureSpec);
        ....
        //計算剩餘空間
        remaining = widthSize - usedWidth;
                        //##2
        //判斷子view的測量寬度是否大於剩餘空間,若是大於則另起一行
        if (childWidth > remaining) {
            //另起一行,已用寬度應該爲零
            usedWidth = 0;
            //添加View
            mLineView = new LineView();
            mLineViewList.add(mLineView);
        }
        mLineView.addView(childView);
        //已用寬度累加
        usedWidth += childWidth;
        mLineView.setTotalWidth(usedWidth);
    }
                     //##3
    for (int i = 0; i < mLineViewList.size(); i++) {
        //總高度=全部行數相加
        totalHeight += mLineViewList.get(i).mHeight;
    }
               
    //父容器的總高度=上下padding的高度,在最後額外加一個底部marginBottom
    totalHeight += getPaddingTop() + getPaddingBottom() + marginBottom;
                     //##4
    setMeasuredDimension(widthSize, heightMode == MeasureSpec.EXACTLY ? heightSize : totalHeight);
}
複製代碼

上面代碼中主要是測量FlowLayout的高度。它是經過遍歷子View(//##1)計算剩餘寬度,再經過子View的寬度和剩餘寬度比較來判斷是否換行(//##2)。FlowLayout的高度若是是在warp_cotent模式下高度就爲子View的行數乘上子View的高度(//##3),最後經過setMeasuredDimension()計算View的寬高並保存起來。佈局

onLayout()函數

onLayout()函數式是ViewGroup的一個抽象函數,ViewGroup的子類必須實現該函數,用於定義View的擺放規則。post

注:該方法有一個指的探討的問題,下面onLayout()是被@Override註釋着的,也就是這個方法是複用了View的onLayout()方法。那麼問題來了,java中父類的方法是否能把子類重寫爲抽象方法?這個問題我在好多技術羣中向大神請教過,有的說能有的說不能。後來本身在項目中親自測試,發現能夠重寫可是不能調用。若是讀者知道這個問題,歡迎在下方留言。測試

@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
複製代碼

該方法的調用是在View的layout被調用,咱們能夠查看ViewGroup的layout()放知道ViewGroup的layout過程是交給父類完成的。ui

@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        //調用父類的layout方法
        super.layout(l, t, r, b);
    } else {
         transition finishes
        mLayoutCalledWhileSuppressed = true;
    }
}
複製代碼

View的layout方法中調用了onLayout()方法this

@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
    ...
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
       ...
    }
  ...
}
複製代碼

對Padding和Margin的處理

Padding的處理比較簡單,只須要getPaddingXXX()來獲取padding的值,在計算ViewGroup的寬高的時候將其加上便可:

paddingLeft = getPaddingLeft();
paddingTop = getPaddingTop();
paddingRight = getPaddingRight();
paddingBottom = getPaddingBottom();
//ViewGroup寬高計算
viewGroupWidth = paddingLeft + viewsWidth + paddingRight;
viewGroupHeight = paddingTop + viewsHeight + paddingBottom;
複製代碼

Margin的處理比較麻煩一點,首先他要先從子View中獲取layoutParams屬性,經過子View的LayoutParams屬性來獲取設置的Margin值。其layoutParams獲取方法爲childView.getLayoutParams()。要注意下面兩點:

  1. 獲取的要是MarginLayoutParams()類型,記得作類型強轉。
  2. 從新generateLayoutParams(),返回類型爲MarginLayoutParams類或他的子類。不然回報類型轉換異常

下面爲實現代碼:

@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    //獲取Margin值
    MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
    marginLeft = Math.max(marginLeft, lp.rightMargin);
    marginTop = Math.max(marginTop, lp.topMargin);
    marginRight = Math.max(marginRight, lp.rightMargin);
    marginBottom = lp.bottomMargin;
    
    //計算子View四個座標位置
    int cLeft = left + marginLeft;
    int cRight = left + childWidth + marginRight;
    int cTop = top + marginTop;
    int cBottom = top + childHeight + marginBottom;
    //設置View的具體位置
    childView.layout(cLeft, cTop, cRight, cBottom);
}
//重寫generateLayoutParams()
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}
複製代碼

在Activity中獲取View的寬高

android中不能再Activity的生命週期onCreate()onStart()onResume()生命週期中獲取到View的寬高,這是由於Activity的生命週期和View的測量過程不是同步執行的。對於上面的問題有四種解決方案。下面爲三種解決方法,第四種方案比較複雜就沒寫出來。

onWindowFoucusChanged()中獲取

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    int width = mFlowL_testCustom.getMeasuredWidth();
    int height = mFlowL_testCustom.getMeasuredHeight();
    Log.i(TAG, "onWindowFocusChanged()測量的寬爲:" + width + "高爲:" + height);
}
複製代碼

經過View的post方法,經請求發送到消息隊列中執行。

mFlowL_testCustom.post(new Runnable() {
    @Override
    public void run() {
        int width = mFlowL_testCustom.getMeasuredWidth();
        int height = mFlowL_testCustom.getMeasuredHeight();
        Log.i(TAG, "mFlowL_testCustom.post()測量的寬爲:" + width + "高爲:" + height);
    }
});
複製代碼

經過爲ViewTreeObserver添加OnGlobalLayoutListener()來實現

ViewTreeObserver treeObserver=mFlowL_testCustom.getViewTreeObserver();
    treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            int width = mFlowL_testCustom.getMeasuredWidth();
            int height = mFlowL_testCustom.getMeasuredHeight();
            Log.i(TAG, "addOnGlobalLayoutListener()測量的寬爲:" + width + "高爲:" + height);
        }
});

複製代碼

總結

文章先寫到這裏吧!最近一直在堅持天天寫技術筆記,但願能慢慢將這種堅持當成一種習慣。最後祝全部看到這篇文章的人工做順利,工資翻番。

參考

Android開發藝術探索完結篇——天道酬勤

相關文章
相關標籤/搜索