android高阶UI—-自定义View(测量、布局、绘制)

时间:2021-7-21 作者:qvyue

说起自定义View ,在我们的实际工作中,用的比较多的就是将系统提供的组件进行组合封装,达到我们需要的效果,但是有些却无法做到,笔者之前遇到一个带文字的仿IOS开关的需求就没有办法使用系统的组合封装,这就需要自己绘制一个,包括添加一些动画,今天我们就学习一下绘制一个View的过程。

自定义View主要包含以下内容:

1)布局: onlayout(), onmeasure()

  1. 显示: onDraw() 其中涉及到了canvas , paint ,matrix, clip , rect ,animation ,line ,path等

3)事件分发:onTouchEvent()

我们常见的自定义View 是继承自View,ViewGroup ,LinearLayout ,RelativeLayout等(本文着重以ViewGroup为例)

绘制流程:

开始 —–》调用View的构造函数(此处完成View的初始化工作)—–》 回调onMeasure()函数(来测试视图的大小)—-》onSizeChanged()确定View的大小 —–》onLayout()来确定当前view再其parent中的位置—》onDraw()绘制视图到画布上—–》添加事件

android高阶UI----自定义View(测量、布局、绘制)
自定义View绘制流程.png

不论是measure测量过程,还是layout布局过程,或者是draw绘制过程,永远都是从树根节点开始测试或计算,一层一层的,一个树枝一个树枝的来完成(树形递归的方式)

1) 先来介绍一下构造函数

自定义View默认是都需要实现三个构造函数,有些则需要实现四个,而这些构造函数仅仅是参数的个数不同而已,我们依次来解释一下这些构造函数:

一个参数的构造函数 : 主要是用于我们在java代码中创建对象时调用;

两个参数的构造函数:主要用于我们在xml中使用自定义view时,我们在加载xml文件时使用(关于这一点我们在下一篇文章XML加载原理中可以明白)

三个参数的构造函数:是用于带有主题Style的构造

四个参数的构造函数:当我们有自己定义属性的构造函数

public class FlowLayout extends ViewGroup {
/**
     * TODO java 创建对象使用构造
     */
    public FlowLayout(Context context) {
        super(context);
    }

    /**
     * TODO xml使用的构造函数
     */
    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * TODO 主题style的构造
     */
    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * TODO 自定义属性
     */
    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
         init(context, attrs);
    }
    
    /**
    * TODO 解析自定义属性
    */
    private void init(Context context, AttributeSet attributeSet) {
        if (attributeSet != null) {
            TypedArray a = context.obtainStyledAttributes(attributeSet, R.styleable.permission_attr);
            assert a != null;
            String permissionCode = a.getString(R.styleable.permission_attr_permissioncode);
            if (StrUtils.isNotEmpty(permissionCode)) {
                if (BaseApplication.getApplication().Permissions.indexOf(permissionCode) != -1) {
                    this.setVisibility(View.VISIBLE);
                } else {
                    this.setVisibility(View.INVISIBLE);
                }
            }
        }
    }
}

关于自定义属性:

1)在values目录下添加一个attrs.xml的文件(若已经存在则忽略此步);

2)在attrs.xml中添加 如下的属性名和格式

3)在xml布局文件中使用:

2) 测量回调 onMeasure()

说明:

1)这个回调函数是用来测试自定义view及其child view的大小,包括padding ,margin等信息

2)所有view测试的位置信息都是基于其父亲的,而不是相对于屏幕的

3)大部分的测量都是先测试childview 然后再来度量自己的,但是ViewPager李伟

4)关于getMeasureWidth()与 getWidth()的理解:

 **getMeasureWidth(): 是在measure()测量过程结束后就可以获取到对应的值,需要使用setMeasureDemension()方法来进行设置**

getWidth(): 是在layout()布局过程结束后才能获取到的,通过视图右边的坐标减去左边的坐标计算出来的。

5)关于padding ,margin的理解:

  **padding :就是视图内部的间距,他会影响视图的宽高,所有在计算视图的大小时需要计算在内**

  **margin: 是视图彼此间的间距,不会影响视图的宽高**

6) 理解 EXACTLY、AT_MOST、UNSPECIFIED三个模式

EXACTLY:父亲指定了孩子的大小,不管孩子想要多大,就只给指定的大小

AT_MOST:父亲给孩子一个允许的最大大小,孩子想要多大,就给多大

UNSPECIFIED:父亲不限制孩子的大小,由孩子自己决定

7) onMeasure()的两个参数,是父亲给当前view的两个宽高尺寸

上面是parent Mode
左侧是 childLayoutParams
EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTILY EXACTILY EXACTILY
match_parent EXACTILY AT_MOST UNSPECIFIED
wrap_content AT_MOST AT_MOST UNSPECIFIED

这个表格其实就是在表达父亲给孩子的测试的规格,然后对应孩子不同的诉求,最终给出的孩子的尺寸规格。

解释:

1)当孩子是dp/px时: 当孩子是指定的大小的,那么不管父亲是什么模式,都是精确模式并使用他自己的大小

  1. 当孩子是match_parent 时: 当父亲是精准模式,那么孩子也是精准模式,并且孩子的大小是父亲剩余的大小;

                                               当父亲是最大模式,那么孩子也是最大模式,但是孩子的大小不会超过父亲的剩余空间;
    

3)当孩子是wrap_content时,不管父亲的模式是什么,孩子的模式总是最大的,但是孩子不能超过父亲的剩余空间;

4)UNSPECIFIED 一般是用于系统内部,不用关注此模式

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        clearParams();
        //父亲的信息
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

        int selfWidth = MeasureSpec.getSize(widthMeasureSpec);  //解析的父亲给我的宽度
        int selfHeight = MeasureSpec.getSize(heightMeasureSpec); //解析的父亲给我的高度

        List lineViews = new ArrayList(); //保存一行中的所有的view
        int lineWidthUsed = 0; //记录这行已经使用了多宽的size
        int lineHeight = 0; // 一行的行高

        int parentNeededWidth = 0;  // measure过程中,子View要求的父ViewGroup的宽
        int parentNeededHeight = 0; // measure过程中,子View要求的父ViewGroup的高

        int childCount = getChildCount();

        for (int i = 0; i  selfWidth) {
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;

                    allLines.add(lineViews);
                    lineHeights.add(lineHeight);

                    lineViews = new ArrayList();
                    lineWidthUsed = 0;
                    lineHeight = 0;
                }

                //将child添加到一行视图数组中去
                lineViews.add(childView);
                lineWidthUsed += childMeasureWidth + mHorizontalSpacing;
                lineHeight = Math.max(lineHeight, childMeasureHeight);

                if (i == childCount - 1) {
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;

                    allLines.add(lineViews);
                    lineHeights.add(lineHeight);
                }
            }

        }

        //配置自己的宽高
        int width_mode = MeasureSpec.getMode(parentNeededWidth);
        int height_mode = MeasureSpec.getMode(parentNeededHeight);

        int realParentWidth = width_mode == MeasureSpec.EXACTLY ? selfWidth : parentNeededWidth;
        int realParentHeigt = height_mode == MeasureSpec.EXACTLY ? selfHeight : parentNeededHeight;
        setMeasuredDimension(realParentWidth, realParentHeigt);
    }

3) onSizeChanged()

上一步测量完每一个view的大小的时候,如果view的大小有变化,在这里完成view大小的修改,并且确定view的最终的大小,然后进入下一步布局环节。

4) onLayout()

这里我们就需要开始布局我们的视图了,会根据上一步确定的布局的大小,并按照父视图的左上角作为坐标原点,开始树形递归的方式绘制每一个View及其Child.

 protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int lineCount = allLines.size();
        int curL = getPaddingLeft();
        int curT = getPaddingTop();
        for (int i = 0; i  lineViews = allLines.get(i);
            int lineHeight = lineHeights.get(i);
            for (int i1 = 0; i1 

5) onDraw()

在上一步的layout布局完成后,就进入到绘制阶段,会根据上一步布局的坐标位置,在画布上绘制每一个视图。这个onDraw()的参数就是用来绘制view本身的,他给我们提供了绘制线,文字,各种图形等操作的方法。

首先我们需要一个Paint对象,他保存着如何绘制的样式颜色等信息;

然后canvas 来控制如何绘制图形,canvas 与paint一起完成了我们试图的绘制工作。

下面我们来看看绘制一下仿苹果开关的onDraw方法的实现

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘制背景图
        //canvas.drawBitmap(bgBitmap, 0, 0, paint);

        paint.setStrokeWidth(2);
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.FILL);

        if (currState) {
            paint.setColor(ctx.getResources().getColor(R.color.black_999));
            //外部
            //左边的圆
            canvas.drawCircle((float) (miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5), paint);
            //右边的圆
            canvas.drawCircle((float) (miniWidth - miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5), paint);
            canvas.drawRect((float) (miniHeight * 0.5), 0, (float) (miniWidth - miniHeight * 0.5), miniHeight, paint);

            //内部
            paint.setColor(ctx.getResources().getColor(R.color.blut_theme_app));
            canvas.drawCircle((float) (miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5)-2, paint);
            canvas.drawCircle((float) (miniWidth - miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5)-2 , paint);
            canvas.drawRect((float) (miniHeight * 0.5), 2, (float) (miniWidth - miniHeight * 0.5), miniHeight - 2, paint);

        } else {
            //外部
            //左边的圆
            paint.setColor(ctx.getResources().getColor(R.color.black_999));
            canvas.drawCircle((float) (miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5), paint);
            canvas.drawCircle((float) (miniWidth - miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5), paint);
            canvas.drawRect((float) (miniHeight * 0.5), 0, (float) (miniWidth - miniHeight * 0.5), miniHeight, paint);

            //内部
            paint.setColor(ctx.getResources().getColor(R.color.white));
            canvas.drawCircle((float) (miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5)-2 , paint);
            canvas.drawCircle((float) (miniWidth - miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5) - 2, paint);
            canvas.drawRect((float) (miniHeight * 0.5), 2, (float) (miniWidth - miniHeight * 0.5), miniHeight -2, paint);
        }


        float textWidth = paint.measureText(showText);

        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        float fontTotalHeight = fontMetrics.bottom - fontMetrics.top;

        float newY = getMeasuredHeight() / 2 - fontMetrics.descent + (fontMetrics.descent - fontMetrics.ascent) / 2;
        float baseX = 0;

        if (!currState) {
            paint.setColor(ctx.getResources().getColor(R.color.default_font_color));
            baseX = (float) ((getMeasuredWidth() - textWidth) - getPaddingRight() - miniHeight * 0.4);
        } else {
            paint.setColor(ctx.getResources().getColor(R.color.white));
            baseX = (float) (0 + getPaddingLeft() + miniHeight * 0.4);
        }
        canvas.drawText(showText, baseX, newY, paint);

        // 绘制滑动图片
        if (currState) {
            paint.setColor(ctx.getResources().getColor(R.color.blut_theme_app));
            canvas.drawCircle((float) (slideLeft + miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5 )-4, paint);

            paint.setColor(ctx.getResources().getColor(R.color.black_999));
            canvas.drawCircle((float) (slideLeft + miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5 )- 6, paint);
        } else {
            paint.setColor(ctx.getResources().getColor(R.color.white));
            canvas.drawCircle((float) (slideLeft + miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5 )-4, paint);

            paint.setColor(ctx.getResources().getColor(R.color.black_999));
            canvas.drawCircle((float) (slideLeft + miniHeight * 0.5), (float) (miniHeight * 0.5), (float) (miniHeight * 0.5 ) - 6, paint);
        }
    }

6)onTouchEvent()

当我们自定义的View需要与用户交互时,这时我们需要给自定义的View添加一些触摸事件,就需要重写该方法,我们可以通过还方法的参数获取到当前用户触摸点的坐标信息,然后处理相关的点击事件。

说明:

 1)get()和getRaw()的区别

      getX(),getY()获取的是触摸点在其所在组件中的坐标系坐标

      getRawX()、getRawY()获取的是触摸点相对于屏幕左上角的坐标系坐标
 public boolean onTouchEvent(MotionEvent event) {
        // super 注释掉以后,onclick事件,就失效了,因为,点击这个动作,也是从onTouchEvent 方法中解析出来,符合一定的要求,就是一个点击事件
        // 系统中,如果发现,view产生了up事件,就认为,发生了onclick动作,就行执行listener.onClick方法
        super.onTouchEvent(event);
        /*
         * 点击切换开关,与触摸滑动切换开关,就会产生冲突
         * 我们自己规定,如果手指在屏幕上移动,超过15个象素,就按滑动来切换开关,同时禁用点击切换开关的动作
         */
        if (isScroll) {
            return true;
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                System.out.println("MotionEvent.ACTION_DOWN");

                downX = lastX = (int) event.getX(); // 获得相对于当前view的坐标
//            event.getRawX(); // 是相对于屏幕的坐标
                // down 事件发生时,肯定不是滑动的动作
                isSliding = false;
                break;
            case MotionEvent.ACTION_MOVE:
                System.out.println("MotionEvent.ACTION_MOVE");

                // 获得距离
                int disX = (int) (event.getX() - lastX);
                // 改变滑动图片的左边界
                slideLeft += disX;
                flushView();

                // 为lastX重新赋值
                lastX = (int) event.getX();

                // 判断是否发生滑动事件
                if (Math.abs(event.getX() - downX) > 15) { // 手指在屏幕上滑动的距离大于15象素
                    isSliding = true;
                }

                break;
            case MotionEvent.ACTION_UP:
                System.out.println("MotionEvent.ACTION_UP");
                // 只有发生了滑动,才执行以下代码
                if (isSliding) {

                    // 如果slideLeft > 最大值的一半 当前是开状态
                    // 否则就是关的状态
                    if (slideLeft > slideLeftMax / 2) { // 开状态
                        currState = true;
                    } else {
                        currState = false;
                    }
                    flushState();
                }
                break;
        }
        return true;
    }

以上我们学习了自定义view里面几个比较重要的方法,分别是做什么的,也了解了一下回绘制的流程,包括一些注意点,和使用的方法。这也仅仅是个入门了,更多漂亮的view 就需要各位看官自由发挥了。好了,今天就到这里了。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。