ListView

参考 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 重新绘制
 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值