自定义View的wrap_content属性失效问题详解

自定义View的wrap_content属性失效问题详解

开篇:自定义view是我们学习和工作当中很常见也是必不可少的一环,对于自定义view,我们很常见的一个问题就是view的wrap_content属性值失效的问题,本文即从view的简明工作原理上来细说这一问题的“模版式”解决方式。

一. view工作原理摘要:
主要介绍两个类:ViewRoot 和DecorView;

ViewRoot:1.对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程(measure,layout,draw)都是通过ViewRoot来完成的;
2.ActivityThread中,Activity创建完成后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并建立两者的关联;
3.View的整个绘制流程就是从ViewRoot类的performTraversals方法开始,依次调用performMeasure、performLayout、performDraw三个方法(对应三大流程), 经过三大流程最终绘制出一个完整的view。
注明:三大流程中measure负责测量view的宽高,layout用来确定view在父容器的位置,draw涌来绘制view到屏幕中,过程大致为measure->layout->draw;

DecorView:1.首先,DecorView是整个Window界面的根View;每个Activity都包含一个Window对象,Window对象通常由PhoneWindow来实现,PhoneWindow将一个DecorView设置为整个应用窗口的根View,其中所有View的监听事件通过WindowManagerService来接收并通过Activity对象回调相应的OnClickListener;
2.DecorView只有一个子元素为LinearLayout。显示上,上面一个TitleView(默认包含TextView的FrameLayout),下面一个ContentView(id为content的FrameLayout);

对于View的测量过程,我们还要提到一个类——MeasureSpec:
这个类很大程度上决定了一个View的尺寸规格(创建过程受父容器的影响),在测量过程中,系统会将View的LayoutParams根据父容器施加的规则转成对应的MeasureSpec,再根据measureSpec来测量出View的宽和高。
MeasureSpec代表一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小)。
对于SpecMode有以下三类:
1.UNSPECIFIED:父容器不对View有任何限制,要多大给多大,一般用于系统内部,表示一种测量状态;
2.EXACTLY:父容器已检测出View所需精确大小,View的最终大小为SpecSize所指定的值,对应于LayoutParams中的match_parent和具体数值;
3.AT_MOST:父容器指定了一个可用大小(SpecSize),View大小最大即为这个值,具体要看不同View的具体实现,对应于LayoutParams中的wrap_content;

对于DecorView和普通View来说,MeasureSpec的转换过程略有不同:前者MeasureSpec由窗口尺寸和自身LayoutParams共同决定,后者MeasureSpec由父容器的MeasureSpec和自身LayoutParams共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽和高。

二. View工作流程
1.View的measure过程:
View的measure过程由其measure方法来完成,measure方法是一个final类型方法(子类不可重写此方法),在View的measure方法中会去调用View的onMeasure方法,方法如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
          getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

而对于getDefaultSize(),我们从源码来剖析一下,源码如下:

public static int getDefaultSize(int size, int measureSpec) {
     int result = size;
     int specMode = MeasureSpec.getMode(measureSpec);
     int specSize = MeasureSpec.getSize(measureSpec);

     switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
 }

从getDefaultSize实现来看,很容易看出View的宽和高由specSize决定。所以回到我们最初的问题上来,我们可以由此得出:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content模式下时的自身大小,否则布局中的wrap_content就相当于match_parent,这也是为什么自定义view之后我们的wrap_content属性失效的原因。

原因总结如下:由上述代码可以看出,View在布局中处于wrap_content模式,那么specMode为AT_MOST,这种模式下View的宽和高等于specSize,而对应的,这里的specSize就是parentSize,parentSize是父容器目前可以使用的大小,即父容器当前剩余空间大小,显然两者相等,效果也自然与match_parent完全一致了。

2,既然如此,我们的解决方式如何呢?
这里给大家总结出以下几个步骤:
1.从MeasureSpec对象提取具体的SpecMode和SpecSize;
2.通过判断测量模式来给出不同的测量值(除EXACTLY使用指定的SpecSize外其余两种需要指定一个默认值);
3.对于wrap_content,即AT_MOST模式,我们需要取出我们指定的大小与specSize中最小的一个作为最后的测量值,指定的大小根据我们实际需要来定。

说了这么多???实际代码呢???
好吧,既然如此,一言不合就上代码,如下:
重写 onMeasure方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureSpecHandler(widthMeasureSpec, DEFAULT_WIDTH),measureSpecHandler(heightMeasureSpec, DEFAULT_HEIGHT));
}

其中的measureSpecHandler提取出来如下:

private int measureSpecHandler(int measureSpec, int defaultSize){
     int result = defaultSize;
     int specMode = MeasureSpec.getMode(measureSpec);      int specSize = MeasureSpec.getSize(measureSpec);

     if(specMode == MeasureSpec.EXACTLY){
         result = specSize;
     } else if(specMode == MeasureSpec.AT_MOST){
         result = Math.min(result, specSize);      
     } else{
         result = defaultSize;
     }
     return result;
}

是不是很简单呢?,最后,附上整体效果:

附1:布局文件关键代码

<com.example.sixer.myapplication.MyView
        android:id="@+id/my_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"/>

**附2:wrap_content模式下设置默认宽高:
MyView.class中加入:**

 private final static int DEFAULT_WIDTH = 200;
 private final static int DEFAULT_HEIGHT = 400;

附3:效果图:(这里只附上wrap_content下效果)

这里写图片描述

好了,主体内容到此结束,太细节的问题大家可以查阅资料或留言,如果对大家有帮助,希望阅读后点赞多多支持哈。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
当父布局的高度属性为 `wrap_content` 时,自定义 View 的高度是根其内容动态确定的,无法直接获取准确的高度值。但可以通过以下方法来获取自定义 View 的准确高度: 1. 使用 ViewTreeObserver 监听布局完成事件:在自定义 View 的代码中,可以使用 `ViewTreeObserver` 来监听布局完成事件,一旦布局完成,就可以获取到自定义 View 的准确高度。示例代码如下: ```java ViewTreeObserver vto = customView.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { customView.getViewTreeObserver().removeOnGlobalLayoutListener(this); int height = customView.getHeight(); // 获取自定义 View 的高度 // 在这里可以使用获取到的高度进行后续操作 } }); ``` 2. 重写 `onMeasure()` 方法:在自定义 View 的代码中,可以重写 `onMeasure()` 方法,在测量过程中获取到准确的高度值,并保存起来供后续使用。示例代码如下: ```java @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); // 在这里可以获取到准确的宽度和高度,并保存起来 } ``` 通过以上方法,你可以在父布局为 `wrap_content` 的情况下获取到自定义 View 的准确高度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值