自定义 View

基础知识

布局测量绘制的流程、事件分发、自定义属性、Android 坐标系、Canvas Api、补间动画原理、LayoutInflater

整体流程

通过 View 加载流程我们可以知道,View 的测量、布局、绘制流程是通过 ViewRootImpl 发起的,ViewRootImpl.addView 会调用 performTraversals 方法,该方法会依次调用 ViewRootImpl 的 getRootMeasureSpec、performMeasure、performLayout、performDraw 方法,最终调用到 DecorView 的 measure、layout、draw

Measure

View 的 measure 方法是 final 类型,我们只能重写 onMeasure 方法,View 的默认实现通过 View.getDefaultSize 来获取,但是 View.getDefaultSize 没有处理 wrap_content 的情况

对于 ViewGroup,我们也是重写 onMeasure 方法,ViewGroup 提供了 measureChild 方法,该方法通过 getChildMeasureSpec 方法处理子 View 的 layoutParams 和 MeasureSpec 参数并返回新生成的 MeasureSpec,然后调用子 View 的 measure 方法

public class CustomLayout extends ViewGroup {

    public CustomLayout(Context context) {
        super(context);
    }

    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int totalWidth = 0;
        int totalHeight = 0;

        // 测量每个子视图的大小,并累加它们的宽度和高度
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            totalWidth += child.getMeasuredWidth();
            totalHeight += child.getMeasuredHeight();
        }

        // 设置自身的测量尺寸为所有子视图的总大小
        setMeasuredDimension(totalWidth, totalHeight);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // 将所有子视图都放置在左上角
        int childLeft = 0;
        int childTop = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
            childLeft += child.getMeasuredWidth();
            childTop += child.getMeasuredHeight();
        }
    }
}

我们在重写 onMeasure 时,一定要调用 setMeasuredDimension,这样测量才能生效

public class MyView extends View {
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 在setMeasuredDimension方法调用之后,我们才能通过getMeasuredWidth和getMeasuredHeight
        // 来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0
        setMeasuredDimension(200, 200);
    }
}

Layout

View 一般不需要处理 Layout 过程,对应 onLayout 是空方法。而 ViewGroup 的 onLayout 是抽象方法,必须要重写

Draw

绘制背景、绘制内容、绘制子 View、绘制滚动条

绘制过程,一般我们是重写 onDraw 方法,但是如果要在 ViewGroup 中绘制内容,一般重写 dispatchDraw 方法,因为当 ViewGroup 没有背景或者背景透明时,会直接调用 dispatchDraw,而不调用 onDraw 方法

LayoutInflator

LayoutInflater layoutInflater = LayoutInflater.from(context);
LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

/**
 * 如果 Root = null,attachToRoot 没有意义
 * 如果 Root != null,attachToRoot 默认为 true,会把 Root 作为布局文件的父布局
 * 如果 Root != null,attachToRoot 设为 false,则会设置布局文件最外层的
 * 所有 layout 属性,当该布局文件被添加到父 View 中时,这些 layout 属性会自动生效
 */
inflate(int resource, ViewGroup root, boolean attachToRoot)
inflate(int resource, ViewGroup root)

以 RecyclerView$Adapter.onCreateViewHolder 为例:root 参数不应该为空,否则 ViewHolder 的布局属性会不生效;attachToRoot 应该为 false,因为 LinearLayoutManager 中会自动把 ViewHolder 添加到 rootView 中,如果 attachToRoot 为 true,则会崩溃

MeasureSpec

onMeasure 方法接收两个参数,widthMeasureSpec 和 heightMeasureSpec,这两个值分别用于确定视图宽度和高度的规格和大小。MeasureSpec 由 specSize 和 specMode 组成,其中specSize 记录的是大小,specMode 记录的是规格。specMode 一共有三种:

  • EXACTLY:父视图希望子视图的大小为 specSize,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小
  • AT_MOST:父视图希望子视图最大是 specSize,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小
  • UNSPECIFIED:表示父视图不限制子视图的大小,这种情况比较少见

getWidth 和 getMeasureWidth 的区别

getMeasureWidth 方法在 Measure 过程结束后就可以获取到,而 getWidth 方法要在 Layout 过程结束后才能获取到。另外 getMeasureWidth 方法中的值是通过 setMeasuredDimension 方法来进行设置的,而 getWidth 方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的

invalidate : Calling invalidate() is done when you want to schedule a redraw of the view. It will result in onDraw being called eventually (soon, but not immediately). An example of when a custom view would call it is when a text or background color property has changed. The view will be redrawn but the size will not change.

requestLayout : If something about your view changes that will affect the size, then you should call requestLayout(). This will trigger onMeasure and onLayout not only for this view but all the way up the line for the parent views. 

Calling requestLayout() is not guaranteed to result in an onDraw (contrary to what the diagram in the accepted answer implies), so it is usually combined with invalidate().

invalidate();
requestLayout();

forceLayout : When there is a requestLayout() that is called on a parent view group, it does not necessary need to remeasure and relayout its child views. However, if a child should be included in the remeasure and relayout, then you can call forceLayout() on the child. forceLayout() only works on a child if it occurs in conjunction with a requestLayout() on its direct parent. Calling forceLayout() by itself will have no effect since it does not trigger a requestLayout() up the view tree.

Paint

  • 设置画笔的文本对齐方式:setTextAlign
  • 设置画笔的颜色、字体大小、字体粗细/倾斜(Typeface)
  • 设置画笔的风格:setStyle(FILL、STROKE、FILL_AND_STROKE)
  • 设置 Stroke 宽度:setStrokeWidth
  • 设置 Paint 标记为:setFlags,只有最后一次设置生效
  • measureText(计算宽度)、getFontMetrics(计算高度信息)
  • 颜色过滤:setColorFilter(ColorMatrix)

Canvas

  • PorterDuffXfermode : 混合涂层
  • Shader:着色器,常见的有BitmapShader 和 LinearGradient
  • translate、scale、rotate
  • drawText、drawLine、drawRect、drawRoundRect、drawArc、drawCicle、drawOval
  • save : 用来保存画布设置
  • saveLayer : 用来保存当前画布,后面在新的画布上绘制,并自动混合涂层
  • restore : 恢复保存的画布设置 或 恢复保存的画布
  • restoreToCount : 恢复到固定的保存点

Text

Canvas.drawText 的 Y 坐标参数是 BaseLine 的位置,Top、Ascent、Descent、Bottom 都是相对于 Baseline 的坐标位置

paint.getTextBounds(DEFAULT_TEXT, 0, DEFAULT_TEXT.length(), textBound);
startX = getPaddingLeft() + reachedWidth + (textWidth - textBound.width()) / 2;
Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
startY = (height - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;
canvas.drawText(currentText, startX, startY, paint);

自定义注意事项

  • 继承 View 重写 onDraw 方法,这种情况需要处理 wrap_content 和 padding
  • 尽量不要在 View 中使用 Handler,View 中已经提供了 post 功能
  • View 中如果有线程或者动画,需要及时停止(onDetachedFromWindow)
  • 避免在 onDraw 中创建临时对象

双缓冲机制

CPU访问内存的速度要远远快于访问屏幕的速度。如果需要绘制大量复杂的图像时,每次都一个个从内存中读取图形然后绘制到屏幕就会造成多次地访问屏幕,从而导致效率很低。这就跟CPU和内存之间还需要有三级缓存一样,需要提高效率

第一层缓冲

在绘制图像时不用上述一个一个绘制的方案,而采用先在内存中将所有的图像都绘制到一个Bitmap对象上,然后一次性将内存中的Bitmap绘制到屏幕,从而提高绘制的效率。Android中View的onDraw()方法已经实现了这一层缓冲。onDraw()方法中不是绘制一点显示一点,而是都绘制完后一次性显示到屏幕

第二层缓冲

onDraw()方法的Canvas对象是和屏幕关联的,而onDraw()方法是运行在UI线程中的,如果要绘制的图像过于复杂,则有可能导致应用程序卡顿,甚至ANR。因此我们可以先创建一个临时的Canvas对象,将图像都绘制到这个临时的Canvas对象中,绘制完成之后再将这个临时Canvas对象中的内容(也就是一个Bitmap),通过drawBitmap()方法绘制到onDraw()方法中的canvas对象中。这样的话就相当于是一个Bitmap的拷贝过程,比直接绘制效率要高,可以减少对UI线程的阻塞

SurfaceView

  • View主要适用于主动更新的情况下,而SurfaceView主要适用于被动更新,例如频繁的刷新
  • View在主线程中对画面进行刷新,而SurfaceView通常会通过一个子线程来进行页面的刷新
  • View在绘图时没有使用双缓冲机制,而SurfaceView在底层实现机制中已经实现了双缓冲机制

postInvalidate

我们在自定义View时,当需要刷新View时,如果是在UI线程中,那就直接调用invalidate方法,如果是在非UI线程中,那就通过postInvalidate方法来刷新View

postInvalidate方法实现了消息机制,最终调用的也是invalidate方法来刷新View

自定义 View 分类

  • 自绘控件(点击计数器)
  • 组合控件(标题栏)
  • 继承控件(ListView + GestureDetector)

Demo : 实现一个进度条功能

 关键代码如下:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawArc(oval, 120, 300, false, paint);
        drawLine(canvas);
        drawScoreText(canvas);
    }

    private void drawLine(Canvas canvas) {
        canvas.save();
        canvas.translate(radius, radius);
        canvas.rotate(30);
        Paint linePaint = new Paint();
        linePaint.setColor(Color.WHITE);
        linePaint.setStrokeWidth(2);

        Paint targetPaint = new Paint();
        targetPaint.setColor(Color.GREEN);
        targetPaint.setStrokeWidth(2);

        float rotateAngle = sweepAngle / 100;

        float hasDraw = 0;
        for (int i = 0; i <= 100; i++) {
            if (hasDraw <= targetAngle) {
                float percent = hasDraw / sweepAngle;
                red = 255 - (int) (255 * percent);
                green = (int) (255 * percent);
                targetPaint.setARGB(255, red, green, 0);
                canvas.drawLine(0, radius - 40, 0, radius, targetPaint);
            } else {
                canvas.drawLine(0, radius - 40, 0, radius, linePaint);
            }
            hasDraw += rotateAngle;
            canvas.rotate(rotateAngle);
        }
        canvas.restore();
    }

    private void drawScoreText(Canvas canvas) {
        Paint smallPaint = new Paint();
        smallPaint.setARGB(100, red, green, 0);
        float smallRadius = radius - 60;
        canvas.drawCircle(radius, radius, smallRadius, smallPaint);

        Paint textPaint = new Paint();
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextSize(smallRadius / 2);
        canvas.drawText("" + score, radius, radius, textPaint);

        textPaint.setTextSize(smallRadius / 6);
        canvas.drawText("分", radius + smallRadius / 2, radius - smallRadius / 4, textPaint);
        canvas.drawText("点击优化", radius, radius + smallRadius / 2, textPaint);
    }

进度条:基于ProgressBar(setProgress、getProgress),重写onDraw方法,通过Canvas.drawArc、Canvas.drawCicle实现环形进度条;通过Canvas.drawLine、Canvas.drawText实现直线进度条(Paint.setStrokeWidth、Paint.setStyle)

MeasureSpec 取值

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

little-sparrow

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值