首页 > 代码库 > 利用LruCache加载网络图片实现图片瀑布流效果(改进版)

利用LruCache加载网络图片实现图片瀑布流效果(改进版)

MainActivity如下:
package cc.patience4;

import cc.patience4.R;
import android.os.Bundle;
import android.app.Activity;
/**
 * Demo描述:
 * 采用瀑布流的形式加载大量网络图片
 * 详细分析参见WaterfallScrollView
 * 
 * 更新说明:
 * 该示例在基础版的基础上加入了图片查看功能.
 * 点击瀑布流中一张图片后可欣赏图片并能对图片进行单指拖动和两指缩放.
 * 对于图片的操作主要用到了自定义的View,具体代码和注释请参见
 * ZoomImageView.
 * 
 * 
 * 参考资料:
 * 1 http://blog.csdn.net/guolin_blog/article/details/10470797
 * 2 http://blog.csdn.net/lfdfhl/article/details/18350601
 *   Thank you very much
 *   
 */
public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
	}

}


效果图如下:


WaterfallScrollView如下:

package cc.patience4;

import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import cc.patience4.R;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Toast;
/**
 * Demo功能:
 * 加载网络图片实现图片瀑布流效果(参见截图)
 * 
 * Demo流程:
 * 1 为了加载的众多图片可以在屏幕上滑动显示,所以需要一个ScrollView控件.
 *   于是自定义ScrollView
 * 2 将自定义ScrollView作为布局xml文件的根布局.
 *   在根布局下有一个LinearLayout它就是该自定义ScrollView的第一个子孩子.
 *   即代码中waterfallScrollView.getChildAt(0)
 *   将该LinearLayout均分成三个子LinearLayout,它们三的宽度平分屏幕的宽度.
 *   这样我们就可以往这三个LinearLayout中不断添加图片,形成瀑布流
 * 3 将网络图片添加到瀑布流的过程
 *   3.1 当手指在屏幕上停止滑动时(ACTION_UP)加载图片
 *   3.2 从网络中下载图片
 *   3.3 找到三个LinearLayout中当前高度最小的,将图片添加进去
 *   3.4 在添加图片后对ScrollView中所有ImageView进行检查.
 *       对于不在屏幕上显示的ImageView将其所加载的网络图片替换成本地一张小图片.
 * 4 为了加载速度和内存的有效使用,示例中采用了LruCache.
 * 
 * 
 * 错误总结:
 * 在使用ImageView.setTag(key, tag)看到第一个参数为int,于是为其指定一个final的int
 * 运行报错:
 * java.lang.IllegalArgumentException: The key must be an application-specific resource id.  
 * 原因是不可自己指定该值,而应该使用系统指定的int值.这么做大概是为了防止自己指定的值与系统某个值冲突吧.
 * 解决办法:在Strings.xml中指定值string值然后使用其在R文件中的int值即可,例如:
 * imageView.setTag(R.string.IMAGE_URL_TAG, imageUrl);其中:
 * R.string.IMAGE_URL_TAG就是字符串IMAGE_URL_TAG在R文件中的int值
 * 
 * 在此可见setTag方法的用途:为某个View保存数据.
 * 该方法还是挺有用的,可以把属于该View的某些属性保存到该View里面,而不用单独找个地方来存这些数据
 *
 */
public class WaterfallScrollView extends ScrollView implements OnTouchListener {
	// 每页加载的图片数量
	public final int PAGE_SIZE = 15;
	// 当前页码
	private int currentPage;
	// 每一列的宽度
	private int everyColumnWidth;
	// 第一列的高度
	private int firstColumnHeight;
	// 第一列的布局
	private LinearLayout mFirstLinearLayout;
	// 第二列的高度
	private int secondColumnHeight;
	// 第二列的布局
	private LinearLayout mSecondLinearLayout;
	// 第三列的高度
	private int thirdColumnHeight;
	// 第三列的布局
	private LinearLayout mThirdLinearLayout;
	// 是否已经进入该界面
	private boolean isFirstEnterThisScrollView = false;
	// LruCache
	private LruCacheImageLoader mLruCacheImageLoader;
	// 记录所有正在下载或等待下载的异步任务
	private HashSet<LoadImageAsyncTask> mLoadImageAsyncTaskHashSet;
	// 记录ScrollView中的所有ImageView
	private ArrayList<ImageView> mAllImageViewArrayList;
	// 该WaterfallScrollView控件的高度
	private int waterfallScrollViewHeight;
	// ScrollView顶端已经向上滑出屏幕长度
	private int scrollY=0;
	private int lastScrollY=-1;
	// 处理消息的Handle
	private Handler mHandler;
	// Context
	private Context mContext;
	private final int REFRESH=9527;
	public WaterfallScrollView(Context context) {
		super(context);
		init(context);
	}

	public WaterfallScrollView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context);
	}

	public WaterfallScrollView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init(context);
	}
	
	/**
	 * 判断scrollView是否滑动到底部的三个值:
	 * scrollY:ScrollView顶端已经滑出去的高度 
	 * waterfallScrollViewHeight:ScrollView的布局高度
	 * scrollView.getChildAt(0).getMeasuredHeight():ScrollView内容的高度.
	 * 常常有一部分内容要滑动后才可见,这部分的高度也包含在了这里面
	 */
	private void init(Context context){
		mContext=context;
		this.setOnTouchListener(this);
		mAllImageViewArrayList=new ArrayList<ImageView>();
		mLoadImageAsyncTaskHashSet=new HashSet<LoadImageAsyncTask>();
		mLruCacheImageLoader=LruCacheImageLoader.getLruCacheImageLoaderInstance();
		mHandler=new Handler(){
			@Override
			public void handleMessage(Message msg) {
				super.handleMessage(msg);
				if (msg.what==9527) {
					WaterfallScrollView waterfallScrollView=(WaterfallScrollView) msg.obj;
					scrollY=waterfallScrollView.getScrollY();
					// 如果当前的滚动位置和上次相同,表示已停止滚动
					if (lastScrollY==scrollY) {
						// 当滚动的最底部,并且当前没有正在下载的任务时,开始加载下一页的图片
						int scrollViewMeasuredHeight=waterfallScrollView.getChildAt(0).getMeasuredHeight();
						boolean isAsyncTashHashSetEmpty=mLoadImageAsyncTaskHashSet.isEmpty();
						if (waterfallScrollViewHeight+scrollY>=scrollViewMeasuredHeight&&isAsyncTashHashSetEmpty) {
							waterfallScrollView.loadNextPageImages();
						}
						//检查所有ImageView的可见性
						checkAllImageViewVisibility();
					} else {
						lastScrollY=scrollY;
						Message message=new Message();
						message.what=REFRESH;
						message.obj=WaterfallScrollView.this;
						// 5毫秒后再次对滚动位置进行判断
						mHandler.sendMessageDelayed(message, 5);
					}
				}
			}
		};
	}
	
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}
	
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		if (!isFirstEnterThisScrollView) {
			isFirstEnterThisScrollView=true;
			waterfallScrollViewHeight=getHeight();
			mFirstLinearLayout=(LinearLayout) findViewById(R.id.firstLinearLayout);
			mSecondLinearLayout=(LinearLayout) findViewById(R.id.secondLinearLayout);
			mThirdLinearLayout=(LinearLayout) findViewById(R.id.thirdLinearLayout);
			everyColumnWidth=mFirstLinearLayout.getWidth();
			loadNextPageImages();
		}
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
	}

	/**
	 * 这里对于手指抬起时(ACTION_UP)时,监听ScrollView是否已经停止滚动的判断的思路不错.
	 * 在ACTION_UP时直接用Handler发送一个消息在handleMessage中处理判断,如果此时还
	 * 没有停止滚动,则延时一定时间再次发送消息判断滚动是否停止.
	 * 这样做避免的在ACTION_UP时去加载图片而是在ScrollView停止滚动时去加载.
	 */
	@Override
	public boolean onTouch(View view, MotionEvent motionEvent) {
		if (motionEvent.getAction()==MotionEvent.ACTION_UP) {
			Message message=new Message();
			message.obj=this;
			message.what=REFRESH;
			mHandler.sendMessageDelayed(message, 5);
		}
		return false;
	}
	
	private void loadNextPageImages(){
		if (Utils.isExistSDCard()) {
			int start=PAGE_SIZE*currentPage;
			int end=PAGE_SIZE*currentPage+PAGE_SIZE;
			LoadImageAsyncTask loadImageAsyncTask;
			if (start<ImagesUrl.urlStringArray.length) {
				if (end>ImagesUrl.urlStringArray.length) {
					end=ImagesUrl.urlStringArray.length;
				}
				Toast.makeText(mContext, "开始加载", Toast.LENGTH_SHORT).show();
				for (int i = start;i < end; i++) {
					loadImageAsyncTask=new LoadImageAsyncTask();
					loadImageAsyncTask.execute(ImagesUrl.urlStringArray[i]);
					mLoadImageAsyncTaskHashSet.add(loadImageAsyncTask);
				}
				currentPage++;
			} else {
				
			}
			
		} else {
			Toast.makeText(mContext, "无SD卡", Toast.LENGTH_LONG).show();
		}
	}
	
	/**
	 * 判断ImageView是否可见
	 * 如果可见:
	 * 1  从LruCache取出图片显示
	 * 2 若不在LruCache中,则开启异步任务下载
	 * 若不可见:
	 * 将ImageView显示的图片替换成本地图片
	 */
	private void checkAllImageViewVisibility(){
		ImageView imageView=null;
		for(int i=0;i<mAllImageViewArrayList.size();i++){
			imageView=mAllImageViewArrayList.get(i);
			int top_border=(Integer) imageView.getTag(R.string.TOP_BORDER_TAG);
			int bottom_border=(Integer) imageView.getTag(R.string.BOTTOM_BORDER_TAG);
			if (bottom_border > getScrollY() && top_border < getScrollY() + waterfallScrollViewHeight) {
				String imageUrl=(String) imageView.getTag(R.string.IMAGE_URL_TAG);
				Bitmap bitmap=mLruCacheImageLoader.getBitmapFromLruCache(imageUrl);
				if (bitmap==null) {
					LoadImageAsyncTask loadImageAsyncTask=new LoadImageAsyncTask();
					loadImageAsyncTask.execute(imageUrl);
				} else {
					imageView.setImageBitmap(bitmap);
				}
				
			} else {
				imageView.setImageResource(R.drawable.empty_photo);
			}
		}
	}
	
	/**
	 * 该LoadImageAsyncTask是获取网络图片的入口:
	 * 1 从LruCache中获取,取到则停止
	 * 2 若不在LruCache,则从SD卡获取
	 * 3 若在则从SD卡获取
	 * 4 若不在SD卡,则从网络获取且保持至SD卡
	 * 5 从SD卡获取下载的图片
	 * 6 添加到LruCache中
	 * 
	 * 注意不管这个图片是在SD卡还是从网络下载,这都是获取图片的入口,这么做的好处
	 * 1   统一了获取图片的入口.
	 *   如果把获取图片分为图片在LruCache,图片在SD卡,图片在网络上这几种不同
	 *   的情况而去分别用对应的函数获取,这样势必会导致该需求的多入口.凌乱,不好优化.
	 *   而且这几种方式放到AsyncTask中都不会出错,尤其是网络请求耗时的情况下.
	 * 2 不管通过哪种方式获取到了图片,我们都要对图片再次修整,比如缩放.
	 *   我们可以把这些操作又统一放到异步操作的onPostExecute()方法中.
	 */
	private class LoadImageAsyncTask extends AsyncTask<String, Void, Bitmap>{
		private String imageUrl;
		private Bitmap bitmap;
		@Override
		protected Bitmap doInBackground(String... params) {
			imageUrl=params[0];
			bitmap=mLruCacheImageLoader.getBitmapFromLruCache(imageUrl);
			if (bitmap==null) {
				String filePath=Utils.getImageFilePath(imageUrl);
				File imageFile=new File(filePath);
				if (!imageFile.exists()) {
					Utils.getBitmapFromNetWorkAndSaveToSDCard(imageUrl, filePath);
				}
				if (filePath!=null) {
					bitmap=Utils.getBitmapFromSDCard(filePath, everyColumnWidth);
					if (bitmap!=null) {
						mLruCacheImageLoader.addBitmapToLruCache(imageUrl, bitmap);
					}
				}
			} else {

			}
			return bitmap;
		}
		
		/**
		 * 在onPostExecute()对图片进行修整
		 * 因为在doInBackground()的loadImage()方法中已经把经过scale的图片存到了SD卡和LruCache中
		 * 并且在计算inSampleSize的时候是以宽width为标准的.
		 * 比如inSampleSize=2,那么保存的图的宽和高都是原来的二分之一.
		 * 但是请注意inSampleSize是int类型的,那么缩放出来的比例多半不是我们期望的刚好屏幕宽度的三分之一,它是有偏差的.
		 * 所以在这里进行修正,尤其是对高进行修正.
		 * 这样就保证了宽是一个定值(屏幕的三分之一),高也得到了调整,不至于严重失真.
		 * 
		 */
		@Override
		protected void onPostExecute(Bitmap bitmap) {
			super.onPostExecute(bitmap);
			mLoadImageAsyncTaskHashSet.remove(this);
			if (bitmap!=null) {
				double ration=bitmap.getWidth()/(everyColumnWidth*1.0);
				int imageViewHeight=(int) (bitmap.getHeight()/ration);
				int imageViewWidth=everyColumnWidth;
				addImageToScrollView(bitmap,imageViewWidth,imageViewHeight,imageUrl);
			}
		}
	}
	
	/**
	 * 将获取到的Bitmap添加到ImageView中.
	 * 这里利用View.setTag()的方式为该ImageView保存了其相关信息.
	 * 比如该ImageView加载的图片的url,它的上下边在ScrollView中的位置信息等.
	 */
	private void addImageToScrollView(Bitmap bitmap,int imageViewWidth,int imageViewHeight,final String imageUrl){
		ImageView imageView=new ImageView(mContext);
		LinearLayout.LayoutParams layoutParams=new LinearLayout.LayoutParams(imageViewWidth, imageViewHeight);
		imageView.setImageBitmap(bitmap);
		imageView.setLayoutParams(layoutParams);
		imageView.setScaleType(ScaleType.FIT_XY);
		imageView.setPadding(5, 5, 5, 5);
		imageView.setTag(R.string.IMAGE_URL_TAG, imageUrl);
		imageView.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View view) {
				Intent intent=new Intent(mContext, ShowImageActivity.class);
				intent.putExtra("imageUrl", imageUrl);
				mContext.startActivity(intent);
			}
		});
		addImageToColumn(imageView);
		mAllImageViewArrayList.add(imageView);
	}
	
	
	/**
	 * 找到高度最小的LinearLayout并且将ImageView添加进去
	 */
	private void addImageToColumn(ImageView imageView){
		int imageViewHeight=imageView.getLayoutParams().height;
		if (firstColumnHeight <= secondColumnHeight) {
			if (firstColumnHeight <= thirdColumnHeight) {
				imageView.setTag(R.string.TOP_BORDER_TAG, firstColumnHeight);
				firstColumnHeight += imageViewHeight;
				imageView.setTag(R.string.BOTTOM_BORDER_TAG, firstColumnHeight);
				mFirstLinearLayout.addView(imageView);
			}else{
				imageView.setTag(R.string.TOP_BORDER_TAG, thirdColumnHeight);
				thirdColumnHeight += imageViewHeight;
				imageView.setTag(R.string.BOTTOM_BORDER_TAG, thirdColumnHeight);
				mThirdLinearLayout.addView(imageView);
			}
		} else {
			if (secondColumnHeight <= thirdColumnHeight) {
				imageView.setTag(R.string.TOP_BORDER_TAG, secondColumnHeight);
				secondColumnHeight += imageViewHeight;
				imageView.setTag(R.string.BOTTOM_BORDER_TAG, secondColumnHeight);
				mSecondLinearLayout.addView(imageView);
			}else{
				imageView.setTag(R.string.TOP_BORDER_TAG, thirdColumnHeight);
				thirdColumnHeight += imageViewHeight;
				imageView.setTag(R.string.BOTTOM_BORDER_TAG, thirdColumnHeight);
				mThirdLinearLayout.addView(imageView);
			}
		}
		
	}
}

ZoomImageView如下:
package cc.patience4;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/**
 * 流程说明:
 * 在该自定义View中主要是运用到了Matrix对图片进行缩放和位移
 * 1 初始时,将图片绘制到该控件上
 * 2 每当对图片拖动或者缩放时重新绘制图片
 * 
 * 核心方法:
 * canvas.drawBitmap(bitmap, matrix, paint)
 * 重点在于去操作一个Matrix.
 * 该处主要用到的是利用Matrix实现缩放(Scale)和位移(Translate)
 * 
 * 
 * mMatrix和mCurrentMatrix的说明
 * 这是以前写的Demo了,今天在重新整理看到这两个Matrix居然一下子
 * 没有反应过来.所以在此写下笔记,记录下来.
 * 在这个示例中一共涉及到两个Matrix,它们分别有什么用呢?
 * mMatrix.postScale()和mMatrix.postTranslate()起到实际作用的
 * 是mMatrix.但是请注意,这些postScale和postTranslate是基于以往
 * 的matrix的,就是说现在这个mMatrix执行的操作是在原来的矩阵matrix
 * 的基础上进行的.
 * 比如第一次是scale缩放操作,得到的矩阵是matrix1,这个时候停止操作
 * 图片已经改变了原来的样子
 * 然后接着进行第二次的操作,再进行translate位移操作,它就是在第一次
 * 的结果上继续上操作的;从代码上来看,现在的matrix要在上一次的matrix
 * 进行操作.
 * 所以我们需要一个变量来记录上次操作后的矩阵,即此处的mCurrentMatrix
 * 
 * 
 * 
 * 关于CURRENT_MODE == ZOOM_MODE时的说明:
 * 每次的缩放scale都是相对于两指头放在屏幕上的最初状态而言的.
 * 什么意思呢?解释如下:
 * if (CURRENT_MODE == ZOOM_MODE) {
 *    在这段代码中twoFingers_distance_before_move是不变的.
 *    但是twoFingers_distance_after_move在两指操作缩放的过程
 *    中是持续变大或者变小的.
 *    这样导致了计算出来的scale是持续变大或者边小的.
 *    比如在两指慢慢放大的过程中,从输出的Log可以发现这个scale在
 *    一直变大,哪怕是放大的动作很小此时的scale也是1.X,但是图片也只
 *    变大了一点点没有突然变很大.因为每次的缩放都是针对缩放前的状态
 *    而言的,而不是针对上一次缩放而言.举例吧:
 *    status1:两指放在屏幕上的状态
 *    然后两指持续在屏幕上慢慢的MOVE实现放大,每一次微小的放大都构成
 *    了一次新的状态
 *    status2:放大了一点
 *    status3:持续放大了一点
 *    status4:又持续放大了一点
 *    status5:再一次持续放大了一点
 *    .........................
 *    status5,status4的放大都是针对status1而言的,而不是针对它们的上一次
 *    status4或者status3而言.
 *    所以每次都要先复制原来的matrix再进行缩放,代码如下:
 *    * mMatrix.set(mCurrentMatrix);
 *    //依据缩放比例和中心点进行缩放
 *    mMatrix.postScale(scale, scale, mMiddlePointF.x,mMiddlePointF.y);
 *  }
 * 
 * 
 * 
 * 注意事项:
 * 在该Demo中对于ImageView的设置
 * android:layout_width="match_parent"
 * android:layout_height="match_parent"
 * 是不太合理的,在具体项目中应调整
 * 
 */
public class ZoomImageView extends View {
	//从SD卡获取的图片
	private Bitmap mRawBitmap;
	//该缩放控件自身的宽
	private int zoomImageViewWidth;
	//该缩放控件自身的高
	private int zoomImageViewHeight;
	//TAG
	private final String TAG="ZoomImageView";
	// 开始点
	private PointF mStartPoinF;
	// 图片位置的变换矩阵
	private Matrix mMatrix;
	// 图片当前矩阵
	private Matrix mCurrentMatrix;
	// 模式参数
	private int CURRENT_MODE = 0;
	// 初始模式
    private static final int INIT_MODE = 1;
	// 拖拉模式
	private static final int DRAG_MODE = 2;
	// 缩放模式
	private static final int ZOOM_MODE = 3;
	// 开启缩放的阈值
	private static final float ZOOM_THRESHOLD = 10.0f;
	// 缩放前两指间的距离
	private float twoFingers_distance_before_move;
	// 缩放后两指间的距离
	private float twoFingers_distance_after_move;
	// 两指间中心点
	private PointF mMiddlePointF;
    
	public ZoomImageView(Context context) {
		super(context);
	}

	public ZoomImageView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public ZoomImageView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
	}
	
	public void setBitmap(Bitmap bitmap){
		CURRENT_MODE=INIT_MODE;
		mRawBitmap=bitmap;
		mStartPoinF = new PointF();
		mMatrix = new Matrix();
		mCurrentMatrix = new Matrix();
		invalidate();
	}
	
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}
	
	@Override
	protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
		super.onLayout(changed, left, top, right, bottom);
		if (changed) {
			zoomImageViewWidth=getWidth();
			zoomImageViewHeight=getHeight();
		}
	}
	

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch (event.getAction() & MotionEvent.ACTION_MASK) {
		case MotionEvent.ACTION_DOWN:
			CURRENT_MODE = DRAG_MODE;
			// 记录图片当前matrix
			mCurrentMatrix.set(mMatrix);
			// 记录开始坐标point
			mStartPoinF.set(event.getX(), event.getY());
			break;

		// 当屏幕上已经有触点(手指),再有手指按下时触发该事件
		case MotionEvent.ACTION_POINTER_DOWN:
			CURRENT_MODE = ZOOM_MODE;
			twoFingers_distance_before_move = getTwoPointsDistance(event);
			if (twoFingers_distance_before_move > ZOOM_THRESHOLD) {
				// 计算两触点的中心点
				mMiddlePointF = getMiddlePoint(event);
			}
			break;

		case MotionEvent.ACTION_MOVE:
			//拖动模式下--->处理图片的拖动
			if (CURRENT_MODE == DRAG_MODE) {
				// 获取X轴移动距离
				float distanceX = event.getX() - mStartPoinF.x;
				// 获取Y轴移动距离
				float distanceY = event.getY() - mStartPoinF.y;
				// 在mCurrentMatrix的基础上平移图片,所以将mCurrentMatrix复制到mMatrix
				mMatrix.set(mCurrentMatrix);
				mMatrix.postTranslate(distanceX, distanceY);
				
			} 
			//缩放模式下--->处理图片的缩放
			if (CURRENT_MODE == ZOOM_MODE) {
				twoFingers_distance_after_move = getTwoPointsDistance(event);
				if (twoFingers_distance_after_move > ZOOM_THRESHOLD) {
					// 计算缩放比例
					float scale = twoFingers_distance_after_move / twoFingers_distance_before_move;
					// 在mCurrentMatrix的基础上缩放图片,所以将mCurrentMatrix复制到mMatrix
				    mMatrix.set(mCurrentMatrix);
					// 依据缩放比例和中心点进行缩放
					mMatrix.postScale(scale, scale, mMiddlePointF.x,mMiddlePointF.y);
				}
			}
			break;

		case MotionEvent.ACTION_UP:
		// 当手指离开屏幕,但屏幕上仍有其他触点(手指)时触发该事件
		case MotionEvent.ACTION_POINTER_UP:
			CURRENT_MODE = 0;
			break;
		}
		invalidate();
		return true;
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		switch (CURRENT_MODE) {
		case INIT_MODE:
			initZoomImageView(canvas);
			break;
		default:
			canvas.drawBitmap(mRawBitmap, mMatrix, null);
			break;
		}
	}
	
	
    /**
	* 将从SD卡获取的图片显示到该ZoomImageView控件
	* 1  图片的宽或高大于ZoomImageView控件本身的宽或者高则缩放.
	*   1.1 判断以宽为标准或者以高为标准进行压缩,如果:
	*      rawBitmapWidth-zoomImageViewWidth>rawBitmapHeight-zoomImageViewHeight
	*      则说明图片的宽超出控件的宽的程度要大于图片的高超出控件的高的程度.所以必须要满足对于宽的压缩,即以宽为
	*      压缩基准.反之同理,不再赘述.
	*   1.2 在以宽为基准压缩图片后,图片的宽即为ZoomImageView控件的宽,但是图片的高必然小于
	*      ZoomImageView控件的高.所以在Y方向位移,使得图片在控件中心位置绘制.
	*      反之同理,不再赘述
	* 2  图片的宽或高均不大于ZoomImageView控件本身的宽或者高.
	*   则在ZoomImageView控件中心位置绘制图片
	*/
	private void initZoomImageView(Canvas canvas){
		if (mRawBitmap!=null) {
			Matrix matrix=new Matrix();
			int rawBitmapWidth=mRawBitmap.getWidth();
			int rawBitmapHeight=mRawBitmap.getHeight();
			Log.i(TAG, "控件本身宽="+zoomImageViewWidth+",控件本身高="+zoomImageViewHeight);
			Log.i(TAG, "图片宽="+rawBitmapWidth+",图片高="+rawBitmapHeight);
			if (rawBitmapWidth>zoomImageViewWidth||rawBitmapHeight>zoomImageViewHeight) {
				Log.i(TAG, "rawBitmapWidth-zoomImageViewWidth="+(rawBitmapWidth-zoomImageViewWidth));
				Log.i(TAG, "rawBitmapHeight-zoomImageViewHeight="+(rawBitmapHeight-zoomImageViewHeight));
				//以宽为基准压缩
				if (rawBitmapWidth-zoomImageViewWidth>rawBitmapHeight-zoomImageViewHeight) {
					//1 压缩
					float scaleXY=zoomImageViewWidth/(rawBitmapWidth*1.0f);
					matrix.postScale(scaleXY, scaleXY);
					//2在Y方向上平移,使图片居中
					float translateY=(zoomImageViewHeight-rawBitmapHeight*scaleXY)/2.0f;
					matrix.postTranslate(0, translateY);
					Log.i(TAG, "以宽为基准压缩 scaleXY="+scaleXY+",translateY="+translateY);
				//以高为基准压缩
				} else {
					//1 压缩
					float scaleXY=zoomImageViewHeight/(rawBitmapHeight*1.0f);
					matrix.postScale(scaleXY, scaleXY);
					//2在X方向上平移,使图片居中
					float translateX=(zoomImageViewWidth-rawBitmapWidth*scaleXY)/2.0f;
					matrix.postTranslate(translateX, 0);
					Log.i(TAG, "以高为基准压缩 scaleXY="+scaleXY+",translateX="+translateX);
				}
			} else {
				float translateX=(zoomImageViewWidth-rawBitmapWidth)/2.0f;
				float translateY=(zoomImageViewHeight-rawBitmapHeight)/2.0f;
				matrix.postTranslate(translateX, translateY);
				Log.i(TAG, "不压缩,图片居中显示 translateX="+translateX+",translateY="+translateY);
			}
			canvas.drawBitmap(mRawBitmap, matrix, null);
			//将图片初始化完成后的matrix保存到mMatrix.
			//后续进行的操作都是在mMatrix上进行的
			mMatrix.set(matrix);
		}
	}
	
	// 计算两点之间的距离
	public static float getTwoPointsDistance(MotionEvent event) {
		float disX = event.getX(1) - event.getX(0);
		float disY = event.getY(1) - event.getY(0);
		return FloatMath.sqrt(disX * disX + disY * disY);
	}

	// 计算两点之间的中间点
	public static PointF getMiddlePoint(MotionEvent event) {
		float midX = (event.getX(0) + event.getX(1)) / 2;
		float midY = (event.getY(0) + event.getY(1)) / 2;
		return new PointF(midX, midY);
	}

}

LruCacheImageLoader如下:
package cc.patience4;

import android.graphics.Bitmap;
import android.util.LruCache;

public class LruCacheImageLoader {
	
	private static LruCacheImageLoader mLruCacheImageLoader;
	
	private static LruCache<String, Bitmap> mLruCache;
	
	private LruCacheImageLoader(){
		int maxMemory=(int) Runtime.getRuntime().maxMemory();
		int size=maxMemory/6;
		//设定LruCache的缓存为可用内存的六分之一
		mLruCache=new LruCache<String, Bitmap>(size){
			@Override
			protected int sizeOf(String key, Bitmap bitmap) {
				return bitmap.getByteCount();
			}
			
		};
	}
	
	public static LruCacheImageLoader getLruCacheImageLoaderInstance(){
		if (mLruCacheImageLoader==null) {
			mLruCacheImageLoader=new LruCacheImageLoader();
		}
		return mLruCacheImageLoader;
	}
	
	/**
	 * 从LruCache中获取图片,若不存在返回null
	 */
	public static Bitmap getBitmapFromLruCache(String key){
		return mLruCache.get(key);
	}
	
	/**
	 * 往LruCache中添加图片.
	 * 当然要首先判断LruCache中是否已经存在该图片,若不存在再添加
	 */
	public static void addBitmapToLruCache(String key,Bitmap bitmap){
		if (getBitmapFromLruCache(key)==null) {
			mLruCache.put(key, bitmap);
		}
	}

}

ShowImageActivity如下:
package cc.patience4;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.Window;

public class ShowImageActivity extends Activity {
	private Bitmap mBitmap;
	private ZoomImageView mZoomImageView;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		requestWindowFeature(Window.FEATURE_NO_TITLE);
		setContentView(R.layout.showimage);
	    init();
	}
	
	
	private void init(){
		String imageUrl=getIntent().getStringExtra("imageUrl");
		String filePath=Utils.getImageFilePath(imageUrl);
		mZoomImageView=(ZoomImageView) findViewById(R.id.zoomImageView);
		mBitmap=BitmapFactory.decodeFile(filePath);
		mZoomImageView.setBitmap(mBitmap);
	}
	
	
	/**
	 * 回收Bitmap避免内存溢出
	 */
	@Override
	protected void onDestroy() {
		super.onDestroy();
		if (mBitmap!=null) {
			mBitmap.recycle();
		}
	}

}

Utils如下:
package cc.patience4;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import org.apache.http.HttpStatus;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.os.Environment;

public class Utils {
	public final static String IMAGES_DIR_NAME="waterfallImages";
	public final static String IMAGES_DIR_PATH=Environment.getExternalStorageDirectory()+File.separator+IMAGES_DIR_NAME;

	/**
	 * 判断SD卡是否存在
	 */
	public static boolean isExistSDCard() {
		boolean isExist = false;
		if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
			isExist = true;
		}
		return isExist;
	}
	
	/**
	 * 从SD卡中获取图片
	 */
	public static Bitmap getBitmapFromSDCard(String filePath,int requestWidth){
		Bitmap bitmap=null;
		Options options=new Options();
		BitmapFactory.decodeFile(filePath, options);
		options.inJustDecodeBounds=true;
		options.inSampleSize=calculateInSampleSize(options,requestWidth);
		options.inJustDecodeBounds=false;
		bitmap=BitmapFactory.decodeFile(filePath, options);
		return bitmap;
	}
	
	public static Bitmap fixBitmap(){
		return null;
	}
	
	/**
	 * 计算图片的缩放比例
	 */
	public static int calculateInSampleSize(Options options,int requestWidth){
		int inSampleSize=1;
		//SD卡中图片的宽
		int outWidth=options.outWidth;
		if (outWidth>requestWidth) {
			inSampleSize=Math.round((float) outWidth / (float) requestWidth);
		 }
		return inSampleSize;
	}
	
	/**
	 * 依据图片的Url获取其在SDCard的存储路径
	 */
	public static String getImageFilePath(String imageUrl){
		File dir=new File(IMAGES_DIR_PATH);
		if (!dir.exists()) {
			dir.mkdirs();
		}
		String imageFilePath=null;
		String imageName=null;
		int start=imageUrl.lastIndexOf("/");
		int end=imageUrl.lastIndexOf(".");
		imageName=imageUrl.substring(start+1, end);
		imageFilePath=IMAGES_DIR_PATH+File.separator+imageName;
		return imageFilePath;
	}
	
	
	/**
	 * 从网络获取图片且保存至SD卡
	 */
	public static void getBitmapFromNetWorkAndSaveToSDCard(String imageUrl,String filePath){
		URL url=null;
		File imageFile=null;
		HttpURLConnection httpURLConnection=null;
		FileOutputStream fileOutputStream=null;
		BufferedOutputStream bufferedOutputStream=null;
		InputStream inputStream=null;
		BufferedInputStream bufferedInputStream=null;
		try {
			url=new URL(imageUrl);
			httpURLConnection=(HttpURLConnection) url.openConnection();
			httpURLConnection.setConnectTimeout(5*1000);
			httpURLConnection.setReadTimeout(10*1000);
			httpURLConnection.setDoInput(true);
			httpURLConnection.setDoOutput(true);
			if (httpURLConnection.getResponseCode()==HttpStatus.SC_OK) {
				imageFile=new File(filePath);
				if (!imageFile.getParentFile().exists()) {
					imageFile.getParentFile().mkdirs();
				}
				if (!imageFile.exists()) {
					imageFile.createNewFile();
				}
				fileOutputStream=new FileOutputStream(imageFile);
				bufferedOutputStream=new BufferedOutputStream(fileOutputStream);
				inputStream=httpURLConnection.getInputStream();
				bufferedInputStream=new BufferedInputStream(inputStream);
				int len=0;
				byte [] buffer=new byte[1024];
				while((len=bufferedInputStream.read(buffer))!=-1){
					bufferedOutputStream.write(buffer, 0, len);
					bufferedOutputStream.flush();
				}
			} else {
				System.out.println("图片请求失败");
			}
		} catch (Exception e) {
			    System.out.println("e="+e.toString());
		}finally{
			try {
				if (fileOutputStream!=null) {
					fileOutputStream.close();
				}
				if (bufferedOutputStream!=null) {
					bufferedOutputStream.close();
				}
				if (inputStream!=null) {
					inputStream.close();
				}
				if (bufferedInputStream!=null) {
					bufferedInputStream.close();
				}
				if (httpURLConnection!=null) {
					httpURLConnection.disconnect();
				}
			} catch (Exception e) {
				 System.out.println("e="+e.toString());
			}
		}
		
	}


}

ImagesUrl如下:
package cc.patience4;

public class ImagesUrl {
	
	public static String urlStringArray []=new String []{
		"http://img.my.csdn.net/uploads/201309/01/1378037235_3453.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037235_9280.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037234_3539.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037234_6318.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037193_1687.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037193_1286.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037192_8379.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037178_9374.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037177_1254.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037177_6203.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037152_6352.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037151_9565.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037151_7904.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037148_7104.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037129_8825.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037128_5291.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037128_3531.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037127_1085.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037095_7515.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037094_8001.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037093_7168.jpg",
		"http://img.my.csdn.net/uploads/201309/01/1378037091_4950.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949643_6410.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949642_6939.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949630_4505.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949630_4593.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949629_7309.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949629_8247.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949615_1986.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949614_8482.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949614_3743.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949614_4199.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949599_3416.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949599_5269.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949598_7858.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949598_9982.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949578_2770.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949578_8744.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949577_5210.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949577_1998.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949482_8813.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949481_6577.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949480_4490.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949455_6792.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949455_6345.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949442_4553.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949441_8987.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949441_5454.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949454_6367.jpg",
		"http://img.my.csdn.net/uploads/201308/31/1377949442_4562.jpg"
	};

}

main.xml如下:
<cc.patience4.WaterfallScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" >

        <LinearLayout
            android:id="@+id/firstLinearLayout"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical" />
        
         <LinearLayout
            android:id="@+id/secondLinearLayout"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical" />
         
          <LinearLayout
            android:id="@+id/thirdLinearLayout"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical" />
        
    </LinearLayout>

</cc.patience4.WaterfallScrollView>

showimage.xml如下:
<cc.patience4.ZoomImageView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/zoomImageView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000" >

   

</cc.patience4.ZoomImageView>