1. 程式人生 > >自定義元件開發七 自定義容器

自定義元件開發七 自定義容器

概述

自定義容器本質上也是一個元件,常見的 LinearLayout、FrameLayout、GridLayout、ScrollView和 RelativeLayout 等等元件都是容器,容器除了有自己的外觀,還能用來容納各種元件,以一種特定的規則規定元件應該在什麼位置、顯示多大。

一般情況下,我們更關注自定義元件的外觀及功能,但自定義容器則更關注其內的元件怎麼排列和擺放,比如線性佈局 LinearLayout 中的元件只能水平排列或垂直排列,幀佈局 FrameLayout中的元件可以重疊,相對佈局 RelativeLayout 中的元件可以以某一個元件為參照定位自身的位置……容器還關注元件與容器四個邊框之間的距離(padding),或者容器內元件與元件之間的距離(margin)。

事實上,容器是可以巢狀的,一個容器中,既可以是普通的子元件,也可以是另一個子容器。

容器類一般要繼承 ViewGroup 類,ViewGroup 類同時也是 View 的子類,如圖所示,ViewGroup 又是一個抽象類,定義了 onLayout()等抽象方法。當然,根據需要,我們也可以讓容器類繼承自 FrameLayout 等 ViewGroup 的子類,比如 ListView 繼承自 ViewGroup,而 ScrollView水平滾動容器類則從 FrameLayout 派生。
這裡寫圖片描述

ViewGroup 類

ViewGroup 常用方法

ViewGroup 作為容器類的父類,自然有他自己鮮明的特徵,開發自定義容器必須先要了解ViewGroup。

在 ViewGroup 中,定義了一個 View[]型別的陣列 mChildren,該陣列儲存了容器中所有的子元件,負責維護元件的新增、移除、管理元件順序等功能,另一個成員變數 mChildrenCount 則儲存了容器中子元件的數量。在佈局檔案(layout)中,容器中的子元素會根據順序自動新增到 mChildren 陣列中。

ViewGroup 具備了容器類的基本特徵和運作流程,也定義了相關的方法用於訪問容器內的
元件,主要的方法有:

public int getChildCount()
獲取容器內的子元件的個數;
public View getChildAt(int index
) 容器內的所有子元件都儲存在名為 mChildren 的 View[]陣列中, 該方法通過索引 index找到指定位置的子元件;
public void addView(View child, int index, LayoutParams params)
向容器中新增新的子元件,child 表示子元件(也可以是子容器),index 表示索引,指
定元件所在的位置,params 引數為元件指定佈局引數,該方法還有兩個簡化的版本:

public void addView(View child, LayoutParams params):新增 child 子元件,併為該子元件指定佈局引數;

public void addView(View child, int index):佈局引數使用預設的 ViewGroup.LayoutParams,其中 layout_width 和 layout_height 均為 wrap_content;

public void addView(View child):佈局引數同上,但 index 為-1,表示將 child 元件新增到 mChildren 陣列的最後。

向容器中新增新的子元件時,子元件不能有父容器,否則會丟擲“The specified child
already has a parent(該元件已有父容器)”的異常。
public void removeViewAt(int index)
移除 index 位置的子元件

類似的方法還有:
public void removeView(View view)
移除子元件 view;

public void removeViews(int start, int count)
移除從 start 開始連續的 count 個子元件。
protected  void  measureChild(View child,
int parentWidthMeasureSpec,  int parentHeightMeasureSpec)
測量子元件的尺寸。

類似的方法還有:
protected void measureChildren(int widthMeasureSpec,
 int heightMeasureSpec):
測量所有子元件的尺寸;

public final void measure(int widthMeasureSpec, int heightMeasureSpec):
該方法從 View類 中 繼 承 , 用 於 測 量 組 件 或 容 器 自 己 的 尺 寸 ,
引數 widthMeasureSpec 和heightMeasureSpec 為 0 時表示按實際大小進行測量,
將 0 傳入方法常常會有奇效。

ViewGroup 執行的基本流程大致為:

1) 測量容器尺寸
重寫 onMeasure()方法測量容器大小,和自定義元件有所區別的是,在測量容器大小之
前,必須先呼叫 measureChildren()方法測量所有子元件的大小,不然結果永遠為 0。

2) 確定每個子元件的位置
重寫 onLayout()方法確定每個子元件的位置(這個其實挺麻煩,也是定義容器的難點部
分),在 onLayout()方法中,呼叫 View 的 layout()方法確定子元件的位置。

3) 繪製容器
重寫 onDraw()方法,其實 ViewGroup 類並沒有重寫 onDraw()方法,除非有特別的要求,自定義容器也很少去重寫。比如 LinearLayout 重寫了該方法用於繪製水平或垂直分割條,而 FrameLayout 則是重寫了 draw()方法,作用其實是一樣的。

我們來看看容器類的基本結構。

public class MyViewGroup extends ViewGroup {
    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 確定每個子元件的位置
     *
     * @param changed 是否有新的尺寸或位置
     * @param l       left
     * @param t       top
     * @param r       right
     * @param b       bottom
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    }

    /**
     * 測量容器的尺寸
     *
     * @param widthMeasureSpec  寬度模式與大小
     * @param heightMeasureSpec 高度模式與大小
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //先測量所有子元件的大小

        this.measureChildren(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * 繪製容器
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
}

ViewGroup 的工作原理

第一節介紹過 View 的工作流程,ViewGroup 作為 View 的子類,流程基本是相同的,但
另一方面 ViewGroup 作為容器的父類,又有些差異,我們通過閱讀原始碼來了解 ViewGroup 的工作原理。

前面說到,重寫 ViewGroup 的 onMeasure()方法時,必須先呼叫 measureChildren()方法測量子元件的尺寸,該方法原始碼如下:

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

measureChildren()方法中,迴圈遍歷每一個子元件,如果當前子元件的可見性不為 GONE 也就是沒有隱藏則繼續呼叫 measureChild(child, widthMeasureSpec, heightMeasureSpec)方法測量當前子元件 child 的大小,我們繼續進入 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()方法結合父容器的 MeasureSpec、子元件的 Padding 和 LayoutParams 三個因素 利 用 getChildMeasureSpec() 計 算 出 子 組 件 的 尺 寸 模 式 和 尺 寸 大 小 ( 可 以 跟 蹤 到getChildMeasureSpec()方法中檢視),並呼叫子元件的 measure()方法進行尺寸測量。measure()方法的實現如下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
……
onMeasure(widthMeasureSpec, heightMeasureSpec);
……
}

真相慢慢露出水面,measure()方法呼叫了 onMeasure(widthMeasureSpec, heightMeasureSpec)方法,該方法正是我們重用的用來測量元件尺寸的方法,至此,測量元件尺寸的工作已掌握到開發人員手中。

根據上面的程式碼跟蹤我們發現,從根元素出發,一步步向下遞迴驅動測量,每個元件又負責計算自身大小,OOP 的神奇之處就這樣在實際應用中體現出來了。

接下來呼叫 onLayout()方法定位子元件,以確定子元件的位置和大小,在 onLayout()方法中,我們將呼叫子元件的 layout()方法,這裡要一分為二,如果子元件是一個 View,定位流程到此結束,如果子元件又是一個容器呢?我們進入 layout()方法進行跟蹤。

public void layout(int l, int t, int r, int b) {
    ……
    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;
    ……
    }
    ……
}

如果子元件是一個容器,又會繼續呼叫該容器的 onLayout()方法對孫元件進行定位,所以,onLayout()方法也是一個遞迴的過程。

onMeasure()方法和 onLayout()方法呼叫完成後,該輪到 onDraw()方法了,ViewGroup 類並沒有重寫該方法,但是,從第一章中我們都知道每一個元件在繪製時是會呼叫 View 的 draw()方法的,我們進入 draw()方法進行跟蹤。

public void draw(Canvas canvas) {
  ……
/*
* 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
   int saveCount;
   if (!dirtyOpaque) {
       drawBackground(canvas);
   }
   // skip step 2 & 5 if possible (common case)
   final int viewFlags = mViewFlags;
   boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
   boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
   if (!verticalEdges && !horizontalEdges) {
       // Step 3, draw the content
       if (!dirtyOpaque) onDraw(canvas);
       // Step 4, draw the children
       dispatchDraw(canvas);
       // Step 6, draw decorations (scrollbars)
       onDrawScrollBars(canvas);
       if (mOverlay != null && !mOverlay.isEmpty()) {
           mOverlay.getOverlayView().dispatchDraw(canvas);
       }
       // we're done...
       return;
   }
 ……
}

draw()方法中執行了語句 dispatchDraw(canvas),但是,當我們跟蹤到 View類的 dispatchDraw()方法時發現該方法是空的,但對於 ViewGroup 來說,該方法的作用非同小可,ViewGroup 重寫了dispatchDraw()方法。

protected void dispatchDraw(Canvas canvas) {
     final int childrenCount = mChildrenCount;
     final View[] children = mChildren;

     ……
     for (int i = 0; i < childrenCount; i++) {
         int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
         final View child = (preorderedList == null)
                 ? children[childIndex] : preorderedList.get(childIndex);
         if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                 || child.getAnimation() != null) {
             more |= drawChild(canvas, child, drawingTime);
         }
     }
     ……
 }

dispatchDraw()方法的作用是將繪製請求紛發到給子元件,並呼叫 drawChild()方法來完成子元件的繪製,drawChild()方法的原始碼如下:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
 return child.draw(canvas, this, drawingTime);
}

drawChild()方法再次呼叫了子元件的 boolean draw(Canvas canvas, ViewGroup parent, longdrawingTime)方法,該方法定義如下:

  boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ……
      if (!layerRendered) {
          if (!hasDisplayList) {
           // Fast path for layouts with no backgrounds
              if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                  mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                  dispatchDraw(canvas);
                  if (mOverlay != null && !mOverlay.isEmpty()) {
                      mOverlay.getOverlayView().draw(canvas);
                  }
              } else {
                  draw(canvas);
              }
              drawAccessibilityFocus(canvas);
          } else {
              mPrivateFlags &= ~PFLAG_DIRTY_MASK;
              ((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags);
          }

      }
   ……
  }

上面的方法又呼叫了 draw(Canvas canvas)方法,如果子元件不再是一個容器,將呼叫 if
(!dirtyOpaque) onDraw(canvas)語句完成元件的繪製,同樣地,onDraw(canvas)正是需要我們重寫的方法。所以,元件的繪製同樣是一個不斷遞迴的過程。

重寫 onLayout() 方法

在容器類的基本結構中,我們最陌生的是 onLayout()方法,該方法原型為: protected voidonLayout(boolean changed, int l, int t, int r, int b),其中,引數 changed 判斷是否有新的大小和位置,l 表示 left,t 表示 top,r 表示 right,b 表示 bottom,後面的 4 個引數表示容器自己相對父容器的位置以及自身的大小,通常情況下,r - l 的值等同於方法 getMeasuredWidth()方法的返回值,b - t 的值等同於 getMeasuredHeight()方法的返回值。關於 l、t、r、b 引數的值的理解如圖所示。
這裡寫圖片描述

在 onLayout()方法中,需要呼叫 View 的 layout()方法用於定義子元件和子容器的位置,
layout()方法的原理如下:

public void layout(int l, int t, int r, int b)
引數 l、t、r、b 四個引數的作用與上面相同,通過這 4 個引數,基本可以確定子元件的位置與大小了。

下面我們來看一個案例,該案例一是為了說明自定義容器的定義方法,二是瞭解重寫ViewGroup 方法的一些細節。

public class SizeViewGroup extends ViewGroup {
    public SizeViewGroup(Context context) {
        this(context, null, 0);
    }

    public SizeViewGroup(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SizeViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //建立一個按鈕
        TextView textView = new TextView(context);
        ViewGroup.LayoutParams layoutParams =
                new ViewGroup.LayoutParams(200, 200);
        textView.setText("Android");
        textView.setBackgroundColor(Color.YELLOW);
        //在當前容器中新增子元件
        this.addView(textView, layoutParams);
        //設定容器背景顏色
        this.setBackgroundColor(Color.alpha(255));
    }

    /**
     * 確定元件位置與大小
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //設定子元件(此處為 TextView)的位置和大小
        //只有一個元件,索引為 0
        View textView = this.getChildAt(0);
        textView.layout(50, 50, textView.getMeasuredWidth() + 50,
                textView.getMeasuredHeight() + 50);
    }

    /**
     * 測量元件
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //先測量所有子元件的大小
        this.measureChildren(widthMeasureSpec, heightMeasureSpec);
        //測量自身的大小,此處直接寫死為 500 * 500
        this.setMeasuredDimension(500, 500);
    }

    /**
     * 繪製
     */
    @Override
    protected void onDraw(Canvas canvas) {
        //為容器畫一個紅色邊框
        RectF rect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
        rect.inset(2, 2);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(2);
        paint.setColor(Color.BLACK);

        Path path = new Path();
        path.addRoundRect(rect, 20, 20, Path.Direction.CCW);
        canvas.drawPath(path, paint);
        super.onDraw(canvas);
    }
}

上面的程式碼中,我們定義了一個容器,為了不增加難度,刻意做了簡化。在 public
SizeViewGroup(Context context, AttributeSet attrs, int defStyleAttr)構造方法中,使用程式碼建立了一個 TextView 子元件,並將 SizeViewGroup 的背景設定成了透明(如果要繪製一個不規則的自定義元件,將背景設定成透明是個不錯的辦法)。onMeasure()方法用來測量容器大小,在測量容器之前,必須先呼叫 measureChildren()方法測量所有子元件的大小,本例中將容器的大小設定成了一個不變的值 500 * 500,所以,儘管在佈局檔案中將 layout_width 和 layout_height 都定義為match_parent,但事件上這個值並不起作用。onLayout()方法負責為子元件定位並確認子元件大小,因為只有一個子元件,所以先通過 getChildAt(0)獲取子元件(TextView 物件)物件,再呼叫子元件的 layout()方法確定元件區域,子元件的 left 為 50,top 為 50,right 為 50 加上測量寬度,bottom為 50 加上測量高度。onDraw()方法為容器繪製了一個圓角矩形作為邊框。執行效果如圖 所示。
這裡寫圖片描述

我們知道,一個元件多大,取決於 layout_width 和 layout_height 的大小,但真正決定元件
大小的是 layout()方法,我們將 onLayout()方法改成如下的實現:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
     //設定子元件(此處為 TextView)的位置和大小
     View textView = this.getChildAt(0);//只有一個元件,索引為 0
     textView.layout(50, 50, 200, 100);
 }

再次執行,結果如圖所示。從結果中看出,此時 TextView 元件變小了,說明一個元件的真正大小不是取決於 layout_width 和 layout_height,而是取決於 layout()方法。但是很顯然,通過layout()方法計算元件大小時,layout_width 和 layout_height 屬性是非常重要的參考因素。
這裡寫圖片描述

CornerLayout 佈局

基本原理

CornerLayout 佈局是一個自定義容器,用於將子元件分別顯示在容器的 4 個角落,不接受超過 4 個子元件的情形,預設情況下,子元件按照從左往右、從上往下的順序放置,但可以為子元件指定位置(左上角 left_top、右上角 right_top、左下角 left_bottom、右下角 right_bottom)。如果說前面的案例為了簡化形式有些偷工減料,那麼本小節將以一個完整的案例來講述自定義容器的基本實現。

先畫一個草圖來幫助我們分析,如圖。
這裡寫圖片描述
上圖中,藍色框表示 CornerLayout 佈局的區域,A、B、C、D 是 CornerLayout 內的 4 個子元件,對於 CornerLayout 來說,首先要測量的是他的尺寸大小,當 layout_width 為 wrap_content 時,其寬度為 A、C 的最大寬度和 B、D 的最大寬度之和,當 layout_height 為 wrap_content 時,其高度為 A、B 的最大高度 C、D 的最大高度之和,這樣才不至於子元件出現重疊。當然,如果layout_width 和 layout_height 指定了具體值或者螢幕不夠大的情況下設定為 match_parent,子元件仍有可能會出現重疊現象。

我們先將 CornerLayout 容器的基本功能開發出來,基本功能包括:支援 0~4 個子元件;每個子元件放置在不同的角落;完美支援 layout_width 和 layout_height 屬性。

public class CornerLayout extends ViewGroup {
    public CornerLayout(Context context) {
        super(context);
    }

    public CornerLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CornerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 定位子元件
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {

            View child = getChildAt(i);
            if (i == 0) {
                //定位到左上角
                child.layout(0, 0, child.getMeasuredWidth(),
                        child.getMeasuredHeight());
            } else if (i == 1) {
                //定位到右上角
                child.layout(getMeasuredWidth() - child.getMeasuredWidth(),
                        0, getMeasuredWidth(), child.getMeasuredHeight());
            } else if (i == 2) {
                //定位到左下角
                child.layout(0, getMeasuredHeight() - child.getMeasuredHeight(),
                        child.getMeasuredWidth(), getMeasuredHeight());
            } else if (i == 3) {
                //定位到右下角
                child.layout(getMeasuredWidth() - child.getMeasuredWidth(),
                        getMeasuredHeight() - child.getMeasuredHeight(),
                        getMeasuredWidth(), getMeasuredHeight());
            }
        }
    }

    /**
     * 測量尺寸
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //先測量所有子元件的大小
        this.measureChildren(widthMeasureSpec, heightMeasureSpec);
        //再測量自己的大小
        int width = this.measureWidth(widthMeasureSpec);
        int height = this.measureHeight(heightMeasureSpec);
        //應用尺寸
        this.setMeasuredDimension(width, height);

    }

    /**
     * 測量容器的寬度
     *
     * @param widthMeasureSpec
     * @return
     */
    private int measureWidth(int widthMeasureSpec) {
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        int width = 0;
        if (mode == MeasureSpec.EXACTLY) {
           //match_parent 或具體值
            width = size;
        } else if (mode == MeasureSpec.AT_MOST) {
          //wrap_content
            int aWidth = 0;
            int bWidth = 0;
            int cWidth = 0;
            int dWidth = 0;
            for (int i = 0; i < this.getChildCount(); i++) {
                if (i == 0)
                    aWidth = getChildAt(i).getMeasuredWidth();
                else if (i == 1)
                    bWidth = getChildAt(i).getMeasuredWidth();
                else if (i == 2)
                    cWidth = getChildAt(i).getMeasuredWidth();
                else if (i == 3)
                    dWidth = getChildAt(i).getMeasuredWidth();
            }
            width = Math.max(aWidth, cWidth) + Math.max(bWidth, dWidth);
        }
        return width;
    }

    /**
     * 測量容器的高度
     *
     * @param heightMeasureSpec
     * @return
     */
    private int measureHeight(int heightMeasureSpec) {
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);
        int height = 0;
        if (mode == MeasureSpec.EXACTLY) {
          //match_parent 或具體值
            height = size;
        } else if (mode == MeasureSpec.AT_MOST) {
          //wrap_content
            int aHeight = 0;
            int bHeight = 0;
            int cHeight = 0;
            int dHeight = 0;
            for (int i = 0; i < this.getChildCount(); i++) {
                if (i == 0)
                    aHeight = getChildAt(i).getMeasuredHeight();
                else if (i == 1)
                    bHeight = getChildAt(i).getMeasuredHeight();
                else if (i == 2)
                    cHeight = getChildAt(i).getMeasuredHeight();
                else if (i == 3)
                    dHeight = getChildAt(i).getMeasuredHeight();
            }
            height = Math.max(aHeight, bHeight) + Math.max(cHeight, dHeight);
        }
        return height;
    }
}

我猜測大家可能有一個疑問,在計算每個子元件的寬度和高度時,用了 4 個 if 語句進行判斷,如圖所示,這樣做的目的是為了在讀取子元件物件時防止因子元件數量不夠而出現下標越界,這個判斷是必須而且有用的。

這裡寫圖片描述

接下來對該元件進行測試,看是否達到我們預期的要求。我們通過修改 layout_width 和
layout_height 兩個屬性值來測試不同的執行結果。第一個測試的執行結果如圖 所示,定義cornetlayout.xml 佈局檔案,內容如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp">

    <bczm.com.day0617.CornerLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@android:color/holo_blue_bright" />

        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@android:color/holo_blue_dark" />

        <TextView

            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@android:color/holo_red_dark" />

        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@android:color/holo_green_light" />
    </bczm.com.day0617.CornerLayout>
</LinearLayout>

這裡寫圖片描述
如圖所示,將第一個子元件的寬度設定為 100dp,第四個子元件的高度設定為 100dp;
這裡寫圖片描述

如圖所示的執行結果中,我們只定義了三個子元件,效果依舊完美。
這裡寫圖片描述
最後一種情況,就是將 CornerLayout 的 layout_width 和 layout_height 屬性都設定為
match_parent,此時容器佔用了整個螢幕空間,4 個子元件向四周擴散,如圖 所示(橫屏顯示)。

這裡寫圖片描述

內邊距 padding

我們為 CornerLayout 容器新增一個灰色的背景,因為沒有設定子元件與容器邊框的距離
(padding),所以子元件與容器邊框是重疊的,如圖所示。
這裡寫圖片描述

如果考慮 padding 對容器帶來的影響,那事情就變得複雜些了。預設情況下,容器內的
padding 會自動留出來,但如果不改子元件位置會導致子元件不能完全顯示。另外,View 已經將padding 屬性定義好開發人員無須自行定義,並且定義了 4 個方法分別用於讀取 4 個方向的padding:

Ø public int getPaddingLeft() 離左邊的 padding
Ø public int getPaddingRight() 離右邊的 padding
Ø public int getPaddingTop() 離頂部的 padding
Ø public int getPaddingRight() 離底部的 padding

考慮 padding 屬性之後,將給容器的寬度和高度以及子元件的定位帶來影響,當 CornerLayout的 layout_width 為 wrap_content 時,其寬度 = A、C 的最大寬度 + B、D 的最大寬度 + 容器左邊的 padding + 容器右邊的 padding,當 layout_height 為 wrap_content 時,其高度 = A、B 的最大高度 + C、D 的最大高度 + 容器頂部的 padding + 容器底部的 padding。而在 onLayout()方法中定位子元件時,也同樣需要為 padding 留出空間。
總結一句話就是容器的寬度和高度都變大了。
在 measureWidth()方法中,計算容器寬度時,加上了左邊的 padding 和右邊的 padding:

private int measureWidth(int widthMeasureSpec){
    ……
    width = Math.max(aWidth, cWidth) + Math.max(bWidth, dWidth)
    + getPaddingLeft() + getPaddingRight();
    ……
    return width;
}

在 measureHeight()方法中,計算容器高度時,加上了頂部的 padding 和底部的 padding:

private int measureHeight(int heightMeasureSpec){
    ……
    height = Math.max(aHeight, bHeight) + Math.max(cHeight, dHeight)
    + getPaddingTop() + getPaddingBottom();
    ……
}

onLayout()方法的改動則比較大,大家最好是根據自己的思路和理解自行實現,這樣才能真正消化。我們給大家提供的只是作為參考。

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int leftPadding = getPaddingLeft();
        int rightPadding = getPaddingRight();
        int topPadding = getPaddingTop();
        int bottomPadding = getPaddingBottom();
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (i == 0) {
                //定位到左上角

                child.layout(leftPadding, topPadding,
                        child.getMeasuredWidth() + leftPadding,
                        child.getMeasuredHeight() + topPadding);
            } else if (i == 1) {
                //定位到右上角
                child.layout(getMeasuredWidth() - child.getMeasuredWidth()
                                - rightPadding,
                        topPadding, getMeasuredWidth() - rightPadding,
                        child.getMeasuredHeight() + topPadding);
            } else if (i == 2) {
                //定位到左下角
                child.layout(leftPadding,
                        getMeasuredHeight() - child.getMeasuredHeight()
                                - bottomPadding,
                        child.getMeasuredWidth() + leftPadding,
                        getMeasuredHeight() - bottomPadding);
            } else if (i == 3) {
                //定位到右下角
                child.layout(getMeasuredWidth() - child.getMeasuredWidth()
                                - rightPadding,
                        getMeasuredHeight() - child.getMeasuredHeight()
                                - bottomPadding,
                        getMeasuredWidth() - rightPadding,
                        getMeasuredHeight() - bottomPadding);
            }
        }
    }

在佈局檔案中,將 CornerLayout 的 paddingLeft、paddingTop、paddingRight 和 paddingBottom四個屬性分別設定為 10dp、20dp、30dp 和 40dp,執行結果如圖所示。

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp">

    <bczm.com.day0617.CornerLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FFCCCCCC"
        android:paddingBottom="40dp"
        android:paddingLeft="10dp"
        android:paddingRight="30dp"
        android:paddingTop="20dp">

        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@android:color/holo_blue_bright" />

        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@android:color/holo_blue_dark" />

        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@android:color/holo_red_dark" />

        <TextView
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:background="@android:color/holo_green_light" />
    </bczm.com.day0617.CornerLayout>
</LinearLayout>

這裡寫圖片描述

CornerLayout 並不具備實用價值,因為 FrameLayout 佈局能輕易實現 CornerLayout 的功能,但是,對於理解佈局容器的開發卻能提供一種非常清晰的方法和思路(這個才是最重要的,不是麼?)。

外邊距 margin

熟悉 css 的朋友應該知道,當 div 定義了 maring:15px;的樣式時,div 各方向離相鄰元素的距離都將是 15 個畫素(不考慮垂直合併的情況),其實,Android 元件的外邊距 margin 在理解的時候與 css 是基本相同的。我們修改 cornerlayout.xml 檔案如下:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp">

    <bczm.com.day0617.CornerLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FFCCCCCC"
        android:padding="10dp">

        <TextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_margin="10dp"
            android:background="@android:color/holo_blue_bright" />

    </bczm.com.day0617.CornerLayout>
</LinearLayout>

xml 程式碼中我們將第一個 TextView 的 layout_margin 屬性設定為 10dp(限於篇幅其他
TextView 省略了),我們希望第一個 TextView 離相鄰元件的距離為 10dp,但執行後卻發現沒有任何效果。原因是我們在 onLayout()方法中定位子元件時沒有考慮 margin 這個屬性。

如果要考慮 margin,則將影響以下幾個方面:
Ø 影響 onMeasure()方法測量的容器尺寸;
Ø 影響 onLayout()方法對子元件的定位;
Ø 必須為子元件提供預設的 MarginLayoutParams(或其子類)。

向容器新增子元件時,需要呼叫 addView()方法,該方法有幾個過載版本,如果呼叫 publicvoid addView(View child, LayoutParams params)方法,則必須手動指定 LayoutParams,LayoutParams中定義了兩個重要的屬性:width 和 height,對應了 xml 中的 layout_width 和 layout_height 屬性。

如果要讓元件支援 margin,則必須使用 MarginLayoutParams 類,該類是 LayoutParams 的子類,下面是 MarginLayoutParams 類的原始碼片段:

public static class MarginLayoutParams extends ViewGroup.LayoutParams {
    public int leftMargin; //對應 layout_marginLeft 屬性
    public int topMargin; //對應 layout_marginTop 屬性
    public int rightMargin; //對應 layout_marginRight 屬性
    public int bottomMargin; //對應 layout_marginBottom 屬性
}

然而,當我們呼叫 public void addView(View child)方法來新增子元件時,並不需要指定
LayoutParams,此時,ViewGroup 會呼叫其 generateDefaultLayoutParams()方法獲取預設的LayoutParams,對於支援子元件 margin 來說,這是必要的,addView()的原始碼如下:

public void addView(View child, int index) {
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();
        if (params == null) {
                throw new llegalArgumentException("generateDefaultLayout-Params() cannot
                return null");
            }
    }
    addView(child, index, params);
}

自定義容器如果要支援 margin 特性,容器類必須重寫 generateDefault-LayoutParams()方法,返回 MarginLayoutParams 物件。另外,還需要重寫另外兩個方法:

Ø public LayoutParams generateLayoutParams(AttributeSet attrs)
建立 LayoutParams (或子類)物件,通過 attrs 可以讀取到佈局檔案中的自定義屬性值,該方法必須重寫;

Ø protected LayoutParams generateLayoutParams(LayoutParams p)
建立 LayoutParams(或子類)物件,可以重用引數 p,該方法建議重寫。

為了讓 CornerLayout 支援 margin 特徵,需要重寫 generateDefaultLayout-Params()和
generateLayoutParams()方法,程式碼如下(為了不影響前面的案例,將類名改名為 CornerLayout2):

public class CornerLayout2 extends ViewGroup {

    ……

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(this.getContext(), attrs);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    
            
           

相關推薦

定義元件開發 定義容器

概述 自定義容器本質上也是一個元件,常見的 LinearLayout、FrameLayout、GridLayout、ScrollView和 RelativeLayout 等等元件都是容器,容器除了有自己的外觀,還能用來容納各種元件,以一種特定的規則規定元件應該在什麼位置、顯示多大。

定義元件開發定義元件

概述 Android SDK 為我們提供了一套完整的元件庫,數量多、功能強,涉及到方方面面,但是,我們依然看到軟體市場上的每個 App 都有自己獨特的東西,絕不是千遍一律的,而且也會和 IOS相互借鑑,這就需要我們對元件進行定製,實現自己獨樹一幟的使用者體驗和介面風格。自定義元件到底

定義元件開發九 側邊欄

概述 側邊欄是一種常見的 UI 結構,使用者使用手指左滑或者右滑,可以像抽屜一樣拉出隱藏在屏 幕邊界之外的內容,既能增加 App 的 UI 的內容,又能給使用者帶來更新鮮的使用者體驗,網易新聞、QQ(如圖所示)等主流 App 都有類似的設計。 側邊欄和上一章節講的觸控滑屏有

定義元件開發八 Scroller與平滑滾動

概述 Scroller 譯為“滾動器”,是 ViewGroup 類中原生支援的一個功能。我們經常有這樣的體驗:開啟聯絡人,手指向上滑動,聯絡人列表也會跟著一起滑動,但是,當我們鬆手之後,滑動並不會因此而停止,而是伴隨著一段慣性繼續滑動,最後才慢慢停止。這樣的使用者體驗完全照顧了人的習

定義元件開發五 陰影、 漸變和點陣圖運算

介紹陰影、漸變和點陣圖運算等技術 陰影只是一個狹義的說法,實際上也包括髮光等效果;Android 也提供了強大的漸變功能,漸變能為物體帶來更真實的質感,比如可以用漸變繪製一顆五子棋或一根金屬圓棒;點陣圖運算就更有趣了,Android 為 Bitmap 的運算提供了多達16 種運算方法,

定義元件開發四 雙快取技術

雙快取 為什麼叫“雙快取”?說白了就是有兩個繪圖區,一個是 Bitmap 的 Canvas,另一個就是當前View 的 Canvas。先將圖形繪製在 Bitmap 上,然後再將 Bitmap 繪製在 View 上,也就是說,我們在 View 上看到的效果其實就是 Bitmap 上的內容

定義元件開發三 Graphics 繪製動態效果

概述 上文https://blog.csdn.net/u011733020/article/details/80220513主要介紹了Graphics相關的api的繪圖方法。繪製的都是靜態的,這裡使用Graphics 來實現動態效果繪圖,來達到讓畫面動起來,或者讓圖案與我們的手指互動

定義元件開發二 Graphics API

Graphics是Android SDK 中的一個包含一系列繪圖相關的api的包,本文介紹並使用常用的繪圖方法。 Point 類和 和 PointF 我們都知道在座標系中給定x/y兩個座標就可以確定一個點。 Point類就是表示一個點,他有兩個成員變數x、y代表點的 x 座標和

定義元件開發一 View 的繪製流程

Activity 的組成結構 Activity對於安卓開發來說,是熟悉的不能再熟悉的,它是安卓四大元件之一,用來做介面顯示用的,那麼我相信,並不是所有的朋友都對Activity的組成結構有清晰的認識,這裡簡單聊聊Activity的組成。 實際介面展示的是Activity中的Wind

React-Native開發八 react-navigation之定義元件Counter

1 前言 我們知道RN中任何介面元素都可以看成元件,小到一個按鈕,大到一個頁面。RN開發就是不停的開發元件和使用元件,並讓他們協同工作,這樣高效率協同的執行起來,這樣就能完成一個APP的功能了 在實際的開發中,我們經常需要自定義一些滿足我們專案開發的自定義元件,類似於Android

009-Ambari二次開發之新增定義元件Redis(二)

上一篇我們主要介紹了Ambari新增元件的答題流程並以REDIS為例說明了流程,本篇在上一篇的基礎上,進一步完善說明流程並介紹如何給元件新增metric 掃描二維碼,關注BearData,獲取最新文章 上篇中,我們已經制作出了redis的rpm包,並重新編譯了我們修改後的Ambar

008-Ambari二次開發之新增定義元件Redis(一)

Ambari目前支援的元件有HDFS、YARN、HBase、Hive、Pig、ZooKeeper、Sqoop、Storm、Flume、Tez、Oozie、Falcon、Storm、Altas、Knox、Spark、Ranger、Mahout、Kerberos等,已經涵蓋了從大資料應用的

android開發定義組合控制元件

內容介紹 本文記錄,自定義組合控制元件,為了可以程式碼複用,減少程式碼量 配置控制元件屬性檔案 開啟res/values/目錄下的arss.xml檔案,新增下面屬性程式碼,如果沒有建立arrs.xml檔案。 <?xml version="1.0" enc

iOS開發學習-定義控制元件賦值問題--在model的set方法中給控制元件賦值

在自定義控制元件的過程中,剛開始的時候碰到問題是如何給各控制元件動態賦值,最初的想法是把各控制元件屬性放在.h檔案中定義.然後在控制器內獲取資料一一賦值(可行),但是這樣就增加了控制器中的程式碼,比如給定一個場景: collectionViewCell中,有10個控制元件,需要顯示10個數據,這

Android 開發定義控制元件開發-01

最近一直在忙於公司的專案,因為要去現場測試正式使用,專案不大但是經手了三個人,到我這裡只能去填坑了,不說這個了,說一下今天得主題,自定義控制元件之基本圖形繪製。 我們平時畫圖需要兩種工具:紙和筆。在Android中 Paint 就是畫筆,而Canvas類就是紙,在這裡叫做畫布。 所以

spring-security-oauth2() 定義簡訊登陸開發

簡訊登陸開發 原理 基本原理:SmsAuthenticationFilter接受請求生成SmsAuthenticationToken,然後交給系統的AuthenticationManager進行管理,然後找到SmsAuthenticationProvider,然後再呼叫UserDeta

Android 開發定義控制元件開發-02

1.畫筆的基本設定 : 1.setColor() 該函式的作用是設定畫筆顏色,完整的函式宣告如下: void setColor(int color) 我們知道,一種顏色是由紅、綠、藍三色合成出來的,所以引數 color 只能取8位的0xAARRGGBB樣式顏色值。 其中:

Android軟體開發定義控制元件

Android軟體開發之 自定義控制元件 雖然Android系統提供了各種各樣的控制元件供我們開發使用,但在實際的開發中,系統提供的控制元件有時候不能滿足我們的需求,這時我們就需要自定義一個控制元件。 下面的例子就來自定義一個簡單的Button: 首先是佈局,image_btn.xml: <?xml

Android開發定義控制元件--ViewPager

package com.itheima18.viewpager; import java.util.ArrayList; import java.util.Timer; import java.util.concurrent.Executors; import java.util.concurrent.Sc

定義select控制元件開發

目的:select下拉框條目太多(上百),當用戶選擇具體項時會浪費使用者很多時間去尋找,因此需要一個搜尋框讓使用者輸入關鍵字來匹配列表,便於使用者選擇 示例圖: 1、html結構 <div class="custom-select-container" data-name="oilBran