package com.example.mycustomviewdemo;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.util.AttributeSet;import android.view.View;/** * 繼承View的自定義控件 * 注意 view是wrap_content時需要手動測量View的寬高 * View有padding值時需要處理 */public class MyCircleView extends View { PRivate Paint mPaint; private int mRadius; public MyCircleView(Context context) { this(context,null); } public MyCircleView(Context context, AttributeSet attrs) { this(context, attrs,0); } public MyCircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mPaint = new Paint(); //初始化畫筆 mPaint.setColor(Color.GREEN); mPaint.setAntiAlias(true); mRadius = 80; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width = 0; int height =0; if(widthMode == MeasureSpec.EXACTLY) { width = widthSize; }else { //widthMode == MeasureSpec.AT_MOST模式 自己設置控件寬度 //當是wrap_content或者給具體dp的時候會走這里 width = mRadius * 2 + getPaddingRight() + getPaddingLeft(); } if(heightMode == MeasureSpec.EXACTLY) { height = heightSize; }else { height = mRadius * 2 + getPaddingTop() + getPaddingBottom(); } //注意最后 調用這個方法 讓屬性生效 setMeasuredDimension(width,height); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //處理padding int pl = getPaddingLeft(); int pr = getPaddingRight(); int pt = getPaddingTop(); int pb = getPaddingBottom(); int width = getWidth() - pl - pr; //控件本身的寬度 int height = getHeight() - pt - pb; //控件本身的高度 int centerX = width /2 + pl; //中心點的橫坐標 int centerY = height /2 + pt; //中心點的縱坐標 canvas.drawCircle(centerX,centerY,mRadius,mPaint); }}繼承ViewGroup實例
當我們自定義View繼承自ViewGroup的時候,需要實現孩子的onLayout方法指定子View的擺放位置,并且需要重寫 onMeasure 方法來測量大小。在這個實例當中,我們簡單模仿下 LinearLayout ,只不過只實現其 Vertical 模式,在這個實例當中,我們需要注意的細節有:1 ViewGroup是wrap_content時需要手動測量2 當ViewGroup本身有padding值的時候需要處理3 當子View有margin值時需要處理規范自定義ViewGroup, 這幾個細節我們要處理,代碼:package com.example.mycustomviewdemo;import android.content.Context;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;/** * 繼承ViewGroup實例 * * 注意: * ViewGroup是wrap_content需要手動測量 * 當ViewGroup本身有padding值時要處理 * 當子view有margin值時要處理 */public class MySimpleVerticalLayout extends ViewGroup { private Context mContext; public MySimpleVerticalLayout(Context context) { this(context,null); } public MySimpleVerticalLayout(Context context, AttributeSet attrs) { this(context, attrs,0); } public MySimpleVerticalLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //獲取ViewGroup測量模式 大小 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //獲取ViewGroup的padding(內邊距)值 int pt = getPaddingTop(); int pb = getPaddingBottom(); int pl = getPaddingLeft(); int pr = getPaddingRight(); //先測量孩子, 才能得到孩子具體的寬高; ------->> 這一步很重要 measureChildren(widthMeasureSpec,heightMeasureSpec); int width = 0; int height = 0; int maxWidth = 0; if(widthMode == MeasureSpec.AT_MOST) { for(int i = 0; i < getChildCount();i++) { View childAt = getChildAt(i); if(childAt.getVisibility() == GONE) { continue; } //寬度為孩子中 最寬的一個 //孩子還有個MarginLayoutParams屬性 MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childAt.getLayoutParams(); int childWidth = childAt.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin; maxWidth = maxWidth > childWidth ? maxWidth : childWidth; } //將遍歷后的最寬的寬度加上左右內邊距 賦值 width = maxWidth + pl + pr; } if(heightMode == MeasureSpec.AT_MOST) { for(int i = 0; i < getChildCount();i++) { View childAt = getChildAt(i); if(childAt.getVisibility() == GONE) { continue; } //高度為所有的孩子高度之和加上內邊距之和 MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childAt.getLayoutParams(); height += childAt.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin; } //最終的高度 height += (pt + pb); } //做判斷, 并將值設置 setMeasuredDimension(widthMode == MeasureSpec.AT_MOST ? width : widthSize,heightMode == MeasureSpec.AT_MOST ? height : heightSize); } /** * 對子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) { //viewGroup的padding值影響孩子的擺放 int pt = getPaddingTop(); int pb = getPaddingBottom(); int pl = getPaddingLeft(); int pr = getPaddingRight(); int cl = 0; int ct = 0; int cr = 0; int cb = 0; int bm = 0; //這個bm很神奇 for(int i =0; i < getChildCount();i++) { //判斷當子view沒有被Gone掉時候 View childAt = getChildAt(i); if(childAt.getVisibility() != GONE) { //計算每個View的位置 MarginLayoutParams marginLayoutParams= (MarginLayoutParams) childAt.getLayoutParams(); cl = marginLayoutParams.leftMargin; ct += marginLayoutParams.topMargin; cr = childAt.getMeasuredWidth() + marginLayoutParams.leftMargin; cb += childAt.getMeasuredHeight() + marginLayoutParams.topMargin; //對子View進行布局, 注意 一定要調用childAt.layout()方法 childAt.layout(cl + pl, ct + pt + bm, cr + pr,cb + pb + bm); ct += childAt.getMeasuredHeight(); bm += marginLayoutParams.bottomMargin; } } } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(mContext, attrs); }}繼承已有View的實例
繼承自系統已有View時,一般是對其原有功能進行擴展或者修改, 比如一個Button 在這里注意監聽器的使用繼承已有ViewGroup的實例
這種自定義 View 的實現方式也叫做:“自定義組合控件”,是一種比較簡單的自定義 View 方式。使用這種方式時,由于是繼承已有的系統控件,所以我們不需去測量、布局、處理 margin、padding等,因為系統控件本身已經處理好了。當我們的項目中有一些布局在很多地方都要用到的話,那么第一時間肯定就要想到復用了。復用的話,有人可能會想到使用 include 復用布局,但是如果這樣的話,當布局改動性很大時,使用 include 并不是很靈活。這時候,就可以使用 ”繼承已有 ViewGroup“ 這種方式了。下面一個實例,就拿我們平時可能經常要寫的 Item 為例吧:package com.example.mycustomviewdemo;import android.content.Context;import android.content.res.TypedArray;import android.util.AttributeSet;import android.view.LayoutInflater;import android.view.View;import android.widget.FrameLayout;import android.widget.ImageView;import android.widget.TextView;/** * 繼承已有的ViewGroup 自定義View的實例,常用item布局 */public class MyCustomItemLayout extends FrameLayout { private Context mContext; private String mLeftText; private int mRightImageResourceId; private String mRightText; private TextView mTxt_left; private TextView mTxt_right; private ImageView mImg_right; public void setLeftText(String leftText) { mLeftText = leftText; } public void setRightImageResourceId(int rightImageResourceId) { mRightImageResourceId = rightImageResourceId; } public void setRightText(String rightText) { mRightText = rightText; } public MyCustomItemLayout(Context context) { this(context,null); } public MyCustomItemLayout(Context context, AttributeSet attrs) { this(context, attrs,0); } public MyCustomItemLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.mContext = context; //取出自定義屬性 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyCustomItemLayout); mLeftText = typedArray.getString(R.styleable.MyCustomItemLayout_leftText); //默認圖片為箭頭 mRightImageResourceId = typedArray.getResourceId(R.styleable.MyCustomItemLayout_rightImage, R.drawable.ic_arrow_right); mRightText = typedArray.getString(R.styleable.MyCustomItemLayout_rightText); typedArray.recycle(); //回收釋放資源 initView(); initData(); } private void initData() { //兩種初始化數據的方法, 外界通過set方法進行設置; 布局中直接定義 mTxt_left.setText(mLeftText); mTxt_right.setText(mRightText); mImg_right.setImageResource(mRightImageResourceId); } private void initView() { //注意 這第二個參數傳 this; 兩個參數的方法默認會調用三個參數的方法, 第二個參數不為null時,相當于三個參數中root不為null,attach為true View view = LayoutInflater.from(mContext).inflate(R.layout.layout_customitem, this); mTxt_left = (TextView) findViewById(R.id.txt_left); mTxt_right = (TextView) findViewById(R.id.txt_right); mImg_right = (ImageView) findViewById(R.id.img_right); }}首先自定義一個類,繼承自 FrameLayout,當然,這里你也可以選擇繼承 LinearLayout 或者其他,根據具體需求來。其中在構造中獲取了自定義屬性,最主要的地方就是填充布局那里,將布局填充到了當前控件也就是自定義的 ViewGroup 上。填充的布局如下:<?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="wrap_content" android:background="?android:selectableItemBackground" android:gravity="center_vertical" android:padding="15dp"> <TextView android:id="@+id/txt_left" android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawablePadding="5dp" android:ellipsize="end" android:maxLines="1" android:textColor="@color/text_black" android:textSize="@dimen/txt14"/> <TextView android:id="@+id/txt_right" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_weight="1" android:ellipsize="end" android:gravity="right" android:maxLines="1" android:textSize="@dimen/txt14"/> <ImageView android:id="@+id/img_right" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dp" android:src="@mipmap/ic_arrow_right"/></LinearLayout>在項目中 有相類似的Item布局的使用時, 可以直接在布局中通過自定義屬性設置數據:<com.example.mycustomviewdemo.MyCustomItemLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" app:leftText="版本更新" app:rightText="V1.1" app:rightImage="@drawable/ic_arrow_right" />也可以通過暴露的方法設置數據至此,自定義控件四種繼承方式講解完畢, 下面看一三個自定義控件的效果
新聞熱點
疑難解答