當自定義ViewGroup時,主要需要重寫onMeasure計算高度和寬度,重寫onLayout為每個子View設置位置。 在onMeasure中設置的寬度和高度時,需要注意的是這個高度和寬度應該是包括padding的;在onLayout中為每個子View設置的位置應該是不包含每個子View的左右上下margin的。 另外需要注意的是,如果需要提供LayoutParams,需要重寫generateLayoutParams(AttributeSet attrs)方法返回一個LayoutParams,這個參數就是其子View調用getLayoutParams返回的LayoutParams,重寫這個這個方法時,如果所有的布局均是在xml中完成的,那么不會出現問題,而如果一旦調用addView方法,則會拋出異常,如果需要支持addView方法,那么需要重寫generateDefaultLayoutParams()方法返回一個默認的LayoutParams。 下面是一個標簽流式布局的示例:
/** * 標簽布局 * Created by Xingfeng on 2016-10-20. */public class TagLayout extends ViewGroup { public TagLayout(Context context) { super(context); } public TagLayout(Context context, AttributeSet attrs) { super(context, attrs); } public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @TargetApi(Build.VERSION_CODES.LOLLipOP) public TagLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } /** * 計算高度和寬度 * @param widthMeasureSpec * @param heightMeasureSpec */ @Override PRotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int count = getChildCount(); int lineHeight = 0; int lineWidth = 0; int width = 0; int height = 0; //遍歷子View for (int i = 0; i < count; i++) { View child = getChildAt(i); //測量子View measureChild(child, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //得到子View占據的寬度和高度,包括marigin int w = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int h = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; //如果一行寬度超過了TagLayout的寬度,不包括左右padding if (lineWidth + w > widthSize - getPaddingLeft() - getPaddingRight()) { //高度加上上一行的高度 height += lineHeight; //高度取所有行中最寬的 width = Math.max(width, lineWidth); lineHeight = h; lineWidth = w; } //不需要換行 else { //每一行的高度以最大的高度為準 lineHeight = Math.max(lineHeight, h); lineWidth += w; } //如果是最后一個View,因為可能沒有轉行,所以要對寬度做個判斷,有可能最后一行就是最寬的,總的高度需要加上最后一行的高度 if (i == count - 1) { width = Math.max(width, lineWidth); height += lineHeight; } } //確定寬高,如果是確定的,則使用約束的,否則使用計算得到值 int w = widthMode == MeasureSpec.EXACTLY ? widthSize : width + getPaddingLeft() + getPaddingRight(); int h = heightMode == MeasureSpec.EXACTLY ? heightSize : height + getPaddingTop() + getPaddingBottom(); //千萬記得調用該方法 setMeasuredDimension(w, h); } /** * 對子View進行位置安放 * @param changed * @param l * @param t * @param r * @param b */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //安放的起始位置是左上角,去除左和上padding部分 int left = getPaddingLeft(); int top = getPaddingTop(); int lineWidth = 0; int lineHeight = 0; int width = getWidth(); int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int vWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int vHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; //換行 if (vWidth + lineWidth > width - getPaddingRight() - getPaddingLeft()) { //計算下一行的高度 top += lineHeight; lineWidth = vWidth; //下一行左邊的開始 left = getPaddingLeft(); } else { //每一行高度取最大的一個 lineHeight = Math.max(lineHeight, vHeight); //行寬 lineWidth += vWidth; } child.layout(left + lp.leftMargin, top + lp.topMargin, left + +lp.leftMargin + child.getMeasuredWidth(), top + lp.topMargin + child.getMeasuredHeight()); //左起始 left += vWidth; } } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } /** * 用于支持addView方法 * @return */ @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(200,700); }}自定義View一般分為三種: 1. 組合View,利用基本View進行組合,得到一個新的View 2. 繼承現有View,增加新功能 3. 繼承View,自定義內容的繪制以及事件的處理
下面以三個例子分為介紹這三種情況。
組合View一個典型的例子,就是應用的頂部類似ActionBar的一個例子,比如說左邊一個返回按鈕,中間是標題,最右邊又是一個按鈕。下面就以這個為例,介紹一下組合View的步驟:
XML布局如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="48dp" android:orientation="horizontal"> <Button android:id="@+id/left_btn" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="返回" /> <TextView android:id="@+id/middle_title" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="4" android:background="#DD0000" android:gravity="center" android:text="組合View" /> <Button android:id="@+id/right_btn" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="更多" /></LinearLayout>雖然是組合View,但依然要依附于某個View,不然就不能在XML中使用了,這里我們使TabBar繼承自FrameLayout,然后將上面的XML文件加載到FrameLayout里,這樣就可以在XML中使用了,而XML中的每個控件也可以找到了,如下:
public class TabBar extends FrameLayout { private Button leftBtn, rightBtn; private TextView titleTv; public TabBar(Context context, AttributeSet attrs) { super(context, attrs); //使上面的XML文件加載為FrameLayout的子View LayoutInflater.from(context).inflate(R.layout.tabbar_layout, this); leftBtn = (Button) findViewById(R.id.left_btn); titleTv = (TextView) findViewById(R.id.middle_title); rightBtn = (Button) findViewById(R.id.right_btn); }}當然,TabBar還可以寫出一些接口供用戶設置按鈕屬性等等,這兒就不介紹了,想了解的朋友可以到最下面的源碼查看。 效果如下:
下面以一個CircleView為例介紹下繼承已有View,CircleView繼承自TextView,主要就是用于沒有設置背景時,在文本后繪制一個圓形的背景。效果如下圖: 代碼如下,主要就是重寫onDraw方法,在調用TextView的onDraw方法之前首先繪制一個盡可能大的圓。
當組合View和繼承已有View不能滿足我們的需求時,那么需要繼承View,一步一步實現自定義View。主要有如下幾步: 1. 編寫attrs.xml文件定義屬性,這些屬性是可以直接在XML中指定了,就像layout_width等等 2. 繼承View,在構造方法中獲取到XML文件中的各屬性以及賦值 3. 重寫onMeasure方法處理高寬為wrap_content的情況 4. 重寫onDraw繪制內容
下面以一個ArcProgress(弧形進度條)為例,模仿魅族5.0.1的垃圾清理進度條。
attrs.xml文件位于res/value目錄下,主要需要定義中間文字尺寸、顏色、當前進度值、弧形的寬度、顏色、底部標題的文字、顏色和尺寸。
<declare-styleable name="ArcProgress"> <attr name="progress_text_size" format="dimension|reference"></attr> <attr name="progress_text_color" format="color|reference"></attr> <attr name="arc_progress" format="integer|reference"></attr> <attr name="arc_stroke_width" format="dimension|reference"></attr> <attr name="arc_color" format="color|reference"></attr> <attr name="arc_bottom_text" format="string|reference"></attr> <attr name="arc_bottom_text_color" format="color|reference"></attr> <attr name="arc_bottom_text_size" format="dimension|reference"></attr> </declare-styleable>在View的構造方法中使用TypedArray進行獲取屬性值并賦值,如下:
public ArcProgress(Context context) { this(context, null); } public ArcProgress(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ArcProgress(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initDefaultValues(context); initAttrs(context, attrs); initPaints(); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public ArcProgress(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initDefaultValues(context); initAttrs(context,attrs); initPaints(); } private void initDefaultValues(Context context){ DEFAULT_COLOR= Color.parseColor(DEFAULT_COLOR_STR); DEFAULT_STROKE_WIDTH= Utils.dp2px(context.getResources(), DEFAULT_STROKE_WIDTH_F); DEFAULT_TEXT_COLOR=Color.parseColor(DEAULT_TEXT_COLOR_STR); DEFAULT_TEXT_SIZE=Utils.sp2px(context.getResources(), DEFAULT_TEXT_SIZE_F); DEFAULT_INSIDE_STORKE_WIDTH= Utils.dp2px(getResources(),DEFAULT_INSIDE_STORKE_WIDTH_F); } private void initAttrs(Context context,AttributeSet attrs){ TypedArray ta=context.obtainStyledAttributes(attrs,R.styleable.ArcProgress); arcColor=ta.getColor(R.styleable.ArcProgress_arc_color,DEFAULT_COLOR); strokeWidth=ta.getDimension(R.styleable.ArcProgress_arc_stroke_width, DEFAULT_STROKE_WIDTH); progressTextColor=ta.getColor(R.styleable.ArcProgress_progress_text_color, DEFAULT_COLOR); progressTextSize=ta.getDimension(R.styleable.ArcProgress_progress_text_size,DEFAULT_TEXT_SIZE); bottomText=ta.getString(R.styleable.ArcProgress_arc_bottom_text); bottomTextColor =ta.getColor(R.styleable.ArcProgress_arc_bottom_text_color, DEFAULT_TEXT_COLOR); bottomTextSize=ta.getDimension(R.styleable.ArcProgress_arc_bottom_text_size, DEFAULT_TEXT_SIZE); progress=ta.getInt(R.styleable.ArcProgress_arc_progress, 0); ta.recycle(); } private void initPaints(){ arcPaint=new Paint(Paint.ANTI_ALIAS_FLAG); arcPaint.setColor(arcColor); arcPaint.setStyle(Paint.Style.STROKE); textPaint=new Paint(Paint.ANTI_ALIAS_FLAG); }}在構造方法內完成初始化操作,包括屬性的默認值,獲取屬性值以及Paint的設置。
當需要處理wrap_content屬性時,需要重寫該方法。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode=MeasureSpec.getMode(widthMeasureSpec); int heightMode=MeasureSpec.getMode(heightMeasureSpec); int widthSize=MeasureSpec.getSize(widthMeasureSpec); int heightSize=MeasureSpec.getSize(heightMeasureSpec); //如果寬度是wrap_content,使用建議的最小寬度 if(widthMode==MeasureSpec.AT_MOST){ widthSize=getSuggestedMinimumWidth(); } //如果高度是wrap_content,使用建議的最小高度 if(heightMode==MeasureSpec.AT_MOST){ heightSize=getSuggestedMinimumHeight(); } setMeasuredDimension(widthSize, heightSize); } @Override protected int getSuggestedMinimumHeight() { return (int) Utils.dp2px(getResources(),MINIMUM_HEIGHT); } @Override protected int getSuggestedMinimumWidth() { return (int)Utils.dp2px(getResources(),MINIMUM_WIDTH); }從上面的代碼可以看到,當使用wrap_content時,使用建議的最小值,重寫了建議值,默認值是80dp。
在完成了整個編寫工作后,還可以給該View提供一些額外的設置方法。首先看一下效果: 上圖模仿了一個下載動作,進度從0變成了100。 該部分關于ArcProgress的代碼可以查看ArcProgress源碼 至此,三種自定義View的方式就都介紹了,具體應該使用哪種方式視情況而定。
本篇博客主要介紹了自定義View。首先介紹了自定義ViewGroup,需要注意的是onMeasure方法和onLayout方法,以及需要返回LayoutParams對象;自定義View時,有三種選擇,可以組合View,可以繼承已有View,也可以直接繼承View,自己編寫邏輯。
關于本篇博客中的所有代碼均在我的Github地址。
新聞熱點
疑難解答