发布于2022年11月4日3年前 Android View绘制流程 如上图,Activity的window组成,Activity内部有个Window成员,它的实例为PhoneWindow,PhoneWindow有个内部类是DecorView,这个DecorView就是存放布局文件的,里面有TitleActionBar和我们setContentView传入进去的layout布局文件 Window类时一个抽象类,提供绘制窗口的API PhoneWindow是继承Window的一个具体的类,该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View DecorView继承FrameLayout,里面id=content的就是我们传入的布局视图 依据面向对象从抽象到具体我们可以类比上面关系就像如下: Window是一块电子屏,PhoneWindow是一块手机电子屏,DecorView就是电子屏要显示的内容,Activity就是手机电子屏安装位置 View 绘制中主要流程分为measure,layout, draw 三个阶段。 measure :根据父 view 传递的 MeasureSpec 进行计算大小。 layout :根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。 draw :把 View 对象绘制到屏幕上。 一、Measure 1、MeasureSpec MeasureSpec 封装了从父级传递到子级的布局要求。每个 MeasureSpec 代表对宽度或高度的要求。MeasureSpec 由大小和模式组成。 MeasureSpec 一个32位二进制的整数型,前面2位代表的是mode,后面30位代表的是size。mode 主要分为3类,分别是 EXACTLY:父容器已经测量出子View的大小。对应是 View 的LayoutParams的match_parent 或者精确数值。 AT_MOST:父容器已经限制子view的大小,View 最终大小不可超过这个值。对应是 View 的LayoutParams的wrap_content UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。(这种不怎么常用) View 的 MeasureSpec 并不是父 View 独自决定,它是根据父 view 的MeasureSpec加上子 View 的自己的 LayoutParams,通过相应的规则转化。 2、流程 在measure 方法,核心就是调用onMeasure( ) 进行View的测量。在onMeasure( )里面,获取到最小建议值,如果父类传递过来的模式是MeasureSpec.UNSPECIFIED,也就是父View大小未定的情况下,使用最小建议值,如果是AT_MOST或者EXACTLY模式,则设置父类传递过来的大小。 然后调用setMeasuredDimension 方法进行存储大小。 二、Layout 确定 View 在父 View 的位置进行排版布局 三、draw draw 过程中一共分成7步,其中两步我们直接直接跳过不分析了。 第一步:drawBackground(canvas): 作用就是绘制 View 的背景。 第三步:onDraw(canvas) :绘制 View 的内容。View 的内容是根据自己需求自己绘制的,所以方法是一个空方法,View的继承类自己复写实现绘制内容。 第三步:dispatchDraw(canvas):遍历子View进行绘制内容。在 View 里面是一个空实现,ViewGroup 里面才会有实现。在自定义 ViewGroup 一般不用复写这个方法,因为它在里面的实现帮我们实现了子 View 的绘制过程,基本满足需求。 第四步:onDrawForeground(canvas):对前景色跟滚动条进行绘制。 第五步:drawDefaultFocusHighlight(canvas):绘制默认焦点高亮 四、自定义视图类 1、步骤 1、自定义View的属性 2、在View的构造方法中获得我们自定义的属性 3、重写onMeasure 4、重写onDraw 2、定义自定义属性 自定义View的属性,首先在res/values/ 下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。 如需在自定义视图中启用此行为,您必须: 在 <declare-styleable> 资源元素中定义视图的自定义属性 在 XML 布局中指定属性值 在运行时检索属性值 将检索到的属性值应用到视图 <resources> <declare-styleable name="CustomView"> <attr name="titleText" format="string"/> <attr name="titleTextColor" format="color"/> <attr name="titleTextSize" format="dimension"/> </declare-styleable> </resources> 定义了自定义属性后,便可像内置属性一样在布局 XML 文件中使用它们。唯一的区别是自定义属性属于另一个命名空间。它们不属于 http://schemas.android.com/apk/res/android 命名空间,而是属于 http://schemas.android.com/apk/res/[your package name]。 当然在我的Android Studio中我改用xmlns:custom="http://schemas.android.com/apk/res-auto",因为当前工程是做为lib使用,那么你如上所写 ,会出现找不到自定义属性的错误 。这时候你就可以写成xmlns:custom="http://schemas.android.com/apk/res-auto" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:custom="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.uidesign.CustomView android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="10dp" android:layout_centerInParent="true" custom:titleText="liming" custom:titleTextColor="#ff0000" custom:titleTextSize="40sp"/> </RelativeLayout> 3、应用自定义属性 通过 XML 布局创建视图时,XML 标记中的所有属性都会从资源包读取,并作为 AttributeSet 传递到视图的构造函数中。虽然可以直接从 AttributeSet 读取值,但这样做有一些弊端: 不解析属性值中的资源引用 不应用样式 请改为将 AttributeSet 传递给 obtainStyledAttributes()。此方法会传回一个 TypedArray 数组,其中包含已解除引用并设置了样式的值。 Android 资源编译器做了大量工作,以便您更轻松地调用 obtainStyledAttributes()。对于 res 目录中的各个 <declare-styleable> 资源,生成的 R.java 定义一个由属性 ID 组成的数组,同时定义一组常量,用于定义该数组中各属性的索引。您可以使用预定义的常量从 TypedArray 读取属性。 public class CustomView extends View { private String mTitleText; private int mTitleTextColor; private int mTitleTextSize; private Rect rect; private Paint paint; public CustomView(Context context, AttributeSet attrs) { super(context,attrs); //为自定义View添加事件 this.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mTitleText=randomText(); postInvalidate(); } }); TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.CustomView, 0, 0); try { mTitleText = a.getString(R.styleable.CustomView_titleText); mTitleTextColor=a.getColor(R.styleable.CustomView_titleTextColor,Color.BLACK); mTitleTextSize=a.getDimensionPixelSize(R.styleable.CustomView_titleTextSize,(int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics())); } finally { a.recycle(); } } //重写onMeasure @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height ; if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else { paint.setTextSize(mTitleTextSize); paint.getTextBounds(mTitleText, 0, mTitleText.length(), rect); float textWidth = rect.width(); int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight()); width = desired; } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { paint.setTextSize(mTitleTextSize); paint.getTextBounds(mTitleText, 0, mTitleText.length(), rect); float textHeight = rect.height(); int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom()); height = desired; } setMeasuredDimension(width, height); } //重写onDraw @Override protected void onDraw(Canvas canvas) { paint.setColor(Color.YELLOW); canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint); paint.setColor(mTitleTextColor); canvas.drawText(mTitleText, getWidth() / 2 - rect.width() / 2, getHeight() / 2 + rect.height() / 2, paint); } } TypedArray 对象是共享资源,必须在使用后回收。 由于系统帮我们测量的高度和宽度都是MATCH_PARNET,当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。 所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法。重写之前先了解MeasureSpec的specMode,一共三种类型: EXACTLY:一般是设置了明确的值或者是MATCH_PARENT AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT UNSPECIFIED:表示子布局想要多大就多大,很少使用
创建帐户或登录后发表意见