自定義View的分類
繼承View
當咱們須要實現的效果是一個不規則效果的時候,那麼這時就須要繼承 View 來實現了,咱們須要重寫 onDraw 方法,在該方法裏實現各類不規則的圖形和效果。當咱們使用這種方式的時候,須要本身去處理 warp_content 和 padding。html
繼承ViewGroup
當系統所提供的 LinearLayout、FrameLayout 等佈局控件沒法知足咱們的需求時,這時咱們就須要使用這種方式來實現本身想要的佈局效果了。當咱們使用這種方式的時候,須要重寫 onLayout 方法來對子 View 進行佈局,以及測量自己和子 View 寬高,還須要處理自己的 padding 和子 View 的 margin。java
繼承已有View
當咱們須要基於已有的 View 進行擴展或修改的時候,那麼就可使用這種方式。好比說,咱們須要一個圓角的 ImageView,那麼這時就能夠繼承 ImageView 進行修改了。當咱們使用這種方式的時候,通常不須要本身去處理 wrap_content 和 padding 等,由於系統控件已經幫咱們作好了。android
繼承已有佈局
這種方式也叫作:自定義組合 View。該方式比較簡單,當咱們須要將一組 View 組合在一塊兒,方便後期複用的時候,就可使用該方法。當咱們使用這種方式的時候,不須要去處理 ViewGroup 的測量和佈局流程,由於系統控件已經幫咱們作好了。git
那麼下面咱們就從實例的角度來看看自定義View吧
繼承View的實例
當咱們自定義View繼承子View時,咱們須要注意的細節有:github
1 View是wrap_content時須要手動測量View的寬高canvas
2 View有padding值的時候須要處理app
在這裏咱們寫一個規範的自定義View, 畫出一個圓ide
注意: 要對 View 的 padding 和 LayoutParams 是 wrap_content 的狀況進行處理,不然 padding 將會沒法生效、wrap_content 的效果會和 match_parent 同樣佈局
其中重寫onMeasure方法, 判斷當是wrap_content的狀況時,本身測量view的寬或高this
[java] view plain copy
- 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, 這幾個細節咱們要處理,代碼:[java] view plain copy
- 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 爲例吧:
[java] view plain copy
- 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 上。填充的佈局以下:
[html] view plain copy
- <?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佈局的使用時, 能夠直接在佈局中經過自定義屬性設置數據:
[html] view plain copy
- <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"
- />
也能夠經過暴露的方法設置數據
至此,自定義控件四種繼承方式講解完畢, 下面看一三個自定義控件的效果