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);
        }
    }

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

Android-自定义View-onMeasure方法续篇_第1张图片
image

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

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

布局activity_main.xml

 


    
    
    
    
    
    


测量和绘制部分

Android-自定义View-onMeasure方法续篇_第2张图片
image

绘制时进行宽高获取和半径约束

    @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()会运行两次?

Android-自定义View-onMeasure方法续篇_第3张图片
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());
    }
}

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




    
    
    
    
    
    


Android-自定义View-onMeasure方法续篇_第4张图片
image

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

心灵鸡汤:

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

你可能感兴趣的:(Android-自定义View-onMeasure方法续篇)