先说ListView给高的正确做法.

android:layout_height属性:

必须将ListView的布局高度属性设置为非“wrap_content”(可以是“match_parent / fill_parent / 400dp等绝对数值”)

废话少说先来张bug图填楼

前言

随着RecyclerView的普及,ListView差不多是安卓快要淘汰的控件了,但是我们有时候还是会用到,基本上可以说是前些年最常用的Android控件之一了.抛开我们的主题,我们先来谈谈ListView的一些小小的细节,可能是很多开发者在开发过程中并没有注意到的细节,这些细节设置会影响到我们的App的性能.

  • android:layout_height属性

我们在使用ListView的时候很可能随手就会写一个layout_height=”wrap_content”或者layout_height=”match_parent”,非常非常普通,咋一看,我写的没错啊...可是实际上layout_height=”wrap_content” 是错误的写法!!!会严重影响程序的性能 我们先来做一个实验:

xml布局文件如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"> <ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
></ListView>
</LinearLayout>

java部分代码

运行log

我们会发现getView总共被调用了15次!其中4次是null的,11次为重复调用,ListView的item数目只有3项!!!太可怕了

我们试着将ListView的高度属性改为layout_height=”match_parent”,然后看看



我们可以看到getView()只被调用了3次!这应该是我们期望的结果!

原因分析:

了解原因前,我们应该先了解View的绘制流程,之前我的博客没有关于View绘制流程的介绍,那么在这边说一下,是一个很重要的知识点.

View的绘制流程是通过 onMeasure()->onLayout()->onDraw()

onMeasure() :主要工作是测量视图的大小.从顶层的父View到子View递归调用measure方法,measure方法又回调onMeasure().

onLayout: 主要工作是确定View的位置,进行页面布局.从顶层的父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子view所得到的布局大小和布局参数,将子view放在合适的位置上

onDraw() 主要工作是绘制视图.ViewRoot创建一个Canvas对象,然后调用onDraw()方法.总共6个步骤.1.绘制视图背景,2.保存当前画布的图层(Layer),3.绘制View内容,4.绘制View的子View视图,没有的话就不绘制,5.还原图层,6.绘制滚动条.

了解了View的绘制流程,那么我们回到这个问题上.设置ListView的属性layout_height=”wrap_content”,就意味着Listview的高度由子View决定,当在onMeasure()的时候,需要测量子View的高度,那我们来看看Listview的onMeasure()方法.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec); int childWidth = 0;
int childHeight = 0;
int childState = 0; mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap); measureScrapChild(child, 0, widthMeasureSpec); childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState()); if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
} if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = mListPadding.left + mListPadding.right + childWidth +
getVerticalScrollbarWidth();
} else {
widthSize |= (childState&MEASURED_STATE_MASK);
} if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
} if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
} setMeasuredDimension(widthSize , heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}

其中

if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}

比较重要

再看measureHeightOfChildren()

final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
final int maxHeight, int disallowPartialChildPosition) { ... for (i = startPosition; i <= endPosition; ++i) {
child = obtainView(i, isScrap); measureScrapChild(child, i, widthMeasureSpec);
... // Recycle the view before we possibly return from the method
if (recyle && recycleBin.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
recycleBin.addScrapView(child, -1);
} returnedHeight += child.getMeasuredHeight(); if (returnedHeight >= maxHeight) {
...
} if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
...
}
}
return returnedHeight;
}

obtainView(i, isScrap)是子View的实例

measureScrapChild(child, i, widthMeasureSpec); 测量子View

recycleBin.addScrapView(child, -1);将子View加入缓存,可以用来复用

if (returnedHeight >= maxHeight) {return ...;}如果已经测量的子View的高度大于maxHeight的话就直接return出循环,这样的做法也很好理解,其实是ListView很聪明的一种做法,你可以想想比如说这个屏幕只能画10个Item高度,你有20个Item,那么画出10个就行了,剩下的十个就没必要画了~

我们现在看下obtainView()方法

View obtainView(int position, boolean[] isScrap) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; // Check whether we have a transient state view. Attempt to re-bind the
// data and discard the view if we fail.
final View transientView = mRecycler.getTransientStateView(position);
if (transientView != null) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); // If the view type hasn't changed, attempt to re-bind the data.
if (params.viewType == mAdapter.getItemViewType(position)) {
final View updatedView = mAdapter.getView(position, transientView, this); // If we failed to re-bind the data, scrap the obtained view.
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position);
mRecycler.addScrapView(updatedView, position);
}
} // Scrap view implies temporary detachment.
isScrap[0] = true;
return transientView;
} final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else {
isScrap[0] = true; child.dispatchFinishTemporaryDetach();
}
} if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
} if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
} setItemViewLayoutParams(child, position); if (AccessibilityManager.getInstance(mContext).isEnabled()) {
if (mAccessibilityDelegate == null) {
mAccessibilityDelegate = new ListItemAccessibilityDelegate();
}
if (child.getAccessibilityDelegate() == null) {
child.setAccessibilityDelegate(mAccessibilityDelegate);
}
} Trace.traceEnd(Trace.TRACE_TAG_VIEW); return child;
}

得到一个视图,它显示的数据与指定的位置。这叫做当我们已经发现的观点不是可供重用的回收站。剩下的唯一的选择是将一个古老的视图或制作一个新的.(这是方法注释的翻译,大致可以理解他的意思)

我们应该关注下以下两行代码:

...
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
...

这两行代码的意思就是说先从缓存里面取出来一个废弃的view,然后将当前的位置跟view作为参数传入到getView()方法中.这个废弃的,然后又作为参数的view就是convertView.

然后我们总结下刚刚的步骤:

A、测量第0项的时候,convertView肯定是null的 View scrapView = mRecycler.getScrapView(position)也是空的,所以我们在log上可以看到.



B、第0项测量结束,这个第0项的View就被加入到复用缓存当中了;

C、开始测量第1项,这时因为是有第0项的View缓存的,所以getView的参数convertView就是这个第0项的View缓存,然后重复B步骤添加到缓存,只不过这个View缓存还是第0项的View;

D、继续测量第2项,重复C。

所以前面说到onMeasure方法会导致getView调用,而一个View的onMeasure方法调用时机并不是由自身决定,而是由其父视图来决定。ListView放在FrameLayout和RelativeLayout中其onMeasure方法的调用次数是完全不同的。在RelativeLayout中oMeasure()方法调用会翻倍.

由于onMeasure方法会多次被调用,上述问题中是两次,其实完整的调用顺序是onMeasure - onLayout - onMeasure - onLayout - onDraw。

所以根据上面的结论我们可以得出,如果LitsView的android:layout_height属性设置为wrap_content将会引起getView的多次测量

现象

如上bug图...

产生的原因

  • ListView的高度设置成了android:layout_height属性设置为wrap_content

  • ListView的父类是RelativeLayout,RelativiLayout布局会使子布局View的Measure周期翻倍,有兴趣可以看下三大基础布局性能比较

解决办法

根据每个Item的高度,然后再根据Adapter的count来动态算高.

代码如下:

public class SetHeight {

    public void setListViewHeightBasedOnChildren(ListView listView, android.widget.BaseAdapter adapter) {

        if (adapter==null){
return;
}
int totalHeight = 0; for (int i = 0; i < adapter.getCount(); i++) { // listAdapter.getCount()返回数据项的数目 View listItem = adapter.getView(i, null, listView); listItem.measure(0, 0); // 计算子项View 的宽高 totalHeight += listItem.getMeasuredHeight(); // 统计所有子项的总高度 } ViewGroup.LayoutParams params = listView.getLayoutParams(); params.height = totalHeight
+ (listView.getDividerHeight() * (adapter.getCount() - 1)); // listView.getDividerHeight()获取子项间分隔符占用的高度 // params.height最后得到整个ListView完整显示需要的高度 listView.setLayoutParams(params); } }

xml布局,注意要将ListView的父类设置为LinearLayout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"> <LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/txt_cancel"
android:orientation="vertical">
<View
android:layout_width="fill_parent"
android:layout_height="@dimen/y2"
android:background="#cccccc" /> <ListView
android:id="@+id/lv_remain_item"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:cacheColorHint="#00000000"
></ListView> <View
android:layout_width="fill_parent"
android:layout_height="@dimen/y2"
android:background="#cccccc" /> </LinearLayout> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
> <TextView
android:id="@+id/txt_cancel"
android:layout_width="fill_parent"
android:layout_height="@dimen/y120"
android:layout_alignParentBottom="true"
android:gravity="center"
android:text="cancel"
android:textSize="@dimen/x32" />
</LinearLayout>
</LinearLayout>

然后在Listview使用处,调用该方法.

 userListDialog.getmListView().setAdapter(scaleUserAdapter);
SetHeight.setListViewHeightBasedOnChildren(userListDialog.getmListView(),scaleUserAdapter);

运行结果

getView()调用情况



GitHub代码地址:ListViewDialog,喜欢的话欢迎Start

由Dialog里面嵌套ListView之后的高度自适应引起的ListView性能优化的更多相关文章

  1. ListView显示Sqlite的数据美化版与性能优化

    在上一篇文章中,我们已经实现在listview显示数据库内容的.但是我们listview中,排版不是很好看,所以这篇文章呢,我们来对listveiw进行美化.哈哈,说白了,就是对listview添加一 ...

  2. Android中ListView嵌套进ScrollView时高度很小的解决方案

    package com.example.test.util; import android.view.View; import android.view.ViewGroup; import andro ...

  3. android 多个listView的向下滚动设置 listView动态设置高度代码

    墨迹天气图: 这里都是用的android里面的shape实现的,实现起来比较简单,只是在滚动的时候有点小麻烦... 当我们多个ListView超出了它的父控件LinearLayout的时候,它们每个L ...

  4. flex布局嵌套之高度自适应

    查遍各大资源无任何flex嵌套布局的例子,经过自己折腾完成了项目中的高度自适应需求(更多应用于前端组件) 效果图: html代码:(关键地方已经用颜色特别标识 ^_^) <!DOCTYPE ht ...

  5. 解决viewpager+多个fragment+listview,listview展示内容高度不自适应出现多余空白问题

    一.重写viewpager import android.content.Context; import android.support.v4.view.ViewPager; import andro ...

  6. [Swift通天遁地]二、表格表单-(3)在表格中嵌套另一个表格并使Cell的高度自适应

    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤微信公众号:山青咏芝(shanqingyongzhi)➤博客园地址:山青咏芝(https://www.cnblogs. ...

  7. iframe高度自适应

    前两天在网上看到了一道面试题,问iframe高度自适应的问题.发现自己之前几乎没有关注过iframe的问题,所以在这里记录一下. 原题目是: 页面A的域名是:http://www.taobao.com ...

  8. 里面的div怎么撑开外面的div,让高度自适应

    关于容器高度自适应的兼容性问题.1.有些时候,我们希望容器有一个固定高度,但当其中的内容多的时候,又希望高度能够自适应,也即容器在纵向能被撑开,且如果有背景,也能够自适应.在一般情况下,使用min-h ...

  9. iframe高度自适应内容

    JS自适应高度,其实就是设置iframe的高度,使其等于内嵌网页的高度,从而看不出来滚动条和嵌套痕迹.对于用户体验和网站美观起着重要作用. 如果内容是固定的,那么我们可以通过CSS来给它直接定义一个高 ...

随机推荐

  1. MySQL的逻辑查询语句的执行顺序

    一.select语句关键字的定义顺序 二.select语句关键字的执行顺序 三.准备表和数据 四.准备SQL逻辑查询测试语句 五.执行顺序分析 一.select语句关键字的定义顺序 SELECT DI ...

  2. python描述符学习

    目录 一.对象属性的访问控制 二.描述符基本理解 三.基本使用 四.使用描述符完成property.classmethod.staticmethod自定义实现 1.property的自定义实现 2.c ...

  3. 如何快速将文本中的tab更换成逗号(图文详解)

    不多说,直接上干货! 现有一份数据如下. 下载日志数据并分析 到搜狗实验室下载用户查询日志 1) 介绍 搜索引擎查询日志库设计为包括约1个月(2008年6月)Sogou搜索引擎部分网页查询需求及用户点 ...

  4. Tomcat *的安装和运行(绿色版和安装版都适用)

    不多说,直接上干货! 前提, Tomcat *的下载(绿色版和安装版都适用) 一.Tomcat的安装版 1.新建安装目录 2.放置安装版的tomcat 3.双击 4.点击 I agree 5.选择“F ...

  5. 多线程编程(二)-Exchanger的使用

    Exchanger的介绍 类Exchanger的功能可以使两个线程之间传输数据. 方法exchange()的使用 package com.wjg.unit; import java.util.conc ...

  6. cnblog博客停用

    本博客从今日起停止更新,后续的文章将会发布在新的博客mrbackkom.github.io

  7. HDU 1465 不容易系列之一(排错公式)

    大家常常感慨,要做好一件事情真的不容易,确实,失败比成功容易多了! 做好“一件”事情尚且不易,若想永远成功而总从不失败,那更是难上加难了,就像花钱总是比挣钱容易的道理一样.  话虽这样说,我还是要告诉 ...

  8. javascript 遍历

    数组的遍历你都会用了,那Promise版本的呢 这里指的遍历方法包括:map.reduce.reduceRight.forEach.filter.some.every因为最近要进行了一些数据汇总,no ...

  9. mysql存储之int

    开始之前给大家出个问题,数据库表test中两个字段  a int(2),b int(3),现在想执行下面的插入语句 ,) 思考是否可以插入? 答案是能插入 再看下面的语句 ,) 思考能不能插入?注意第 ...

  10. C# Winform 跨线程更新UI控件常用方法汇总

    https://www.cnblogs.com/marshal-m/p/3201051.html