自定義View的分類
繼承View
繼承ViewGroup
繼承已有View
繼承已有佈局
那麼下面我們就從實例的角度來看看自定義View吧
繼承View的實例
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實例
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的實例
繼承已有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>
<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"
/>
也可以通過暴露的方法設置數據