瞭解自定義View和繼承View,繼承ViewGroup,繼承已有View,繼承已有ViewGroup實例ji

自定義View的分類

繼承View

當我們需要實現的效果是一個不規則效果的時候,那麼這時就需要繼承 View 來實現了,我們需要重寫 onDraw 方法,在該方法裏實現各種不規則的圖形和效果。當我們使用這種方式的時候,需要自己去處理 warp_content 和 padding。

繼承ViewGroup

當系統所提供的 LinearLayout、FrameLayout 等佈局控件無法滿足我們的需求時,這時我們就需要使用這種方式來實現自己想要的佈局效果了。當我們使用這種方式的時候,需要重寫 onLayout 方法來對子 View 進行佈局,以及測量本身和子 View 寬高,還需要處理本身的 padding 和子 View 的 margin。

繼承已有View

當我們需要基於已有的 View 進行擴展或修改的時候,那麼就可以使用這種方式。比如說,我們需要一個圓角的 ImageView,那麼這時就可以繼承 ImageView 進行修改了。當我們使用這種方式的時候,一般不需要自己去處理 wrap_content 和 padding 等,因爲系統控件已經幫我們做好了。

繼承已有佈局

這種方式也叫做:自定義組合 View。該方式比較簡單,當我們需要將一組 View 組合在一起,方便後期複用的時候,就可以使用該方法。當我們使用這種方式的時候,不需要去處理 ViewGroup 的測量和佈局流程,因爲系統控件已經幫我們做好了。

那麼下面我們就從實例的角度來看看自定義View吧

繼承View的實例

當我們自定義View繼承子View時,我們需要注意的細節有:
1 View是wrap_content時需要手動測量View的寬高
2 View有padding值的時候需要處理

在這裏我們寫一個規範的自定義View, 畫出一個圓
注意: 要對 View 的 padding 和 LayoutParams 是 wrap_content 的情況進行處理,否則 padding 將會無法生效、wrap_content 的效果會和 match_parent 一樣

其中重寫onMeasure方法, 判斷當是wrap_content的情況時,自己測量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實例

當我們自定義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"
       />
也可以通過暴露的方法設置數據


至此,自定義控件四種繼承方式講解完畢,  下面看一三個自定義控件的效果




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章