自定義View二篇,如何自定義一個規範的ViewGroup

  • 2020 年 2 月 15 日
  • 筆記

前言

自定義View開篇,必須跨過的一道坎兒 中,我們介紹了自定義View的幾種方式,以及如何實現一個規範的自定義View,上文中也說了,實現一個規範的自定義ViewGroup是一件比較困難的事情,因為要考慮的情況包含 本身的padding以及子view的margin 與 本身wrap_content 問題。

如何實現一個規範的ViewGroup,以實現垂直布局的LinerLayout為例

  • 新建LinerLayoutView 繼承自ViewGroup

首先我們讓LinerLayoutView 適應wrap_content的情況,在onMeasure中處理如下,同自定義View處理一樣,不同的是我們需要計算子View寬高,代碼如下所示:

@Override  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {      super.onMeasure(widthMeasureSpec, heightMeasureSpec);      int measureWidth = MeasureSpec.getSize(widthMeasureSpec);      int measureHeight = MeasureSpec.getSize(heightMeasureSpec);      int widthMode = MeasureSpec.getMode(widthMeasureSpec);      int heightMode = MeasureSpec.getMode(heightMeasureSpec);      measureChildren(widthMeasureSpec,heightMeasureSpec);      int totalheight = 0;      int totalWidth = 0;        for (int i = 0; i < getChildCount(); i++){          View childView = getChildAt(i);          int childrWidth = childView.getMeasuredWidth();          int childHeight = childView.getMeasuredHeight();          totalheight = totalheight + childHeight;          totalWidth = Math.max(totalWidth,childrWidth);      }        if (heightMode == MeasureSpec.AT_MOST && widthMode == MeasureSpec.AT_MOST){          setMeasuredDimension(totalWidth,totalheight);      }else if (heightMode == MeasureSpec.AT_MOST){          setMeasuredDimension(measureWidth,totalheight);      }else if (widthMode == MeasureSpec.AT_MOST){          setMeasuredDimension(totalWidth,measureHeight);      }  }

我們需要調用下面方法來測量子view

measureChildren(widthMeasureSpec,heightMeasureSpec);

因為這裡我們是垂直排列的,所以要循環計算view的總高度,wrap_content情況對應的寬度為子View最大的寬度,上面代碼比較簡單 我們主要來看onLayout方法。

@Override  protected void onLayout(boolean changed, int l, int t, int r, int b) {        int totalHeight = 0;        for (int i = 0; i < getChildCount(); i++){          View childView = getChildAt(i);          int childrWidth = childView.getMeasuredWidth();          int childHeight = childView.getMeasuredHeight();          childView.layout(0,totalHeight,childrWidth,totalHeight + childHeight);          totalHeight = totalHeight + childHeight;      }  }

view.layout方法 是將view放置在什麼地方,分別對應left、top、right、bottom四個點,這裡我們需要注意的是,務必使用getMeasureWidth不能使用getWidth,因為前者是在測量的時候獲取的,後者在布局完成之後才能獲取到。

在布局文件中 引用這個ViewGroup,並且添加兩個子View,代碼如下所示:

<com.support.hlq.layout.LinerLayoutView      android:layout_width="wrap_content"      android:layout_height="wrap_content"      android:background="@color/colorPrimaryDark">        <TextView          android:layout_width="match_parent"          android:layout_height="wrap_content"          android:text="kkko"          android:textColor="@color/colorAccent" />        <TextView          android:layout_width="match_parent"          android:layout_height="wrap_content"          android:text="kkko"          android:textColor="@color/colorAccent" />    </com.support.hlq.layout.LinerLayoutView>

運行結果如圖所示,可以看到我們已經適配了wrap_content的情況

考慮ViewGroup的padding問題

上面代碼,已經實現了最簡單的垂直排列,我們給LinerLayoutView設置大小為40的邊距,發現邊距並沒有生效,所以我們需要在onMeasure以及onLayout的方法中考慮padding問題。改寫onMeasure方法如下:

for (int i = 0; i < getChildCount(); i++){      View childView = getChildAt(i);      int childrWidth = childView.getMeasuredWidth();      int childHeight = childView.getMeasuredHeight();        totalheight = totalheight + childHeight;      totalWidth = Math.max(totalWidth,childrWidth);  }    totalheight = totalheight + getPaddingTop() + getPaddingBottom();  totalWidth = totalWidth + getPaddingLeft() + getPaddingRight();

因為我們處理的是ViewGroup的邊距,所以我們只需要對最終計算的高度和寬度分別加上上下邊距 和左右邊距即可,這裡你可能會有疑問

int measureWidth = MeasureSpec.getSize(widthMeasureSpec);  int measureHeight = MeasureSpec.getSize(heightMeasureSpec);

為什麼上面兩處代碼不需要加上邊距呢,因為MeasureSpec獲取到的大小是已經包含過padding,所以我們不需要處理通過MeasureSpec獲取的寬高。

接下來,我們修改layout方法

@Override  protected void onLayout(boolean changed, int l, int t, int r, int b) {        int totalHeight = getPaddingTop();        for (int i = 0; i < getChildCount(); i++){          View childView = getChildAt(i);            int childrWidth = childView.getMeasuredWidth();          int childHeight = childView.getMeasuredHeight();            childView.layout(0 + getPaddingLeft(),                  totalHeight,                  childrWidth + getPaddingLeft(),                  totalHeight + childHeight + getPaddingTop());          totalHeight = totalHeight + childHeight;      }  }

left點我們加上getPaddingLeft,總高度由0修改為getPaddingTop,其他兩點也分別加上邊距布局即可,運行結果如下所示

我們可以看出ViewGroup的邊距已經生效了。

考慮子View的Margin問題

到這裡 這個自定義的ViewGroup還是不夠規範,不信我們來給第一個TextView設置下邊距為20dp

<com.support.hlq.layout.LinerLayoutView      android:layout_width="wrap_content"      android:layout_height="wrap_content"      android:background="@color/colorPrimaryDark">        <TextView          android:layout_marginBottom="20dp"          android:layout_width="match_parent"          android:layout_height="wrap_content"          android:text="kkko"          android:textColor="@color/colorAccent" />        <TextView          android:layout_width="match_parent"          android:layout_height="wrap_content"          android:text="kkko"          android:textColor="@color/colorAccent" />    </com.support.hlq.layout.LinerLayoutView>

在這裡,因為要獲取到margin所以必須重寫

generateLayoutParams 方法和 generateDefaultLayoutParams 方法
@Override  public LayoutParams generateLayoutParams(AttributeSet attrs) {      return new MarginLayoutParams(getContext(), attrs);  }    @Override  protected LayoutParams generateDefaultLayoutParams() {      return new MarginLayoutParams(LayoutParams.MATCH_PARENT,              LayoutParams.MATCH_PARENT);  }    @Override  protected LayoutParams generateLayoutParams(LayoutParams p) {      return new MarginLayoutParams(p);  }

重寫上面三個方法後,我們才能獲取margin參數,同樣的我們首先在onMeasure中考慮子view邊距

for (int i = 0; i < getChildCount(); i++) {      View childView = getChildAt(i);        MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();      int childrWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;      int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;      totalheight = totalheight + childHeight;      totalWidth = Math.max(totalWidth, childrWidth);  }

還要在獲取寬高的時候加上對應的邊距即可,同樣還需要在onLayout方法中考慮子view 的邊距問題,修改如下:

@Override  protected void onLayout(boolean changed, int l, int t, int r, int b) {        int totalHeight = getPaddingTop();        for (int i = 0; i < getChildCount(); i++) {          View childView = getChildAt(i);            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();            int childrWidth = childView.getMeasuredWidth() ;          int childHeight = childView.getMeasuredHeight() ;            childView.layout(getPaddingLeft() + lp.leftMargin,                  totalHeight + lp.topMargin,                  childrWidth + getPaddingLeft() + lp.leftMargin,                  totalHeight + childHeight + getPaddingTop() + lp.topMargin);          totalHeight = totalHeight + childHeight + lp.topMargin + lp.bottomMargin;      }  }

在layout的時候考慮子view的邊距,記得在計算總高度的時候 也要加上邊距和下邊距,運行結果如下圖所示

這樣一來,我們就定義了一個比較規範的ViewGroup,加上我們上篇文章講的自定義屬性,相信大家都掌握了自定義View的方法了。