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

作者: MonkeyLei | 来源:发表于2019-07-12 09:49 被阅读0次

    上一篇 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等....

    心灵鸡汤:

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

    相关文章

      网友评论

        本文标题:Android-自定义View-onMeasure方法续篇

        本文链接:https://www.haomeiwen.com/subject/ykgskctx.html