android ui 遍历,Android UI “疑难杂症”大汇总

文中所述问题均来自日常开发过程中遇到的Android UI 问题,部分问题各位大佬肯定遇到过,而问题的原因可能部分知道,也可能并未深究就没管了,下次可能还会犯同样的错误。刚好最近负责项目的UI性能优化这一块,借机回顾总结一下,文中主要借助源码来讲解导致这些问题的根源,正所谓“源码之下,了无秘密”。

主要讲解内容:(1)View::inflate正确使用姿势

(2)ListView的 itemView顶层控件设置margin属性失效

(3)RelativeLayout中最底的View其layout_marginBottom无效 (API 19以下)

(4)ListView Header 或Footer使用问题

(5)动态设置Background(.9图)后Padding无效的问题

(6)ListView  height设置wrap_content 导致getView()重复调用问题

......

一 、View::inflate正确使用姿势

View::inflate使用,想必各位Android 大大们肯定知道,不太清楚的可以快速看看,通常View::inflate()有以下两种方式:

(1)View::inflate(@Layout int resource,@Nullable ViewGroup root)

当root 不为null 时,inflate(resource,root) 等价于inflate(resource,root ,true)

(2)View::inflate(@Layout int resource,@Nullable ViewGroup root,boolean attachRoot)

除此之外,在DataBinding中也提供了一个DataBindingUtil.inflate()接口,内部实现与View::inflate()差不多。

上述就是View::inflate使用的几种方式,这里我直接列举几个错误案例,后面一一解释导致错误案例的真正原因:(1)View::inflate(resource,null)  或 View::inflate(resource,null ,true or false)

当root 为null时,resource 对应布局必须通过addView 才能添加到parent布局

导致问题 :addView后发现resource对应布局的android:layout_xx属性失效(如宽高属性),且 随着parent ViewGroup 不同表现情况也不同。

(2)View::inflate(resource,root)  或 View::inflate(resource,root ,true)

导致问题:addView resource 对应布局的根View ,会报错"java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first"

发生这类问题如果没有想到是什么原因,直接看看View::inflate源码,源码如下:

c44cb17bc741

View::inflate源码

从inflate源码中可以看到有3种情况关于infate出来的View,我们倒着看:

(1)情况3  ,当root == null 或 attachToRoot 为false 时

View::inflate出来的View 是temp (通过createViewFromTag生成的View),是不带LayoutParam信息的;

(2)情况2,root!=null &&attachToRoot==true

直接调用root.addView,这就是为什么在这种情况下主动动用addView 报错的原因。

(3)情况1 ,root!=null&&attachToRoot==false

inflate 传参不当导致  ViewGroup::addView(View child)  ,中child 的 android:layout_xxx(宽高属性失效)且  随着parent ViewGroup 不同表现情况也不同。

一般遇到此类问题,也没啥好怀疑人生的 ,直接看ViewGroup::addView()(代码少而精)一步步分析即可:

c44cb17bc741

ViewGroup::addView

根据addView(View chlid,int index)知,导致上述问题的原因取决于child.getLayoutParams(),进一步看 getLayoutParams()方法,从注释上知 mLayoutParam在child View is not attach to a parent ViewGroup 时 为null 。

c44cb17bc741

ViewGroup.LayoutParams::getLayoutParams()

回到上面addView中 ,如果child.getLayoutParams() 为null ,那么就会生成默认的LayoutParams,这里也不细说,直接列举几个布局的默认LayoutParms  。

c44cb17bc741

ViewGroup&&RelativeLayout::generateDefaultLayoutParams()

c44cb17bc741

LinearLayout::generateDefaultLayoutParams()

c44cb17bc741

FrameLayout::generateDefaultLayoutParams()

c44cb17bc741

AbsListView::generateDefaultLayoutParams()

相信大家一看就明白 ,再次也不过多细讲。

另外,在某些情况下误以为固定高度设置正确,使用android:layout_alignParentBottom = 'true' ,发现占满全屏的问题,案例如下。

c44cb17bc741

满屏案例

原因:

设置的固定高度没有生效,实际上layout_height是wrap_content,既然是wrap_content那为何会铺满整屏了,其实在RelativeLayout注释中早有说明如下图所示,RelativeLayout的大小和child View 位置关系设置不对(如高度设置成wrap_content 同时子View 设置android:layout_alignParentBottom = 'true'),可能导致循环依赖 ,导致RelativeLayout实际高度变成了match_parent ,继而出现一些奇怪的布局问题。

c44cb17bc741

二、ListView的 itemView顶层控件设置margin属性失效

相信大家看到这个问题时,跟我当时反应一样,肯定是inflate的时候将parentView设置为null导致的,真的是这样吗?

当我确认已经传了parentView,我就回去翻看View::inflate源码,终于找到原因了 ,在问题 一 中,已经讲过当root 不为null ,attachToRoot为false时,会将root的LayoutParams 传给child View 。ListView  继承于AbsListView,直接看AbsListView::generateLayoutParams(attrs)源码如下:

c44cb17bc741

AbsListView::generateLayoutParams

我们都知道,ViewGroup中除了LayoutParams外,还有一个MarginLayoutParams,既然是margin属性值失效,只需要确认AbsListView.LayoutParams是否继承MarginLayoutParams。

c44cb17bc741

AbsListView.LayoutParams继承了LayoutParams

c44cb17bc741

AbsListView.LayoutParams未继承MarginLayoutParams

通过上述源码发现,AbsListView.LayoutParams 果然未继承MarginLayoutParams,没有提供margin相关值。因而 itemView 顶层View的margin属性失效也是正常的。

另外,android.support.v7中的RecyclerView 是继承MarginLayoutParams

c44cb17bc741

RecyclerView::measureCh

解决办法 :使用padding代替margin(部分场景)或者嵌套实现(不推荐)或者直接使用RecyclerView 代替ListView

itemView顶层控件设置margin属性失效的原因,相信大家都已知晓,但在这里我还需要补充两个问题:

(1)能否对ItemView动态设置的margin

在对一般控件设置margin值时,我们一般采用ViewGroup.MarginLayoutParams来动态设置,正如上面所说AbsListView 是没有继承MarginLayoutParams的,因而无法对ItemView动态设置margin值。

( 2)如果parentView 类型传入不对,在4.x机型上会发生crash

堆栈信息如下:

c44cb17bc741

crash 错误堆栈

导致该crash是将 PullToRefreshListView 当作parentView 传给了inflate。为何只在4.x上crash了 ,具体分析详见同事hengwu的总结,感兴趣的可以去看看,分析很细致。问题一 和问题二中发生的问题,都与View::inflate相关 ,那么View::inflate使用的正确姿势:

(1)使用 inflate(resource,root,false )

(2)关注传入root 类型及root的LayoutParam类型

三、RelativeLayout中最底的View其layout_marginBottom无效 (API 19以下)

失效原因:RelativeLayout::onMeasure源码(本文对应api 23版本)

c44cb17bc741

RelativeLayout::onMeasure源码

当RelativeLayout的高度设置为wrap_content时,其高度height最开始需要遍历其子View计算得到,从上图中可以看到在api<19时,height 取的是最下面View的mBottom值作为height,并未计算最后一个View的margin_bottom。

解决办法:在最底View下面再添加一个height 为0的Space控件即可或者对RelativeLayout设置paddingBottom(适用于部分场景)

同理:RelativeLayout 宽度设置为wrap_content时(这种情况比较少见),也有类似的情况,唯一不同的是还与RTL Layout 布局有关(Android 4.2 ,Api 17开始支持)

四、 ListView Header 或Footer使用问题(1)设置Header 或Footer状态为GONE后,发现Header和Footer仍然占位,效果相当

于INVISIBLE状态;

(2)在api<=18 时,addHeader 和addFooter调用必须放在setAdapter之前;

(1)导致占位的原因:

在上面分析LinearLayout(其他ViewGroup也一样) 测量源码时,发现当子View 设置成GONE时,是不进行测量的,因而也就不会存在占位情况。

那为什么在ListView 中会存在了 ,只能去看源码 : 在ListView::onMeasure测量函数中,无论其宽 和高设置是什么类型,最终都会调用measureScrapChild()这个方法,如下:

c44cb17bc741

在进行测量前,并未判断View 是否GONE,就直接进行了测量,然后强制布局,因此出现了上述占位问题。

解决办法:1)多嵌套一层 ;2)不将Header或Footer设置成GONE,采用addView/removeView方式

(2)在api<=18 时,addHeader 和addFooter调用必须放在setAdapter之前

1)API<=18时,addHeaderView会先判断mAdapter,如果mAdapter不为null且mAdapter不是HeaderViewListAdapter的实例就会抛异常;但是addFooterView则不会主动抛异常,但是FooterView是不会显示出来的。

c44cb17bc741

API=18 时,addHeaderView

c44cb17bc741

API=18 时,addFooterView

2)API>18时,在addHeaderView 或addFooterView时,如果mAdapter为null或者mAdapter不是HeaderViewListAdapter的实例,则创建一个HeaderViewListAdapter对象给mAdapter。

c44cb17bc741

API=19 时,addHeaderView

综上知,不管是addHeaderView还是addFooterView,为了避免兼容性问题,addHeaderView和addFooterView最好在setAdapter()之前调用。

五、动态设置Background(.9图)后Padding无效的问题

解决办法:在动态设置background之后,再重新设置一遍padding值

六、ListView  height设置wrap_content 导致getView()重复调用问题

大家都知道当ListView对应的Adapter数据发生变化的时候(notifyDataSetChanged())、ItemView设置成GONE、addView 或removeView时,都会触发调用getView(),而我下面要讲的是当ListView 的layout_height设置成wrap_content时,为何会重复调用getView()。

既然getView()被重复调用,那只能找对应调用处,在AbsListView中,只有obtainView()中会mAdapter.getView()。而obtainView 在AbsListView中,只有getHeightForPosition()有使用,用于计算ScrapView 的高度,这个可以忽略。那么直接在子类ListView中去看,发现 obtainView 在onMeasure 、measureHeightOfChildren、makeAndAddView、addViewAbove等中有调用,其他函数比较简单且getView多次调用与ListView的layout_height设置有关,因此 直接分析测量相关即可。

分析过程如下:

(1)在ListView::onMeasure方法中,发现当ListView的高度设置为wrap_content时,ListView的高度heightSize需要测量child View 来确认,具体代码如下:

c44cb17bc741

ListView::onMeasure

(2)ListView::measureHeightofChildren()

c44cb17bc741

ListView::measureHeightofChildren()

在ListView::measureHeightofChildren()方法中,主要关注一下方法内的for循环:

1)obtainView() ,内部会调用 mAdapter.getView();

2)measureScrapChild(),测量废弃的child,进一步浪费资源;

3)recycleBin.addScrapView(child,-1),在某些情况下会导致部分资源无法回收,具体如下:

当ScrapView  有transient State 且 数据未发生变化时 mTransientStateViews会保存这个信息(不管遍历多少次,只会保存最后一个)。

c44cb17bc741

addScrapView()方法

而在对应清除TransientStateView的时候,并未清理掉position==-1的那个,具体代码如下:

c44cb17bc741

clearTransientStateViews()

那是不是将ListView 的 height 设置成match_parent 就不会多次调用 getView() 了 。 亲试 不会完全解决。

如果这时候getView()还重复调用,那就看Listview的上一级的高是不是也是设置也match_parent的,如果不是,也将ListView 的上一级设置成match_parent。

其实,还有很多类似的相关问题,后续慢慢汇总.......

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值