了解自定义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"
       />
也可以通过暴露的方法设置数据


至此,自定义控件四种继承方式讲解完毕,  下面看一三个自定义控件的效果




阅读更多
换一批

没有更多推荐了,返回首页