自定义View初级

对于自定义View,一开始用的都是别人的,当时刚学Android的时候,想到自定义View,觉得是挺难的,也比较抵触,不愿意去看,到后来觉得,不可能一直用别人的,得学会自己开发,然后,就试着学了。我学习的历程,首先肯定是在网上查阅资料,看博客,看视频,跟着敲代码,然后,就自己试着写了。

下面来写一个比较简单易懂的例子,实现一个类似TextView的控件:

  • 自定义属性:

    为一个View提供可自定义的属性非常简单,只需要在res资源目录的values目录下创建一个attr.xml属性定义文件,并在该文件中通过如下定义相应的属性即可。

<declare-styleable name="MyTextView">
    <attr name="title_text" format="string" />
    <attr name="title_color" format="color" />
    <attr name="title_size" format="dimension" />
    <attr name="background_color" format="color" />
</declare-styleable>

在自定义属性的时候,我一开始犯了一个错误,这里得注意一下,在attr标签中定义的属性名不能是已经存在的,不然会报找不到这个属性的错误,这里一定要谨记。

  • 继承View,实现其构造方法:
public MyTextView(Context context) {
    this(context, null);
}

public MyTextView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    obtainStyledAttrs(context, attrs);   //初始化,获取自定义属性 
}

这儿有一个小技巧,在写构造方法的时候,一个参数的使用this调用两个参数的构造方法,以此类推,最后的初始化在三个参数的构造方法中完成,这样,只需把初始化的代码放入三个参数的构造方法中,就可以统一在其他构造方法调用,这样写的好处在于比较简洁,避免在每一个构造方法中都要单独的写上初始化的代码。
还有一点需要特别注意,上面叙述中的是使用this进行构造方法的调用,千万不要用super,一定要注意!!!后面会有讲到。

  • 获取自定义属性值:

    在获取自定义属性值之前,还需要做一件事,就是为属性值设置默认值以及参数的声明,如下:

private static final int DEFAULT_TITLE_SIZE = 10;
private static final int DEFAULT_TITLE_COLOR = 0x000000;
private static final int DEFAULT_BACKGROUND_COLOR = 0xffffff;

private int titleSize = sp2px(DEFAULT_TITLE_SIZE);
private int titleColor = DEFAULT_TITLE_COLOR;
private int background_color = DEFAULT_BACKGROUND_COLOR;
private String titleText = "";

获取属性值的代码如下:

/**
 - 获取自定义属性
 -  * @param attrs
 */
private void obtainStyledAttrs(Context context, AttributeSet attrs) {
    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
    titleSize = (int) array.getDimension(R.styleable.MyTextView_title_size, titleSize);
    titleColor = array.getColor(R.styleable.MyTextView_title_color, titleColor);
    titleText = array.getString(R.styleable.MyTextView_title_text);
    background_color = array.getColor(R.styleable.MyTextView_background_color, background_color);
    array.recycle();
}
  • 接下来是View的测量,相信大家都知道,在自定义View中需要完成几件事,分别是onMeasure、onDraw(尤为重要),下面,进入onMeasure,也就是测量View。

    那么问题来了,如何测量呢?在Android中方为我们提供了一个设计短小精悍却功能强大的类——MeasureSpec类,通过它来帮助我们测量View。MeasureSpec是一个32位的int值,其中高2位为测量的模式,低30位为测量的大小,在计算中使用位运算的原因是为了提高并优化效率。
    测量模式分为三种:

    • EXACTLY:即精确模式,当我们将控件的layout_width属性或layout_height属性指定为具体数值时,比如android:layout_width=”100dp”,或者指定为match_parent时(占据父View的大小),系统使用的是EXACTLY模式。

    • AL_MOST:即最大模式,当控件的layout_width属性或layout_height属性指定为wrap_content时,控件大小一般随着控件的子空间或内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。

    • UPSPECIFIED:这个属性比较奇怪——它不指定其大小测量模式,View想多大就多大。

简单的介绍完了MeasureSpec的三种测量模式之后,下面上代码:

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

public int measureDimension(int defaultSize, int measureSpec) {
    int result;

    //获取测量模式
    int specMode = MeasureSpec.getMode(measureSpec);
    //获取值
    int specSize = MeasureSpec.getSize(measureSpec);

    if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else {
        result = defaultSize;   //UNSPECIFIED
        if (specMode == MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
    }
    return result;
}
  • 最后一步便是onDraw,代码如下:
@Override
protected void onDraw(Canvas canvas) {

    canvas.save();   //保存绘制状态

    Paint paint = new Paint();     //画笔
    paint.setColor(background_color);    //设置绘制的颜色
    canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);
    canvas.save();    //此处绘制矩形

    paint.setColor(titleColor);
    paint.setTextSize(titleSize);
    Rect rect = new Rect();

    //将文本内容放入Rect中,以便获取文本的宽高
    paint.getTextBounds(titleText, 0, titleText.length(), rect);
    canvas.drawText(titleText, getWidth() / 2 - rect.width() / 2, getHeight() / 2 + rect.height() / 2, paint);    //绘制文本

    canvas.restore(); 

}

到此,自定义View已完成,此处需特别注意一个问题,在写构造函数的时候,我一开始没注意,使用了系统默认的super,后来把代码基本写完之后,发现使用的自定义View没有任何效果,屏幕上一片空白,之后发现了问题所在,因为使用super调用的是父类的方法,要想调用本类的构造方法,必须改为this,切记。

完整的代码如下:

public class MyTextView extends View {

    private static final int DEFAULT_TITLE_SIZE = 10;
    private static final int DEFAULT_TITLE_COLOR = 0x000000;
    private static final int DEFAULT_BACKGROUND_COLOR = 0xffffff;

    private int titleSize = sp2px(DEFAULT_TITLE_SIZE);
    private int titleColor = DEFAULT_TITLE_COLOR;
    private int background_color = DEFAULT_BACKGROUND_COLOR;
    private String titleText = "";

    public MyTextView(Context context) {
        this(context, null);
    }

    public MyTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        obtainStyledAttrs(context, attrs);
    }

    /**
     * 获取自定义属性
     *
     * @param attrs
     */
    private void obtainStyledAttrs(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
        titleSize = (int) array.getDimension(R.styleable.MyTextView_title_size, titleSize);
        titleColor = array.getColor(R.styleable.MyTextView_title_color, titleColor);
        titleText = array.getString(R.styleable.MyTextView_title_text);
        background_color = array.getColor(R.styleable.MyTextView_background_color, background_color);
        array.recycle();
    }

    public int getTitleSize() {
        return titleSize;
    }

    public int getTitleColor() {
        return titleColor;
    }

    public String getTitleText() {
        return titleText;
    }

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

    public int measureDimension(int defaultSize, int measureSpec) {
        int result;

        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = defaultSize;   //UNSPECIFIED
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //save方法是将当前的结果推到栈里,相当于保存多个历史记录
        canvas.save();

        Paint paint = new Paint();
        paint.setColor(background_color);
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);
        canvas.save();

        paint.setColor(titleColor);
        paint.setTextSize(titleSize);
        Rect rect = new Rect();
        //getTextBounds方法一定要在setColor...之后
        //此方法是将text文本放入已新建的Rect容器中,由此得到文本内容的宽高
        paint.getTextBounds(titleText, 0, titleText.length(), rect);
        //此处不能用getMeasuredWidth()替换getWidth()
        canvas.drawText(titleText, getWidth() / 2 - rect.width() / 2, getHeight() / 2 + rect.height() / 2, paint);
        //restore方法是将最后一个结果弹出栈,相当于只留下最后一个修改的状态作为最终结果,而之前的状态全部被删除
        canvas.restore();

    }

    /**
     * dp转换为px
     *
     * @param dpVal
     * @return
     */
    protected int dp2px(int dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, getResources().getDisplayMetrics());
    }

    /**
     * sp转换为px
     *
     * @param spVal
     * @return
     */
    protected int sp2px(int spVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, getResources().getDisplayMetrics());
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值