2012-09-18 70 views
12

是否有支持水平和垂直平移/拖拉的視圖的可能性?最重要的是,我希望能夠縮放和雙擊縮放。 此視圖是否存在於Android上,或者有人知道某個項目是誰?水平和垂直平移/拖動和捏放縮放的視圖

爲了使它更加困難,需要將其他視圖(Button,TextView,VideoView等)添加到視圖中。當第一個/父視圖放大或移動時,子視圖(按鈕)需要隨父移動。

我已經嘗試了多種解決方案,但他們都沒有我正在尋找的選項。

+0

嗨,你得到這個問題的最終解決方案?你能分享一下嗎? –

+0

如果你可以分享你的解決方案,這將是很好的... – pawpaw

回答

20

我認爲這是可能實現你想要什麼,但有,據我所知建設解決方案吧。從你問題的第二部分我猜你不想要一個可縮放的View,而是一個ViewGroup這是所有視圖的超類,可以包含其他視圖(例如佈局)。下面是一些代碼,你可以從建立自己的ViewGroup大部分來自this博客文章開始:

import android.content.Context; 
import android.graphics.Canvas; 
import android.graphics.Matrix; 
import android.graphics.Rect; 
import android.view.*; 

public class ZoomableViewGroup extends ViewGroup { 

    private static final int INVALID_POINTER_ID = 1; 
    private int mActivePointerId = INVALID_POINTER_ID; 

    private float mScaleFactor = 1; 
    private ScaleGestureDetector mScaleDetector; 
    private Matrix mScaleMatrix = new Matrix(); 
    private Matrix mScaleMatrixInverse = new Matrix(); 

    private float mPosX; 
    private float mPosY; 
    private Matrix mTranslateMatrix = new Matrix(); 
    private Matrix mTranslateMatrixInverse = new Matrix(); 

    private float mLastTouchX; 
    private float mLastTouchY; 

    private float mFocusY; 

    private float mFocusX; 

    private float[] mInvalidateWorkingArray = new float[6]; 
    private float[] mDispatchTouchEventWorkingArray = new float[2]; 
    private float[] mOnTouchEventWorkingArray = new float[2]; 


    public ZoomableViewGroup(Context context) { 
     super(context); 
     mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); 
     mTranslateMatrix.setTranslate(0, 0); 
     mScaleMatrix.setScale(1, 1); 
    } 

    @Override 
    protected void onLayout(boolean changed, int l, int t, int r, int b) { 
     int childCount = getChildCount(); 
     for (int i = 0; i < childCount; i++) { 
      View child = getChildAt(i); 
      if (child.getVisibility() != GONE) { 
       child.layout(l, t, l+child.getMeasuredWidth(), t + child.getMeasuredHeight()); 
      } 
     } 
    } 

    @Override 
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
     super.onMeasure(widthMeasureSpec, heightMeasureSpec); 

     int childCount = getChildCount(); 
     for (int i = 0; i < childCount; i++) { 
      View child = getChildAt(i); 
      if (child.getVisibility() != GONE) { 
       measureChild(child, widthMeasureSpec, heightMeasureSpec); 
      } 
     } 
    } 

    @Override 
    protected void dispatchDraw(Canvas canvas) { 
     canvas.save(); 
     canvas.translate(mPosX, mPosY); 
     canvas.scale(mScaleFactor, mScaleFactor, mFocusX, mFocusY); 
     super.dispatchDraw(canvas); 
     canvas.restore(); 
    } 

    @Override 
    public boolean dispatchTouchEvent(MotionEvent ev) { 
     mDispatchTouchEventWorkingArray[0] = ev.getX(); 
     mDispatchTouchEventWorkingArray[1] = ev.getY(); 
     mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray); 
     ev.setLocation(mDispatchTouchEventWorkingArray[0], 
       mDispatchTouchEventWorkingArray[1]); 
     return super.dispatchTouchEvent(ev); 
    } 

    /** 
    * Although the docs say that you shouldn't override this, I decided to do 
    * so because it offers me an easy way to change the invalidated area to my 
    * likening. 
    */ 
    @Override 
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) { 

     mInvalidateWorkingArray[0] = dirty.left; 
     mInvalidateWorkingArray[1] = dirty.top; 
     mInvalidateWorkingArray[2] = dirty.right; 
     mInvalidateWorkingArray[3] = dirty.bottom; 


     mInvalidateWorkingArray = scaledPointsToScreenPoints(mInvalidateWorkingArray); 
     dirty.set(Math.round(mInvalidateWorkingArray[0]), Math.round(mInvalidateWorkingArray[1]), 
       Math.round(mInvalidateWorkingArray[2]), Math.round(mInvalidateWorkingArray[3])); 

     location[0] *= mScaleFactor; 
     location[1] *= mScaleFactor; 
     return super.invalidateChildInParent(location, dirty); 
    } 

    private float[] scaledPointsToScreenPoints(float[] a) { 
     mScaleMatrix.mapPoints(a); 
     mTranslateMatrix.mapPoints(a); 
     return a; 
    } 

    private float[] screenPointsToScaledPoints(float[] a){ 
     mTranslateMatrixInverse.mapPoints(a); 
     mScaleMatrixInverse.mapPoints(a); 
     return a; 
    } 

    @Override 
    public boolean onTouchEvent(MotionEvent ev) { 
     mOnTouchEventWorkingArray[0] = ev.getX(); 
     mOnTouchEventWorkingArray[1] = ev.getY(); 

     mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray); 

     ev.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]); 
     mScaleDetector.onTouchEvent(ev); 

     final int action = ev.getAction(); 
     switch (action & MotionEvent.ACTION_MASK) { 
      case MotionEvent.ACTION_DOWN: { 
       final float x = ev.getX(); 
       final float y = ev.getY(); 

       mLastTouchX = x; 
       mLastTouchY = y; 

       // Save the ID of this pointer 
       mActivePointerId = ev.getPointerId(0); 
       break; 
      } 

      case MotionEvent.ACTION_MOVE: { 
       // Find the index of the active pointer and fetch its position 
       final int pointerIndex = ev.findPointerIndex(mActivePointerId); 
       final float x = ev.getX(pointerIndex); 
       final float y = ev.getY(pointerIndex); 

       final float dx = x - mLastTouchX; 
       final float dy = y - mLastTouchY; 

       mPosX += dx; 
       mPosY += dy; 
       mTranslateMatrix.preTranslate(dx, dy); 
       mTranslateMatrix.invert(mTranslateMatrixInverse); 

       mLastTouchX = x; 
       mLastTouchY = y; 

       invalidate(); 
       break; 
      } 

      case MotionEvent.ACTION_UP: { 
       mActivePointerId = INVALID_POINTER_ID; 
       break; 
      } 

      case MotionEvent.ACTION_CANCEL: { 
       mActivePointerId = INVALID_POINTER_ID; 
       break; 
      } 

      case MotionEvent.ACTION_POINTER_UP: { 
       // Extract the index of the pointer that left the touch sensor 
       final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 
       final int pointerId = ev.getPointerId(pointerIndex); 
       if (pointerId == mActivePointerId) { 
        // This was our active pointer going up. Choose a new 
        // active pointer and adjust accordingly. 
        final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 
        mLastTouchX = ev.getX(newPointerIndex); 
        mLastTouchY = ev.getY(newPointerIndex); 
        mActivePointerId = ev.getPointerId(newPointerIndex); 
       } 
       break; 
      } 
     } 
     return true; 
    } 

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { 

     @Override 
     public boolean onScale(ScaleGestureDetector detector) { 
      mScaleFactor *= detector.getScaleFactor(); 
      if (detector.isInProgress()) { 
       mFocusX = detector.getFocusX(); 
       mFocusY = detector.getFocusY(); 
      } 
      mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f)); 
      mScaleMatrix.setScale(mScaleFactor, mScaleFactor, 
        mFocusX, mFocusY); 
      mScaleMatrix.invert(mScaleMatrixInverse); 
      invalidate(); 
      requestLayout(); 


      return true; 
     } 
    } 
} 

這個類應該是能夠做到的,是圍繞拖動內容,並允許指縮放什麼,雙擊現在無法進行縮放,但在onTouchEvent方法中應該很容易實現。

如果您有任何疑問如何佈局的孩子的在你的ViewGroup中,我發現這個video非常有益的,或者如果你有任何問題,單一的方法是如何工作的或其他任何隨意問的意見。

+0

這裏有一些錯誤,包括你需要擴展ViewGroup,你錯過了一些'}'。 – Marche101

+0

@Artjom我想知道,爲什麼不在視圖組上使用setScaleX/Y,而不必從視頻剪輯,觸點等自己轉移? –

+0

當視圖達到1:1比例時是否可以停止縮小?實現起來很簡單嗎? – edoardotognoni

0

此自定義視圖是Android標準的ImageView的子類,並增加了它(多)觸摸平移和縮放(和雙抽頭變焦,以及):

https://github.com/sephiroth74/ImageViewZoom

http://blog.sephiroth.it/2011/04/04/imageview-zoom-and-scroll/

它類似於你已經知道的MikeOrtiz的TouchImageView,但增加了一些更多的功能。

您可以在視圖「堆棧」(Android FrameLayout或類似的東西)中使用它,以及其他需要的textView。 (我的意思是「一堆」視圖,就像一堆盤子或一堆牌一樣,換句話說,一堆視圖堆疊在Z軸上)

將所有視圖一起移動需要您可以控制Android手勢(多點觸控)機制並編寫所需的代碼。您的(非常複雜)需求沒有任何現成的解決方案。看看這篇文章:@Artjom答案固定,小錯誤

http://android-developers.blogspot.it/2010/06/making-sense-of-multitouch.html

13

轉播即牙套,進口,和擴展的ViewGroup。

import android.content.Context; 
import android.graphics.Canvas; 
import android.graphics.Matrix; 
import android.graphics.Rect; 
import android.view.*; 

public class ZoomableViewGroup extends ViewGroup { 

    private static final int INVALID_POINTER_ID = 1; 
    private int mActivePointerId = INVALID_POINTER_ID; 

    private float mScaleFactor = 1; 
    private ScaleGestureDetector mScaleDetector; 
    private Matrix mScaleMatrix = new Matrix(); 
    private Matrix mScaleMatrixInverse = new Matrix(); 

    private float mPosX; 
    private float mPosY; 
    private Matrix mTranslateMatrix = new Matrix(); 
    private Matrix mTranslateMatrixInverse = new Matrix(); 

    private float mLastTouchX; 
    private float mLastTouchY; 

    private float mFocusY; 

    private float mFocusX; 

    private float[] mInvalidateWorkingArray = new float[6]; 
    private float[] mDispatchTouchEventWorkingArray = new float[2]; 
    private float[] mOnTouchEventWorkingArray = new float[2]; 


    public ZoomableViewGroup(Context context) { 
     super(context); 
     mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); 
     mTranslateMatrix.setTranslate(0, 0); 
     mScaleMatrix.setScale(1, 1); 
    } 

    @Override 
    protected void onLayout(boolean changed, int l, int t, int r, int b) { 
     int childCount = getChildCount(); 
     for (int i = 0; i < childCount; i++) { 
      View child = getChildAt(i); 
      if (child.getVisibility() != GONE) { 
       child.layout(l, t, l+child.getMeasuredWidth(), t + child.getMeasuredHeight()); 
      } 
     } 
    } 

    @Override 
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
     super.onMeasure(widthMeasureSpec, heightMeasureSpec); 

     int childCount = getChildCount(); 
     for (int i = 0; i < childCount; i++) { 
      View child = getChildAt(i); 
      if (child.getVisibility() != GONE) { 
       measureChild(child, widthMeasureSpec, heightMeasureSpec); 
      } 
     } 
    } 

    @Override 
    protected void dispatchDraw(Canvas canvas) { 
     canvas.save(); 
     canvas.translate(mPosX, mPosY); 
     canvas.scale(mScaleFactor, mScaleFactor, mFocusX, mFocusY); 
     super.dispatchDraw(canvas); 
     canvas.restore(); 
    } 

    @Override 
    public boolean dispatchTouchEvent(MotionEvent ev) { 
     mDispatchTouchEventWorkingArray[0] = ev.getX(); 
     mDispatchTouchEventWorkingArray[1] = ev.getY(); 
     mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray); 
     ev.setLocation(mDispatchTouchEventWorkingArray[0], 
       mDispatchTouchEventWorkingArray[1]); 
     return super.dispatchTouchEvent(ev); 
    } 

    /** 
    * Although the docs say that you shouldn't override this, I decided to do 
    * so because it offers me an easy way to change the invalidated area to my 
    * likening. 
    */ 
    @Override 
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) { 

     mInvalidateWorkingArray[0] = dirty.left; 
     mInvalidateWorkingArray[1] = dirty.top; 
     mInvalidateWorkingArray[2] = dirty.right; 
     mInvalidateWorkingArray[3] = dirty.bottom; 


     mInvalidateWorkingArray = scaledPointsToScreenPoints(mInvalidateWorkingArray); 
     dirty.set(Math.round(mInvalidateWorkingArray[0]), Math.round(mInvalidateWorkingArray[1]), 
       Math.round(mInvalidateWorkingArray[2]), Math.round(mInvalidateWorkingArray[3])); 

     location[0] *= mScaleFactor; 
     location[1] *= mScaleFactor; 
     return super.invalidateChildInParent(location, dirty); 
    } 

    private float[] scaledPointsToScreenPoints(float[] a) { 
     mScaleMatrix.mapPoints(a); 
     mTranslateMatrix.mapPoints(a); 
     return a; 
    } 

    private float[] screenPointsToScaledPoints(float[] a){ 
     mTranslateMatrixInverse.mapPoints(a); 
     mScaleMatrixInverse.mapPoints(a); 
     return a; 
    } 

    @Override 
    public boolean onTouchEvent(MotionEvent ev) { 
     mOnTouchEventWorkingArray[0] = ev.getX(); 
     mOnTouchEventWorkingArray[1] = ev.getY(); 

     mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray); 

     ev.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]); 
     mScaleDetector.onTouchEvent(ev); 

     final int action = ev.getAction(); 
     switch (action & MotionEvent.ACTION_MASK) { 
      case MotionEvent.ACTION_DOWN: { 
       final float x = ev.getX(); 
       final float y = ev.getY(); 

       mLastTouchX = x; 
       mLastTouchY = y; 

       // Save the ID of this pointer 
       mActivePointerId = ev.getPointerId(0); 
       break; 
      } 

      case MotionEvent.ACTION_MOVE: { 
       // Find the index of the active pointer and fetch its position 
       final int pointerIndex = ev.findPointerIndex(mActivePointerId); 
       final float x = ev.getX(pointerIndex); 
       final float y = ev.getY(pointerIndex); 

       final float dx = x - mLastTouchX; 
       final float dy = y - mLastTouchY; 

       mPosX += dx; 
       mPosY += dy; 
       mTranslateMatrix.preTranslate(dx, dy); 
       mTranslateMatrix.invert(mTranslateMatrixInverse); 

       mLastTouchX = x; 
       mLastTouchY = y; 

       invalidate(); 
       break; 
      } 

      case MotionEvent.ACTION_UP: { 
       mActivePointerId = INVALID_POINTER_ID; 
       break; 
      } 

      case MotionEvent.ACTION_CANCEL: { 
       mActivePointerId = INVALID_POINTER_ID; 
       break; 
      } 

      case MotionEvent.ACTION_POINTER_UP: { 
       // Extract the index of the pointer that left the touch sensor 
       final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 
       final int pointerId = ev.getPointerId(pointerIndex); 
       if (pointerId == mActivePointerId) { 
        // This was our active pointer going up. Choose a new 
        // active pointer and adjust accordingly. 
        final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 
        mLastTouchX = ev.getX(newPointerIndex); 
        mLastTouchY = ev.getY(newPointerIndex); 
        mActivePointerId = ev.getPointerId(newPointerIndex); 
       } 
       break; 
      } 
     } 
     return true; 
    } 

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { 

     @Override 
     public boolean onScale(ScaleGestureDetector detector) { 
      mScaleFactor *= detector.getScaleFactor(); 
      if (detector.isInProgress()) { 
       mFocusX = detector.getFocusX(); 
       mFocusY = detector.getFocusY(); 
      } 
      mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f)); 
      mScaleMatrix.setScale(mScaleFactor, mScaleFactor, 
        mFocusX, mFocusY); 
      mScaleMatrix.invert(mScaleMatrixInverse); 
      invalidate(); 
      requestLayout(); 


      return true; 
     } 
    } 
} 
+0

完美的解決方案。禮炮。 –

+0

我在ZoomableViewGroup中添加一個子視圖,在縮放後,孩子的觸摸座標出錯。我該如何解決它? –

+1

爲面向像我的問題誰:改變方法
'私人浮子[] screenPointsToScaledPoints的順序(浮動[]一個)'到:
'私人浮子[] screenPointsToScaledPoints(浮動[]一個){ mScaleMatrixInverse。 mapPoints(一); mTranslateMatrixInverse.mapPoints(a); return a; }' –

8

基於給出的答案,我使用此代碼來獲得平移和縮放功能的工作。首先遇到支點問題。

public class ZoomableViewGroup extends ViewGroup { 

    // these matrices will be used to move and zoom image 
    private Matrix matrix = new Matrix(); 
    private Matrix matrixInverse = new Matrix(); 
    private Matrix savedMatrix = new Matrix(); 
    // we can be in one of these 3 states 
    private static final int NONE = 0; 
    private static final int DRAG = 1; 
    private static final int ZOOM = 2; 
    private int mode = NONE; 
    // remember some things for zooming 
    private PointF start = new PointF(); 
    private PointF mid = new PointF(); 
    private float oldDist = 1f; 
    private float[] lastEvent = null; 

    private boolean initZoomApplied=false; 

    private float[] mDispatchTouchEventWorkingArray = new float[2]; 
    private float[] mOnTouchEventWorkingArray = new float[2]; 

    @Override 
    public boolean dispatchTouchEvent(MotionEvent ev) { 
     mDispatchTouchEventWorkingArray[0] = ev.getX(); 
     mDispatchTouchEventWorkingArray[1] = ev.getY(); 
     mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray); 
     ev.setLocation(mDispatchTouchEventWorkingArray[0], 
       mDispatchTouchEventWorkingArray[1]); 
     return super.dispatchTouchEvent(ev); 
    } 

    private float[] scaledPointsToScreenPoints(float[] a) { 
     matrix.mapPoints(a); 
     return a; 
    } 

    private float[] screenPointsToScaledPoints(float[] a){ 
     matrixInverse.mapPoints(a); 
     return a; 
    } 

    public ZoomableViewGroup(Context context) { 
     super(context); 
     init(context); 
    } 

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

    public ZoomableViewGroup(Context context, AttributeSet attrs, 
      int defStyleAttr) { 
     super(context, attrs, defStyleAttr); 
     init(context); 
    } 

    /** 
    * Determine the space between the first two fingers 
    */ 
    private float spacing(MotionEvent event) { 
     float x = event.getX(0) - event.getX(1); 
     float y = event.getY(0) - event.getY(1); 
     return (float)Math.sqrt(x * x + y * y); 
    } 

    /** 
    * Calculate the mid point of the first two fingers 
    */ 
    private void midPoint(PointF point, MotionEvent event) { 
     float x = event.getX(0) + event.getX(1); 
     float y = event.getY(0) + event.getY(1); 
     point.set(x/2, y/2); 
    } 


    private void init(Context context){ 

    } 


    @Override 
    protected void onLayout(boolean changed, int l, int t, int r, int b) { 
     int childCount = getChildCount(); 
     for (int i = 0; i < childCount; i++) { 
      View child = getChildAt(i); 
      if (child.getVisibility() != GONE) { 
       child.layout(l, t, l+child.getMeasuredWidth(), t + child.getMeasuredHeight()); 
      } 
     } 
    } 

    @Override 
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
     super.onMeasure(widthMeasureSpec, heightMeasureSpec); 

     int widthSize = MeasureSpec.getSize(widthMeasureSpec); 
     int heightSize = MeasureSpec.getSize(heightMeasureSpec); 

     float[] values = new float[9]; 
     matrix.getValues(values); 
     float container_width = values[Matrix.MSCALE_X]*widthSize; 
     float container_height = values[Matrix.MSCALE_Y]*heightSize; 

     //Log.d("zoomToFit", "m width: "+container_width+" m height: "+container_height); 
     //Log.d("zoomToFit", "m x: "+pan_x+" m y: "+pan_y); 

     int childCount = getChildCount(); 
     for (int i = 0; i < childCount; i++) { 
      View child = getChildAt(i); 
      if (child.getVisibility() != GONE) { 
       measureChild(child, widthMeasureSpec, heightMeasureSpec); 

       if(i==0 && !initZoomApplied && child.getWidth()>0){ 
        int c_w = child.getWidth(); 
        int c_h = child.getHeight(); 

        //zoomToFit(c_w, c_h, container_width, container_height); 
       } 
      } 
     }   

    } 

    private void zoomToFit(int c_w, int c_h, float container_width, float container_height){ 
     float proportion_firstChild = (float)c_w/(float)c_h; 
     float proportion_container = container_width/container_height; 

     //Log.d("zoomToFit", "firstChildW: "+c_w+" firstChildH: "+c_h); 
     //Log.d("zoomToFit", "proportion-container: "+proportion_container); 
     //Log.d("zoomToFit", "proportion_firstChild: "+proportion_firstChild); 

     if(proportion_container<proportion_firstChild){ 
      float initZoom = container_height/c_h; 
      //Log.d("zoomToFit", "adjust height with initZoom: "+initZoom); 
      matrix.postScale(initZoom, initZoom); 
      matrix.postTranslate(-1*(c_w*initZoom-container_width)/2, 0); 
      matrix.invert(matrixInverse); 
     }else { 
      float initZoom = container_width/c_w; 
      //Log.d("zoomToFit", "adjust width with initZoom: "+initZoom); 
      matrix.postScale(initZoom, initZoom); 
      matrix.postTranslate(0, -1*(c_h*initZoom-container_height)/2); 
      matrix.invert(matrixInverse); 
     } 
     initZoomApplied=true; 
     invalidate(); 
    } 

    @Override 
    protected void dispatchDraw(Canvas canvas) { 
     canvas.save(); 
     canvas.setMatrix(matrix); 
     super.dispatchDraw(canvas); 
     canvas.restore(); 
    } 

    @Override 
    public boolean onTouchEvent(MotionEvent event) { 
     // handle touch events here 
     mOnTouchEventWorkingArray[0] = event.getX(); 
     mOnTouchEventWorkingArray[1] = event.getY(); 

     mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray); 

     event.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]); 

     switch (event.getAction() & MotionEvent.ACTION_MASK) { 
      case MotionEvent.ACTION_DOWN: 
       savedMatrix.set(matrix); 
       start.set(event.getX(), event.getY()); 
       mode = DRAG; 
       lastEvent = null; 
       break; 
      case MotionEvent.ACTION_POINTER_DOWN: 
       oldDist = spacing(event); 
       if (oldDist > 10f) { 
        savedMatrix.set(matrix); 
        midPoint(mid, event); 
        mode = ZOOM; 
       } 
       lastEvent = new float[4]; 
       lastEvent[0] = event.getX(0); 
       lastEvent[1] = event.getX(1); 
       lastEvent[2] = event.getY(0); 
       lastEvent[3] = event.getY(1); 
       //d = rotation(event); 
       break; 
      case MotionEvent.ACTION_UP: 
      case MotionEvent.ACTION_POINTER_UP: 
       mode = NONE; 
       lastEvent = null; 
       break; 
      case MotionEvent.ACTION_MOVE: 
       if (mode == DRAG) { 
        matrix.set(savedMatrix); 
        float dx = event.getX() - start.x; 
        float dy = event.getY() - start.y; 
        matrix.postTranslate(dx, dy); 
        matrix.invert(matrixInverse); 
       } else if (mode == ZOOM) { 
        float newDist = spacing(event); 
        if (newDist > 10f) { 
         matrix.set(savedMatrix); 
         float scale = (newDist/oldDist); 
         matrix.postScale(scale, scale, mid.x, mid.y); 
         matrix.invert(matrixInverse); 
        } 
       } 
       break; 
     } 

     invalidate(); 
     return true; 
    } 

} 

積分到onTouch功能,請訪問:http://judepereira.com/blog/multi-touch-in-android-translate-scale-and-rotate/ 感謝Artjom對他的做法dipatch觸摸事件。

我加了一個zoomToFit方法,這個方法在這一點上被評論,因爲大多數人不需要這個。它適合孩子的容器大小,並把第一個孩子作爲比例因子的參考。

+0

如何限制比例因子?例如,我希望縮放比例在0.75和4之內。 – techtinkerer

+0

你可以定義一個MAX_ZOOM:'int MAX_ZOOM = 4;',然後在你發佈比例(matrix.postScale)之前添加這一行:'float [] values = new float [9]; matrix.getValues(values);如果(比例*值[Matrix.MSCALE_X]> = MAX_ZOOM){ \t scale = MAX_ZOOM/values [Matrix.MSCALE_X]; }' – Thomas

+0

好代碼!縮放比較好(立即開始,這在Alex的代碼中並不是這樣)並且沒有樞軸點問題。但是,它不允許使用雙擊拖動操作進行縮放。 – Donkey

0

要獲得Alex的代碼添加放大以下更改

更好permormance
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { 

    @Override 
    public boolean onScale(ScaleGestureDetector detector) { 
     mScaleFactor *= detector.getScaleFactor(); 
     if (detector.isInProgress()) { 
      mFocusX = detector.getFocusX(); 
      mFocusY = detector.getFocusY(); 
     } 

     mFocusX = (mFocusX + mLastTouchX)/2; // get center of touch 
     mFocusY = (mFocusY + mLastTouchY)/2; // get center of touch 

     mScaleFactor = Math.max(1f, Math.min(mScaleFactor, 2.0f)); 
     mScaleMatrix.setScale(mScaleFactor, mScaleFactor,mFocusX, mFocusY); 
     mScaleMatrix.invert(mScaleMatrixInverse); 
     invalidate(); 
     requestLayout(); 

     return true; 
    } 
} 
6

Thomas答案几乎是最好的(我有我的手機上的一個位置錯誤):縮放立即開始(這是不是這樣的與Alex的代碼),並在右側的中心點進行縮放。

但是,與Alex的代碼相反,無法使用「雙擊 - 拖動」手勢進行縮放(不是衆所周知的手勢,而是非常有用的手勢,只能用一個手指進行縮放,就像Google Chrome或Google地圖應用)。因此,這裏是Thomas的代碼的修改,以使其能夠(和固定子視圖位置錯誤):

public class ZoomableView extends ViewGroup { 

    // States. 
    private static final byte NONE = 0; 
    private static final byte DRAG = 1; 
    private static final byte ZOOM = 2; 

    private byte mode = NONE; 

    // Matrices used to move and zoom image. 
    private Matrix matrix = new Matrix(); 
    private Matrix matrixInverse = new Matrix(); 
    private Matrix savedMatrix = new Matrix(); 

    // Parameters for zooming. 
    private PointF start = new PointF(); 
    private PointF mid = new PointF(); 
    private float oldDist = 1f; 
    private float[] lastEvent = null; 
    private long lastDownTime = 0l; 

    private float[] mDispatchTouchEventWorkingArray = new float[2]; 
    private float[] mOnTouchEventWorkingArray = new float[2]; 


    @Override 
    public boolean dispatchTouchEvent(MotionEvent ev) { 
     mDispatchTouchEventWorkingArray[0] = ev.getX(); 
     mDispatchTouchEventWorkingArray[1] = ev.getY(); 
     mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray); 
     ev.setLocation(mDispatchTouchEventWorkingArray[0], mDispatchTouchEventWorkingArray[1]); 
     return super.dispatchTouchEvent(ev); 
    } 

    public ZoomableView(Context context) { 
     super(context); 
     init(context); 
    } 

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

    public ZoomableView(Context context, AttributeSet attrs, int defStyleAttr) { 
     super(context, attrs, defStyleAttr); 
     init(context); 
    } 


    private void init(Context context) { 

    } 


    /** 
    * Determine the space between the first two fingers 
    */ 
    private float spacing(MotionEvent event) { 
     float x = event.getX(0) - event.getX(1); 
     float y = event.getY(0) - event.getY(1); 
     return (float) Math.sqrt(x * x + y * y); 
    } 

    /** 
    * Calculate the mid point of the first two fingers 
    */ 
    private void midPoint(PointF point, MotionEvent event) { 
     float x = event.getX(0) + event.getX(1); 
     float y = event.getY(0) + event.getY(1); 
     point.set(x/2, y/2); 
    } 

    private float[] scaledPointsToScreenPoints(float[] a) { 
     matrix.mapPoints(a); 
     return a; 
    } 

    private float[] screenPointsToScaledPoints(float[] a) { 
     matrixInverse.mapPoints(a); 
     return a; 
    } 


    @Override 
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 
     int childCount = getChildCount(); 
     for (int i = 0; i < childCount; i++) { 
      View child = getChildAt(i); 
      if (child.getVisibility() != GONE) { 
       child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight()); 
      } 
     } 
    } 

    @Override 
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
     super.onMeasure(widthMeasureSpec, heightMeasureSpec); 

     int childCount = getChildCount(); 
     for (int i = 0; i < childCount; i++) { 
      View child = getChildAt(i); 
      if (child.getVisibility() != GONE) { 
       measureChild(child, widthMeasureSpec, heightMeasureSpec); 
      } 
     } 
    } 

    @Override 
    protected void dispatchDraw(Canvas canvas) { 
     float[] values = new float[9]; 
     matrix.getValues(values); 
     canvas.save(); 
     canvas.translate(values[Matrix.MTRANS_X], values[Matrix.MTRANS_Y]); 
     canvas.scale(values[Matrix.MSCALE_X], values[Matrix.MSCALE_Y]); 
     super.dispatchDraw(canvas); 
     canvas.restore(); 
    } 

    @Override 
    public boolean onTouchEvent(MotionEvent event) { 
     // handle touch events here 
     mOnTouchEventWorkingArray[0] = event.getX(); 
     mOnTouchEventWorkingArray[1] = event.getY(); 

     mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray); 

     event.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]); 

     switch (event.getAction() & MotionEvent.ACTION_MASK) { 
      case MotionEvent.ACTION_DOWN: 
       savedMatrix.set(matrix); 
       mode = DRAG; 
       lastEvent = null; 
       long downTime = event.getDownTime(); 
       if (downTime - lastDownTime < 300l) { 
        float density = getResources().getDisplayMetrics().density; 
        if (Math.max(Math.abs(start.x - event.getX()), Math.abs(start.y - event.getY())) < 40.f * density) { 
         savedMatrix.set(matrix); 
         mid.set(event.getX(), event.getY()); 
         mode = ZOOM; 
         lastEvent = new float[4]; 
         lastEvent[0] = lastEvent[1] = event.getX(); 
         lastEvent[2] = lastEvent[3] = event.getY(); 
        } 
        lastDownTime = 0l; 
       } else { 
        lastDownTime = downTime; 
       } 
       start.set(event.getX(), event.getY()); 
       break; 
      case MotionEvent.ACTION_POINTER_DOWN: 
       oldDist = spacing(event); 
       if (oldDist > 10f) { 
        savedMatrix.set(matrix); 
        midPoint(mid, event); 
        mode = ZOOM; 
       } 
       lastEvent = new float[4]; 
       lastEvent[0] = event.getX(0); 
       lastEvent[1] = event.getX(1); 
       lastEvent[2] = event.getY(0); 
       lastEvent[3] = event.getY(1); 
       break; 
      case MotionEvent.ACTION_UP: 
      case MotionEvent.ACTION_POINTER_UP: 
       mode = NONE; 
       lastEvent = null; 
       break; 
      case MotionEvent.ACTION_MOVE: 
       final float density = getResources().getDisplayMetrics().density; 
       if (mode == DRAG) { 
        matrix.set(savedMatrix); 
        float dx = event.getX() - start.x; 
        float dy = event.getY() - start.y; 
        matrix.postTranslate(dx, dy); 
        matrix.invert(matrixInverse); 
        if (Math.max(Math.abs(start.x - event.getX()), Math.abs(start.y - event.getY())) > 20.f * density) { 
         lastDownTime = 0l; 
        } 
       } else if (mode == ZOOM) { 
        if (event.getPointerCount() > 1) { 
         float newDist = spacing(event); 
         if (newDist > 10f * density) { 
          matrix.set(savedMatrix); 
          float scale = (newDist/oldDist); 
          matrix.postScale(scale, scale, mid.x, mid.y); 
          matrix.invert(matrixInverse); 
         } 
        } else { 
         matrix.set(savedMatrix); 
         float scale = event.getY()/start.y; 
         matrix.postScale(scale, scale, mid.x, mid.y); 
         matrix.invert(matrixInverse); 
        } 
       } 
       break; 
     } 

     invalidate(); 
     return true; 
    } 

} 
+0

如何在'onTouchEvent'上獲得轉換後的x,y座標? – isuru

+0

我對Thomas的回答有問題。我的視圖的點擊框低於實際視圖的位置(對於android <25 API)。我想通過dispatchDraw方法解決問題。因爲我對矩陣一無所知,所以我非常失望。你dispatchDraw修復了這個問題。我真的想知道這個魔法是如何工作的。我想了解矩陣,也許還有使用Rect對象的ViewPort技術。你能建議好的資源來學習嗎? 我真的不喜歡做點什麼,當我無法弄清楚它是如何工作的。 謝謝你的代碼,謝謝你@Thomas aswell。 – Kotsu

+0

這是最好的解決方案。 – Chandru

0

對於那些誰感興趣的縮放/平移的LinearLayout,我修改了由Alex發佈的版本將事情垂直放置,並將平移視爲可見視圖。我使用這個來自PDFRenderer的位圖。我已經測試過這個,但是如果你發現任何錯誤,請發帖,因爲我也想知道它們!

注:我選擇不實施雙擊,因爲QuickScale的作品。

public class ZoomableLinearLayout extends ViewGroup { 

    private static final int INVALID_POINTER_ID = 1; 
    private int mActivePointerId = INVALID_POINTER_ID; 

    private float mScaleFactor = 1; 
    private ScaleGestureDetector mScaleDetector; 
    private Matrix mScaleMatrix = new Matrix(); 
    private Matrix mScaleMatrixInverse = new Matrix(); 

    private float mPosX; 
    private float mPosY; 
    private Matrix mTranslateMatrix = new Matrix(); 
    private Matrix mTranslateMatrixInverse = new Matrix(); 

    private float mLastTouchX; 
    private float mLastTouchY; 

    private float mFocusY; 
    private float mFocusX; 

    private int mCanvasWidth; 
    private int mCanvasHeight; 

    private float[] mInvalidateWorkingArray = new float[6]; 
    private float[] mDispatchTouchEventWorkingArray = new float[2]; 
    private float[] mOnTouchEventWorkingArray = new float[2]; 

    private boolean mIsScaling; 

    public ZoomableLinearLayout(Context context) { 
     super(context); 
     mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); 
     mTranslateMatrix.setTranslate(0, 0); 
     mScaleMatrix.setScale(1, 1); 
    } 

    public ZoomableLinearLayout(Context context, AttributeSet attributeSet) { 
     super(context, attributeSet); 
     mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); 
     mTranslateMatrix.setTranslate(0, 0); 
     mScaleMatrix.setScale(1, 1); 
    } 

    @Override 
    protected void onLayout(boolean changed, int l, int t, int r, int b) { 
     int childCount = getChildCount(); 
     for (int i = 0; i < childCount; i++) { 
     View child = getChildAt(i); 
     if (child.getVisibility() != GONE) { 
      child.layout(l, t, l+child.getMeasuredWidth(), t += child.getMeasuredHeight()); 
     } 
     } 
    } 

    @Override 
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
     super.onMeasure(widthMeasureSpec, heightMeasureSpec); 

     int height = 0; 
     int width = 0; 
     int childCount = getChildCount(); 
     for (int i = 0; i < childCount; i++) { 
     View child = getChildAt(i); 
     if (child.getVisibility() != GONE) { 
      measureChild(child, widthMeasureSpec, heightMeasureSpec); 
      height += child.getMeasuredHeight(); 
      width = Math.max(width, child.getMeasuredWidth()); 
     } 
     } 
     mCanvasWidth = width; 
     mCanvasHeight = height; 
    } 

    @Override 
    protected void dispatchDraw(Canvas canvas) { 
     canvas.save(); 
     canvas.translate(mPosX, mPosY); 
     canvas.scale(mScaleFactor, mScaleFactor, mFocusX, mFocusY); 
     super.dispatchDraw(canvas); 
     canvas.restore(); 
    } 

    @Override 
    public boolean dispatchTouchEvent(MotionEvent ev) { 
     mDispatchTouchEventWorkingArray[0] = ev.getX(); 
     mDispatchTouchEventWorkingArray[1] = ev.getY(); 
     mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray); 
     ev.setLocation(mDispatchTouchEventWorkingArray[0], 
      mDispatchTouchEventWorkingArray[1]); 
     return super.dispatchTouchEvent(ev); 
    } 

    /** 
    * Although the docs say that you shouldn't override this, I decided to do 
    * so because it offers me an easy way to change the invalidated area to my 
    * likening. 
    */ 
    @Override 
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) { 

     mInvalidateWorkingArray[0] = dirty.left; 
     mInvalidateWorkingArray[1] = dirty.top; 
     mInvalidateWorkingArray[2] = dirty.right; 
     mInvalidateWorkingArray[3] = dirty.bottom; 

     mInvalidateWorkingArray = scaledPointsToScreenPoints(mInvalidateWorkingArray); 
     dirty.set(Math.round(mInvalidateWorkingArray[0]), Math.round(mInvalidateWorkingArray[1]), 
      Math.round(mInvalidateWorkingArray[2]), Math.round(mInvalidateWorkingArray[3])); 

     location[0] *= mScaleFactor; 
     location[1] *= mScaleFactor; 
     return super.invalidateChildInParent(location, dirty); 
    } 

    private float[] scaledPointsToScreenPoints(float[] a) { 
     mScaleMatrix.mapPoints(a); 
     mTranslateMatrix.mapPoints(a); 
     return a; 
    } 

    private float[] screenPointsToScaledPoints(float[] a){ 
     mTranslateMatrixInverse.mapPoints(a); 
     mScaleMatrixInverse.mapPoints(a); 
     return a; 
    } 

    @Override 
    public boolean onTouchEvent(MotionEvent ev) { 
     mOnTouchEventWorkingArray[0] = ev.getX(); 
     mOnTouchEventWorkingArray[1] = ev.getY(); 

     mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray); 

     ev.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]); 
     mScaleDetector.onTouchEvent(ev); 

     final int action = ev.getAction(); 
     switch (action & MotionEvent.ACTION_MASK) { 
     case MotionEvent.ACTION_DOWN: { 
      final float x = ev.getX(); 
      final float y = ev.getY(); 

      mLastTouchX = x; 
      mLastTouchY = y; 

      // Save the ID of this pointer 
      mActivePointerId = ev.getPointerId(0); 
      break; 
     } 

     case MotionEvent.ACTION_MOVE: { 
      // Find the index of the active pointer and fetch its position 
      final int pointerIndex = ev.findPointerIndex(mActivePointerId); 
      final float x = ev.getX(pointerIndex); 
      final float y = ev.getY(pointerIndex); 

      if (mIsScaling && ev.getPointerCount() == 1) { 
       // Don't move during a QuickScale. 
       mLastTouchX = x; 
       mLastTouchY = y; 

       break; 
      } 

      float dx = x - mLastTouchX; 
      float dy = y - mLastTouchY; 

      float[] topLeft = {0f, 0f}; 
      float[] bottomRight = {getWidth(), getHeight()}; 
      /* 
      * Corners of the view in screen coordinates, so dx/dy should not be allowed to 
      * push these beyond the canvas bounds. 
      */ 
      float[] scaledTopLeft = screenPointsToScaledPoints(topLeft); 
      float[] scaledBottomRight = screenPointsToScaledPoints(bottomRight); 

      dx = Math.min(Math.max(dx, scaledBottomRight[0] - mCanvasWidth), scaledTopLeft[0]); 
      dy = Math.min(Math.max(dy, scaledBottomRight[1] - mCanvasHeight), scaledTopLeft[1]); 

      mPosX += dx; 
      mPosY += dy; 

      mTranslateMatrix.preTranslate(dx, dy); 
      mTranslateMatrix.invert(mTranslateMatrixInverse); 

      mLastTouchX = x; 
      mLastTouchY = y; 

      invalidate(); 
      break; 
     } 

     case MotionEvent.ACTION_UP: { 
      mActivePointerId = INVALID_POINTER_ID; 
      break; 
     } 

     case MotionEvent.ACTION_CANCEL: { 
      mActivePointerId = INVALID_POINTER_ID; 
      break; 
     } 

     case MotionEvent.ACTION_POINTER_UP: { 
      // Extract the index of the pointer that left the touch sensor 
      final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 
      final int pointerId = ev.getPointerId(pointerIndex); 
      if (pointerId == mActivePointerId) { 
       // This was our active pointer going up. Choose a new 
       // active pointer and adjust accordingly. 
       final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 
       mLastTouchX = ev.getX(newPointerIndex); 
       mLastTouchY = ev.getY(newPointerIndex); 
       mActivePointerId = ev.getPointerId(newPointerIndex); 
      } 
      break; 
     } 
     } 
     return true; 
    } 

    private float getMaxScale() { 
     return 2f; 
    } 

    private float getMinScale() { 
     return 1f; 
    } 

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { 
     @Override 
     public boolean onScaleBegin(ScaleGestureDetector detector) { 
     mIsScaling = true; 

     mFocusX = detector.getFocusX(); 
     mFocusY = detector.getFocusY(); 

     float[] foci = {mFocusX, mFocusY}; 
     float[] scaledFoci = screenPointsToScaledPoints(foci); 

     mFocusX = scaledFoci[0]; 
     mFocusY = scaledFoci[1]; 

     return true; 
     } 

     @Override 
     public void onScaleEnd(ScaleGestureDetector detector) { 
     mIsScaling = false; 
     } 

     @Override 
     public boolean onScale(ScaleGestureDetector detector) { 
     mScaleFactor *= detector.getScaleFactor(); 
     mScaleFactor = Math.max(getMinScale(), Math.min(mScaleFactor, getMaxScale())); 
     mScaleMatrix.setScale(mScaleFactor, mScaleFactor, mFocusX, mFocusY); 
     mScaleMatrix.invert(mScaleMatrixInverse); 
     invalidate(); 

     return true; 
     } 
    } 

} 
0

我使用的是這裏發佈的代碼的一些修改版本。此ZoomLayout使用Android手勢識別器進行滾動和縮放。在縮放或平移時,它還保留了數據透視和邊界。

https://github.com/maxtower/ZoomLayout/blob/master/app/src/main/java/com/maxtower/testzoomlayout/ZoomLayout.java

爲了保持鍋的界限:

if (contentSize != null) 
    { 
     float[] values = new float[9]; 
     matrix.getValues(values); 
     float totX = values[Matrix.MTRANS_X] + distanceX; 
     float totY = values[Matrix.MTRANS_Y] + distanceY; 
     float sx = values[Matrix.MSCALE_X]; 

     Rect viewableRect = new Rect(); 
     ZoomLayout.this.getDrawingRect(viewableRect); 
     float offscreenWidth = contentSize.width() - (viewableRect.right - viewableRect.left); 
     float offscreenHeight = contentSize.height() - (viewableRect.bottom - viewableRect.top); 
     float maxDx = (contentSize.width() - (contentSize.width()/sx)) * sx; 
     float maxDy = (contentSize.height() - (contentSize.height()/sx)) * sx; 
     if (totX > 0 && distanceX > 0) 
     { 
      distanceX = 0; 
     } 
     if (totY > 0 && distanceY > 0) 
     { 
      distanceY = 0; 
     } 

     if(totX*-1 > offscreenWidth+maxDx && distanceX < 0) 
     { 
      distanceX = 0; 
     } 
     if(totY*-1 > offscreenHeight+maxDy && distanceY < 0) 
     { 
      distanceY = 0; 
     } 

    }