如何自定义View

首先奉上AndroodDeveloper的教程

假设我们以自定义一个View,实现圆形的按钮功能。

说一下简单的流程:

  • 继承View
  • 重写构造函数
  • 重写OnMeasure()方法
  • 重写OnDraw()方法
  • 配置XML
    我把配置XML放到最后不是因为需要最后去处理,而是它相对来说比较独立。

继承View,重写构造函数

首先重写View的构造函数

private CustomView (Context context){
    this(context,null);
 }
private CustomView(Context context, AttributeSet attrs){
    this(context,attrs,0);
}
private CustomView(Contxt context, AttributeSet attrs, int defStyleAttr){
    super(context,attrs,defStyleAttr);
 }

重写OnMeasure()函数

这一块着重的讲一下,之前我这里也不是特别的理解。我觉得在XML文件中其实已经将View 的尺寸宽高已经固定好了,何必在View中再次测量并设置么。同理可以再往上层分析下,若Google在父View中直接获取XML里面的尺寸岂不更好。

在我们布局XML的时候,有两个属性wrap_contentmatch_parent。可以看到这两个属性并没有去告诉系统,我要多少尺寸的大小,而是描述了一种关系,即内容包裹填充父空间,因此在我们绘制到屏幕的过程中,就必须知道View的具体宽高,所以我们必须去处理尺寸。当View默认处理,无法满足我们需求的时候,就需要重写OnMeasure()函数了。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = measure(widthMeasureSpec, 100);
        int height = measure(heightMeasureSpec, 100);
        
        if (width < height){
            height = width;
        }else{
            width = height;
        }
    
        setMeasuredDimension(width, height);
    }

在这个函数中传了两个参数,这里需要注意的是参数是int型的,却包含了两个重要的信息:测量的模式以及测量的大小。Google将int数据的前两个bit用于区分不同的布局模式,后面三十个bit存放的是尺寸的数据。一般我们需要通过移位操作来获取数据,Android中的MeasureSpec中有两个函数getMode()getSize()就可以很方便的获取测量的模式和大小。

这样恐怕你会有疑问,既然已经获取了View的Size了,那要Mode有何用?其实这里的Size只是父级View提供的参考大小而已。Mode分为下面三种:

| 测量模式 | 英文 | 中文 |
| UNSPECIFIED | The parent has not imposed any constraint on the child. It can be whatever size it wants | 父容器对当前View没有任何限制,当前View可以取任意尺寸
| EXACTLY |   The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be | 父容器给子容器了确定的大小,无论子容器想要多大,它只能接收父容器给的大小
| AT_MOST | The child can be as large as it wants up to the specified size | 子容器可以获得它想要的尺寸大小

简单点来说,和warp_contentmatch_parten做下对比不难发现。

match_parent--->EXACTLY。match_parent就是要利用父View给我们提供的所有剩余空间,而父View剩余空间是确定的,即Size。

wrap_content--->AT_MOST。怎么理解:就是我们想要将大小设置为包裹我们的view内容,那么尺寸大小就是父View给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。

固定尺寸(如100dp)--->EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。

    private int measure(int measureSpec, int defaultSize) {
        int result = defaultSize;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        
        switch(mode){
            case MeasureSpec.EXACTLY:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
                result = size;
                break;     
            case MeasureSpec.UNSPECIFIED:
                result = defaultSize;
                break;
            default:
                break;
           }
        return result;
    }

假设我们在XML中设置该控件的长宽属性都是match_parent,则效果如下

重写OnDraw()方法

上面我们通过OnMeasure()来设定了View的大小,接下来需要通过OnDraw()来绘制这个View的样子。

这里需要注意下Canvas和Paint的区别,下面一段话说明Canvas确定了你在屏幕中所能展现的形状,而Paint用来定义具体的颜色,样式,字体等。

Simply put, Canvasdefines shapes that you can draw on the screen, while Paint defines the color, style, font, and so forth of each shape you draw.

OK,假设我们去绘制一个原谅色的原型,代码如下:

@override
protected void OnDraw(Canvas canvas){
    Super.onDraw(canvas);
    int r = getMeasureWidth() / 2;
    int x = getLeft() + r;
    int y = getTop() + r;
    
    Paint paint = new Paint();
    paint.setColor(Color.Green);
    canvas.drawCircle(x, y, r, paint);
}

效果如下:

设置监听事件

自定义XML属性

如果我们需要给用户一些更加灵活的设置,就需设置一些属性。首先我们在res/values/styles.xml中声明我们自己的属性:

<resources>
    <declare-styleable name="CostomView">
        <attr name="default_size" format="dimension"/>
    </declare-styleable>
</resources>

接着在布局文件中使用我们的声明的属性:

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="domon.cn.coustomerview.MainActivity">

    <domon.cn.coustomerview.View.RectView
        android:id="@+id/my_rv"
        android:layout_width="match_parent"
        android:background="#f2e"
        app:default_size="100dp"
        android:layout_height="100dp" />
    </LinearLayout>

在引用自己的属性的时候,需要注意一下命名空间的问题,基本上我们的自定义View就已经好了。

案例分析

下面我代码家的一个自定义控件NumberProgressBar来简单分析一下。

  • 构造函数
public NumberProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        default_reached_bar_height = dp2px(1.5f);
        default_unreached_bar_height = dp2px(1.0f);
        default_text_size = sp2px(10);
        default_progress_text_offset = dp2px(3.0f);

        //load styled attributes.
        final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.NumberProgressBar,
                defStyleAttr, 0);

        mReachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_reached_color, default_reached_color);
        mUnreachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_unreached_color, default_unreached_color);
        mTextColor = attributes.getColor(R.styleable.NumberProgressBar_progress_text_color, default_text_color);
        mTextSize = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_size, default_text_size);

        mReachedBarHeight = attributes.getDimension(R.styleable.NumberProgressBar_progress_reached_bar_height, default_reached_bar_height);
        mUnreachedBarHeight = attributes.getDimension(R.styleable.NumberProgressBar_progress_unreached_bar_height, default_unreached_bar_height);
        mOffset = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_offset, default_progress_text_offset);

        int textVisible = attributes.getInt(R.styleable.NumberProgressBar_progress_text_visibility, PROGRESS_TEXT_VISIBLE);
        if (textVisible != PROGRESS_TEXT_VISIBLE) {
            mIfDrawText = false;
        }

        setProgress(attributes.getInt(R.styleable.NumberProgressBar_progress_current, 0));
        setMax(attributes.getInt(R.styleable.NumberProgressBar_progress_max, 100));

        attributes.recycle();
        initializePainters();
    }

通过在构造函数中,获取在XML中设置的属性。

  • 重写OnMeasure()方法
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false));
    }

    private int measure(int measureSpec, boolean isWidth) {
        int result;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom();
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight();
            result += padding;
            if (mode == MeasureSpec.AT_MOST) {
                if (isWidth) {
                    result = Math.max(result, size);
                } else {
                    result = Math.min(result, size);
                }
            }
        }
        return result;
    }

根据不同模式测量不同的尺寸

  • 重写OnDraw()方法
@Override
    protected void onDraw(Canvas canvas) {
        if (mIfDrawText) {
            calculateDrawRectF();
        } else {
            calculateDrawRectFWithoutProgressText();
        }

        if (mDrawReachedBar) {
            canvas.drawRect(mReachedRectF, mReachedBarPaint);
        }

        if (mDrawUnreachedBar) {
            canvas.drawRect(mUnreachedRectF, mUnreachedBarPaint);
        }

        if (mIfDrawText)
            canvas.drawText(mCurrentDrawText, mDrawTextStart, mDrawTextEnd, mTextPaint);
    }

在OnDraw()中根据条件判断不同的绘制对象。我们就来看看calculateDrawRectFWithoutProgressText()这个方法。

private void calculateDrawRectFWithoutProgressText() {
        mReachedRectF.left = getPaddingLeft();
        mReachedRectF.top = getHeight() / 2.0f - mReachedBarHeight / 2.0f;
        mReachedRectF.right = (getWidth() - getPaddingLeft() - getPaddingRight()) / (getMax() * 1.0f) * getProgress() + getPaddingLeft();
        mReachedRectF.bottom = getHeight() / 2.0f + mReachedBarHeight / 2.0f;

        mUnreachedRectF.left = mReachedRectF.right;
        mUnreachedRectF.right = getWidth() - getPaddingRight();
        mUnreachedRectF.top = getHeight() / 2.0f + -mUnreachedBarHeight / 2.0f;
        mUnreachedRectF.bottom = getHeight() / 2.0f + mUnreachedBarHeight / 2.0f;
    }

ReachedRectf是已经完成部分的矩形,UnReachedRectF是未完成的矩形。



因此在绘制的时候,我们需要通过上下左右的位置坐标,将这个View画出来。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容