施用Observer模式解决图片拖动与缩放

使用Observer模式解决图片拖动与缩放

1、java内置的观察者模式:

由Java 中的Observable 类和 Observer 接口组成

(1) Observable 类代表 被观察者 (java.util.Observable )

主要方法有:

void setChanged() : 设置被观察者的状态已经被改变
void addObserver(Observer observer) : 添加观察者
int countObservers() : 返回所有观察者的数目
void deleteObserver(Observer observer) :删除指定观察者
void deleteObservers() : 删除所有观察者
boolean hasChanged() : 被观察者的状态是否被改变,如果是则返回true,否则返回false
void notifyObservers() : 通知所有观察者

void notifyObservers(Object arg) : 通知所有观察者(参数一般设定为被改变的属性)
void clearChanged() :清除被观察者状态的改变

(2) Observer 接口代表 观察者 (java.util.Observer )

void update(Observable observable, Object arg) :当 被观察者 调用 notifyObservers() 方法
时,会根据被观察者的 hasChanged() 方法 来判断 它的状态是否被改变, 如果被观察者的状态被改变了,则
会调用 观察者 的 update 方法,参数 observable 为 被观察者对象, arg 为调用 notifyObservers( Object arg ) 时传入的参数 arg ,如果调用的是 notifyObservers() 方法, 则 arg 为 null。

值得注意的是:在Observable里,在notify()时必须先调用setChanged()方法,此方式表明状态更新...

贴上代码:

ImageZoomState类:

package hfut.gmm;

import java.util.Observable;

public class ImageZoomState extends Observable {
	private float mZoom = 1.0f;// 控制图片缩放的变量,表示缩放倍数,值越大图像越大
	private float mPanX = 0.5f;// 控制图片水平方向移动的变量,值越大图片可视区域的左边界距离图片左边界越远,图像越靠左,值为0.5f时居中
	private float mPanY = 0.5f;// 控制图片水平方向移动的变量,值越大图片可视区域的上边界距离图片上边界越远,图像越靠上,值为0.5f时居中

	public float getmZoom() {
		return mZoom;
	}

	public void setmZoom(float mZoom) {
		if (this.mZoom != mZoom) {
			this.mZoom = mZoom < 1.0f ? 1.0f : mZoom;// 保证图片最小为原始状态
			if (this.mZoom == 1.0f) {// 返回初始大小时,使其位置也恢复原始位置
				this.mPanX = 0.5f;
				this.mPanY = 0.5f;
			}
			this.setChanged();
		}
	}

	public float getmPanX() {
		return mPanX;
	}

	public void setmPanX(float mPanX) {
		if (mZoom == 1.0f) {// 使图为原始大小时不能移动
			return;
		}
		if (this.mPanX != mPanX) {
			this.mPanX = mPanX;
			this.setChanged();
		}
	}

	public float getmPanY() {
		return mPanY;
	}

	public void setmPanY(float mPanY) {
		if (mZoom == 1.0f) {// 使图为原始大小时不能移动
			return;
		}
		if (this.mPanY != mPanY) {
			this.mPanY = mPanY;
			this.setChanged();
		}
	}

	public float getZoomX(float aspectQuotient) {
		return Math.min(mZoom, mZoom * aspectQuotient);
	}

	public float getZoomY(float aspectQuotient) {
		return Math.min(mZoom, mZoom / aspectQuotient);
	}
}

SimpleImageZoomListener类:

package hfut.gmm;

import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class SimpleImageZoomListener implements View.OnTouchListener {
	private ImageZoomState mState;// 图片缩放和移动状态
	private final float SENSIBILITY = 0.8f;// 图片移动时的灵敏度

	/**
	 * 变化的起始点坐标
	 */
	private float sX;
	private float sY;
	/**
	 * 不变的起始点坐标,用于判断手指是否进行了移动,从而在UP事件中判断是否为点击事件
	 */
	private float sX01;
	private float sY01;
	/**
	 * 两触摸点间的最初距离
	 */
	private float sDistance;

	@Override
	public boolean onTouch(View v, MotionEvent event) {
		int action = event.getAction();
		int pointNum = event.getPointerCount();// 获取触摸点数
		if (pointNum == 1) {// 单点触摸,用来实现图像的移动和相应点击事件
			Log.d("Infor", "一个触点");
			float mX = event.getX();// 记录不断移动的触摸点x坐标
			float mY = event.getY();// 记录不断移动的触摸点y坐标
			switch (action) {
			case MotionEvent.ACTION_DOWN:
				// 记录起始点坐标
				sX01 = mX;
				sY01 = mY;
				sX = mX;
				sY = mY;
				return false;// 必须return false 否则不响应点击事件
			case MotionEvent.ACTION_MOVE:
				float dX = (mX - sX) / v.getWidth();
				float dY = (mY - sY) / v.getHeight();
				mState.setmPanX(mState.getmPanX() - dX * SENSIBILITY);
				mState.setmPanY(mState.getmPanY() - dY * SENSIBILITY);
				mState.notifyObservers();
				// 更新起始点坐标
				sX = mX;
				sY = mY;
				break;
			case MotionEvent.ACTION_UP:
				if (event.getX() == sX01 && event.getY() == sY01) {
					return false;// return false 执行点击事件
				}
				break;
			}
		}

		if (pointNum == 2) {// 多点触摸,用来实现图像的缩放
			// 记录不断移动的一个触摸点坐标
			Log.d("Infor", "二个触点");
			float mX0 = event.getX(event.getPointerId(0));
			float mY0 = event.getY(event.getPointerId(0));
			// 记录不断移动的令一个触摸点坐标
			float mX1 = event.getX(event.getPointerId(1));
			float mY1 = event.getY(event.getPointerId(1));

			float distance = this.getDistance(mX0, mY0, mX1, mY1);
			switch (action) {
			case MotionEvent.ACTION_POINTER_2_DOWN:
			case MotionEvent.ACTION_POINTER_1_DOWN:
				sDistance = distance;
				break;
			case MotionEvent.ACTION_POINTER_1_UP:
				// 注意:松开第一个触摸点后的手指滑动就变成了以第二个触摸点为起始点的移动,所以要以第二个触摸点坐标值为起始点坐标赋值
				sX = mX1;
				sY = mY1;
				break;
			case MotionEvent.ACTION_POINTER_2_UP:
				// 注意:松开第二个触摸点后的手指滑动就变成了以第二个触摸点为起始点的移动,所以要以第一个触摸点坐标值为起始点坐标赋值
				sX = mX0;
				sY = mY0;
				break;
			case MotionEvent.ACTION_MOVE:
				// float dDistance = (distance - sDistance) / sDistance;
				// mState.setmZoom(mState.getmZoom()
				// * (float) Math.pow(5, dDistance));
				mState.setmZoom(mState.getmZoom() * distance / sDistance);
				mState.notifyObservers();
				sDistance = distance;
				break;
			}
		}
		return true;// 必须返回true,具体请查询Android事件拦截机制的相关资料
	}

	/**
	 * //返回( mX0, mY0)与(( mX1, mY1)两点间的距离
	 * 
	 * @param mX0
	 * @param mX1
	 * @param mY0
	 * @param mY1
	 * @return
	 */
	private float getDistance(float mX0, float mY0, float mX1, float mY1) {
		double dX2 = Math.pow(mX0 - mX1, 2);// 两点横坐标差的平法
		double dY2 = Math.pow(mY0 - mY1, 2);// 两点纵坐标差的平法
		return (float) Math.pow(dX2 + dY2, 0.5);
	}

	public void setZoomState(ImageZoomState state) {
		mState = state;
	}
}

ImageZoomView类:

package hfut.gmm;

import java.util.Observable;
import java.util.Observer;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

public class ImageZoomView extends View implements Observer {
	private Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
	private Rect mRectSrc = new Rect();
	private Rect mRectDst = new Rect();
	private float mAspectQuotient;

	private Bitmap mBitmap;
	private ImageZoomState mZoomState;

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

	@Override
	public void update(Observable observable, Object data) {
		this.invalidate();
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		if (mBitmap != null && mZoomState != null) {
			int viewWidth = this.getWidth();
			int viewHeight = this.getHeight();
			int bitmapWidth = mBitmap.getWidth();
			int bitmapHeight = mBitmap.getHeight();

			float panX = mZoomState.getmPanX();
			float panY = mZoomState.getmPanY();
			float zoomX = mZoomState.getZoomX(mAspectQuotient) * viewWidth
					/ bitmapWidth;// 相当于viewHeight/bitmapHeight*mZoom
			float zoomY = mZoomState.getZoomY(mAspectQuotient) * viewHeight
					/ bitmapHeight;// 相当于viewWidth/bitmapWidth*mZoom

			// Setup source and destination rectangles
			// 这里假定图片的高和宽都大于显示区域的高和宽,如果不是在下面做调整
			mRectSrc.left = (int) (panX * bitmapWidth - viewWidth / (zoomX * 2));
			mRectSrc.top = (int) (panY * bitmapHeight - viewHeight
					/ (zoomY * 2));
			mRectSrc.right = (int) (mRectSrc.left + viewWidth / zoomX);
			mRectSrc.bottom = (int) (mRectSrc.top + viewHeight / zoomY);

			mRectDst.left = this.getLeft();
			mRectDst.top = this.getTop();
			mRectDst.right = this.getRight();
			mRectDst.bottom = this.getBottom();

			// Adjust source rectangle so that it fits within the source image.
			// 如果图片宽或高小于显示区域宽或高(组件大小)或者由于移动或缩放引起的下面条件成立则调整矩形区域边界
			if (mRectSrc.left < 0) {
				mRectDst.left += -mRectSrc.left * zoomX;
				mRectSrc.left = 0;
			}
			if (mRectSrc.right > bitmapWidth) {
				mRectDst.right -= (mRectSrc.right - bitmapWidth) * zoomX;
				mRectSrc.right = bitmapWidth;
			}

			if (mRectSrc.top < 0) {
				mRectDst.top += -mRectSrc.top * zoomY;
				mRectSrc.top = 0;
			}
			if (mRectSrc.bottom > bitmapHeight) {
				mRectDst.bottom -= (mRectSrc.bottom - bitmapHeight) * zoomY;
				mRectSrc.bottom = bitmapHeight;
			}

			// 把bitmap的一部分(就是src所包括的部分)绘制到显示区中dst指定的矩形处.关键就是dst,它确定了bitmap要画的大小跟位置
			// 注:两个矩形中的坐标位置是相对于各自本身的而不是相对于屏幕的。
			canvas.drawBitmap(mBitmap, mRectSrc, mRectDst, mPaint);
		}
	}

	@Override
	protected void onLayout(boolean changed, int left, int top, int right,
			int bottom) {
		// TODO Auto-generated method stub
		super.onLayout(changed, left, top, right, bottom);
		this.calculateAspectQuotient();
	}

	public void setImageZoomState(ImageZoomState zoomState) {
		if (mZoomState != null) {
			mZoomState.deleteObserver(this);
		}
		mZoomState = zoomState;
		mZoomState.addObserver(this);
		invalidate();
	}

	public void setImage(Bitmap bitmap) {
		mBitmap = bitmap;
		this.calculateAspectQuotient();
		invalidate();

	}

	private void calculateAspectQuotient() {
		if (mBitmap != null) {
			mAspectQuotient = (float) (((float) mBitmap.getWidth() / mBitmap
					.getHeight()) / ((float) this.getWidth() / this.getHeight()));
		}
	}
}

入口MainActivity类:

package hfut.gmm;

import android.app.Activity;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.View.OnClickListener;
import android.widget.ZoomControls;

public class MainActivity extends Activity {
    /** Called when the activity is first created. */
	private ZoomControls zoomCtrl;// 系统自带的缩放控制组件
	private ImageZoomView zoomView;// 自定义的图片显示组件
	private ImageZoomState zoomState;// 图片缩放和移动状态类
	private SimpleImageZoomListener zoomListener;// 缩放事件监听器
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.main);
        zoomState = new ImageZoomState();

		zoomListener = new SimpleImageZoomListener();
		zoomListener.setZoomState(zoomState);

		zoomCtrl = (ZoomControls) findViewById(R.id.zoomCtrl);
		this.setImageController();

	   zoomView = (ImageZoomView) findViewById(R.id.zoomView);
	 //	zoomView.setImage(bitmap);
	    zoomView.setImage(BitmapFactory.decodeResource(this.getResources(), R.drawable.index));
		zoomView.setImageZoomState(zoomState);
		zoomView.setOnTouchListener(zoomListener);
		zoomView.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				setFullScreen();
			}
		});

	}

	private void setImageController() {
		zoomCtrl.setOnZoomInClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				float z = zoomState.getmZoom() + 0.25f;
				zoomState.setmZoom(z);
				zoomState.notifyObservers();
			}
		});
		zoomCtrl.setOnZoomOutClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				float z = zoomState.getmZoom() - 0.25f;// 图像大小减少原来的0.25倍
				zoomState.setmZoom(z);
				zoomState.notifyObservers();
			}
		});
	}

	/**
	 * 隐藏处ImageZoomView外地其他组件,全屏显示
	 */
	private void setFullScreen() {
		if (zoomCtrl != null) {
			if (zoomCtrl.getVisibility() == View.VISIBLE) {
				// zoomCtrl.setVisibility(View.GONE);
				zoomCtrl.hide(); // 有过度效果

			} else if (zoomCtrl.getVisibility() == View.GONE) {
				// zoomCtrl.setVisibility(View.VISIBLE);
				zoomCtrl.show();// 有过渡效果

			}
		} 

}
}


XML文件:

<?xml version="1.0" encoding="UTF-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <hfut.gmm.ImageZoomView
        android:id="@+id/zoomView"
        android:layout_width="fill_parent"
        
        android:layout_height="fill_parent" >
    </hfut.gmm.ImageZoomView>

    <ZoomControls
        android:id="@+id/zoomCtrl"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
       
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true" >
    </ZoomControls>

</RelativeLayout>




结果截图如下:

施用Observer模式解决图片拖动与缩放

施用Observer模式解决图片拖动与缩放


参考英文文献:http://developer.sonymobile.com/wp/2010/05/18/android-one-finger-zoom-tutorial-part-1/

英语水平太菜、英文文献看的相当费劲啊....