1. 程式人生 > >自定義ViewGroup(0)

自定義ViewGroup(0)

ViewGroup的職能

Google官網上給出的ViewGroup的功能如下:

*A ViewGroup is a special view that can contain other views (called children.) The view group is the base class for layouts and views containers. This class also defines the ViewGroup.LayoutParams
 class which serves as the base class for layouts parameters.*

簡單來說,ViewGroup是一個包含其他view(稱之為子View)的特殊View。ViewGroup是佈局和view容器的基類。ViewGroup中定義了ViewGroup.LayoutParams作為佈局引數的基類。

也就是說,ViewGroup的LayoutParams非常重要。

LayoutParams

LayoutParams are used by views to tell their parents how they want to be laid out.
LayoutParams 是view用來告訴它們的父佈局,它們想如何佈局的。

LayoutParams 有很多子類,如下圖所示。比如MarginLayoutParams,則表明該ViewGroup支援margin,當然這個也可以沒有。我們熟悉的

LinearLayout.LayoutParamsRelativeLayout.LayoutParams也在其中。
image.png

總得說來,LayoutParams儲存了子View在加入ViewGroup中時的一些引數資訊。當我們在寫xml檔案的時候,當在LinearLayout中寫childView的時候,我們可以寫layout_gravity,layout_weight等屬性,但是到了RelativeLayout中,childView就沒有了,為什麼呢?

**這是因為每個ViewGroup需要指定一個LayoutParams,用於確定支援childView支援哪些屬性,比如LinearLayout指定LinearLayout.LayoutParams等。
所以,我們在自定義ViewGroup的時候,一般也需要新建一個新的LayoutParams類繼承自ViewGroup.LayoutParams。**

public static class LayoutParams extends ViewGroup.LayoutParams {

  public int left = 0;
  public int top = 0;

  public LayoutParams(Context arg0, AttributeSet arg1) {
      super(arg0, arg1);
  }

  public LayoutParams(int arg0, int arg1) {
      super(arg0, arg1);
  }

  public LayoutParams(android.view.ViewGroup.LayoutParams arg0) {
      super(arg0);
  }

}

自定義的LayoutParams已經有了,那麼如何讓我們自定義的ViewGroup使用我們自定義的LayoutParams類來新增子View呢,答案是重寫generateLayoutParams返回我們自定義的LayoutParams物件即可。

@Override
public android.view.ViewGroup.LayoutParams generateLayoutParams(
      AttributeSet attrs) {
  return new MyCustomView.LayoutParams(getContext(), attrs);
}

@Override
protected android.view.ViewGroup.LayoutParams generateDefaultLayoutParams() {
  return new LayoutParams(LayoutParams.WRAP_CONTENT,
          LayoutParams.WRAP_CONTENT);
}

@Override
protected android.view.ViewGroup.LayoutParams generateLayoutParams(
      android.view.ViewGroup.LayoutParams p) {
  return new LayoutParams(p);
}

@Override
protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
  return p instanceof MyCustomView.LayoutParams;
}
自定義ViewGroup的一般步驟

自定義ViewGroup大致有三個步驟measure,layout,draw
- Measure
對於ViewGroup來說,除了要完成自身的measure過程,還要遍歷完成子View的measure方法,各個子View再去遞迴地完成measure過程,但是ViewGroup被定義成了抽象類,ViewGroup裡並沒有重寫onMeasure()方法,而是提供了一個measureChildren()方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  int childCount = this.getChildCount();
  for (int i = 0; i < childCount; i++) {
      View child = this.getChildAt(i);
      this.measureChild(child, widthMeasureSpec, heightMeasureSpec);
      int cw = child.getMeasuredWidth();
      int ch = child.getMeasuredHeight();
  }
}

可以看出,在迴圈對子View進行measure過程,呼叫的是measureChild()方法。

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

可以看出,measureChild()方法中通過getChildMeasureSpec()方法,獲取子View的childWidthMeasureSpec 和childHeightMeasureSpec,然後傳遞給子View進行測量。
我們知道,ViewGroup是一個抽象類,它並沒有實現onMeasure()方法,所以測量過程的onMeasure()方法就要到各個子類中去實現。
也就是說,在我們自定義的ViewGroup中,我們需要重寫onMeasure()方法。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        //自定義ViewGroup的測量在此新增

    }

在這裡面,直接呼叫父類的measureChildren()方法,測量所有的子控制元件的,讓然,自定義ViewGroup的測量還有待去完善。
- Layout
Layout的作用是ViewGroup用來確定View的位置,而這都在onLayout()方法中實現,在ViewGroup中,onLayout()方法被定義成了抽象方法,需要在自定義ViewGroup中具體實現。在onLayout()方法中遍歷所有子View並呼叫子View的layout方法完成子View的佈局。

@Override
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
  int childCount = this.getChildCount();
  for (int i = 0; i < childCount; i++) {
      View child = this.getChildAt(i);
      LayoutParams lParams = (LayoutParams) child.getLayoutParams();
      child.layout(lParams.left, lParams.top, lParams.left + childWidth,
              lParams.top + childHeight);
  }
}

其中child.layout(left,top,right,bottom)方法可以對子View的位置進行設定,四個引數的意思大家通過變數名都應該清楚了。

 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);
            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;
    }

可以看到,在layout()方法中, 子View的onLayout()方法又被呼叫,這樣layout()方法就會一層層地呼叫下去,完成佈局。

  • Draw
    draw過程是在View的draw()方法裡進行。draw地址分為以下幾個過程

(1)drawBackground(canvas):繪製背景
(2)onDraw(canvas) :繪製自己
(3) dispatchDraw(canvas):繪製children
(4)onDrawForeground(canvas):繪製裝飾 (foreground, scrollbars)

在自定義View的時候,我們不需要重寫draw()方法,只需重寫onDraw()方法即可。
值得注意的是ViewGroup容器元件的繪製,當它沒有背景時直接呼叫的是dispatchDraw()方法, 而繞過了draw()方法,當它有背景的時候就呼叫draw()方法,而draw()方法裡包含了dispatchDraw()方法的呼叫。因此要在ViewGroup上繪製東西的時候往往重寫的是dispatchDraw()方法而不是onDraw()方法。