Android自定义View入门

View架构简介:

在Android中,控件主要以ViewGroup和View的形式存在。ViewGroup控件可以包含多个View控件,该复合控件负责其内部所有子控件的测量和绘制,并传递交互事件。如图,

Android自定义View入门_第1张图片

在Android的移动开发中,每个Activity都包含了一个PhoneWindow对象,该对象将DecorView设置为应用窗口的根View。该视图上的所有监听事件都通过WindowManagerService来进行接收,并通过Activity来回调相应的onClickListener。DecorView将屏幕分为了两部分,TitleView和ContentView,而我们的setContentView方法则用于处理这个位置的布局显示。我们的activity_main即处于这样一个ID为content的FrameLayout中。如图:
Android自定义View入门_第2张图片


自定义View的实现

自定义View流程,

  1. 自定义View初始化(创建类,引入自定义属性等)
  2. View的测量(重写onMeasure获取自定义的大小并设定)
  3. View的绘制(重写onDraw进行View的绘制)
  4. 设置自定义View的监听接口

View的测量

MeasureSpec

一个32位int值,高2位代表测量模式,低30位代表测量的大小,用于辅助View的测量,模式如下:

EXACTLY(精准模式):表示父控件已经确切的指定了子View的大小。 
AT_MOST(最大模式):依据子控件的变化而变化,但存在上限
UNSPECIFIED(自定义模式):大小设置无限制,比较扯淡吧我觉得

首先必须重写onMeasure方法,至于为什么,源码走一波

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

在AS中通过Ctrl+B来观看onMeasure的源码,

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

显然,这里将自定义的MeasureSpec对象作为参数传递给setMeasuredDimension.

再来看看getDefaultSize的作用

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

getDefaultSize则是根据对应的测量模式选取对应的Size。

在该方法的第二个参数,是一个getSuggestedMinimumWidth()或getSuggestedMinimumHeight()方法,以getSuggestedMinimumWidth()为例,

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

官方的解释是:Returns the suggested minimum width that the view should use. This
returns the maximum of the view’s minimum width。最合适的宽度就这样诞生了,,

由于View类默认支持EXACTLY模式,所以为了实现自定义View的wrapContent,就必须使用其他模式,即重写onMeasure方法。

其实onMeasure的重写很简单,就是通过Google提供的MeasureSpec获取Mode和Size,之后依据获得的Mode选取xml设定的数据,之后将数据传递给setMeasuredDimension即可。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        //从MeasureSpec中获取Mode和Size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height ;
        //依据模式获取预设的width和height
        if (widthMode == MeasureSpec.EXACTLY)
        {
            width = widthSize;
        } else
        {
            mPaint.setTextSize(mTitleTextSize);
            mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
            float textWidth = mBound.width();
            int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
            width = desired;
        }

        if (heightMode == MeasureSpec.EXACTLY)
        {
            height = heightSize;
        } else
        {
            mPaint.setTextSize(mTitleTextSize);
            mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
            float textHeight = mBound.height();
            int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
            height = desired;
        }
        //殊途同归,传递我们自定义的数据
        setMeasuredDimension(width, height);
    }

View的绘制

对于Android的2D绘图,我们主要借助于graphics包,其中主要包含了Canvas类、Paint类、Color类和Bitmap类等。

绘制图形必然要用到颜色,在Android开发中对颜色的使用和获取有疑问的看这篇文章:颜色

Paint类

要绘制图形,首先得调整画笔,按照自己的开发需要设置画笔的相关属性。

setAntiAlias: 设置画笔的锯齿效果。
setColor: 设置画笔颜色 。
setARGB: 设置画笔的a,r,p,g值。
setAlpha: 设置Alpha值 。
setTextSize: 设置字体尺寸。
setStyle: 设置画笔风格,空心或者实心。
setStrokeWidth: 设置空心的边框宽度。
getColor: 得到画笔的颜色 。
getAlpha: 得到画笔的Alpha值

详细参见:Paint详解

CanVas类

Canvas我们可以称之为画布,能够在上面绘制各种东西,是安卓平台2D图形绘制的基础,在使用该类前需要设置好paint。

Cnavas图形绘制参考:
Android自定义View入门_第3张图片

除了进行基本的绘制外,Canvas也可以进行一些基本操作。位移、缩放、旋转、倾斜以及快照和回滚。

有需要自行参见:Canvas的基本操作

一个简单的圆形绘制示例

        Paint paint = new Paint();
        paint.setColor(Color.CYAN);
        paint.setStrokeWidth(3);
        paint.setAntiAlias(true);
        //空心
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(160,60,50,paint);

        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(160,180,50,paint);

自定义属性

自定义属性流程

  1. 自定义一个CustomView类
  2. 创建attrs.xml文件,并添加所需的Item和declare-styleable
  3. 在布局文件中引用自定义属性并初始化
  4. 在CustomView类中的构造方法中通过TypedArray获取自定义属性和其值

该类的构造方法最多可有四个参数,

public void CustomView(Context context) {}
public void CustomView(Context context, AttributeSet attrs) {}
public void CustomView(Context context, AttributeSet attrs, int defStyleAttr) {}
public void CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}

在构造方法中AttributeSet中保存的是该自定义View所有属性的值名称和值,获取其中属性代码:

int count = attrs.getAttributeCount();
        for (int i = 0; i < count; i++) {
            String attrName = attrs.getAttributeName(i);
            String attrVal = attrs.getAttributeValue(i);
            Log.e(TAG, "attrName = " + attrName + " , attrVal = " + attrVal);
        }

通过这种方式我们只可以获取那些在xml中直接定义的属性,比如android:text = "horzion",但对于诸如android:text = “@String/name”则会直接反馈给你一段R文件中的标识码,为了简化工作这里引入TypeArray。

TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);

此时,typeArray中的集合便是经过简化处理,可以直接获取使用。

一个完整的示例:

public CustomTitleView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
        //获得我们所定义的自定义样式属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);
        int n = typedArray.getIndexCount();
        for (int i = 0; i < n; i++)
        {
            int attr = typedArray.getIndex(i);
            switch (attr)
            {
                case R.styleable.CustomTitleView_titleText:
                    mTitleText = typedArray.getString(attr);
                    break;
                case R.styleable.CustomTitleView_titleTextColor1:
                    // 默认颜色设置为黑色
                    mTitleTextColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.CustomTitleView_titleTextSize:
                    // 默认设置为16sp,TypeValue也可以把sp转化为px
                    mTitleTextSize = typedArray.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
            }
        }
        typedArray.recycle();//记得回收
    }

此外,declare-styleable标签既能告诉系统这是自定义的属性,又会帮助我们在R.java中生成一些常量方便我们使用。
最后,在获取完所需的属性后,我们将其设置到对应的paint上即可,创建paint并调用onDraw方法进行重绘。

在UI线程外,使用postInvalidate方法启动绘制,当需要重绘的时候则可以直接在UI线程中中使用inValidate方法实现重绘。
详细参见:postinvalidate 和invalidate的区别


设置监听接口

自定义完View之后,一般会对外暴露一些接口,用于控制View的状态等,或者监听View的变化.
直接在CustomView的构造方法在即可为View设置监听事件

        //设置监听事件
        this.setOnClickListener(new OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                mTitleText = randomText();
                postInvalidate();
            }

        });

一个验证码小示例:
Android自定义View入门_第4张图片

源码地址:Custom


原来学RecyclerView,走到半截发觉对自定义分割线有很多不理解的地方,于是有转过头来重学自定义View,看了群英传和开发艺术探索,以及网络博客诸多,终于写够了这篇基础的自定义。
关于更深入的Canvas学习,可以参加这位大牛的最后一篇文章,Canvas之图片以及张洪洋一整个系列自定义View实战
路漫漫其修远兮,吾将上下而求索。纪念第一篇MarkDown博客

你可能感兴趣的:(Android自定义View入门)