博客地址:http://blog.csdn.net/u010593680/article/details/43771857(轉載請保留原文地址)
項目地址:https://git.oschina.net/0-0Xuan/XWaterFall
分析問題:
在做項目中遇到了需要使用瀑布流的情況,於是便和往常一樣使用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的使用就介紹完畢了,源碼在博客頂部,如有任何問題,歡迎反饋!!