分析问题:
在做项目中遇到了需要使用瀑布流的情况,于是便和往常一样使用ScroollView模式的瀑布流,但是瀑布流效果容易实现,可一旦加载大量图片,则一不了心就内存溢出了,而瀑布流往往需要添加大量的图片,内存管理可以说是必要之举,那么问题就来了,如何在瀑布流中进行内存管理呢?
解决方法一: 使用各种ImageLoader。将图片的Bitmap全都在ImageLoader中进行统计管理,如果图片使用内存超过限制了,则释放部分图片的Bitmap,但这里就有个显而易见的问题,即释放哪个图片的bitmap?这往往是最困难的一步,很多模式采取释放最近最少使用的Bitmap,但统计bitmap的使用本身就是一件比较难的事情,所以ImageLoader主要是用于快速高效的加载图片
解决方法二:使用自定义View,当View出现在手机屏幕上时,WindowsManager会调用View的onDraw方法来描绘View,这样即可实时的确定哪个View的图片需要使用,再加载对应的图片进内存,这样就可最大限度减少内存的使用,这个办法几乎能完美解决内存溢出的问题,但是,使用该方法需要在onDraw()方法调用时,再加载图片,那么加载图片的延迟将使用户体验大打折扣,当然也可以在代码中添加一些联系的代码,当某个View显示时,一并加载该View附近的View的图片,但如此一来各个View的联系就十分密切,代码编写难度会提高不少。
解决方法三:模拟ListView使得该瀑布流可以回收不显示的View。知道了哪些view没有被显示,那么只要回收这些View的图片的bitmap即可。
本博客主要介绍第三种解决方法!
实现目标:一、使waterfall能够发现正在屏幕中显示的View,并且能够设置滑动时,加载View的正向和反向需要缓存的View的个数,以提前加载View,使用户体验更好,避免ListView回收过快、以及直到显示才调用getView造成的加载太迟的问题。
实现目标二、使waterfall能实现类似于ListView的notifyDataSetChange()方法,使得调用notifyDataSetChange()后waterfall将检查原来的View是否有更新,如果有更新则进行相应的更新。这样就能方便的修改、增加、删除waterfall中的View了。
XWaterFall的使用:
第一步继承XWaterFall,继承后会显示7个必需实现的方法:
/**
* notifyDataSetChange时调用来获取子View,不建议在此进行大量的内存操作,如图片bitmap的加载,应将该操作放置在onResumeView(int position, View view)中
*
* @param position
* @return 返回需要添加的View
*/
@Override
public View getView(int position) {
// TODO Auto-generated method stub
return null;
}
/**
* 如果不能确定对应的View的高度,可返回0
*
* @param position
* @return
*/
@Override
public int getChildHeight(int position) {
// TODO Auto-generated method stub
return 0;
}
/**
* 返回position对应的View的标识,用以判断View是否需要更新
*
* @param position
* @return
*/
@Override
public Object getMark(int position) {
// TODO Auto-generated method stub
return null;
}
/**
*
* @return WaterFall中子View的数量
*/
@Override
public int getContentChildCount() {
// TODO Auto-generated method stub
return 0;
}
/**
*
* @return 一次notifyDataSetChanged()最多添加的View的个数
*/
@Override
public int getNewChildNum() {
// TODO Auto-generated method stub
return 0;
}
/**
* 有新超出显示屏和缓冲区的View时调用该方法
*
* @param position
* @param view
*/
@Override
protected void onRecycleView(int position, View view) {
// TODO Auto-generated method stub
}
/**
* 有新进入显示屏和缓冲区的View时调用该方法
*
* @param position
* @param view
*/
@Override
protected void onResumeView(int position, View view) {
// TODO Auto-generated method stub
}
了解了方法的实现要求,我们现在来实现一个显示美女和美女position的瀑布流,实现效果如下:
实现的代码是:
public class ImageWaterFall extends XWaterFall {
private String TAG = "ImageWaterFall";
private Context context;
private List<String> imgs;
private LayoutInflater inflater;
public ImageWaterFall(Context context, List<String> imgs) {
super(context,3,0);//设置waterfall的context、列数、宽度
// TODO Auto-generated constructor stub
this.context = context;
this.inflater = LayoutInflater.from(context);
this.imgs = imgs;
}
@Override
public View getView(int position) {
// TODO Auto-generated method stub
View view = inflater.inflate(R.layout.item_my_image, null);
MyImageView iv = (MyImageView) view.findViewById(R.id.iv);
iv.setName("美女"+position);
TextView tv = (TextView) view.findViewById(R.id.tvName);
tv.setText("美女: " + position);
return view;
}
@Override
public int getContentChildCount() {
// TODO Auto-generated method stub
return imgs.size();
}
@Override
public int getNewChildNum() {
// TODO Auto-generated method stub
return 12;
}
@Override
public void onScrollToBottom() {
// TODO Auto-generated method stub
notifyDataSetChanged();
}
@Override
public Object getMark(int position) {
// TODO Auto-generated method stub
return imgs.get(position);
}
@Override
protected void onRecycleView(int position, View view) {
// TODO Auto-generated method stub
MyImageView iv = (MyImageView) view.findViewById(R.id.iv);
TextView tv = (TextView)view.findViewById(R.id.tvName);
tv.setBackgroundColor(context.getResources().getColor(R.color.orange));
String url = imgs.get(position);
Bitmap bm = ImageLoader.getBitmap(url);
if (bm != null && !bm.isRecycled())
bm.recycle();
iv.setImageBitmap(null);
ImageLoader.deleteBitmap(url);
android.view.ViewGroup.LayoutParams params = iv.getLayoutParams();
params.height = iv.getHeight();
params.width = iv.getWidth();
iv.setLayoutParams(params);
iv.recycle();
}
@Override
protected void onResumeView(int position, View view) {
// TODO Auto-generated method stub
TextView tv = (TextView)view.findViewById(R.id.tvName);
tv.setBackgroundColor(context.getResources().getColor(R.color.green));
MyImageView iv = (MyImageView) view.findViewById(R.id.iv);
ImageLoader.loadImg(imgs.get(position), iv);
iv.resume();
}
@Override
public int getChildHeight(int position) {
// TODO Auto-generated method stub
return 0;
}
}
需要注意的是:
一、getChildHeight()中返回的是子View的高度,如果不能事先知道View的高度,则可以返回0或负数,那么会自动使用默认的选择最短列的算法
二、当发生View的更新和添加等操作,将会调用getView方法,但可能该View没在显示屏和缓冲区内,在这里耗费大量内存显然不合适,而当View第一次出现在显示屏和缓冲区中时,将会调用onResumeView()方法,所以onResumeView()更适合作为加载图片或其他非常耗费内存方法调用的地方
三、保证某个View的onResumeView()方法会在getView()之后,并且只有当View第一次出现在显示屏和缓冲区中时才会被调用
当某个View第一次离开显示屏和缓冲区中时,onRecycleView()会被调用
onResumeView()和onRecycleView()会被交替调用,并且保证先调用onResumeView(),才有可能调用onRecycleView()
在ImageWaterFall代码中也进行第三点的测试:当onResumeView()和onRecycleView()被调用时,会分别调用MyImageView的resume()和recycle()
public class MyImageView extends ImageView {
private String TAG = "MyImageView";
private String name;
private int state; //用来检验onResumeView和onRecycleView交替执行,且先onResumeView再有onRecycleView
public MyImageView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
this.setOnClickListener(onclick);//XWaterFall不会影响上层View的OnClickListener等操作
}
public void setName(String name) {
this.name = name;
}
@Override
public void setImageBitmap(Bitmap bm) {
// TODO Auto-generated method stub
// Log.v(name, "setImageBitmap");
super.setImageBitmap(bm);
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
// Log.v(name, "onDraw :" + drawNum++ );
try {
super.onDraw(canvas);
} catch (Exception e) {
e.printStackTrace();
Log.v(TAG, name);
}
}
public void recycle(){
Log.v(TAG, "XX回收" + name);
state--;
if(state!= 0){
Log.v(TAG, "XX错误: state:" + state);
}
}
public void resume(){
Log.v(TAG, "XX恢复" + name);
state++;
if(state != 1){
Log.v(TAG, "XX错误: state:" + state);
}
}
OnClickListener onclick = new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Log.v(TAG, "OnClickListener");
}
};
}
XWaterFall的实现弄好了,接下来就是具体的使用了:
第一步:创建:ImageWaterFall iwf = new ImageWaterFall(this, imgs);
第二步:添加到某容器里:llContent.addView(iwf);
第三步:更新ImageWaterfall中的内容:iwf.notifyDataSetChanged();
前面两步和普通的View的使用类似,第三步则是添加ImageWaterfall子View的操作
还可以设置正反方向上缓存的数量,设置较大的缓冲数量有助于减少用户等待查看的时间,改善用户体验
(手指向上滑,则准备显示的是下方的View,所以下方为正方向,上方为反方向,手指向下滑,则准备显示的是上方的View,所以上方为正方向,下方为反方向)
iwf.setPositionCacheNum(18);//设置加载View时正向的缓存的View的数量
iwf.setOppositeCacheNum(18);//设置加载View时反向的缓存的View的数量
接下来介绍如何更新、删除、插入View到ImageWaterFall中:
switch(view.getId()){
case R.id.btRefrash://更新position为1的View
imgs.set(1, "http://g.hiphotos.baidu.com/image/pic/item/1ad5ad6eddc451da9f2e8e8cb5fd5266d11632f8.jpg");
iwf.notifyDataSetChanged();
break;
case R.id.btDelete://删除position为1的View
imgs.remove(1);
iwf.notifyDataSetChanged();
break;
case R.id.btInseart://在position为1的View前插入新View
imgs.add(1, "http://h.hiphotos.baidu.com/image/pic/item/810a19d8bc3eb1350c58efbca41ea8d3fd1f441d.jpg");
iwf.notifyDataSetChanged();
break;
}
注意上面的imgs对应的是ImageWaterFall中的private List<String> imgs;这个用法和ListView中的用法非常相似
XWaterFall的使用就介绍完毕了,源码在博客顶部,如有任何问题,欢迎反馈!!