Android-自定义View-onMeasure方法续篇

上一篇 Android-自定义View-onMeasure方法

我们继续....

之前我们针对控件大小做了重新测量,同时兼容了下wrap_content等问题。现在还需要做半径的处理,也就是需要根据最后计算得到的控件宽高作为实际绘制的参考, 同时还需要约束半径:

    @Override
    protected void onDraw(Canvas canvas) {
        //super.onDraw(canvas);
        ///< 2.进行绘制
        ///< 先绘制一个背景 - 如果自定义控件背景存在的情况下,则进行背景绘制
        if (null != bgDrawable) {
            canvas.drawBitmap(bgDrawable, 0, 0, paint);
        }
        ///< 绘制一个圆圈吧-> drawCircle(float cx, float cy, float radius, Paint paint)
        canvas.drawCircle(width / 2, height / 2,
                radius + changeRadius, paint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ///< 采用默认的onMeasure看看
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        ///< 自己进行相关测量
        int defaultW = 12;
        int defaultH = 12;

        int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);

        ///< 在wrap_content的情况下默认长度为默认宽/高与背景图片对比后的最大值,假设背景图片是200*200,则显示200*200
        int minWSize = Math.max(dp2px(context, defaultW), getSuggestedMinimumWidth());
        int minHSize = Math.max(dp2px(context, defaultH), getSuggestedMinimumHeight());
        ///< wrap_content的specMode是AT_MOST模式,这种情况下宽/高等同于specSize
        //  查表得这种情况下specSize等同于parentSize,也就是父容器当前剩余的大小
        //  在wrap_content的情况下如果不特殊处理,效果等同martch_parent

        ///< 打印看看妮
        //        switch (wSpecMode) {
        //            case UNSPECIFIED:   ///< 父控件没有针对子控件进行大小限制
        //                Log.e("test", "w's UNSPECIFIED");
        //                break;
        //            case EXACTLY:       ///< 父控件决定了子控件准确的尺寸
        //                Log.e("test", "w's EXACTLY");
        //                break;
        //            case AT_MOST:       ///< 子控件会成为一定的尺寸大小
        //                Log.e("test", "w's AT_MOST");
        //                break;
        //        }
        //
        //        switch (hSpecMode) {
        //            case UNSPECIFIED:
        //                Log.e("test", "h's UNSPECIFIED");
        //                break;
        //            case EXACTLY:
        //                Log.e("test", "h's EXACTLY");
        //                break;
        //            case AT_MOST:
        //                Log.e("test", "h's AT_MOST");
        //                break;
        //        }

        ///< 进行控件尺寸设置,同时更新绘制宽高
        if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
            width = minWSize;
            height = minHSize;
            setMeasuredDimension(minWSize, minHSize);
        } else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {
            width = wSize;
            height = hSize;
            setMeasuredDimension(wSize, hSize);
        } else if (wSpecMode == MeasureSpec.AT_MOST) {
            width = minWSize;
            height = hSize;
            setMeasuredDimension(minWSize, hSize);
        } else if (hSpecMode == MeasureSpec.AT_MOST) {
            width = wSize;
            height = minHSize;
            setMeasuredDimension(wSize, minHSize);
        }

        ///< 做一个兼容,如果半径超过了控件宽或者高
        int minWH = width;
        if (width > height) {
            minWH = height;
        }
        if ((radius * 2) > minWH) {
            radius = minWH / 2;
            Log.e("attrs", "纠正一下 " + radius);
        }
    }

反正目前来看,没什么问题,理解的浅或者不对的也就先这样看到起。完事了我们继续深入完善纠正就是了...

image

在一开始小白在onDraw方法里面有去用getWidth()或者getHeight()去获取控件的宽高,但是由于当时没有进行onMeasure()的计算,所以获取的宽高都是0,肯定用这个宽高进行绘制时错误的啦!

而现在我们再用这个去获取宽高,同时纠正半径的范围问题,那肯定就OK啦!

布局activity_main.xml

 <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".MainActivity">

    <!--android:background="@drawable/luffy"-->
    <!--android:minWidth=""-->
    <!--android:minHeight=""-->
    <!--app:bgdrawable="@drawable/luffy"-->
    <!--android:minWidth="12dp"-->
    <me.heyclock.hl.customcopy.MyTextView01
        style="@style/MyTextView01"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="@color/colorPrimary"
        app:ccolor="#F50808"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:radius="1000dp" />

</android.support.constraint.ConstraintLayout>

测量和绘制部分

image

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">绘制时进行宽高获取和半径约束</figcaption>

    @Override
    protected void onDraw(Canvas canvas) {
        //super.onDraw(canvas);
        ///< 2.进行绘制
        ///< 先绘制一个背景 - 如果自定义控件背景存在的情况下,则进行背景绘制
        if (null != bgDrawable) {
            canvas.drawBitmap(bgDrawable, 0, 0, paint);
        }
        ///< 绘制一个圆圈吧-> drawCircle(float cx, float cy, float radius, Paint paint)
        ///< 做一个兼容,如果半径超过了控件宽或者高
        int minWH = getWidth();
        if (getWidth() > getHeight()) {
            minWH = getHeight();
        }
        if ((radius * 2) > minWH) {
            radius = minWH / 2;
            Log.e("attrs", "纠正一下 " + radius);
        }
        canvas.drawCircle(getWidth() / 2, getHeight() / 2,
                radius + changeRadius, paint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ///< 采用默认的onMeasure看看
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        ///< 自己进行相关测量
        int defaultW = 12;
        int defaultH = 12;

        int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);

        ///< 在wrap_content的情况下默认长度为默认宽/高与背景图片对比后的最大值,假设背景图片是200*200,则显示200*200
        int minWSize = Math.max(dp2px(context, defaultW), getSuggestedMinimumWidth());
        int minHSize = Math.max(dp2px(context, defaultH), getSuggestedMinimumHeight());
        ///< wrap_content的specMode是AT_MOST模式,这种情况下宽/高等同于specSize
        //  查表得这种情况下specSize等同于parentSize,也就是父容器当前剩余的大小
        //  在wrap_content的情况下如果不特殊处理,效果等同martch_parent

        ///< 打印看看妮
        //        switch (wSpecMode) {
        //            case UNSPECIFIED:   ///< 父控件没有针对子控件进行大小限制
        //                Log.e("test", "w's UNSPECIFIED");
        //                break;
        //            case EXACTLY:       ///< 父控件决定了子控件准确的尺寸
        //                Log.e("test", "w's EXACTLY");
        //                break;
        //            case AT_MOST:       ///< 子控件会成为一定的尺寸大小
        //                Log.e("test", "w's AT_MOST");
        //                break;
        //        }
        //
        //        switch (hSpecMode) {
        //            case UNSPECIFIED:
        //                Log.e("test", "h's UNSPECIFIED");
        //                break;
        //            case EXACTLY:
        //                Log.e("test", "h's EXACTLY");
        //                break;
        //            case AT_MOST:
        //                Log.e("test", "h's AT_MOST");
        //                break;
        //        }

        if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
            width = minWSize;
            height = minHSize;
            setMeasuredDimension(minWSize, minHSize);
        } else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {
            width = wSize;
            height = hSize;
            setMeasuredDimension(wSize, hSize);
        } else if (wSpecMode == MeasureSpec.AT_MOST) {
            width = minWSize;
            height = hSize;
            setMeasuredDimension(minWSize, hSize);
        } else if (hSpecMode == MeasureSpec.AT_MOST) {
            width = wSize;
            height = minHSize;
            setMeasuredDimension(wSize, minHSize);
        }

        ///< 做一个兼容,如果半径超过了控件宽或者高
        //        int minWH = width;
        //        if (width > height) {
        //            minWH = height;
        //        }
        //        if ((radius * 2) > minWH) {
        //            radius = minWH / 2;
        //            Log.e("attrs", "纠正一下 " + radius);
        //        }
    }

当然如果你不想每次绘制都去做半径测处理,也可以在测量里面就把这些处理好. 调试过程中,我发现onMeasure()会运行两次?

image

关于这个有hyman的解释:

你好,这个官方文档有一定的解释,
地址:http://developer.android.com/guide/topics/ui/how-android-draws.html ;
中文翻译地址:http://blog.csdn.net/jewleo/article/details/39547631 。 
stackoverflow中也有很多类似的问题,你可以看下大家的解答。

具体的我大概看了下,就是关于可能的多次测量最终得到控件的尺寸。相信后面我再自定义ViewGroup时应该会比较明显的体验到。这里暂时做个记录....

最后我们拷贝一份之前的自定义控件类作为备份,整理下代码:

MyTextView01.java

package me.heyclock.hl.customcopy;

import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;

import java.util.Timer;
import java.util.TimerTask;

/*
 *@Description: 自定义绘制文本
 *@Author: hl
 *@Time: 2018/10/12 9:37
 */
public class MyTextView01 extends View {
    /* 官方文档:
        https://developer.android.google.cn/reference/android/graphics/Canvas
        https://developer.android.google.cn/reference/android/graphics/Paint
    */
    private Context context;///< 上下文
    private Canvas canvas;  ///< 画布
    private Paint paint;    ///< 画笔

    ///< 做红色点击区域限制
    private boolean bIsDownInRedRegion = false;
    ///< 定时刷新
    private Timer timer = null;
    ///< 圆圈半径
    private int radius;
    ///< 圆圈颜色
    private String color;
    ///< 控件自定义背景
    private Bitmap bgDrawable = null;
    ///< 控件宽度和高度
    private int width = 12;
    private int height = 12;

    /**
     * 刷新绘制+增量变化
     */
    private static final int STEP_RADIUS = 10;  ///< 每次半径增加10
    private int changeRadius = 0;               ///< 变化量记录,达到50时则开始减;达到0就开始增加
    private boolean addFlag = true;             ///< 标记是否增加增量

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

    public MyTextView01(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, 0, 0);
    }

    public MyTextView01(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.context = context;

        ///< TypedArray的方式
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyTextView01);
        ///< getDimension() getDimensionPixelOffset() getDimensionPixelSize()
        ///  --这三个方法都是根据DisplayMetrics获取相应的值,不同在于方法1直接保存float型数据,方法2直接对float取整,方法3对float小数先四舍五入后取整。
        radius = ta.getDimensionPixelOffset(R.styleable.MyTextView01_radius, 6);
        color = ta.getString(R.styleable.MyTextView01_ccolor);
        Drawable drawable = ta.getDrawable(R.styleable.MyTextView01_bgdrawable);
        if (null != drawable) {
            BitmapDrawable bd = (BitmapDrawable) drawable;
            bgDrawable = bd.getBitmap();
        }
        ta.recycle();

        ///< 1\. 做一些绘制初始化
        canvas = new Canvas();  ///< 也可以指定绘制到Bitmap上面 -> Canvas(Bitmap bitmap)
        paint = new Paint();
        paint.setColor(Color.parseColor(color));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //super.onDraw(canvas);
        ///< 2.进行绘制
        ///< 先绘制一个背景 - 如果自定义控件背景存在的情况下,则进行背景绘制
        if (null != bgDrawable) {
            canvas.drawBitmap(bgDrawable, 0, 0, paint);
        }
        ///< 绘制一个圆圈吧-> drawCircle(float cx, float cy, float radius, Paint paint)
        canvas.drawCircle(width / 2, height / 2,
                radius + changeRadius, paint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ///< 采用默认的onMeasure看看
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        ///< 自己进行相关测量
        int defaultW = 12;
        int defaultH = 12;

        int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);

        ///< 在wrap_content的情况下默认长度为默认宽/高与背景图片对比后的最大值,假设背景图片是200*200,则显示200*200
        int minWSize = Math.max(dp2px(context, defaultW), getSuggestedMinimumWidth());
        int minHSize = Math.max(dp2px(context, defaultH), getSuggestedMinimumHeight());

        ///< wrap_content的specMode是AT_MOST模式,这种情况下宽/高等同于specSize
        //  查表得这种情况下specSize等同于parentSize,也就是父容器当前剩余的大小
        //  在wrap_content的情况下如果不特殊处理,效果等同martch_parent
        if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
            width = minWSize;
            height = minHSize;
            setMeasuredDimension(minWSize, minHSize);
        } else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {
            width = wSize;
            height = hSize;
            setMeasuredDimension(wSize, hSize);
        } else if (wSpecMode == MeasureSpec.AT_MOST) {
            width = minWSize;
            height = hSize;
            setMeasuredDimension(minWSize, hSize);
        } else if (hSpecMode == MeasureSpec.AT_MOST) {
            width = wSize;
            height = minHSize;
            setMeasuredDimension(wSize, minHSize);
        }

        ///< 做一个兼容,如果半径超过了控件宽或者高
        int minWH = width;
        if (width > height) {
            minWH = height;
        }
        if ((radius * 2) > minWH) {
            radius = minWH / 2;
            Log.e("attrs", "纠正一下 " + radius);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        ///< 点击区域坐标范围
        int minX = (width - radius * 2) / 2;
        int maxX = width / 2 + radius;
        int minY = (height - radius * 2) / 2;
        int maxY = height / 2 + radius;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (x >= minX && x <= maxX &&
                        y >= minY && y <= maxY) {
                    bIsDownInRedRegion = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                if (bIsDownInRedRegion) {
                    bIsDownInRedRegion = false;

                    if (x >= minX && x <= maxX &&
                            y >= minY && y <= maxY) {
                        ///< 抬手时我们就可以启动定时器进行绘制刷新了
                        Log.e("test", "红色区域点击了呀,sb");
                        if (null == timer) {
                            timer = new Timer();
                            timer.schedule(new TimerTask() {
                                @Override
                                public void run() {
                                    ///< Handler也行
                                    ((Activity) context).runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            updateDraw();
                                        }
                                    });
                                }
                            }, 0, 100);
                        } else {
                            timer.cancel();
                            timer = null;
                        }
                    }
                }
                break;
        }
        return true;
    }

    /**
     * 刷新绘制+增量变化
     */
    private void updateDraw() {
        changeRadius = addFlag ? (changeRadius += STEP_RADIUS) : (changeRadius -= STEP_RADIUS);
        if (changeRadius > 50) {
            addFlag = false;
        } else if (changeRadius < 0) {
            addFlag = true;
        }
        invalidate();
    }

    /**
     * dp转px
     *
     * @param dp
     * @return
     */
    public static int dp2px(Context context, int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
    }
}

或许不规范,或者说都不对。不过效果还是正确的啦。。先这样理解。我们先把自定义流程走一遍,然后再去接触更复杂的自定义的时候,相信肯定还能去理解和完善。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".MainActivity">

    <!--android:background="@drawable/luffy"-->
    <!--android:minWidth=""-->
    <!--android:minHeight=""-->
    <!--app:bgdrawable="@drawable/luffy"-->
    <!--android:minWidth="12dp"-->
    <me.heyclock.hl.customcopy.MyTextView01
        style="@style/MyTextView01"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="@color/colorPrimary"
        app:ccolor="#F50808"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:radius="60dp" />

</android.support.constraint.ConstraintLayout>
image

下一篇onLayout,以及padding等....

心灵鸡汤:

拼搏如一汪清水,有源则灵,沉寂而终;拼搏如向日之花,有光则茁,无温而萎;拼搏如扬起之帆,顺风则行,无风则止;拼搏如穿石之滴,有恒则稳,无疾而终。

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

推荐阅读更多精彩内容