http://files.cnblogs.com/files/liaolandemengxiang/PhotoWallFallsDemo.rar
http://files.cnblogs.com/files/liaolandemengxiang/ViewPager_imageview%E7%9A%84%E7%BC%A9%E6%94%BE%E4%BC%98%E5%8C%96%E5%90%8E.rar
第一個地址是中心縮放的demo,值得注意的是,這里面的位移是相對於(0,0)位置的偏移量,每次都是畫布重新畫圖片
第二個是優化了以后的viewpager 和zoomimageview的組合的demo,是通過imageview的回調函數還控制viewpager是否可以縮放。並對原來的bug進行修改。bug主要的問題是存在於沒有考慮高度大於屏幕高度的長圖片的縮放,已經中心位置的計算錯誤。還有就是對於最大縮放倍數的理解,初始化縮放比例是X,可是視為1,最大縮放倍數應該是X的倍數。
引自http://www.cnblogs.com/linjzong/p/4212474.html
在上一篇Android:手把手教你打造可縮放移動的ImageView最后提出了一個注意點:當自定義的MatrixImageView如ViewPager、ListView等帶有滑動效果的ViewGroup中時,ImageView自定義的拖動事件會和ViewGroup的滑動事件沖突,並且指出了沖突原因是由於ViewGroup攔截了Move事件的緣故。如果對於Touch事件的分發機制不甚了解的話,可以參考下這篇Android:30分鍾弄明白Touch事件分發機制。
這篇文章將會在MatrixImageView的基礎上,以ViewPager作為測試容器做進一步優化。
實現功能
- 當進行縮放操作時,手勢不會同時觸發ViewPager的滑動切換Item事件。
- 當進行拖動操作時,除非圖片已經到達左右邊界,否則不觸發ViewPager的滑動切換Item事件。
- 當進行拖動操作時,若圖片左邊緣到達左邊界,則可以向左滑動觸發ViewPager切換至前一個Item;當圖片右邊緣到達右邊界,則可以向右滑動觸發ViewPager的切換至后一個Item。
- 每個down-up(cancel)事件周期內只執行一種類型的事務操作(縮放、拖動或者ViewPager切換Item),防止多重事務互相干擾。
- 將事務處理封裝到MatrixImageView類內,提供狀態接口給ViewPager使用,方便適配多種ViewGroup。
實現原理
當ViewPager內嵌MatrixImageView時,由於MatrixImgaeView在Down事件中返回了true,表明ViewPager將捕獲本次完整的Touch事件(Move-Ponit_Down-UP等等),其中最重要的一個事件便是Move事件,因為ViewPager自身需要捕獲Move事件在onTouch中進行切換Item操作,MatrixImageView的捕獲意味着它將無法響應。不過,ViewPager本身控制着Touch事件的下發操作,每個Touch事件的下發都遵從從上至下層層遞歸,在MatrixImageView真正獲得Move事件前,Move事件必須經過ViewPager的onInterceptTouchEvent和dispatchTouchEvent事件,前者執行攔截操作后者執行下發操作。ViewPager便是在onInterceptTouchEvent中對Move事件進行了過濾,當移動距離超過一定值時,它會攔截掉Move事件,阻止MatrixImageView繼續處理Touch事件的權利,轉而讓自身的onTouch事件處理。於是,我們要做的便是重寫onInterceptTouchEvent事件,通過判斷MatrixImageView的狀態決定是否攔截。
具體實現
由於容器ViewPager在滿足條件的時候會攔截掉子View的touch事件,因此需要自定義個ViewPager修改攔截邏輯。當MatriImageView進行縮放和拖動時,我們不希望ViewPager攔截。具體代碼如下:
public class AlbumViewPager extends ViewPager implements OnChildMovingListener {
/** 當前子控件是否處理拖動狀態 */
private boolean mChildIsBeingDragged=false;
@Override
public boolean onInterceptTouchEvent(MotionEvent arg0) {
if(mChildIsBeingDragged)
return false;
return super.onInterceptTouchEvent(arg0);
}
@Override
public void startDrag() {
// TODO Auto-generated method stub
mChildIsBeingDragged=true;
}
@Override
public void stopDrag() {
// TODO Auto-generated method stub
mChildIsBeingDragged=false;
}
}
public interface OnChildMovingListener{
public void startDrag();
public void stopDrag();
}
通過判斷變量mChildIsBeingDragged的值決定是否攔截,而mChildIsBeingDragged的值通過OnChildMovingListener接口由MatriImageView進行設置。別忘了在PagerAdapter的instantiateItem中給MatriImageView設置監聽接口
MatrixImageView imageView = (MatrixImageView) imageLayout.findViewById(R.id.image);
imageView.setOnMovingListener(AlbumViewPager.this);
ViewPager的改造便完成了,只需要新增一個變量和實現一個接口,之后對於事件的攔截操作都轉到了MatrixImageView中。
接下去看下改造后的MatrixImageView的onTouch方法。
/** 和ViewPager交互相關,判斷當前是否可以左移、右移 */
boolean mLeftDragable;
boolean mRightDragable;
/** 是否第一次移動 */
boolean mFirstMove=false;
private PointF mStartPoint = new PointF();
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
//設置拖動模式
mMode=MODE_DRAG;
mStartPoint.set(event.getX(), event.getY());
isMatrixEnable();
startMove();
checkDragable();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
reSetMatrix();
stopMove();
break;
case MotionEvent.ACTION_MOVE:
if (mMode == MODE_ZOOM) {
setZoomMatrix(event);
}else if (mMode==MODE_DRAG) {
setDragMatrix(event);
}else {
stopMove();
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
if(mMode==MODE_UNABLE) return true;
mMode=MODE_ZOOM;
mStartDis = distance(event);
break;
case MotionEvent.ACTION_POINTER_UP:
break;
default:
break;
}
return mGestureDetector.onTouchEvent(event);
}
其中加紅部分的代碼都是修改后的代碼,逐一分析。
/**
* 子控件開始進入移動狀態,令ViewPager無法攔截對子控件的Touch事件
*/
private void startDrag(){
if(moveListener!=null) moveListener.startDrag();
}
/**
* 子控件開始停止移動狀態,ViewPager將攔截對子控件的Touch事件
*/
private void stopDrag(){
if(moveListener!=null) moveListener.stopDrag();
}
startDrag和stopDrag方法很簡單,就是調用ViewPager傳遞進來的OnChildMovingListener接口進行mChildIsBeingDragged的設置。當監聽到down事件時,表示開始拖動,當接收到up和cancel事件時,表示結束拖動,以這個邏輯來說,ViewGroup將永遠無法攔截touch事件,所以我們還需要在其他地方設置stopDrag事件,后面說明。
接下去是在down事件中執行checkDragable方法:
/**
* 根據當前圖片左右邊緣設置可拖拽狀態
*/
private void checkDragable() {
mLeftDragable=true;
mRightDragable=true;
mFirstMove=true;
float[] values=new float[9];
getImageMatrix().getValues(values);
//圖片左邊緣離開左邊界,表示不可右移
if(values[Matrix.MTRANS_X]>=0)
mRightDragable=false;
//圖片右邊緣離開右邊界,表示不可左移
if((mImageWidth)*values[Matrix.MSCALE_X]+values[Matrix.MTRANS_X]<=getWidth()){
mLeftDragable=false;
}
}
該方法將會重置mLeftDragable、mRightDragable、mFirstMove三個參數的狀態。mLeftDragable表示當前狀態下的Matrix可以向左拖動,mRightDragable表示當前狀態下的Matrix可以向右拖動,mFirstMove為每次完整touch事件(從down到up或cancel)中的第一次拖動操作標志。其中mLeftDragable和mRightDragable都是根據Matrix矩陣的數值計算出來的。
由於前面功能需求的時候說過"每個down-up(cancel)事件周期內只執行一種類型的事務操作(縮放、拖動或者ViewPager切換Item)",因此當進行縮放操作時,就不會再執行切換Item操作了,可以等縮放結束后執行up操作時stopDrag。而Move操作重點就是要識別是切換item還是拖動圖片了。查看修改后的setDragMatrix代碼
/**
* 設置拖拽狀態下的Matrix
* @param event
*/
public void setDragMatrix(MotionEvent event) {
if(isZoomChanged()){
float dx = event.getX() - mStartPoint.x; // 得到x軸的移動距離
float dy = event.getY() - mStartPoint.y; // 得到x軸的移動距離
//避免和雙擊沖突,大於10f才算是拖動
if(Math.sqrt(dx*dx+dy*dy)>10f){
mStartPoint.set(event.getX(), event.getY());
//在當前基礎上移動
mCurrentMatrix.set(getImageMatrix());
float[] values=new float[9];
mCurrentMatrix.getValues(values);
dy=checkDyBound(values,dy);
dx=checkDxBound(values,dx,dy);
mCurrentMatrix.postTranslate(dx, dy);
setImageMatrix(mCurrentMatrix);
}
}else {
stopDrag();
}
}
/**
* 和當前矩陣對比,檢驗dx,使圖像移動后不會超出ImageView邊界
* @param values
* @param dx
* @return
*/
private float checkDxBound(float[] values,float dx,float dy) {
float width=getWidth();
if(!mLeftDragable&&dx<0){
//加入和y軸的對比,表示在監聽到垂直方向的手勢時不切換Item
if(Math.abs(dx)*0.4f>Math.abs(dy)&&mFirstMove){
stopDrag();
}
return 0;
}
if(!mRightDragable&&dx>0){
//加入和y軸的對比,表示在監聽到垂直方向的手勢時不切換Item
if(Math.abs(dx)*0.4f>Math.abs(dy)&&mFirstMove){
stopDrag();
}
return 0;
}
mLeftDragable=true;
mRightDragable=true;
if(mFirstMove) mFirstMove=false;
if(mImageWidth*values[Matrix.MSCALE_X]<width){
return 0;
}
if(values[Matrix.MTRANS_X]+dx>0){
dx=-values[Matrix.MTRANS_X];
}
else if(values[Matrix.MTRANS_X]+dx<-(mImageWidth*values[Matrix.MSCALE_X]-width)){
dx=-(mImageWidth*values[Matrix.MSCALE_X]-width)-values[Matrix.MTRANS_X];
}
return dx;
}
處理邏輯是這樣的:
1.判斷當前縮放級別是否是原始縮放級別(isZoomChanged()),如果未縮放過那將可以直接切換Item,在這直接stopDrag。
2.若進行了縮放,那判斷是否累移動了10f,當移動了10f之后計算出x軸和y軸的移動量,並且通過checkDyBound方法計算出y軸的真實移動量
3.進入checkDxBound方法,首先判斷當前是否能夠左移,如果不能左移而實際的x軸偏移量是向左的,那就返回x的偏移量為0,防止左移。同時,如果當前是第一次移動,那就表示本次不是左移操作,而是向前切換item,於是執行stopDrag方法,令ViewPager攔截掉對MatrixImageView的事件分發。另外在這里加入和Y軸偏移量的對比,是為了防止執行的是垂直方向的滑動而導致stopDrag,ViewPager自身對於X軸偏移量/2小於Y軸偏移量的情況是不當成切換Item意圖的,這里設置為*0.4可以保證不沖突。
4.右移同理。
5.當第一次左移和右移判斷結果都不是切換Item后,將mLeftDragable和mRightDragable都設置為true,表示可以正常移動了。之后就和單個MatrixImageView的拖動處理一樣了。
到此便完成了內嵌到ViewGroup內的MatriImageView的改造。下面還有兩點顯示優化。
首先在reSetMatrix中加入了新的功能:當縮放后的圖片高度未達到ImageView高度時,在up和cancel之后會將其Y軸居中,防止“放大圖片-Y軸移動圖片-縮小圖片”導致圖片位置不對稱。異常圖效果如下:

/**
* 重置Matrix
*/
private void reSetMatrix() {
if(checkRest()){
mCurrentMatrix.set(mMatrix);
setImageMatrix(mCurrentMatrix);
}else {
//判斷Y軸是否需要更正
float[] values=new float[9];
getImageMatrix().getValues(values);
float height=mImageHeight*values[Matrix.MSCALE_Y];
if(height<getHeight()){
//在圖片真實高度小於容器高度時,Y軸居中,Y軸理想偏移量為兩者高度差/2,
float topMargin=(getHeight()-height)/2;
if(topMargin!=values[Matrix.MTRANS_Y]){
mCurrentMatrix.set(getImageMatrix());
mCurrentMatrix.postTranslate(0, topMargin-values[Matrix.MTRANS_Y]);
setImageMatrix(mCurrentMatrix);
}
}
}
}
優化了縮放操作的縮放x軸對稱軸選擇問題。在"圖片放大-移動X軸-縮小圖片"時,若直接以ImageView中心點為縮放原點,可能會導致縮放后的圖片邊緣離開ImageView邊界。
出錯圖效果如下:

/**
* 設置縮放Matrix
* @param event
*/
private void setZoomMatrix(MotionEvent event) {
//只有同時觸屏兩個點的時候才執行
if(event.getPointerCount()<2) return;
float endDis = distance(event);// 結束距離
if (endDis > 10f) { // 兩個手指並攏在一起的時候像素大於10
float scale = endDis / mStartDis;// 得到縮放倍數
mStartDis=endDis;//重置距離
mCurrentMatrix.set(getImageMatrix());//初始化Matrix
float[] values=new float[9];
mCurrentMatrix.getValues(values);
scale = checkMaxScale(scale, values);
PointF centerF=getCenter(scale,values);
mCurrentMatrix.postScale(scale, scale,centerF.x,centerF.y);
setImageMatrix(mCurrentMatrix);
}
}
/**
* 獲取縮放的中心點。
* @param scale
* @param values
* @return
*/
private PointF getCenter(float scale,float[] values) {
//縮放級別小於原始縮放級別時或者為放大狀態時,返回ImageView中心點作為縮放中心點
if(scale*values[Matrix.MSCALE_X]<mScale||scale>=1){
return new PointF(getWidth()/2,getHeight()/2);
}
float cx=getWidth()/2;
float cy=getHeight()/2;
//以ImageView中心點為縮放中心,判斷縮放后的圖片左邊緣是否會離開ImageView左邊緣,是的話以左邊緣為X軸中心
if((getWidth()/2-values[Matrix.MTRANS_X])*scale<getWidth()/2)
cx=0;
//判斷縮放后的右邊緣是否會離開ImageView右邊緣,是的話以右邊緣為X軸中心
if((mImageWidth*values[Matrix.MSCALE_X]+values[Matrix.MTRANS_X])*scale<getWidth())
cx=getWidth();
return new PointF(cx,cy);
}
通過判斷圖片寬度,決定是以ImageView中點為X軸縮放原點,還是以左右邊緣為縮放原點。
目前為止MatrixImageView的功能基本完善了,具體代碼還是放在我的Github上的照相機Demo。該View如果有問題的可以在這篇文章下留言或私信我。

