Android自定义View入门

一丶概述

自定义View是一个综合的技术体系,涉及到的知识点主要有:View的层次架构,事件分发机制和View的工作原理等。
系统已经提供了很多现成View,可以满足大部分情况下的应用,
但是当我们想要实现一些绚丽的界面或特殊效果时,仅靠系统提供的控件经常是不够的,这个时候就需要自定义View。比如要屏蔽一些事件或者添加一些其他效果等,都可以用自定义View实现。
二丶自定义View的分类
1、继承View重写onDraw方法
作用:用于实现一些不规则的效果。
特点:这种方式不方便通过系统布局组合的方式来达到,往往需要静态或者动态的显示一些不规则的图形。一般需要重写onDraw方法,并且需要自己支持wrap_content,并且padding也需要自己处理。
2、继承ViewGroup派生的Layout
作用:用于实现自定义布局(类似于LinearLayout等由几个View组合在一起,即当某种效果像几种View组合在一起的时候,可以采用这种方式来实现)
特点:这种方式需要合适的处理ViewGroup的测量丶布局这两个过程,并同时处理子元素的测量和布局过程。
3、继承特定的View(比如TextView)
作用:扩展某种已有的View的功能,如TextView。
特点:不需要自己支持wrap_content和padding等。
4、继承特定的ViewGroup(比如LinearLayout)
作用:当某种效果看起来像几种View组合在一起的时候,可以采用这种方式实现。
特点:和第二种方式不同的是,不需要自己ViewGroup的测量和布局这两个过程,一般来说方式2能实现的效果方式4也能实现,不过方式2更接近于View的底层。

三丶自定义View的注意点

自定义View有很多注意点,如果处理不好可能会导致View不能正常显示或者内存泄露等。

让View支持wrap_content
如果你的自定义View直接继承于View或者ViewGRoup,需要在onMeasure中对wrap_content做特殊处理,否则当外界使用wrap_content时可能无法达到预期的效果。

如果有必要,让你的View支持padding
如果你的自定义View直接继承于View,需要在draw方法中处理padding,否则padding属性是不起作用的。并且直接继承于ViewGRoup的话,需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。

尽量不要在View中使用Handler,没必要 这是因为View本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你很明确地要使用Handler来发送消息。 View中如果有线程或者动画,需要及时停止 View中如果有线程或者动画需要停止时,那么,需要在onDetachedFromWindow中做一些处理。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachToWindow,当包含此View的Activity启动时,View的onAttachToWindow方法会被调用。同时,当View变得不可见时,我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄露。 View带有滑动嵌套情形时,需要处理好滑动冲突 如果有滑动冲突,应该合适处理,否则会影响View的效果。

四丶自定义View示例

主要是对自定义分类中的方式一和方式二中的一些常见问题通过代码的方式体现出来。

这里演示一下继承View重写onDraw方法

这种方式主要是用于实现一些不规则的效果,一般需要重写onDraw方法,采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。

这里写一个小Demo,就是画一个圆,在Demo中会对wrap_content和padding进行处理,同时为了提供便捷性,还提供了自定义属性。

第一步:显示一个圆

自定义CircleView代码
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class CircleView extends View{
    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        //半径
        int radius = Math.min(height,width) / 2;
        canvas.drawCircle(width/2,height/2,radius,mPaint);
    }
}

代码很简单,就是重写onDraw方法,以宽/高的最小值为直径绘制一个红色的实心圆。

activity_main.xml布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical">

    <com.lizx.circleview.CircleView
        android:id="@+id/mCircleView"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#000000" />
</LinearLayout>

运行结果:
这里写图片描述

第二步:调整CircleView 的布局参数,为其设置20dp的margin,调整后的布局文件如下:

activity_main
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical">

    <com.lizx.circleview.CircleView
        android:id="@+id/mCircleView"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:background="#000000" />
</LinearLayout>

运行效果如下:
这里写图片描述

这说明margin属性是生效的,因为margin属性是由父容器控制的,因此不需要在CircleView 中做特殊处理。

第三步:为CircleView 设置20dp的padding,调整后的布局文件如下:

activity_main
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical">

    <com.lizx.circleview.CircleView
        android:id="@+id/mCircleView"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:padding="20dp"
        android:background="#000000" />
</LinearLayout>

运行效果如下:
这里写图片描述

结果发现padding根本没有生效,这是因为直接继承于View和ViewGroup的控件,padding是默认无法生效的,需要自己处理。

第四步:将CircleView的宽度由match_parent改为wrap_content,修改后的布局文件如下:

activity_main
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical">

    <com.lizx.circleview.CircleView
        android:id="@+id/mCircleView"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:padding="20dp"
        android:background="#000000" />
</LinearLayout>

运行效果如下:
这里写图片描述

结果发现wrap_content和match_parent没有任何区别,这是因为对于直接继承自View或者ViewGroup的控件,如果不对wrap_content进行处理,那么就相当于使用的是match_parent。

总结以上几步发现在自定义View时主要有几个问题:①padding无法生效②wrap_content无法生效,和match_parent没有任何区别。

下面针对这两个问题进行一一解决:
解决wrap_content无法生效的问题

我们知道View的宽高由自己的MeasureSpec决定,而View自身MeasureSpec又由View自身的LayoutParams和父容器的MeasureSpec决定。下面借鉴了一张图:

这里写图片描述

由图发现,如果View在布局中使用wrap_content,那么它的specMode是AT_MOST(最大模式)。这种模式下,它的宽高等于specSize,而此时specSize就等于parentSize,而parentSize就是父容器目前可以使用的大小,即剩余的空间大小,那么这种情况下当然和match_parent没有任何区别。

解决方法:我们只需要给在onMeasure中为View指定一个默认的内部宽/高(mWidth和mHeight),并在wrap_content时设置此宽/高即可。对于非wrap_content情形,沿用系统的测量值即可。

重写CircleView的onMeasure方法为View的wrap_content属性设置一个默认宽高即可。修改代码如下:

CircleView
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class CircleView extends View{
    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private int mWidth = 400;
    private int mHeight = 400;

    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        //半径
        int radius = Math.min(height,width) / 2;
        canvas.drawCircle(width/2,height/2,radius,mPaint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //如果你需要让自己的自定义控件支持wrap_content,可以参照以下代码写
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        Log.e("TAG",widthSpecSize+":"+heightSpecSize);
        if(widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,mHeight);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,mHeight);
        }
    }
}

运行结果如下:
这里写图片描述

我们的宽使用的是wrap_content,而在CircleView中我们为该模式设置了默认的400的宽度。也就是说如果我们为宽或者高设置了wrap_content,那么就是默认的400的宽度。如果你想要实现和TextView等系统控件的wrap_content效果一样,只要对 mWidth 和 mHeight 的值进行自己想要的一些计算即可,或者参照系统控件的写法。
解决padding无法生效的问题

问题剖析:为什么padding无法生效?
答:我们的CircleView自定义控件主要是为了画一个圆,而我们设置padding其实就是为了让我们所画的圆与自己的容器有一定的内边距,并且,我们知道margin是由父容器处理的,而padding则应该由自身处理。所以,我们应该对半径进行处理,即减去用户设置的padding属性,而我们在画圆的时候并未对圆进行任何处理,当然padding无法生效。
解决padding问题只要在onDraw方法中考虑padding即可。比如我们这个自定义CircleView(其实就是画一个圆),首先获取到用户设置的padding值,然后在画圆的时候,让圆的半径减去这些padding值,再让圆心加上某些padding值。

重写onDraw方法,处理padding

CircleView
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class CircleView extends View{
    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private int mWidth = 300;
    private int mHeight = 300;

    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingBottom = getPaddingBottom();
        final int paddingTop = getPaddingTop();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingBottom - paddingTop;
        //半径
        int radius = Math.min(height,width) / 2;
        canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,mPaint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        Log.e("TAG",widthSpecSize+":"+heightSpecSize);
        if(widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,mHeight);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,mHeight);
        }
    }
}

运行效果图:
这里写图片描述

发现padding确实生效了,其实质就是我们内部自己处理了padding。
提供自定义属性

像android:layout_width,android:padding这样的属性是系统自带的属性,那么想要自定义自己的属性,应该遵循以下几步:
1. 在values目录下创建自定义属性的XML,比如attrs.xml,文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
    </declare-styleable>
</resources>

在上面的XML中声明了一个自定义属性集合”CircleView”,在这个集合中可以有很多自定义属性,比如我们定义的circle_color,format则表示我们自定义属性的取值类型。

2.在自定义View的构造器中解析自定义属性的值并做相应处理,而我们这个例子就是解析circle_color这个属性的值。代码如下:

 public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
        a.recycle();
        init();
    }

上面这段代码首先加载自定义属性集合CircleView,接着解析CircleView属性集合中的circle_color属性,它的id为CircleView_circle_color,在这一步骤中,如果在使用时没有指定circle_color这个属性,那么就会选择红色作为默认的颜色,解析完自定义属性后,通过recycle方法来释放资源。

在布局文件中使用自定义属性,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/com.lizx.circleview"
    xmlns:app1="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical">

    <com.lizx.circleview.CircleView
        android:id="@+id/mCircleView"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:padding="20dp"
        app:circle_color="#00ff00"
        android:background="#000000" />


</LinearLayout>

上面的布局文件有一点需要注意,要使用自定义属性,必须在布局文件中添加schemas声明:xmlns:app1=”http://schemas.android.com/apk/res-auto”,在这个声明中,app1是自定义属性的前缀,当然可以换其他名字,引用格式为: app1:circle_color=”#00ff00”。并且你注意到的话,我们在这行的上面还有一个schemas声明,xmlns:app=”http://schemas.android.com/apk/res/com.lizx.circleview”,如果使用这个声明,用app而不是app1当前缀,当然这两种声明格式都是可以的,后面这种最后的部分是应用的包名。

运行效果是:
这里写图片描述
显而易见,我们的自定义属性起作用了,中间的圆变成绿色的了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值