参考 https://blog.csdn.net/hzw19920329/article/details/51383864
ListView优化
当 convertView 为空时,用 setTag()方法为每个 View 绑定一个存放控件的 ViewHolder 对象。当convertView 不为空,重复利用已经创建的 view 的时候,使用 getTag()方法获取绑定的 ViewHolder对象,这样就避免了 findViewById 对控件的层层查询,而是快速定位到控件。
- 复用convertView。在getItemView中,判断converView是否为空,如果不为空,可复用。 自定义静态类 ViewHolder,减少 findViewById 的次数。
- 使用分页加载,不要一次性加载所有数据。(快速滑动时,不显示图片。当快速滑动列表(SCROLL_STATE_FLING),item中的图片或获取需要消耗资源的view,可以不显示来;而处于其他两种状态(SCROLL_STATE_IDLE和SCROLL_STATE_TOUCH_SCROLL),则将那些view显示出来)
- 异步加载图片。Item中如果包含有webimage,那么最好异步加载。
- 使用 WeakRefrence 引用 ImageView 对象
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
System.out.println("before: "+list.get(position)+"------- "+convertView);
view = inflater.inflate(R.layout.item, null);
System.out.println("after: "+list.get(position)+"------- "+view);
TextView textView = (TextView) view.findViewById(R.id.textView);
textView.setText(list.get(position));
return view;
}
在调用getView方法的时候,他的第一个参数可能不会是null的,原因就是RecycleBin缓存帮我们暂存了那些划出屏幕的view,所以我们在convertView非空的情况下我们也没什么必要重新调用inflate方法加载布局了,因为这个方法毕竟也是要解析xml文件的,至少是要花时间的,直接使用从缓存中取出的view即可啦,先来看看ListView提供的RecycleBin缓存图解
从上面的图上可以看到当item 1被划出屏幕之后,会被放到Recycle缓存中,当item 8要划入屏幕的时候,如果他和item 1的类型相同的话,则直接从Recycle中获得即可,即此时的convertView不再是null;如果他和item 1类型不一致的话,则会新建view视图,即此时convertView等于null; 好的,那我们接下来就该充分使用android提供给我们的RecycleBin机制来优化ListView了; 只需要修改ListViewAdapter类的getView方法即可
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
System.out.println("before: "+list.get(position)+"------- "+convertView);
if(convertView == null)
{
view = inflater.inflate(R.layout.item, null);
}else
view = convertView;
TextView textView = (TextView) view.findViewById(R.id.textView);
textView.setText(list.get(position));
return view;
}
复用之前的view,这也就是说我们的RecycleBin缓存中将只有11个view了,不像前面那样要一个就新建一个,这在很大程度上节约了内存。
接下来我们看看getView方法,里面在获取TextView的时候,我们使用了findViewById方法,这个方法是与IO有关的操作,想必也会影响性能吧,他只要的目的是获得某一个view的布局罢了,我们如果有了view的话,其实只需要第一次将该view和其布局绑定到一起就可以了,没必要每次都为view设置布局了,这也就是使用setTag的目的了
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
ViewHolder viewHolder = null;
System.out.println("before: "+list.get(position)+"------- "+convertView);
if(convertView == null)
{
view = inflater.inflate(R.layout.item, null);
viewHolder = new ViewHolder();
viewHolder.textView = (TextView) view.findViewById(R.id.textView);
view.setTag(viewHolder);
}else
{
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.textView.setText("item "+list.get(position));
return view;
}
static class ViewHolder
{
TextView textView;
}
这里采用静态内部类的方式用于定义item 的各个控件 ,如果convertView非空的话,表示该view对应的布局已经存在了,只需要调用getTag获取到即可了,如果convertView为空的话,则需要通过findViewById来获取这个布局中的控件,并且最后将该布局通过setTag设置到view上面即可。
异步加载图片
- 先从内存缓存中获取图片显示(内存缓冲)
- 获取不到的话从 SD 卡里获取(SD 卡缓冲)
- 都获取不到的话从网络下载图片并保存到 SD 卡同时加入内存并显示(视情况看是否要显示)
优化一:先从内存中加载,没有则开启线程从 SD 卡或网络中获取,这里注意从 SD 卡获取图片是放在子线程里执行的,否则快速滑屏的话会不够流畅。
优化二:于此同时,在 adapter 里有个 busy 变量,表示 listview 是否处于滑动状态,如果是滑动状态则仅从内存中获取图片,没有的话再开启线程去外存或网络获取图片。
优化三:ImageLoader 里的线程使用了线程池,从而避免了过多线程频繁创建和销毁,如果每次总是 new 一个线程去执行这是非常不可取的,好一点的用的 AsyncTask 类,其实内部也是用到了线程池。在从网络获取图片时,先是将其保存到 sd 卡,然后再加载到内存,这么做的好处是在加载到内存时可以做个压缩处理,以减少图片所占内存。
ListView 如何实现分页加载
① 设置 ListView 的滚动监听器:setOnScrollListener(new OnScrollListener{….})在监听器中有两个方法:
- 滚动状态发生变化的方法(onScrollStateChanged)
- listView 被滚动时调用的方法(onScroll)
② 在滚动状态发生改变的方法中,有三种状态:
- 手指按下移动的状态: SCROLL_STATE_TOUCH_SCROLL: // 触摸滑动
- 惯性滚动(滑翔(flgin)状态): SCROLL_STATE_FLING: // 滑翔
- 静止状态: SCROLL_STATE_IDLE: // 静止
对不同的状态进行处理:
分批加载数据,只关心静止状态:关心最后一个可见的条目,如果最后一个可见条目就是数据适配器(集合)里的最后一个,此时可加载更多的数据。在每次加载的时候,计算出滚动的数量,当滚动的数量大于等于总数量的时候,可以提示用户无更多数据了。
我们实现ListView的OnScrollListener接口,监听它的滚动。在其中的一个方法onScroll,我们可以获得到当前第一个可见item的编号以及当前有多少个可见item和总共有多少个item。这样子我们就可以轻易计算得出是否滚动到最底部了。然后在onSrollStateChanged方法中做判断,如果滚动到最底部,就显示出正在加载数据的进度条,并完成数据的加载。怎么在MyListView中获得加载的数据?
这就要使用接口回调,我们在MyListView中设定一个回调接口,然后在MainAcivty中回调,就可以实现MyListView获得加载的数据。
package com.fuly.load;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ListView;
public class MyListView extends ListView implements OnScrollListener{
private int lastVisibleItem;//最后一个可见的item
private int totalItemCount;//总的item
private boolean isLoading = false;//是否正在加载数据
private ILoadListener mListener;//回调接口,用来加载数据
private View footer;//底布局
//注意,三个构造方法都要重写
public MyListView(Context context) {
super(context);
initView(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public MyListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView(context);
}
//定义一个回调接口,用来获得要加载的数据
public interface ILoadListener{
void loadData();
}
public void setOnILoadListener(ILoadListener listener){
this.mListener = listener;
}
//初始化view
private void initView(Context context){
footer = LayoutInflater.from(context).inflate(R.layout.footer, null);
//注意,这句代码的意思是给自定义的ListView加上底布局
this.addFooterView(footer);
//首先需要隐藏这个底部布局
footer.findViewById(R.id.load_layout).setVisibility(View.GONE);
this.setOnScrollListener(this);//千万别忘记设定监听器
}
//加载数据完成后,需要执行的操作
public void loadFinish(){
isLoading = false;//不再加载了
//底布局也要隐藏
footer.findViewById(R.id.load_layout).setVisibility(View.GONE);
}
//参数scrollState表示滑动的状态
public void onScrollStateChanged(AbsListView view, int scrollState) {
//如果最后一个可见item等于总的item,且当前滚动状态为滚动停止,就应该开始加加载数据了
if(lastVisibleItem == totalItemCount && scrollState==SCROLL_STATE_IDLE){
if(!isLoading){
isLoading = true;
//加载数据
mListener.loadData();
//设置底布局可见
footer.findViewById(R.id.load_layout).setVisibility(View.VISIBLE);
}
}
}
/***
* 该方法用来监听实时滚动中的item
* firstVisibleItem:当前第一个可见的item
* visibleItemCount:当前总共有多少个可见的item
* totalItemCount:总的item
*/
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
this.lastVisibleItem = firstVisibleItem + visibleItemCount;
this.totalItemCount = totalItemCount;
}
}
package com.fuly.load;
import java.util.ArrayList;
import java.util.List;
import com.fuly.load.MyListView.ILoadListener;
import android.os.Bundle;
import android.os.Handler;
import android.app.Activity;
public class MainActivity extends Activity implements ILoadListener{
private MyListView lv;
private List<MyData> mDatas = new ArrayList<MyData>();
private MyAdapter mAdapter;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();//该方法初始化数据
lv = (MyListView) findViewById(R.id.list_view);
lv.setOnILoadListener(this);
mAdapter = new MyAdapter(this, mDatas);
lv.setAdapter(mAdapter);
}
/**
* 该方法初始化数据,即提供初始的素材
*/
private void initData() {
for(int i = 0;i<12;i++){
MyData md = new MyData("你好,我是提前设定的");
mDatas.add(md);
}
}
/**
* 该方法提供模拟的加载数据
*/
private void getLoadData() {
for(int i = 0;i<3;i++){
MyData md = new MyData("你好,我是加载进来的");
mDatas.add(md);
}
}
//重写回调方法
public void loadData() {
//注意之所以使用Handlder,主要是想让下面的
//操作延迟5秒钟,以体现效果。实际开发中不需要
Handler mHandler = new Handler();
mHandler.postDelayed(new Runnable(){
public void run() {
//获得加载数据
getLoadData();
//然后通知MyListView刷新界面
mAdapter.notifyDataSetChanged();
//然后通知加载数据已经完成了
lv.loadFinish();
}
}, 5000);
}
}
ListView 显示多种类型的条目
- 我们可以通过getViewTypeCount()获得布局的种类
- 通过getItemViewType(int)获得当前位置上的布局到底是属于哪一类;
- 在 getView 方法中我们可以根据不同的 viewtype 加载不同的布局文件。
修改ListViewAdapter类如下:
public class ListViewAdapter extends BaseAdapter{
List<String> list = new ArrayList<String>();
LayoutInflater inflater = null;
public ListViewAdapter(List<String> list,Context context) {
this.list = list;
inflater = LayoutInflater.from(context);
}
@Override
public int getItemViewType(int position) {
//0表示显示的是TextView,1表示显示的是Button
if(position % 4 != 0)
return 0;
else
return 1;
}
@Override
public int getViewTypeCount() {
//表示有两种类型的item布局
return 2;
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
TextViewHolder textViewHolder = null;
ButtonViewHolder buttonViewHolder = null;
System.out.println("before: "+list.get(position)+"------- "+convertView);
int type = getItemViewType(position);
if(convertView == null)
{
switch (type) {
case 0:
view = inflater.inflate(R.layout.item, null);
textViewHolder = new TextViewHolder();
textViewHolder.textView = (TextView) view.findViewById(R.id.textView);
view.setTag(textViewHolder);
textViewHolder.textView.setText(list.get(position));
break;
case 1:
view = inflater.inflate(R.layout.button, null);
buttonViewHolder = new ButtonViewHolder();
buttonViewHolder.button = (Button) view.findViewById(R.id.button);
view.setTag(buttonViewHolder);
buttonViewHolder.button.setText("button "+list.get(position));
break;
default:
break;
}
}else
{
view = convertView;
switch (type) {
case 0:
textViewHolder = (TextViewHolder) view.getTag();
textViewHolder.textView.setText(list.get(position));
break;
case 1:
buttonViewHolder = (ButtonViewHolder) view.getTag();
buttonViewHolder.button.setText("button "+list.get(position));
break;
default:
break;
}
}
return view;
}
static class TextViewHolder
{
TextView textView;
}
static class ButtonViewHolder
{
Button button;
}
}
通过getViewTypeCount()返回的是你到底有多少种布局,我们这里有两种 通过getItemViewType(int)返回的是根据你的position得到的对应布局的ID,当然这个ID是可以由你来定的;
修改Activity类
public class MainActivity extends Activity {
List<String> list = new ArrayList<String>();
ListView listView = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.listview);
listView = (ListView) findViewById(R.id.listView);
for(int i = 1;i < 50;i++)
{
if(i % 4 == 0)
{
list.add("button "+i);
}else
list.add("item "+i);
}
ListViewAdapter adapter = new ListViewAdapter(list, this);
listView.setAdapter(adapter);
}
}
当 ListView 数据集改变后,如何更新 ListView
使用该 ListView 的 adapter 的 notifyDataSetChanged()方法。该方法会使 ListView 重新绘制