Android中的自定义View(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lyz_zyx/article/details/78142774

我们在实际开发过程中,如果想要在界面做出酷炫的效果,单单靠系统提供的控件是远远不够的,所以这时候就需要自定义View了。在Google下也有好多比较成熟的开源项目/开源控件可下载来使用,但是毕竟代码的设计都是原作者的思路,我们在使用起来也有可能受到意外的限制或多余功能的冗余。自己掌握自定义View的制作还是很有必要的,毕竟有时项目中还是可能需要独一无二的样式控件,而且自己通过自定义View绘制出来的效果还会带来更多的成就感以及提高后期可维护性。


一般地自定义View分几种方式:


一、继承现有View,扩展现有View的一些功能

例如我们要实现一个支持配置字体的TextView,那么可以继承TextView,并扩展一个叫typeface属性,可以使在XML界面布局时直接配置该属性便可出现特定字体的TextView。

又例如我们要实现一个支持背景颜色渐变动画的容器View,当设置它的背景色变化时它不会立即改变颜色,而是会有一个过渡的渐变过程,这时就要继承LinearLayout 或 RelativeLayout 或 FrameLayout等(根据实现情况定),然后在其内部实现一个当前颜色值,并使循环改变当前背景使实现每帧都不一样的颜色。

二、直接继承View

这种方式主要用于实现一些不规则的效果。采用这种方式一般都是要重写onDraw方法,而且最好对wrap_content和padding做支持,因为不对其进行特殊处理的话,那么当外界在布局中使用wrap_content和padding时就会无法达到预期的效果。wrap_content的支持可以重写onMeasure方法,而padding的支持可在onDraw方法中实现。

三、直接继承ViewGroup

这种方式主要用于实现自定义的布局。采用这种方式稍微有些复杂,它需要合适地处理ViewGroup的measure、layout这两个过程,并同时处理子元素的measure和layout过程,而且还要考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。

示例一、直接继承View

步骤1、修改activity_main.xml布局文件:

<?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-auto"
    android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:background="#ffffff"
   android:orientation="vertical" >

    <project.text.com.myapplication.MyView
        android:id="@+id/myView1"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:padding="10dp"
        android:background="#ff0000"
        app:circle_color="#ffff00" /> 
    </LinearLayout>

说明:

1、xmlns:app=http://schemas.android.com/apk/res-auto这行是使用自定义属性必须要的,其中app是属性前缀,可自定义,也可以写成:xmlns:app=http://schemas.android.com/apk/res/包名

2、app:circle_color="#ffff00"这行就是自定义的属性

步骤2、在values目录下创建自定义属性的XML,比如attrs.xml,也可以选择以attrs_开头的文件名:

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

说明:

这里定义了一个格式为“color”的属性“circle_color”,自定义属性还有其他格式,如: reference是指资源id,dimension是指尺寸,而像string、integer和boolean这种是指基本数据类型,等。

步骤3、创建我们的自定义控件类MyView:

public class MyView extends View {

    private final static int DEF_WIDTH = 200;
    private final static int DEF_HEIGHT = 200;

    private int mColor = Color.YELLOW;
    private Paint mPaint;

    public MyView(Context context) {
        this(context, null, 0);
    }
    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(mColor);

        // 获取自定义属性的值
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView);
        mColor = typedArray.getColor(R.styleable.MyView_circle_color, Color.YELLOW);
        typedArray.recycle();
    }

    // 重写onDraw
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 加入padding值的绘制,使padding属性起作用
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingLeft();
        final int paddingBottom = getPaddingLeft();

        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        // 绘圆
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
    }

    // 重写onMeasure为了能使wrap_content起作用
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(DEF_WIDTH, DEF_HEIGHT);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(DEF_WIDTH, heightSpaceSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpaceSize, DEF_HEIGHT);
        }
    }
}

说明:

1、MyView直接继承View,可以看到在构造函数中通过TypedArray对象获得原来在XML中定义的自定义属性的值;

2、重写onDraw方法,该方法参数是一个Canvas(画布)对象,本例中只是简单地通过drawCircle方法绘制了一个圆形,其中,第一、二参数是圆的圆心坐标,第三参数是圆的半径,第四参数是一个Paint(画笔)对象,Paint决定绘出的方式,包括笔的粗细、颜色等,相关的内容知识会很多,我们这里暂不详细介绍;

3、上面提到,如若直接继承View的自定义控件最好对wrap_content和padding做支持,可以看到onDraw方法中,我们通过getPadding一系列方法获得padding的值,然后再通过这些值来算出圆心和半径;

4、关于wrap_content的支持,就是重写了onMeasure方法中实现,否则wrap_content的值就相当于match_parent。因为如果View在布局上使用wrap_content,那么它的specMode是AT_MOST模式,在这模式下,它的宽/高等于specSize,而specSize就是parentSize,这种效果就跟使用match_parent完全一样。详细原理可以回顾《View的工作原理(一)之 View的三大过程 和 认识MeasureSpec》

5、onMeasure方法接收宽和高的MeasureSpec值,我们在《View的工作原理(一)之 View的三大过程 和 认识MeasureSpec》 中介绍也过MeasureSpec高2位代表SpecMode,低30位代表SpecSize,此两个值就是用于测量出View的宽和高。所以在omMeasure的实现中先获得View的SpecMode,然后再判断若果是AT_MOST,则传递一个我们想要的值,此值可根据实际开发情况决定,如TextView就是按字的长度来对应变大,本示例中我们给定死了两个值DEF_WIDTH 和 DEF_HEIGHT。

自定义控件onDetachedFromWindow和onAttachedToWindow方法

在自定义View中如果有线程或者动画,需要及时停止,那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachedToWindow,当包含此View的Activity启动时,View的onAttachedToWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。

猜你喜欢

转载自blog.csdn.net/lyz_zyx/article/details/78142774