View基础与自定义

自定义View基础

view树绘制起点

android中大多数情况下我们的界面都是使用view进行显示,(目前我知道的除过使用surface进行显示的视图,其余的都是使用View进行显示,如果有其他方式大家可以一起讨论~)
android中界面使用view进行显示的时候界面可以看作是一颗树,父节点会包含对应的孩子节点,而这棵树的根节点为DecorView,界面上的视图都在这个父节点中显示。而Activity只负责生命周期和事件处理,不进行ui和视图控制相关的处理,视图的绘制和控制是由ViewRoot完成的。
当activity创建完毕后,会将ViewRoot和DecorView进行关联,

		root = new ViewRootImpl(view.getContext(), display);
        ...
        root.setView(view, wparams, panelParentView);

源码见WindowManagerGlobal.java,Activity创建的时候如何调用到这里可以参见博客 链接
ViewRoot和DecorView进行关联的时候调用了ViewRoot的setVIew方法,(ViewRoot实现对应于ViewRootImpl类,具体源码可以在该类中进行查看),setView方法中调用了requestLayout()方法,在这里插入图片描述
而requestLayout会调用scheduleTraversal,在scheduleTraversal方法中会发送mTraversalRunnable事件,
在这里插入图片描述
mTraversalRunnable中会执行doTraversal方法,该方法中会调用performTraversal方法,从该方法开始,view树开始调用performMeasure(),performLayout(),performDraw()进行绘制。
performTraversal绘制流程
View绘制流程
performTraversal()方法调用后会首先调用父容器的measure,layout,draw方法,然后递归调用到子View的measure,layout,draw方法。

MeasureSpec介绍

查看MeasureSpec的注解:
在这里插入图片描述
MeasureSpec携带了父view对子view宽高的要求,而该要求是通过view自身的layoutParams和父容器施加的转换规则转换得到的。
MeasureSpec实现时为了减少创建对象的开销,使用了数字进行实现。使用高两位表示SpecMode,使用低30位表示SpecSize,MeasureSpec在实现的时候也提供了相应的方法进行Mode和Size的提取,以及Mode和Size的组合。
该方法进行mode和size的组合

public static int makeMeasureSpec(int size,int mode) {
    
    
    if (sUseBrokenMakeMeasureSpec) {
    
    
        return size + mode;
    } else {
    
    
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

下面的两个方法则分别获得相应的size和mode

public static int getMode(int measureSpec) {
    
    
    //noinspection ResourceType
    return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
    
    
    return (measureSpec & ~MODE_MASK);
}

MeasureSpec在实现的时候规定了三种mode,如下:
在这里插入图片描述
UNSPECIFIED :这种情况下父容器不对view的大小做任何限制。

EXACTLY :父容器已经给出view的大小,这个时候SpecSize大小就是View最后的大小。这种情况对应于layoutparam为match_parent和具体数值的这两种情况,给出view一个明确的大小。

AT_MOST :父容器指定了一个SpecSize,View的大小不能大于这个值,具体是什么值需要看不用view的具体实现。这种情况对应于layoutparams为wrap_content,限定了view的范围,但是没有明确要求view的大小。
View在进行绘制的时候是以自身的MeasureSpec为标准的,而View 的MeasureSpec的是根据父容器的MeasureSpec和View自身的Layoutparams得到的,具体的方法参见getChildMeasureSpec()方法,将该方法中的规则总结出来表格如下:
在这里插入图片描述
而对于顶级View(DecorView),其MeasureSpec由自身的LayoutParams决定,具体可以参见方法ViewRootImpl中的getRootMeasureSpec()方法。

DecorView的测量规则
LayoutParames SpecMode
MATCH_PARENT EXACTLY,大小就是窗口的大小
WRAP_CONTENT AT_MOST,大小不能超过屏幕大小
固定大小如(100px) 精确模式,大小为指定的大小

将顶层view的测量可以总结成上表

View绘制流程介绍

measure

View的measure过程由measure方法开始,该方法为final方法,无法被继承,measure的过程主要由onMeasure方法完成,子类在使用的时候重写该方法即可,通过该方法我们可以使用viewgroup中传递过来的measureSpec来确定view测量出的大小。在这里插入图片描述
查看上述代码,onMeasure()方法中会调用setMeasuredDimension方法保存view的宽和高,view的宽和高通过getDefaultSize和getSuggestedMinmumWidth方法获取。
ViewGroup的measure过程:
相比于view的measure过程,ViewGroup在measure的过程会需要测量自身的大小并且还需要调用子view的measure方法。
但是ViewGroup自身是一个抽象类,没有定义具体的测量过程,即没有实现onMeasure方法,因为不同布局的测量方式不同,比如说RelativeLayout和LinearLayout的测量方式必然不同,所以onMeasure的具体过程需要子类去自己实现,ViewGroup没有实现具体测测量过程,具体的测量过程还得依靠子类去实现,但是ViewGroup为子类提供了measureChild方法,该方法会进行所有孩子的测量,子类在实现时候可以默认调用该方法完成自身孩子的布局,但是自身的布局还得靠自己根据自己的使用场景去实现对应的onMeasure方法完成布局:
在这里插入图片描述

layout

Layout方法用来确定view自身的位置,而onLayout方法用来确认所有的子元素的位置,在layout方法中会调用setFrame方法首先确定自身的位置,

public void layout(int l, int t, int r, int b) {
    
    
    ...
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
...
    onLayout(changed, l, t, r, b);

然后调用onLayout方法布局孩子的位置,

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
    }

而onLayout方法可以看到view自身并没有实现,因为每一个布局自己对自己的孩子的布局方式不同,具体的实现需要相应的子类去重写。

draw

View在绘制的时候分为如下几步:

...
    drawBackground(canvas);  // 1.绘制背景
...
    if (!dirtyOpaque) onDraw(canvas);// 2.绘制自身内容

    dispatchDraw(canvas);//3.绘制孩子(该方法需要子类重写,从而根据子类自身的规则去绘制相应的view)

    drawAutofilledHighlight(canvas);// 4.绘制相应的装饰

    if (mOverlay != null && !mOverlay.isEmpty()) {
    
    
        mOverlay.getOverlayView().dispatchDraw(canvas); // 4.绘制相应的装饰
    }

    onDrawForeground(canvas);// 4.绘制相应的装饰

    drawDefaultFocusHighlight(canvas);// 4.绘制相应的装饰

ViewGroup在绘制的时候setWillNotDraw()会在中设置为true,用来优化绘制,如果我们需要在viewGroup中绘制内容时,需setWillNotDraw()为false来关闭优化。

自定义View

实现效果: 支持padding,支持自定义属性实现颜色的自定义,同时支持wrap_content。
在这里插入图片描述
通过重写onDraw()我们去实现一个圆形,代码实现比较简单,但是我们需要注意
1:对直接继承自View和ViewGroup的控件,padding属性是默认无法生效的,需要自己处理。
2:继承View和ViewGroup的控件,自己需要处理wrap_content属性
处理padding属性无效的问题:在我们确定宽和高的大小进行绘制的时候我们需要将padding计算在内

int width = getWidth() - getPaddingLeft() - getPaddingRight();
int height = getHeight() - getPaddingTop() - getPaddingBottom();

处理wrap_content无效的问题
通过上表:在这里插入图片描述我们可以看到当设置为wrap_content的时候specSize为parentSize,查看getDefaultSize的源码,mode为AT_MOST的时候view的大小就是specSize,所以设置wrap_content等价于match_parent:
在这里插入图片描述
因此当我们自定义view使用wrap_content属性的时候,我们需要自己处理,处理的代码如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        // 下面的过程处理view的书信给wrap_content
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
    
    
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
    
    
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
    
    
            setMeasuredDimension(widthSpecSize, 200);
        }
    }

上面的代码中,当我们判断view的specMode为AT_MOST的时候,我们设置大小为200(这个值可以自己根据自己的需要设置),通过上面的处理方式我们解决wrap_content不生效的问题。
看到这里自己脑子里面突然有个疑问在这里插入图片描述
红色的圈出来的那一部分也是会让view的specMode为AT_MOST,那我们重写了view对AT_MOST的处理,当我们设置view为match_parent的时候那岂不match_parent没办法生效了?
自己想了想,什么时候父容器的specMode为AT_MOST呢,查看上表《DecorView的测量规则》,只有当我们设置为wrap_content属性的时候才是AT_MOST,那这就意味大小只要满足孩子的大小即可,即我们自己实现的wrap_content。

增加自定义属性

首先我们在values目录 下面新建一个自定义属性文件,文件名字可以随意起,在这里我新建一个attrs.xml文件,文件中我们自定义自己需要的属性,这里定义了color属性:

    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color" />
    </declare-styleable>

上面的format中我们还可以填写如下值,

format 含义
reference 代表资源id
dimension 尺寸
基本数据类型(string/integer/boolean) 需使用者自己定义

代码中我们通过下面的方式使用
在xml跟布局上面增加一个app命名空间,然后我们在该命名空间下使用我们自定义的属性

<LinearLayout ...
    xmlns:app="http://schemas.android.com/apk/res-auto"
	...>
	<com.example.customizeview.CircleView
        ...
        app:circle_color="@color/colorAccent" />
</LinearLayout>

最后我们需要在代码中获取我们自定义的属性

 public CircleView(Context context, @Nullable AttributeSet attrs) {
    
    
        super(context, attrs);
        Log.i(TAG, "CircleView: enter constructor with attrs");
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = typedArray.getColor(R.styleable.CircleView_circle_color, Color.BLUE);
        typedArray.recycle();
        init();
    }

mColor就是我们自己定义的颜色。
完整代码见 CircleView

View布局性能优化

布局优化首先我们应该做的就是尽可能的减少布局文件的层级,当布局的层级减少了,那绘制的性能自然就提高了,同时还可以使用下面的方式来优化我们的布局。
1.优先选择高性能的ViewGroup
android中界面布局使用android中提供的控件就可以,但是同时我们在使用的时候可以根据场景择优选择所需的控件,在选择ViewGroup的时候,可以优先选择性能较高的FrameLayout和LinearLayout,因为RelativeLayout较为费时,但是如果布局需要进行嵌套,可以考虑使用RelativeLayout来实现布局,以达到减少布局层级的目的。
2.使用include和merge标签重复利用布局,同时减少嵌套

  1. 使用include标签我们可以将公共的布局抽取出来,这样公共的布局我们不用总是重新再写一遍,同时include标签只支持 android:layout_ 开头的属性和android:id 属性,其他的属性不支持,同时在使用该属性的时候,如果使用了android:layout_*开头的其他属性,那么layout_width和layout_height属性必须同时存在,否则设置的android:layout_*开头的属性不起作用。
  2. 使用merge标签可以用来减少布局的层级,当使用include标签引用布局的时候,如果被引用布局的根布局和include标签的父布局为同一布局,就可以使用merge标签替换被引用布局的根布局标签。
  • 自己在尝试的过程中发现,使用include标签时,如果被引用布局的根元素使用merge标签,include属性设置android:layout_属性将不起作用,而需要在merge标签内设置include属性,这样include标签的android:layout_属性才会起作用。(此处为自己通过demo简单实践后得到的,如果有错误还请指正)

  • 不起作用的写法:
    layout布局

<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".LayoutPerformanceActivity">

    <!--注意layout前面没android开头的命名空间-->
    <include
        android:id="@+id/reused_image"
        layout="@layout/commonimageview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"/>
</LinearLayout>

被引用布局

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher_round" />
</merge>

运行结果:layout_margin属性不起作用

  • 起作用的写法
    修改被引用布局结构,在merge标签中使用include标签引用布局
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <include
        android:id="@+id/reused_image"
        layout="@layout/stub_import"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp" />
</merge>

stub_layout布局文件仅为一个图片:

<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@mipmap/ic_launcher" />

代码见文末链接
运行后发现自己在merge里面的include使用layout_margin属性起作用了,但是引用merge标签的include布局还是没办法使用android:layout_margin属性。
对于这个例子的简单布局来说,可以直接将stub_layout,即不带merge标签的根布局作为引用布局,这样android:layout_属性就可以起作用了,如果是一个复杂的布局,可以使用上面的方式,将需要自定义的布局包含在merge标签之内,这样就可以使用layout属性进行自定义布局了。
3.使用ViewStub标签进行按需加载
ViewStub继承了View,自身不参与任何布局和绘制过程,当我们在开发中,一些布局在初始化时没有必要一起进行初始化,这时就可以使用ViewStub来按需加载该布局,因为当view设置为Gone或者Invisiable的时候,还是会占资源,具体参见(Android 使用View Gone 与 ViewStub的区别
但是需要注意的是ViewStub的引用布局不支持merge标签
ViewStub使用方式

    <ViewStub
        android:id="@+id/stub_import"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inflatedId="@+id/stub_import_view"
        android:layout="@layout/stub_import" />

android:id指定的即为ViewStub的id,当ViewStub被替换后,加载后得到的布局的Id即为android:inflatedId指定的id。
加载方式:

// mViewStub为 mViewStub = findViewById(R.id.stub_import);
mViewStub.setVisibility(View.VISIBLE);

代码:include标签和merge标签使用以及ViewStub标签使用

猜你喜欢

转载自blog.csdn.net/liu_12345_liu/article/details/107900143
今日推荐