安卓图片视图缩放

75
我正在使用来自Making Sense of Multitouch的代码示例来缩放图像视图。在ScaleListener上,我添加了ScaleGestureDetector.getFocusX()和getFocusY()以便内容围绕手势的焦点进行缩放。它工作得很好。 问题是,在第一次多点触控时,整个图像绘制位置会改变到当前触摸点,并从那里进行缩放。你能帮我解决这个问题吗? 以下是我TouchImageView的代码示例。
public class TouchImageViewSample extends ImageView {

private Paint borderPaint = null;
private Paint backgroundPaint = null;

private float mPosX = 0f;
private float mPosY = 0f;

private float mLastTouchX;
private float mLastTouchY;
private static final int INVALID_POINTER_ID = -1;
private static final String LOG_TAG = "TouchImageView";

// The ‘active pointer’ is the one currently moving our object.
private int mActivePointerId = INVALID_POINTER_ID;

public TouchImageViewSample(Context context) {
    this(context, null, 0);
}

public TouchImageViewSample(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

private ScaleGestureDetector mScaleDetector;
private float mScaleFactor = 1.f;

// Existing code ...
public TouchImageViewSample(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    // Create our ScaleGestureDetector
    mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());

    borderPaint = new Paint();
    borderPaint.setARGB(255, 255, 128, 0);
    borderPaint.setStyle(Paint.Style.STROKE);
    borderPaint.setStrokeWidth(4);

    backgroundPaint = new Paint();
    backgroundPaint.setARGB(32, 255, 255, 255);
    backgroundPaint.setStyle(Paint.Style.FILL);

}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    // Let the ScaleGestureDetector inspect all events.
    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;

        mActivePointerId = ev.getPointerId(0);
        break;
    }

    case MotionEvent.ACTION_MOVE: {
        final int pointerIndex = ev.findPointerIndex(mActivePointerId);
        final float x = ev.getX(pointerIndex);
        final float y = ev.getY(pointerIndex);

        // Only move if the ScaleGestureDetector isn't processing a gesture.
        if (!mScaleDetector.isInProgress()) {
            final float dx = x - mLastTouchX;
            final float dy = y - mLastTouchY;

            mPosX += dx;
            mPosY += dy;

            invalidate();
        }

        mLastTouchX = x;
        mLastTouchY = y;
        break;
    }

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

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

    case MotionEvent.ACTION_POINTER_UP: {
        final int pointerIndex = (ev.getAction() & 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;
}

/*
 * (non-Javadoc)
 * 
 * @see android.view.View#draw(android.graphics.Canvas)
 */
@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    canvas.drawRect(0, 0, getWidth() - 1, getHeight() - 1, borderPaint);
}

@Override
public void onDraw(Canvas canvas) {
    canvas.drawRect(0, 0, getWidth() - 1, getHeight() - 1, backgroundPaint);
    if (this.getDrawable() != null) {
        canvas.save();
        canvas.translate(mPosX, mPosY);

        Matrix matrix = new Matrix();
        matrix.postScale(mScaleFactor, mScaleFactor, pivotPointX,
                pivotPointY);
        // canvas.setMatrix(matrix);

        canvas.drawBitmap(
                ((BitmapDrawable) this.getDrawable()).getBitmap(), matrix,
                null);

        // this.getDrawable().draw(canvas);
        canvas.restore();
    }
}

/*
 * (non-Javadoc)
 * 
 * @see
 * android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable
 * )
 */
@Override
public void setImageDrawable(Drawable drawable) {
    // Constrain to given size but keep aspect ratio
    int width = drawable.getIntrinsicWidth();
    int height = drawable.getIntrinsicHeight();
    mLastTouchX = mPosX = 0;
    mLastTouchY = mPosY = 0;

    int borderWidth = (int) borderPaint.getStrokeWidth();
    mScaleFactor = Math.min(((float) getLayoutParams().width - borderWidth)
            / width, ((float) getLayoutParams().height - borderWidth)
            / height);
    pivotPointX = (((float) getLayoutParams().width - borderWidth) - (int) (width * mScaleFactor)) / 2;
    pivotPointY = (((float) getLayoutParams().height - borderWidth) - (int) (height * mScaleFactor)) / 2;
    super.setImageDrawable(drawable);
}

float pivotPointX = 0f;
float pivotPointY = 0f;

private class ScaleListener extends
        ScaleGestureDetector.SimpleOnScaleGestureListener {

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        mScaleFactor *= detector.getScaleFactor();

        pivotPointX = detector.getFocusX();
        pivotPointY = detector.getFocusY();

        Log.d(LOG_TAG, "mScaleFactor " + mScaleFactor);
        Log.d(LOG_TAG, "pivotPointY " + pivotPointY + ", pivotPointX= "
                + pivotPointX);
        mScaleFactor = Math.max(0.05f, mScaleFactor);

        invalidate();
        return true;
    }
}

以下是我在活动中如何使用它的方法。

ImageView imageView = (ImageView) findViewById(R.id.imgView);

int hMargin = (int) (displayMetrics.widthPixels * .10);
int vMargin = (int) (displayMetrics.heightPixels * .10);

RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(displayMetrics.widthPixels - (hMargin * 2), (int)(displayMetrics.heightPixels - btnCamera.getHeight()) - (vMargin * 2));
params.leftMargin = hMargin;
params.topMargin =  vMargin;
imageView.setLayoutParams(params);
imageView.setImageDrawable(drawable);

当您尝试进行捏合缩放时,您的图像位置也会发生变化。 - Herry
1
不完全正确,在ScaleListener.onScale()中,我正在设置pivotPointX和Y,并且它们在onDraw() matrix.postScale(mScaleFactor, mScaleFactor, pivotPointX, pivotPointY);方法中用作焦点。在第一次多点触控时,pivotPointX和Y被设置为新位置,整个图像会移动(跳跃)到新位置,然后缩放将正常工作。我需要避免图像x y位置的跳跃。 - MobDev
这个类使用了 ImageView 的哪些功能?它没有使用 ImageViewonDraw() 方法,而是直接使用 canvas.drawBitmap(((BitmapDrawable) this.getDrawable()).getBitmap(), matrix, null); 绘制位图。 - alexbirkett
@MobDev,我在你上次分享的活动代码中没有看到TouchImageViewSample类。你能帮忙吗? - Mr. Unnormalized Posterior
7个回答

50

谢谢,这是我见过的最好的课程,是所有网站中最好的一个,易于使用,拥有我所需要的一切。非常感谢。 - user7179690
这绝对是最好的类。它是唯一一个可以在ViewPager中工作并检查图像是否完全可见的类。 - jobbert
这个东西有一个问题。它不能与viewpager图片滑动器一起使用。屏幕会变成空白白色。 - Debasish Ghosh
顺便说一下,这是Kotlin。 - nandur

33

使用 ScaleGestureDetector

当学习新概念时,我不喜欢使用库或代码转储。 我在这里文档中找到了有关如何通过捏合手势调整图像大小的良好描述。 这个答案是一个稍微修改过的总结。 以后您可能会想要添加更多功能,但它将帮助您入门。

动画gif:缩放图像示例

布局

ImageView只使用应用程序标志,因为它已经可用。 不过您可以用任何您喜欢的图像替换它。

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

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@mipmap/ic_launcher"
        android:layout_centerInParent="true"/>

</RelativeLayout>

活动

我们在活动中使用 ScaleGestureDetector 监听触摸事件。当检测到缩放手势时,缩放因子用于调整 ImageView 的大小。

public class MainActivity extends AppCompatActivity {

    private ScaleGestureDetector mScaleGestureDetector;
    private float mScaleFactor = 1.0f;
    private ImageView mImageView;

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

        // initialize the view and the gesture detector
        mImageView = findViewById(R.id.imageView);
        mScaleGestureDetector = new ScaleGestureDetector(this, new ScaleListener());
    }

    // this redirects all touch events in the activity to the gesture detector
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mScaleGestureDetector.onTouchEvent(event);
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

        // when a scale gesture is detected, use it to resize the image
        @Override
        public boolean onScale(ScaleGestureDetector scaleGestureDetector){
            mScaleFactor *= scaleGestureDetector.getScaleFactor();
            mImageView.setScaleX(mScaleFactor);
            mImageView.setScaleY(mScaleFactor);
            return true;
        }
    }
}

注意事项

  • 虽然上面的示例中使用了手势检测器,但也可以将其设置在图像视图上。
  • 您可以使用类似以下代码限制缩放的大小:

    mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
    
  • 再次感谢在Android中使用多点触摸手势进行捏合缩放

  • 文档
  • 在模拟器中使用Ctrl+鼠标拖动来模拟捏合手势。

继续

你可能还想做其他事情,比如平移和缩放到某个焦点。你可以自己开发这些功能,但如果您想使用预制的自定义视图,请将TouchImageView.java复制到您的项目中并像正常的 ImageView 一样使用它。对我来说效果很好,我只遇到了一个错误。我计划进一步编辑代码以删除警告和我不需要的部分。你也可以这样做。


1
嗨@Suragch,这对我来说运作良好,但问题是当两个手指保持缩放位置时,图像会出现一些闪烁。你能看出是什么原因吗? - Harsha
@Harsha,好问题。我不确定是什么原因导致的。我自己没有注意到这个问题。 - Suragch
好的,我已经检查过了。我实现了一个稍微不同的代码。我使用了mImageView.setOnTouchListener,出于某种原因,这导致了这个问题。 - Harsha
1
很好的解释,我也更喜欢动手实践而不是直接使用库。 - Eman
非常好的答案,我不太懂画布和坐标相关的东西。这正是我在寻找的。 - Rainmaker
显示剩余3条评论

16
在build.gradle文件中添加下面的代码行:
compile 'com.commit451:PhotoView:1.2.4'

或者
compile 'com.github.chrisbanes:PhotoView:1.3.0'

在Java文件中:
PhotoViewAttacher photoAttacher;
photoAttacher= new PhotoViewAttacher(Your_Image_View);
photoAttacher.update();

这将解决问题 - user2288580

16

我自己创建了一个支持缩放的自定义ImageView。在Chirag Raval的代码中没有限制或边框,因此用户可以将图片拖出屏幕外。按照以下方式修复此问题:

这是CustomImageView类:

    public class CustomImageVIew extends ImageView implements OnTouchListener {


    private Matrix matrix = new Matrix();
    private Matrix savedMatrix = new Matrix();

    static final int NONE = 0;
    static final int DRAG = 1;
    static final int ZOOM = 2;

    private int mode = NONE;

    private PointF mStartPoint = new PointF();
    private PointF mMiddlePoint = new PointF();
    private Point mBitmapMiddlePoint = new Point();

    private float oldDist = 1f;
    private float matrixValues[] = {0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f};
    private float scale;
    private float oldEventX = 0;
    private float oldEventY = 0;
    private float oldStartPointX = 0;
    private float oldStartPointY = 0;
    private int mViewWidth = -1;
    private int mViewHeight = -1;
    private int mBitmapWidth = -1;
    private int mBitmapHeight = -1;
    private boolean mDraggable = false;


    public CustomImageVIew(Context context) {
        this(context, null, 0);
    }

    public CustomImageVIew(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomImageVIew(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        this.setOnTouchListener(this);
    }

    @Override
    public void onSizeChanged (int w, int h, int oldw, int oldh){
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
    }

    public void setBitmap(Bitmap bitmap){
        if(bitmap != null){
            setImageBitmap(bitmap);

            mBitmapWidth = bitmap.getWidth();
            mBitmapHeight = bitmap.getHeight();
            mBitmapMiddlePoint.x = (mViewWidth / 2) - (mBitmapWidth /  2);
            mBitmapMiddlePoint.y = (mViewHeight / 2) - (mBitmapHeight / 2);

            matrix.postTranslate(mBitmapMiddlePoint.x, mBitmapMiddlePoint.y);
            this.setImageMatrix(matrix);
        }
    }

    @Override
    public boolean onTouch(View v, MotionEvent event){
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            savedMatrix.set(matrix);
            mStartPoint.set(event.getX(), event.getY());
            mode = DRAG;
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            oldDist = spacing(event);
            if(oldDist > 10f){
                savedMatrix.set(matrix);
                midPoint(mMiddlePoint, event);
                mode = ZOOM;
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_POINTER_UP:
            mode = NONE;
            break;
        case MotionEvent.ACTION_MOVE:
            if(mode == DRAG){
                drag(event);
            } else if(mode == ZOOM){
                zoom(event);
            } 
            break;
        }

        return true;
    }



   public void drag(MotionEvent event){
       matrix.getValues(matrixValues);

       float left = matrixValues[2];
       float top = matrixValues[5];
       float bottom = (top + (matrixValues[0] * mBitmapHeight)) - mViewHeight;
       float right = (left + (matrixValues[0] * mBitmapWidth)) -mViewWidth;

       float eventX = event.getX();
       float eventY = event.getY();
       float spacingX = eventX - mStartPoint.x;
       float spacingY = eventY - mStartPoint.y;
       float newPositionLeft = (left  < 0 ? spacingX : spacingX * -1) + left;
       float newPositionRight = (spacingX) + right;
       float newPositionTop = (top  < 0 ? spacingY : spacingY * -1) + top;
       float newPositionBottom = (spacingY) + bottom;
       boolean x = true;
       boolean y = true;

       if(newPositionRight < 0.0f || newPositionLeft > 0.0f){
           if(newPositionRight < 0.0f && newPositionLeft > 0.0f){
               x = false;
           } else{
               eventX = oldEventX;
               mStartPoint.x = oldStartPointX;
           }
       }
       if(newPositionBottom < 0.0f || newPositionTop > 0.0f){
           if(newPositionBottom < 0.0f && newPositionTop > 0.0f){
               y = false;
           } else{
               eventY = oldEventY;
               mStartPoint.y = oldStartPointY;
           }
       }

       if(mDraggable){
           matrix.set(savedMatrix);
           matrix.postTranslate(x? eventX - mStartPoint.x : 0, y? eventY - mStartPoint.y : 0);
           this.setImageMatrix(matrix);
           if(x)oldEventX = eventX;
           if(y)oldEventY = eventY;
           if(x)oldStartPointX = mStartPoint.x;
           if(y)oldStartPointY = mStartPoint.y;
       }

   }

   public void zoom(MotionEvent event){
       matrix.getValues(matrixValues);

       float newDist = spacing(event);
       float bitmapWidth = matrixValues[0] * mBitmapWidth;
       float bimtapHeight = matrixValues[0] * mBitmapHeight;
       boolean in = newDist > oldDist;

       if(!in && matrixValues[0] < 1){
           return;
       }
       if(bitmapWidth > mViewWidth || bimtapHeight > mViewHeight){
           mDraggable = true;
       } else{
           mDraggable = false;
       }

       float midX = (mViewWidth / 2);
       float midY = (mViewHeight / 2);

       matrix.set(savedMatrix);
       scale = newDist / oldDist;
       matrix.postScale(scale, scale, bitmapWidth > mViewWidth ? mMiddlePoint.x : midX, bimtapHeight > mViewHeight ? mMiddlePoint.y : midY); 

       this.setImageMatrix(matrix);


   }





    /** 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);
    }


}

以下是您可以在活动中使用它的方法:

CustomImageVIew mImageView = (CustomImageVIew)findViewById(R.id.customImageVIew1);
mImage.setBitmap(your bitmap);

并且布局:

<your.package.name.CustomImageVIew
        android:id="@+id/customImageVIew1"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_marginBottom="15dp"
        android:layout_marginLeft="15dp"
        android:layout_marginRight="15dp"
        android:layout_marginTop="15dp"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true" 
        android:scaleType="matrix"/> // important

1
“your bitmap” 中应该放什么? - gta0004
1
将CustomImageView.class中的函数名“setBitmap”更改为“setImageBitmap”,并在此函数中将“setImageBitmap(bitmap);”替换为“super.setImageBitmap(bitmap);”,然后您的类将完美地与ImageLoader.jar一起使用。 - NickUnuchek
我该如何拖动图像以适应我的裁剪区域..?? - Sagar Maiyad
这不会移动图像,对吧? - techtinkerer
event.getAction() & MotionEvent.ACTION_MASK 这段代码的含义是什么?能否解释一下? - Muhammad Babar
@techtinkerer 正确,这个解决方案只能从中心点缩放图像,不能平移。 - shagberg

4
TouchImageViewSample类中,缩放是围绕着支点进行的。属于支点的像素不会受到图像缩放的影响。当您更改支点时,视图将被重新绘制,并且缩放将围绕新支点进行。这会更改先前支点的位置,并且每次您触摸图像时都会看到图像移位。您必须通过平移图像来补偿此移位误差。请参阅我的ZoomGestureDetector.updatePivotPoint()方法,了解如何完成此操作。

ZoomGestureDetector

我创建了一个自定义缩放手势检测器类。它可以同时进行缩放、平移和旋转。它还支持快速滑动动画。

import android.graphics.Canvas
import android.graphics.Matrix
import android.view.MotionEvent
import android.view.VelocityTracker
import androidx.core.math.MathUtils
import androidx.dynamicanimation.animation.FlingAnimation
import androidx.dynamicanimation.animation.FloatValueHolder
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.atan2

class ZoomGestureDetector(private val listener: Listener) {

    companion object {
        const val MIN_SCALE = 0.01f
        const val MAX_SCALE = 100f
        const val MIN_FLING_VELOCITY = 50f
        const val MAX_FLING_VELOCITY = 8000f
    }

    // public
    var isZoomEnabled: Boolean = true
    var isScaleEnabled: Boolean = true
    var isRotationEnabled: Boolean = true
    var isTranslationEnabled: Boolean = true
    var isFlingEnabled: Boolean = true

    // local
    private val mDrawMatrix: Matrix = Matrix()
    private val mTouchMatrix: Matrix = Matrix()
    private val mPointerMap: HashMap<Int, Position> = HashMap()
    private val mTouchPoint: FloatArray = floatArrayOf(0f, 0f)
    private val mPivotPoint: FloatArray = floatArrayOf(0f, 0f)

    // transformations
    private var mTranslationX: Float = 0f
    private var mTranslationY: Float = 0f
    private var mScaling: Float = 1f
    private var mPivotX: Float = 0f
    private var mPivotY: Float = 0f
    private var mRotation: Float = 0f

    // previous values
    private var mPreviousFocusX: Float = 0f
    private var mPreviousFocusY: Float = 0f
    private var mPreviousTouchSpan: Float = 1f

    // fling related
    private var mVelocityTracker: VelocityTracker? = null
    private var mFlingAnimX: FlingAnimation? = null
    private var mFlingAnimY: FlingAnimation? = null

    fun updateTouchLocation(event: MotionEvent) {
        mTouchPoint[0] = event.x
        mTouchPoint[1] = event.y
        mTouchMatrix.mapPoints(mTouchPoint)
        event.setLocation(mTouchPoint[0], mTouchPoint[1])
    }

    fun updateCanvasMatrix(canvas: Canvas) {
        canvas.setMatrix(mDrawMatrix)
    }

    fun onTouchEvent(event: MotionEvent): Boolean {
        if (isZoomEnabled) {
            // update velocity tracker
            if (isFlingEnabled) {
                if (mVelocityTracker == null) {
                    mVelocityTracker = VelocityTracker.obtain()
                }
                mVelocityTracker?.addMovement(event)
            }
            // handle touch events
            when (event.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    // update focus point
                    mPreviousFocusX = event.x
                    mPreviousFocusY = event.y
                    event.savePointers()
                    // cancel ongoing fling animations
                    if (isFlingEnabled) {
                        mFlingAnimX?.cancel()
                        mFlingAnimY?.cancel()
                    }
                }
                MotionEvent.ACTION_POINTER_DOWN -> {
                    updateTouchParameters(event)
                }
                MotionEvent.ACTION_POINTER_UP -> {
                    // Check the dot product of current velocities.
                    // If the pointer that left was opposing another velocity vector, clear.
                    if (isFlingEnabled) {
                        mVelocityTracker?.let { tracker ->
                            tracker.computeCurrentVelocity(1000, MAX_FLING_VELOCITY)
                            val upIndex: Int = event.actionIndex
                            val id1: Int = event.getPointerId(upIndex)
                            val x1 = tracker.getXVelocity(id1)
                            val y1 = tracker.getYVelocity(id1)
                            for (i in 0 until event.pointerCount) {
                                if (i == upIndex) continue
                                val id2: Int = event.getPointerId(i)
                                val x = x1 * tracker.getXVelocity(id2)
                                val y = y1 * tracker.getYVelocity(id2)
                                val dot = x + y
                                if (dot < 0) {
                                    tracker.clear()
                                    break
                                }
                            }
                        }
                    }
                    updateTouchParameters(event)
                }
                MotionEvent.ACTION_UP -> {
                    // do fling animation
                    if (isFlingEnabled) {
                        mVelocityTracker?.let { tracker ->
                            val pointerId: Int = event.getPointerId(0)
                            tracker.computeCurrentVelocity(1000, MAX_FLING_VELOCITY)
                            val velocityY: Float = tracker.getYVelocity(pointerId)
                            val velocityX: Float = tracker.getXVelocity(pointerId)
                            if (abs(velocityY) > MIN_FLING_VELOCITY || abs(velocityX) > MIN_FLING_VELOCITY) {
                                val translateX = mTranslationX
                                val translateY = mTranslationY
                                val valueHolder = FloatValueHolder()
                                mFlingAnimX = FlingAnimation(valueHolder).apply {
                                    setStartVelocity(velocityX)
                                    setStartValue(0f)
                                    addUpdateListener { _, value, _ ->
                                        mTranslationX = translateX + value
                                        updateDrawMatrix()
                                        listener.onZoom(mScaling, mRotation, mTranslationX to mTranslationY, mPivotX to mPivotY)
                                    }
                                    addEndListener { _, _, _, _ ->
                                        updateTouchMatrix()
                                    }
                                    start()
                                }
                                mFlingAnimY = FlingAnimation(valueHolder).apply {
                                    setStartVelocity(velocityY)
                                    setStartValue(0f)
                                    addUpdateListener { _, value, _ ->
                                        mTranslationY = translateY + value
                                        updateDrawMatrix()
                                        listener.onZoom(mScaling, mRotation, mTranslationX to mTranslationY, mPivotX to mPivotY)
                                    }
                                    addEndListener { _, _, _, _ ->
                                        updateTouchMatrix()
                                    }
                                    start()
                                }
                            }
                            tracker.recycle()
                            mVelocityTracker = null
                        }
                    }
                }
                MotionEvent.ACTION_MOVE -> {
                    val (focusX, focusY) = event.focalPoint()
                    if (event.pointerCount > 1) {
                        if (isScaleEnabled) {
                            val touchSpan = event.touchSpan(focusX, focusY)
                            mScaling *= scaling(touchSpan)
                            mScaling = MathUtils.clamp(mScaling, MIN_SCALE, MAX_SCALE)
                            mPreviousTouchSpan = touchSpan
                        }
                        if (isRotationEnabled) {
                            mRotation += event.rotation(focusX, focusY)
                        }
                        if (isTranslationEnabled) {
                            val (translationX, translationY) = translation(focusX, focusY)
                            mTranslationX += translationX
                            mTranslationY += translationY
                        }
                    } else {
                        if (isTranslationEnabled) {
                            val (translationX, translationY) = translation(focusX, focusY)
                            mTranslationX += translationX
                            mTranslationY += translationY
                        }
                    }
                    mPreviousFocusX = focusX
                    mPreviousFocusY = focusY
                    updateTouchMatrix()
                    updateDrawMatrix()
                    event.savePointers()
                    listener.onZoom(mScaling, mRotation, mTranslationX to mTranslationY, mPivotX to mPivotY)
                }
            }
            return true
        }
        return false
    }

    // update focus point, touch span and pivot point
    private fun updateTouchParameters(event: MotionEvent) {
        val (focusX, focusY) = event.focalPoint()
        mPreviousFocusX = focusX
        mPreviousFocusY = focusY
        mPreviousTouchSpan = event.touchSpan(focusX, focusY)
        updatePivotPoint(focusX, focusY)
        updateTouchMatrix()
        updateDrawMatrix()
        event.savePointers()
        listener.onZoom(mScaling, mRotation, mTranslationX to mTranslationY, mPivotX to mPivotY)
    }

    // touch matrix is used to transform touch points
    // on the child view and to find pivot point
    private fun updateTouchMatrix() {
        mTouchMatrix.reset()
        mTouchMatrix.preTranslate(-mTranslationX, -mTranslationY)
        mTouchMatrix.postRotate(-mRotation, mPivotX, mPivotY)
        mTouchMatrix.postScale(1f / mScaling, 1f / mScaling, mPivotX, mPivotY)
    }

    // draw matrix is used to transform child view when drawing on the canvas
    private fun updateDrawMatrix() {
        mDrawMatrix.reset()
        mDrawMatrix.preScale(mScaling, mScaling, mPivotX, mPivotY)
        mDrawMatrix.preRotate(mRotation, mPivotX, mPivotY)
        mDrawMatrix.postTranslate(mTranslationX, mTranslationY)
    }

    // this updates the pivot point and translation error caused by changing the pivot point
    private fun updatePivotPoint(focusX: Float, focusY: Float) {
        // update point
        mPivotPoint[0] = focusX
        mPivotPoint[1] = focusY
        mTouchMatrix.mapPoints(mPivotPoint)
        mPivotX = mPivotPoint[0]
        mPivotY = mPivotPoint[1]
        // correct pivot error
        mDrawMatrix.mapPoints(mPivotPoint)
        mTranslationX -= mTranslationX + mPivotX - mPivotPoint[0]
        mTranslationY -= mTranslationY + mPivotY - mPivotPoint[1]
    }

    private fun MotionEvent.focalPoint(): Pair<Float, Float> {
        val upIndex = if (actionMasked == MotionEvent.ACTION_POINTER_UP) actionIndex else -1
        var sumX = 0f
        var sumY = 0f
        var sumCount = 0
        for (pointerIndex in 0 until pointerCount) {
            if (pointerIndex == upIndex) continue
            sumX += getX(pointerIndex)
            sumY += getY(pointerIndex)
            sumCount++
        }
        val focusX = sumX / sumCount
        val focusY = sumY / sumCount
        return focusX to focusY
    }

    private fun MotionEvent.touchSpan(
        currentFocusX: Float,
        currentFocusY: Float
    ): Float {
        var spanSumX = 0f
        var spanSumY = 0f
        var sumCount = 0
        val ignoreIndex = if (actionMasked == MotionEvent.ACTION_POINTER_UP) actionIndex else -1
        for (pointerIndex in 0 until pointerCount) {
            if (pointerIndex == ignoreIndex) continue
            spanSumX += abs(currentFocusX - getX(pointerIndex))
            spanSumY += abs(currentFocusY - getY(pointerIndex))
            sumCount++
        }
        if (sumCount > 1) {
            val spanX = spanSumX / sumCount
            val spanY = spanSumY / sumCount
            return spanX + spanY
        }
        return mPreviousTouchSpan
    }

    private fun scaling(currentTouchSpan: Float): Float {
        return currentTouchSpan / mPreviousTouchSpan
    }

    private fun MotionEvent.rotation(
        currentFocusX: Float,
        currentFocusY: Float
    ): Float {
        var rotationSum = 0f
        var weightSum = 0f
        for (pointerIndex in 0 until pointerCount) {
            val pointerId = getPointerId(pointerIndex)
            val x1 = getX(pointerIndex)
            val y1 = getY(pointerIndex)
            val (x2, y2) = mPointerMap[pointerId] ?: continue
            val dx1 = x1 - currentFocusX
            val dy1 = y1 - currentFocusY
            val dx2 = x2 - currentFocusX
            val dy2 = y2 - currentFocusY
            // dot product is proportional to the cosine of the angle
            // the determinant is proportional to its sine
            // sign of the rotation tells if it is clockwise or counter-clockwise
            val dot = dx1 * dx2 + dy1 * dy2
            val det = dy1 * dx2 - dx1 * dy2
            val rotation = atan2(det, dot)
            val weight = abs(dx1) + abs(dy1)
            rotationSum += rotation * weight
            weightSum += weight
        }
        if (weightSum > 0f) {
            val rotation = rotationSum / weightSum
            return rotation * 180f / PI.toFloat()
        }
        return 0f
    }

    private fun translation(
        currentFocusX: Float,
        currentFocusY: Float
    ): Pair<Float, Float> {
        return (currentFocusX - mPreviousFocusX) to (currentFocusY - mPreviousFocusY)
    }

    private fun MotionEvent.savePointers() {
        mPointerMap.clear()
        for (pointerIndex in 0 until pointerCount) {
            val id = getPointerId(pointerIndex)
            val x = getX(pointerIndex)
            val y = getY(pointerIndex)
            mPointerMap[id] = x to y
        }
    }

    interface Listener {
        fun onZoom(scaling: Float, rotation: Float, translation: Position, pivot: Position)
    }

}

typealias Position = Pair<Float, Float>

我在一个 FrameLayout 中使用了 ZoomGestureDetector,如下所示。

import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout

class ZoomLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),
    ZoomGestureDetector.Listener {

    private val gestureDetector = ZoomGestureDetector(this)

    var isZoomEnabled
        get() = gestureDetector.isZoomEnabled
        set(value) {
            gestureDetector.isZoomEnabled = value
        }

    var isScaleEnabled
        get() = gestureDetector.isScaleEnabled
        set(value) {
            gestureDetector.isScaleEnabled = value
        }

    var isRotationEnabled
        get() = gestureDetector.isRotationEnabled
        set(value) {
            gestureDetector.isRotationEnabled = value
        }

    var isTranslationEnabled
        get() = gestureDetector.isTranslationEnabled
        set(value) {
            gestureDetector.isTranslationEnabled = value
        }

    var isFlingEnabled
        get() = gestureDetector.isFlingEnabled
        set(value) {
            gestureDetector.isFlingEnabled = value
        }

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        if (isZoomEnabled) return true
        gestureDetector.updateTouchLocation(event)
        return super.onInterceptTouchEvent(event)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        return gestureDetector.onTouchEvent(event)
    }

    override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
        gestureDetector.updateCanvasMatrix(canvas)
        return super.drawChild(canvas, child, drawingTime)
    }

    override fun onZoom(scaling: Float, rotation: Float, translation: Position, pivot: Position) {
        invalidate()
    }

}

更新:

我已经在github.com/UdaraWanasinghe/android-transform-layout上发布了一个库。它使用基于变换矩阵的串联属性的不同算法。


1

在Kotlin中自定义缩放视图

 import android.content.Context
 import android.graphics.Matrix
 import android.graphics.PointF
 import android.util.AttributeSet
 import android.util.Log
 import android.view.MotionEvent
 import android.view.ScaleGestureDetector
 import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
 import androidx.appcompat.widget.AppCompatImageView

 class ZoomImageview : AppCompatImageView {
var matri: Matrix? = null
var mode = NONE

// Remember some things for zooming
var last = PointF()
var start = PointF()
var minScale = 1f
var maxScale = 3f
lateinit var m: FloatArray
var viewWidth = 0
var viewHeight = 0
var saveScale = 1f
protected var origWidth = 0f
protected var origHeight = 0f
var oldMeasuredWidth = 0
var oldMeasuredHeight = 0
var mScaleDetector: ScaleGestureDetector? = null
var contex: Context? = null

constructor(context: Context) : super(context) {
    sharedConstructing(context)
}

constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
    sharedConstructing(context)
}

private fun sharedConstructing(context: Context) {
    super.setClickable(true)
    this.contex= context
    mScaleDetector = ScaleGestureDetector(context, ScaleListener())
    matri = Matrix()
    m = FloatArray(9)
    imageMatrix = matri
    scaleType = ScaleType.MATRIX
    setOnTouchListener { v, event ->
        mScaleDetector!!.onTouchEvent(event)
        val curr = PointF(event.x, event.y)
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                last.set(curr)
                start.set(last)
                mode = DRAG
            }
            MotionEvent.ACTION_MOVE -> if (mode == DRAG) {
                val deltaX = curr.x - last.x
                val deltaY = curr.y - last.y
                val fixTransX = getFixDragTrans(deltaX, viewWidth.toFloat(), origWidth * saveScale)
                val fixTransY = getFixDragTrans(deltaY, viewHeight.toFloat(), origHeight * saveScale)
                matri!!.postTranslate(fixTransX, fixTransY)
                fixTrans()
                last[curr.x] = curr.y
            }
            MotionEvent.ACTION_UP -> {
                mode = NONE
                val xDiff = Math.abs(curr.x - start.x).toInt()
                val yDiff = Math.abs(curr.y - start.y).toInt()
                if (xDiff < CLICK && yDiff < CLICK) performClick()
            }
            MotionEvent.ACTION_POINTER_UP -> mode = NONE
        }
        imageMatrix = matri
        invalidate()
        true // indicate event was handled
    }
}

fun setMaxZoom(x: Float) {
    maxScale = x
}

private inner class ScaleListener : SimpleOnScaleGestureListener() {
    override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
        mode = ZOOM
        return true
    }

    override fun onScale(detector: ScaleGestureDetector): Boolean {
        var mScaleFactor = detector.scaleFactor
        val origScale = saveScale
        saveScale *= mScaleFactor
        if (saveScale > maxScale) {
            saveScale = maxScale
            mScaleFactor = maxScale / origScale
        } else if (saveScale < minScale) {
            saveScale = minScale
            mScaleFactor = minScale / origScale
        }
        if (origWidth * saveScale <= viewWidth || origHeight * saveScale <= viewHeight) matri!!.postScale(mScaleFactor, mScaleFactor, viewWidth / 2.toFloat(), viewHeight / 2.toFloat()) else matri!!.postScale(mScaleFactor, mScaleFactor, detector.focusX, detector.focusY)
        fixTrans()
        return true
    }
}

fun fixTrans() {
    matri!!.getValues(m)
    val transX = m[Matrix.MTRANS_X]
    val transY = m[Matrix.MTRANS_Y]
    val fixTransX = getFixTrans(transX, viewWidth.toFloat(), origWidth * saveScale)
    val fixTransY = getFixTrans(transY, viewHeight.toFloat(), origHeight * saveScale)
    if (fixTransX != 0f || fixTransY != 0f) matri!!.postTranslate(fixTransX, fixTransY)
}

fun getFixTrans(trans: Float, viewSize: Float, contentSize: Float): Float {
    val minTrans: Float
    val maxTrans: Float
    if (contentSize <= viewSize) {
        minTrans = 0f
        maxTrans = viewSize - contentSize
    } else {
        minTrans = viewSize - contentSize
        maxTrans = 0f
    }
    if (trans < minTrans) return -trans + minTrans
    if (trans > maxTrans) return -trans + maxTrans
    return 0f
}

fun getFixDragTrans(delta: Float, viewSize: Float, contentSize: Float): Float {
    if (contentSize <= viewSize) {
        return 0f
    } else {
        return delta
    }
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    viewWidth = MeasureSpec.getSize(widthMeasureSpec)
    viewHeight = MeasureSpec.getSize(heightMeasureSpec)
    //
    // Rescales image on rotation
    //
    if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight || viewWidth == 0 || viewHeight == 0) return
    oldMeasuredHeight = viewHeight
    oldMeasuredWidth = viewWidth
    if (saveScale == 1f) {
        //Fit to screen.
        val scale: Float
        val drawable = drawable
        if (drawable == null || drawable.intrinsicWidth == 0 || drawable.intrinsicHeight == 0) return
        val bmWidth = drawable.intrinsicWidth
        val bmHeight = drawable.intrinsicHeight
        Log.d("bmSize", "bmWidth: $bmWidth bmHeight : $bmHeight")
        val scaleX = viewWidth.toFloat() / bmWidth.toFloat()
        val scaleY = viewHeight.toFloat() / bmHeight.toFloat()
        scale = Math.min(scaleX, scaleY)
        matri!!.setScale(scale, scale)
        // Center the image
        var redundantYSpace = viewHeight.toFloat() - scale * bmHeight.toFloat()
        var redundantXSpace = viewWidth.toFloat() - scale * bmWidth.toFloat()
        redundantYSpace /= 2.toFloat()
        redundantXSpace /= 2.toFloat()
        matri!!.postTranslate(redundantXSpace, redundantYSpace)
        origWidth = viewWidth - 2 * redundantXSpace
        origHeight = viewHeight - 2 * redundantYSpace
        imageMatrix = matri
    }
    fixTrans()
}

companion object {
    // We can be in one of these 3 states
    const val NONE = 0
    const val DRAG = 1
    const val ZOOM = 2
    const val CLICK = 3
}
 }

没有双击缩放吗? - Kishan Solanki

0
我使用Zoomageview为ImageView编写了捏合缩放的代码,因此用户可以将图像拖动到屏幕外并对其进行缩放。你可以点击这个链接获取逐步代码,并查看输出截图。

https://dev59.com/SGw15IYBdhLWcg3wYasx#58074642


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接