注:该文章为(男人应似海)原创,如需转载请注明出处!
组件功能
实现图片的手势缩放和移动
效果图
说明:效果图中下方得放大缩小组件是Android系统自带的组件ZoomControls,这里为了看上去美观和实现点击该组件放大图片,所以加上了该组件。点击图片时会隐藏该组件实现全屏浏览,具体实现请查看后面的代码。
实现方式
利用Java观察者模式实现(具体请查看java文章文章“java观察者模式介绍”),共包括三个类,它们的名称和功能分别是:
(1)ImageZoomView.java : 该类继承了View类,相当于观察者,随状态改变更新图片显示。
(2)ImageZoomState.java : 相当于被观察者,记录图片缩放和移动等状态。
(3)SimpleImageZoomListener.java: 这个类继承了View的OnTouchListener接口,相当于一个协调者,工作方式是:监听ImageZoomView上的手势动作------->根据手势改变ImageZoomState的值并调用notifyObservers()方法通知ImageZoomView(观察者)执行update()方法------->update()方法调用ImageZoomView的 invalidate()方法,该方法会自动调用onDraw()方法重绘图像更新图片显示。
代码
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()));
}
}
}
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);
}
}
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) {// 单点触摸,用来实现图像的移动和相应点击事件
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) {// 多点触摸,用来实现图像的缩放
// 记录不断移动的一个触摸点坐标
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,返回false的话对点击事件的逻辑处理有影响。具体请查询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;
}
}
public class ImageViewZoomActivity extends Activity {
private ZoomControls zoomCtrl;// 系统自带的缩放控制组件
private ImageZoomView zoomView;// 自定义的图片显示组件
private ImageZoomState zoomState;// 图片缩放和移动状态类
private SimpleImageZoomListener zoomListener;// 缩放事件监听器
private Bitmap bitmap;// 要显示的图片位图
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_zoom_layout);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.img);
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.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;// 图像大小增加原来的0.25倍
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 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" >
<test.ts.wader.imagezoom.view.ImageZoomView
android:id="@+id/zoomView"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
</test.ts.wader.imagezoom.view.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>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="test.ts.wader.imagezoom"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="7" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:label="@string/app_name"
android:name=".ImageViewZoomActivity"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" >
<intent-filter >
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
(1)ImageZoomView 中的onDraw()方法是通过
void android.graphics.Canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)这个方法实现图片缩放和移动的。所以关键点就在于scr和dst这两个矩形区域位置和大小的确定,即scr.left、scr.right、scr.top、scr.bottom和dst.left、dst.right、dst.top、dst.bottom这几个值的确定。
(2)这个组件是用java观察者模式实现的,可能大家自己写过或看过别人的实现没有用java观察者模式,而是把所有的功能写到一个类里面。之前我也这样写过,但是我们会发现这样的类扩展性太差了,每当改变一点需求时我们就要改变整体代码。而上面介绍的模式实现的话我们只需要按需求改变SimpleImageZoomListener.java中的touch处理事件就可以了。当然如果大家有更好的实现方式还请不吝告知。