安卓ImageView缩放和平移问题

10
我正在开发一个安卓应用程序(API 19 4.4),并且在ImageViews上遇到了一些问题。我有一个SurfaceView,在其中动态添加ImageViews,我希望它们能够对触摸事件做出反应。到目前为止,我已经成功地使ImageView平滑移动和缩放,但是我有一个令人讨厌的行为。
当我将图像缩小到一定限度(我会说是原始大小的一半)并尝试移动它时,图像会闪烁。经过简短的分析,似乎它在手指在屏幕上的位置对称地切换其位置,累积距离,最终消失在视线之外(所有这些都发生得非常快(<1s)。我认为我在触摸事件相对于ImageView / SurfaceView的相对值方面存在问题,但我是个新手,陷入了困境...
以下是我的代码:
public class MyImageView extends ImageView {
private ScaleGestureDetector mScaleDetector ;
private static final int MAX_SIZE = 1024;

private static final String TAG = "MyImageView";
PointF DownPT = new PointF(); // Record Mouse Position When Pressed Down
PointF StartPT = new PointF(); // Record Start Position of 'img'

public MyImageView(Context context) {
    super(context);
    mScaleDetector = new ScaleGestureDetector(context,new MySimpleOnScaleGestureListener());
    setBackgroundColor(Color.RED);
    setScaleType(ScaleType.MATRIX);
    setAdjustViewBounds(true);
    RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);

    lp.setMargins(-MAX_SIZE, -MAX_SIZE, -MAX_SIZE, -MAX_SIZE);
    this.setLayoutParams(lp);
    this.setX(MAX_SIZE);
    this.setY(MAX_SIZE);

}

int firstPointerID;
boolean inScaling=false;
@Override
public boolean onTouchEvent(MotionEvent event) {
    // get pointer index from the event object
    int pointerIndex = event.getActionIndex();
    // get pointer ID
    int pointerId = event.getPointerId(pointerIndex);
    //First send event to scale detector to find out, if it's a scale
    boolean res = mScaleDetector.onTouchEvent(event);

    if (!mScaleDetector.isInProgress()) {
        int eid = event.getAction();
        switch (eid & MotionEvent.ACTION_MASK)
        {
        case MotionEvent.ACTION_MOVE :
            if(pointerId == firstPointerID) {

                PointF mv = new PointF( (int)(event.getX() - DownPT.x), (int)( event.getY() - DownPT.y));

                this.setX((int)(StartPT.x+mv.x));
                this.setY((int)(StartPT.y+mv.y));
                StartPT = new PointF( this.getX(), this.getY() );

            }
            break;
        case MotionEvent.ACTION_DOWN : {
            firstPointerID = pointerId;
            DownPT.x = (int) event.getX();
            DownPT.y = (int) event.getY();
            StartPT = new PointF( this.getX(), this.getY() );
            break;
        }
        case MotionEvent.ACTION_POINTER_DOWN: {
            break;
        }
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_POINTER_UP:
        case MotionEvent.ACTION_CANCEL: {
            firstPointerID = -1;
            break;
        }
        default :
            break;
        }
        return true;
    }
    return true;

}

public boolean onScaling(ScaleGestureDetector detector) {

    this.setScaleX(this.getScaleX()*detector.getScaleFactor());
    this.setScaleY(this.getScaleY()*detector.getScaleFactor());
    invalidate();
    return true;
}

private class MySimpleOnScaleGestureListener extends SimpleOnScaleGestureListener {


    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        return onScaling(detector);
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        Log.d(TAG, "onScaleBegin");
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector arg0) {
        Log.d(TAG, "onScaleEnd");
    }
}

我还有另一个关于旋转的问题。我应该如何实现它? 我能否以某种方式使用ScalegestureDetector,或者必须在视图触摸事件中进行操作?我希望能够在同一手势中缩放和旋转(在另一个手势中移动)。

谢谢您的帮助,我将不胜感激!

对不起,我的英语不好。


使用矩阵(canvas.drawBitmap(Bitmap, Matrix, Paint)) - pskink
我的意思是:我如何检测我的手指正在与哪个位图交互? - Tifoo
看看我在其中一个项目中是如何做到的:https://github.com/pskink/PatchworkDrawable/blob/master/PatchworkDrawableLibrary/src/org/pskink/patchworkdrawable/drawable/PatchworkDrawable.java,在getLayersAt()方法中。 - pskink
谢谢您的代码,但是:-不推荐使用ImageView吗?它似乎比我的代码复杂得多,而且我仍然不明白如何使每个位图只需跟随手指移动,或者在捏取时缩放/旋转。 - Tifoo
复杂吗?你所需做的只是对矩阵进行缩放/平移/旋转,简单、明显易懂。顺便提一下,使用矩阵你可以在位图上应用任何SRD动画:只需大约十行代码即可。 - pskink
显示剩余9条评论
4个回答

24

这是一个双指移动/缩放/旋转的工作示例(注意:由于使用了智能检测器,请参见MatrixGestureDetector,因此代码非常简短):

class ViewPort extends View {
    List<Layer> layers = new LinkedList<Layer>();
    int[] ids = {R.drawable.layer0, R.drawable.layer1, R.drawable.layer2};

    public ViewPort(Context context) {
        super(context);
        Resources res = getResources();
        for (int i = 0; i < ids.length; i++) {
            Layer l = new Layer(context, this, BitmapFactory.decodeResource(res, ids[i]));
            layers.add(l);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        for (Layer l : layers) {
            l.draw(canvas);
        }
    }

    private Layer target;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            target = null;
            for (int i = layers.size() - 1; i >= 0; i--) {
                Layer l = layers.get(i);
                if (l.contains(event)) {
                    target = l;
                    layers.remove(l);
                    layers.add(l);
                    invalidate();
                    break;
                }
            }
        }
        if (target == null) {
            return false;
        }
        return target.onTouchEvent(event);
    }
}

class Layer implements MatrixGestureDetector.OnMatrixChangeListener {
    Matrix matrix = new Matrix();
    Matrix inverse = new Matrix();
    RectF bounds;
    View parent;
    Bitmap bitmap;
    MatrixGestureDetector mgd = new MatrixGestureDetector(matrix, this);

    public Layer(Context ctx, View p, Bitmap b) {
        parent = p;
        bitmap = b;
        bounds = new RectF(0, 0, b.getWidth(), b.getHeight());
        matrix.postTranslate(50 + (float) Math.random() * 50, 50 + (float) Math.random() * 50);
    }

    public boolean contains(MotionEvent event) {
        matrix.invert(inverse);
        float[] pts = {event.getX(), event.getY()};
        inverse.mapPoints(pts);
        if (!bounds.contains(pts[0], pts[1])) {
            return false;
        }
        return Color.alpha(bitmap.getPixel((int) pts[0], (int) pts[1])) != 0;
    }

    public boolean onTouchEvent(MotionEvent event) {
        mgd.onTouchEvent(event);
        return true;
    }

    @Override
    public void onChange(Matrix matrix) {
        parent.invalidate();
    }

    public void draw(Canvas canvas) {
        canvas.drawBitmap(bitmap, matrix, null);
    }
}

class MatrixGestureDetector {
    private static final String TAG = "MatrixGestureDetector";

    private int ptpIdx = 0;
    private Matrix mTempMatrix = new Matrix();
    private Matrix mMatrix;
    private OnMatrixChangeListener mListener;
    private float[] mSrc = new float[4];
    private float[] mDst = new float[4];
    private int mCount;

    interface OnMatrixChangeListener {
        void onChange(Matrix matrix);
    }

    public MatrixGestureDetector(Matrix matrix, MatrixGestureDetector.OnMatrixChangeListener listener) {
        this.mMatrix = matrix;
        this.mListener = listener;
    }

    public void onTouchEvent(MotionEvent event) {
        if (event.getPointerCount() > 2) {
            return;
        }

        int action = event.getActionMasked();
        int index = event.getActionIndex();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
                int idx = index * 2;
                mSrc[idx] = event.getX(index);
                mSrc[idx + 1] = event.getY(index);
                mCount++;
                ptpIdx = 0;
                break;

            case MotionEvent.ACTION_MOVE:
                for (int i = 0; i < mCount; i++) {
                    idx = ptpIdx + i * 2;
                    mDst[idx] = event.getX(i);
                    mDst[idx + 1] = event.getY(i);
                }
                mTempMatrix.setPolyToPoly(mSrc, ptpIdx, mDst, ptpIdx, mCount);
                mMatrix.postConcat(mTempMatrix);
                if(mListener != null) {
                    mListener.onChange(mMatrix);
                }
                System.arraycopy(mDst, 0, mSrc, 0, mDst.length);
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                if (event.getPointerId(index) == 0) ptpIdx = 2;
                mCount--;
                break;
        }
    }
}

你好,抱歉回复晚了。再次感谢你的时间。我通过设置布局参数而不是使用“setscale”来缩放视图解决了我的问题。Universalimageloader加载程序可以很好地处理位图。 - Tifoo
4
这段代码真是杰作啊 :) 做得很漂亮,谢谢分享 :) - ik024
希望我能给这个点赞一百万次。已经寻找了几周一个简单的解决方案... - LukeWaggoner
我在ViewPort中添加了一个方法,允许用户从MainActivity添加图像。不幸的是,在我的方法中调用invalidate()时,onDraw没有被调用,因此图像没有出现: public void addLayer(Integer id){ Resources res = getResources(); ids.add(id); Layer l = new Layer(mContext, this, BitmapFactory.decodeResource(res, id)); layers.add(l); invalidate(); } - Daniel Viglione
@pskink 你好!我正在尝试将这段代码实现到我的应用程序中。目前我已经将其作为一个类导入了。您能告诉我如何实际使用它吗? - Timothy Bomer
显示剩余10条评论

1

我尝试在视图上而不是位图上使用矩阵实现多点触控,现在我成功了。现在我认为这对于多个图像的单独手势会很有帮助。试试吧,对我来说效果最好。

public class MultiTouchImageView extends ImageView implements OnTouchListener{

float[] lastEvent = null;
float d = 0f;
float newRot = 0f;
public static String fileNAME;
public static int framePos = 0;
//private ImageView view;
private boolean isZoomAndRotate;
private boolean isOutSide;
// 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;

private PointF start = new PointF();
private PointF mid = new PointF();
float oldDist = 1f;
public MultiTouchImageView(Context context) {
    super(context);
}


public MultiTouchImageView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
}


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


@SuppressWarnings("deprecation")
@Override
public boolean onTouch(View v, MotionEvent event) {
    //view = (ImageView) v;
    bringToFront();
    // Handle touch events here...
    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) {
            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:
        isZoomAndRotate = false;
    case MotionEvent.ACTION_OUTSIDE:
        isOutSide = true;
        mode = NONE;
        lastEvent = null;
    case MotionEvent.ACTION_POINTER_UP:
        mode = NONE;
        lastEvent = null;
        break;
    case MotionEvent.ACTION_MOVE:
        if(!isOutSide){
            if (mode == DRAG && !isZoomAndRotate) {
                isZoomAndRotate = false;
                setTranslationX((event.getX() - start.x) + getTranslationX());
                setTranslationY((event.getY() - start.y) + getTranslationY());
            } else if (mode == ZOOM && event.getPointerCount() == 2) {
                isZoomAndRotate = true;
                boolean isZoom = false;
                if(!isRotate(event)){
                    float newDist = spacing(event);
                    if (newDist > 10f) {
                        float scale = newDist / oldDist * getScaleX();
                        setScaleX(scale);
                        setScaleY(scale);
                        isZoom = true;
                    }
                }
                else if(!isZoom){
                    newRot = rotation(event);
                    setRotation((float)(getRotation() + (newRot - d)));
                }
            }
        }

        break;
    }
    new GestureDetector(new MyGestureDectore());
    Constants.currentSticker = this;
    return true;
}
private class MyGestureDectore extends GestureDetector.SimpleOnGestureListener{

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        bringToFront();
        return false;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        return false;
    }

}
private float rotation(MotionEvent event) {
    double delta_x = (event.getX(0) - event.getX(1));
    double delta_y = (event.getY(0) - event.getY(1));
    double radians = Math.atan2(delta_y, delta_x);
    return (float) Math.toDegrees(radians);
}
private float spacing(MotionEvent event) {
    float x = event.getX(0) - event.getX(1);
    float y = event.getY(0) - event.getY(1);
    return FloatMath.sqrt(x * x + y * y);
}

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 boolean isRotate(MotionEvent event){
    int dx1 = (int) (event.getX(0) - lastEvent[0]);
    int dy1 = (int) (event.getY(0) - lastEvent[2]);
    int dx2 = (int) (event.getX(1) - lastEvent[1]);
    int dy2 = (int) (event.getY(1) - lastEvent[3]);
    Log.d("dx1 ", ""+ dx1);
    Log.d("dx2 ", "" + dx2);
    Log.d("dy1 ", "" + dy1);
    Log.d("dy2 ", "" + dy2);
    //pointer 1
    if(Math.abs(dx1) > Math.abs(dy1) && Math.abs(dx2) > Math.abs(dy2)) {
        if(dx1 >= 2.0 && dx2 <=  -2.0){
            Log.d("first pointer ", "right");
            return true;
        }
        else if(dx1 <= -2.0 && dx2 >= 2.0){
            Log.d("first pointer ", "left");
            return true;
        }
    }
    else {
         if(dy1 >= 2.0 && dy2 <=  -2.0){
                Log.d("seccond pointer ", "top");
                return true;
            }
            else if(dy1 <= -2.0 && dy2 >= 2.0){
                Log.d("second pointer ", "bottom");
                return true; 
            }

    }

    return false;
}
}

请问您能否给我提供 setTranslationX()、setTranslationY()、setRotation()、setScaleX() 和 setScaleY() 方法的代码? - Aman Srivastava

0

我最终使用了这个(间距用于计算两个手指之间的距离),在缩放后偏移图像视图以保持其居中,目前运行良好:

    float newDist = spacing(event);
            float scale = newDist / oldDist;

            int oldH =getLayoutParams().height;
            int oldW =getLayoutParams().width;

            int newH =(int) (getLayoutParams().height*scale);
            int newW =(int) (getLayoutParams().width*scale);

            if(newH<MAX_SIZE && newW<MAX_SIZE){
                //scale the height and width of the view
                getLayoutParams().height = newH;
                getLayoutParams().width = newW;

                //calculate the X and Y offset to apply after scaling to keep the image centered
                int xOffset = (int)(getLayoutParams().height - oldH)/2;
                int yOffset = (int)(getLayoutParams().width - oldW)/2;

                setX(getX()-xOffset);
                setY(getY()-yOffset);
                requestLayout();
                setAdjustViewBounds(true);

                oldDist=newDist; 

很棒,但你真的认为它比使用矩阵更紧凑、更简单和更优雅吗? - pskink
实际上我没有,这就是为什么我将你的代码放在我的包里以备将来使用…… 我对矩阵模式的测试在我的视图中缩放了画布,而不是视图本身,也许我漏掉了什么。 - Tifoo
不好意思,我不明白你的意思。顺便问一下,如果两个或更多的ImageView重叠,你的代码还能正常工作吗? - pskink
是的,当多个视图重叠时它可以正常工作(这基本上是我对你的代码担心的问题!)。它可以很好地响应在一个图像上进行两个手指交互,或者在每个图像上使用一个手指同时移动。 - Tifoo
3
你能把你的代码粘贴到pastebin.com上吗?我想检查一下。 - pskink
显示剩余3条评论

0
所有这些示例都存在手势支持故障,因为scaleType被设置为matrix。当我尝试缩放时,无法将图像保持在中心并控制缩放量。因此,我进行了一些研究,并编写了一个小型、简单但非常令人愉悦的代码来解决这个问题:https://stackoverflow.com/a/65697376/13339685

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