自定义View的实现

今天的主题是自定义View,其实自定义View,对于一些刚刚从事android的兄弟可能还是比较惧怕的,听起来自定义View还是一个比较 "高大上” 的技术。但是其实吧,自定义View很简单,自定义一个View大致分为以下步骤:

(1)在attrs.xml 文件下自定义属性。

(2)在xml布局文件中使用自定义的属性。

(3)在构造体中使用获取自定义的属性值。

(4)复写onMeasure()方法,计算出测量尺寸。

(5)复写onDraw()方法,是用画笔将所需要的图形画到canvas画布上即可。

以上5个步骤就是典型的自定义View的标准步骤。首先说下:1,2,3 步骤并不是必须的,毕竟有时候在定义一些简单的自定义View的时候并不一定需要使用到自定义属性。接下来我们从一个简单小例子上一步一步的开始分析自定义View。首先看一下效果图:



这是一个自定义的星级显示View,有点类似于系统提供给我们的RatingBar,在这个自定义控件中我设置了selectedNumber这个属性,selectedNumber 为几就代表当前是几星,totalStar 总共的星级数,itemWidth:一个星星的宽,itemDistance :星星与星星Item之间的间距。

(1)在attrs.xml 文件下自定义属性

attrs是位于values文件夹下面的文件,假如没有attrs.xml这个文件的话,你需要手动建一个。在attrs.xml 定义自定义属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="StarView">
    <attr name="selectedNum" format="integer" />
    <attr name="totalStar" format="integer" />
    <attr name="itemWidth" format="dimension" />
    <attr name="itemDistance" format="dimension" />
</declare-styleable>
</resources>

其中<declare-styeable> 中的name 命名为自定义View的类的名字就行。<attr>为自定义的属性,在这里我选择的格式是integer类型,当然还可以选择其他的类型如:String float  boolean 这个格式根据你自己的需求进行设置。定义完自定义属性,接下来就在布局文件中使用。
(2)在xml布局文件中使用自定义的属性

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   <span style="color:#ff0000;"> xmlns:app="http://schemas.android.com/apk/res-auto"</span>
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.myapplication.StarView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:selectedNum="3"
        app:totalStar="5"
        app:itemDistance="4dp"
        app:itemWidth="12dp"/>

</RelativeLayout>

设置通过星级为5星级,当前星级为3星级,星星宽12dp,Item间距为4dp。画红线的是命名空间,假如不加那个代码的话,自定义属性不能使用。


(3)在构造体中使用获取自定义的属性值

这一步开始我就要开始自定义View这个类了。首先看获取自定义属性值:

public class StarView extends View {

    private int mSelectedNum;
    private int mTotalNum;
    private int mItemDistance;
    private int mItemWidth;

    private Paint mPaint;
    Bitmap selectedImg;
    Bitmap normalImg;

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

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

    public StarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        initResource();

        //获取我们自定义的属性
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.StarView, defStyleAttr, 0);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.StarView_selectedNum:
                    mSelectedNum = a.getInt(i, 1);//默认评价的等级为一星级
                    break;
                case R.styleable.StarView_totalStar:
                    mTotalNum = a.getInt(i, 5);//默认一共是五星级
                    break;
                case R.styleable.StarView_itemDistance:
                    mItemDistance = a.getInt(i, 0);//默认设置item间距为0
                    break;
                case R.styleable.StarView_itemWidth:
                    mItemWidth = a.getInt(i, selectedImg.getWidth());//默认情况下为图形本身的宽
                    break;
            }
        }

        initPaint();

    }


    private void initResource() {
        selectedImg = BitmapFactory.decodeResource(getResources(), R.drawable.sheetmusic_plectrum_orange);
        normalImg = BitmapFactory.decodeResource(getResources(), R.drawable.sheetmusic_plectrum_grey);
    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);//设置画出的图像边缘平滑而非锯齿形状
    }


在构造体中获取自定义的属性值,这部分代码基本上属于固定的代码步骤。将获取到的属性值赋值给变量即可。另外在构造体中我们还会初始化画笔,以便在onDraw()中进行使用,由于我这个是在onDraw()方法中画图形所以也初始化了bitmap。


(4)复写onMeasure()方法,计算出测量尺寸

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //获取父容器推荐的模式和尺寸
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
      
        int width = 0;
        int height = 0;

        if (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST
                || heightMode == MeasureSpec.EXACTLY || heightMode == MeasureSpec.AT_MOST) {
            
            width = mTotalNum * mItemWidth + mItemDistance * (mTotalNum - 1) + getPaddingRight() + getPaddingRight();
            height = mItemWidth + getPaddingTop() + getPaddingBottom();
        }

        setMeasuredDimension(width, height);
    }

widthMode 和 heightMode 都有 MeasureSpec.EXACTLY,MeasureSpec.AT_MOST,MeasureSpec.UNSPECIFIED 这三种模式,第三种模式我们几乎是碰不到的所以一般情况我们会判断前两种模式,当我们的layout_width 或者 layout_height 设置成match_parent 或者 设置成一个固定的数值的话,就是EXACTLY模式,但是当我们设置成为wrap_content的时候就是AT_MOST 模式。上面是这个自定义View中定义的onMeausre()方法。说句实话并不能真正的反应出来onMeausre()的一个典型写法,在这我列举一个其他的自定义View的onMeasure()例子,大家可看一下:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //获取父容器推荐的模式和尺寸
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;
        
        if(widthMode == MeasureSpec.EXACTLY){
            width = widthMode-getPaddingLeft()-getPaddingRight();
        }else {
            width = 100; //<span style="color:#ff0000;">也就是MeasureSpec.AT_MOST情况下,必须的我们自己去设置一个初始大小才行,在这我只是举个例子随便设置成100.</span>
        }

        if(heightMode == MeasureSpec.EXACTLY){
            height = heightSize-getPaddingTop()-getPaddingBottom();
;
        }else {
            height = 100; //也就是MeasureSpec.AT_MOST情况下
        }
        
        setMeasuredDimension(width, height);
    }
这算是比较标准的一种情况了,在EXACTLY 模式下父容器推荐的大小基本上就是我们的测量大小, 减去getPaddingLeft() getPaddingRight()是因为如果我们不这样的话我们的自定义控件的padding 属性就不起作用了。你想如果我们自定义控件中不设置padding还好,但是一旦设置了padding属性你发现不好使是不是很坑....但是如果我们设置的是margin相关的属性的话,在自定义控件就不需要做任何操作的,margin属性是由子View的父容器去操作的,这个我们不用担心,但是padding 相关属性必须我们自己处理。如果你对mesureSpec不了解的请看http://blog.csdn.net/wning1/article/details/52686596  这篇博客讲解的很详细。

(5)复写onDraw()方法,是用画笔将所需要的图形画到canvas画布上

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

        for (int i = 0; i < mTotalNum; i++) {
            if (i < mSelectedNum) {
                canvas.drawBitmap(selectedImg, null, new RectF(i * (mItemDistance + mItemWidth)+getPaddingLeft(),getPaddingTop(), i * (mItemDistance + mItemWidth)+getPaddingLeft() + mItemWidth, mItemWidth+getPaddingTop()), mPaint);
            } else {
                canvas.drawBitmap(normalImg, null, new RectF(i * (mItemDistance + mItemWidth)+getPaddingLeft(), getPaddingTop(), i * (mItemDistance + mItemWidth)+getPaddingLeft() + mItemWidth, mItemWidth+getPaddingTop()), mPaint);
            }
        }
    }


这个就是将bitmap话到cavas画布上。如果你对paint不是太了解得话,可参照http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2012/1212/703.html


下面我们对自定义View的各个属性进行测试:

<com.myapplication.StarView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:layout_marginLeft="20dp"
        android:background="#f00"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:paddingTop="20dp"
        app:itemDistance="2dp"
        app:itemWidth="8dp"
        app:selectedNum="3"
        app:totalStar="5" />
效果如下:


下载星级自定义View点击这里

好了介绍到此结束。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值