在Android开发中我们经常会用到listview的数据和界面刷新动作,我们每次可能会用到的都是Adapter.notifyDataSetChanged()方法。这个方法的原理是利用观察者模式对我们的数据源进行监听,当我们的数据源发生变化的时候,会调用Adapter的getView()方法进行整个界面的刷新。这样的话我们发现,getview()会调用多次,刷新了好多个不需要刷新的item,这样的话相对而言,降低了效率。但是,我们有的情况下是只需要对某个item的数据进行刷新就可以了。这样的话,当数据很多的时候,会提高效率。
有的人可能会说,没有必要去优化这个。怎么说呢,至少这样会让我们更深入的去了解listview的特性。


1.先看效果图

Android ListView优化之局部刷新(更新)(非notifyDataSetChanged)_万能适配器

2.先看一般的Adapter.notifyDataSetChanged()方法刷新界面



1.主界面的布局文件activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
tools:context="cn.bluemobi.dylan.listviewupdate.MainActivity">


<ListView
android:id="@+id/listview"
android:divider="#666666"
android:dividerHeight="1px"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>


2.listview中的item的布局文件item.xml

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

<TextView
android:text="万能适配器测试"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center"
android:textSize="18sp"
android:textColor="#000000"
android:id="@+id/textView" />
</LinearLayout>

3.activity中的代码
这里面用到了万能ViewHolder
同时也用到了万能适配器

package cn.bluemobi.dylan.listviewupdate;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

import cn.bluemobi.dylan.listviewupdate.adapter.CommonAdapter;
import cn.bluemobi.dylan.listviewupdate.adapter.CommonViewHolder;

public class MainActivity extends AppCompatActivity {

private ListView listView;
private List<String> datas;
private CommonAdapter commonAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
updateTest();
}

/**
* 一般的更新界面
*/
private void updateTest() {
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.listview);
datas = new ArrayList<>();
for (int i = 0; i < 10; i++) {
datas.add("万能适配器测试" + i);
}
final CommonAdapter commonAdapter = new CommonAdapter<String>(this, datas, R.layout.item) {

@Override
protected void convertView(View item, String s) {
TextView textView = CommonViewHolder.get(item, R.id.textView);
textView.setText(s);
}
};
listView.setAdapter(commonAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
datas.set(position, "update 万能适配器测试" + position);
commonAdapter.notifyDataSetChanged();

}
});
}
}


以上代码是较为常见的代码,我们在点击的时候将当前点击的item中的内容改变,我们会发现getView()方法会调用多次的情况:
Android ListView优化之局部刷新(更新)(非notifyDataSetChanged)_ide_02


3.ListView局部刷新方法一:更新对应view的内容


这种方法先通过listView.getChildAt(position)拿到要更新的对应的item布局文件,然后再通过findViewById找到对应的控件进行设置。


package cn.bluemobi.dylan.listviewupdate;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

import cn.bluemobi.dylan.listviewupdate.adapter.CommonAdapter;
import cn.bluemobi.dylan.listviewupdate.adapter.CommonViewHolder;

public class MainActivity extends AppCompatActivity {

private ListView listView;
private List<String> datas;
private CommonAdapter commonAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
updateOneTest();
}

/**
* 只是局部更新某个界面
*/
private void updateOneTest() {
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.listview);
datas = new ArrayList<>();
for (int i = 0; i < 20; i++) {
datas.add("万能适配器测试" + i);
}
commonAdapter = new CommonAdapter<String>(this, datas, R.layout.item) {

@Override
protected void convertView(View item, String s) {
TextView textView = CommonViewHolder.get(item, R.id.textView);
textView.setText(s);
}
};
listView.setAdapter(commonAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
datas.set(position, "update 万能适配器测试" + position);
updateSingle(position);

}
});
}

/**
* 第一种方法 更新对应view的内容
*
* @param position 要更新的位置
*/
private void updateSingle(int position) {
/**第一个可见的位置**/
int firstVisiblePosition = listView.getFirstVisiblePosition();
/**最后一个可见的位置**/
int lastVisiblePosition = listView.getLastVisiblePosition();

/**在看见范围内才更新,不可见的滑动后自动会调用getView方法更新**/
if (position >= firstVisiblePosition && position <= lastVisiblePosition) {
/**获取指定位置view对象**/
View view = listView.getChildAt(position - firstVisiblePosition);
TextView textView = (TextView) view.findViewById(R.id.textView);
textView.setText(datas.get(position));
}
}
}

4.ListView局部刷新方法二:通过ViewHolder去设置值

通过Item找出对应的ViewHolder,然后通过ViewHolder去设置值

/**
* 第二种方法 找出对应的ViewHolder,通过ViewHolder去设置值
*
* @param position 要更新的位置
*/
private void updateOne(int position) {
/**第一个可见的位置**/
int firstVisiblePosition = listView.getFirstVisiblePosition();
/**最后一个可见的位置**/
int lastVisiblePosition = listView.getLastVisiblePosition();

/**在看见范围内才更新,不可见的滑动后自动会调用getView方法更新**/
if (position >= firstVisiblePosition && position <= lastVisiblePosition) {
/**获取指定位置view对象**/
View view = listView.getChildAt(position - firstVisiblePosition);
/**通过ViewHolder找出缓存的对应控件**/
TextView textView = CommonViewHolder.get(view, R.id.textView);
textView.setText(datas.get(position));

}
}

5.ListView局部刷新方法三:调用一次getView()方法


这种方法是调用适配器对应的getView方法,用它里面的代码对界面进行刷新。这也是google在IO大会上推荐的做法


package cn.bluemobi.dylan.listviewupdate;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

import cn.bluemobi.dylan.listviewupdate.adapter.CommonAdapter;
import cn.bluemobi.dylan.listviewupdate.adapter.CommonViewHolder;

public class MainActivity extends AppCompatActivity {

private ListView listView;
private List<String> datas;
private CommonAdapter commonAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
updateOneTest();
}

/**
* 只是局部更新某个界面
*/
private void updateOneTest() {
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.listview);
datas = new ArrayList<>();
for (int i = 0; i < 20; i++) {
datas.add("万能适配器测试" + i);
}
commonAdapter = new CommonAdapter<String>(this, datas, R.layout.item) {

@Override
protected void convertView(View item, String s) {
TextView textView = CommonViewHolder.get(item, R.id.textView);
textView.setText(s);
}
};
listView.setAdapter(commonAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
datas.set(position, "update 万能适配器测试" + position);
updateItem(position);

}
});
}

/**
* 第三种方法 调用一次getView()方法;Google推荐的做法
*
* @param position 要更新的位置
*/
private void updateItem(int position) {
/**第一个可见的位置**/
int firstVisiblePosition = listView.getFirstVisiblePosition();
/**最后一个可见的位置**/
int lastVisiblePosition = listView.getLastVisiblePosition();

/**在看见范围内才更新,不可见的滑动后自动会调用getView方法更新**/
if (position >= firstVisiblePosition && position <= lastVisiblePosition) {
/**获取指定位置view对象**/
View view = listView.getChildAt(position - firstVisiblePosition);
commonAdapter.getView(position, view, listView);
}

}
}


我们来看下日志:在初始化加载完listview时调用了多次,在点击更新界面的时候只调用了一次。完美解决。
Android ListView优化之局部刷新(更新)(非notifyDataSetChanged)_ide_03


6.最后封装在万能适配器当中

package cn.bluemobi.dylan.listviewupdate.adapter;

import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;

import java.util.List;

/**
* Created by yuandl on 2016-10-13.
* 万能适配器
*/

public abstract class CommonAdapter<T> extends BaseAdapter {
private Context context;
private List<T> datas;
private int layoutId;

public CommonAdapter(Context context, List<T> datas, int layoutId) {
this.context = context;
this.datas = datas;
this.layoutId = layoutId;
}

@Override
public int getCount() {
return datas == null ? 0 : datas.size();
}

@Override
public T getItem(int position) {
return datas.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(layoutId, null);
}
Log.d("listview", "---------getView()-----------");
T t = getItem(position);
convertView(convertView, t);
return convertView;
}

/**
* 局部更新数据,调用一次getView()方法;Google推荐的做法
*
* @param listView 要更新的listview
* @param position 要更新的位置
*/
public void notifyDataSetChanged(ListView listView, int position) {
/**第一个可见的位置**/
int firstVisiblePosition = listView.getFirstVisiblePosition();
/**最后一个可见的位置**/
int lastVisiblePosition = listView.getLastVisiblePosition();

/**在看见范围内才更新,不可见的滑动后自动会调用getView方法更新**/
if (position >= firstVisiblePosition && position <= lastVisiblePosition) {
/**获取指定位置view对象**/
View view = listView.getChildAt(position - firstVisiblePosition);
getView(position, view, listView);
}

}

/**
* 需要去实现的对item中的view的设置操作
*
* @param item
* @param t
*/
protected abstract void convertView(View item, T t);

}


这样的话,我们每次更新的时候只需要调用notifyDataSetChanged(ListView listView, int position),传入对应的要更新的listview和要更新的位置position即可


7.总结


这三种方法的核心就是找出你要更新Item的contentView.然后再去操作。因为ListView默认只会加载一屏的数据,所以要判断其可见范围。不可见的在滑动的时候getView会自动调用更新数据。最后要强调的一点就是关于布局优化,最好将item的高度设置为一个固定的值,这样能减少getView的调用次数。因为一个不确定的值,ListView会频繁调用多次getView去确定其高度和渲染。