Canvas - 在安卓上进行缩放、移动和缩放

18
我目前正在对网页视图中的一张图片(下面是大象)实现绘图功能。我在绘制方面没有问题,但缩放功能却会对绘图产生一些奇怪的影响(如下面的第二张图所示)。当我放大时,绘图不会跟着缩放而是会移动位置。同时,在缩放后进行绘图也不起作用。我的代码如下:

Normal

enter image description here

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.save();
    canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
    canvas.drawPath(drawPath, drawPaint);
    canvas.scale(mScaleFactor, mScaleFactor);
    canvas.restore();
    clipBounds = canvas.getClipBounds();
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    mScaleDetector.onTouchEvent(event);
    float touchX = event.getX() / mScaleFactor + clipBounds.left;
    float touchY = event.getY() / mScaleFactor + clipBounds.top;
    if (Deal.on)
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                drawPath.moveTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_MOVE:
                drawPath.lineTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_UP:
                drawCanvas.drawPath(drawPath, drawPaint);
                drawPath.reset();
                break;
            default:
                return false;
    }
    else {
        super.onTouchEvent(event);
    }
    invalidate();
    return true;
}

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

        // Don't let the object get too small or too large.
        mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));

        invalidate();

        return true;
    }
}

编辑: 我使用GestureDetector的比例因子使画布重新绘制,实现了缩放效果。此外,我有一个开关,可以从绘图切换到缩放/WebView控件。遇到的问题是WebView上的双击不会触发onScale手势,这意味着画布不会重新绘制,在缩放时会产生偏移。

我需要实现一个功能,检测双击缩放对比例因子的影响程度。如果有人能提出解决方案,请告诉我。以下是更新代码:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    clipBounds = canvas.getClipBounds();
    canvas.save();
    drawPaint.setStrokeWidth(8/mScaleFactor);
    canvas.scale(mScaleFactor, mScaleFactor, 0, 0);
    canvas.drawPath(drawPath, drawPaint);
    canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
    canvas.restore();
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    pointerCount = event.getPointerCount();
    mScaleDetector.onTouchEvent(event);
    float touchX = (event.getX() + clipBounds.left) / mScaleFactor;
    float touchY = (event.getY() + clipBounds.top) / mScaleFactor;
    if (Deal.on){
        if (event.getPointerCount() > 1){
            return false;
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                drawPath.moveTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_MOVE:
                drawPath.lineTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_UP:
                drawCanvas.drawPath(drawPath, drawPaint);
                drawPath.reset();
                break;
            default:
                return false;
        }
    }
    else {
        super.onTouchEvent(event);
    }
    invalidate();
    return true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on our minimum
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

   //int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(MeasureSpec.getSize(w), heightMeasureSpec, 0);
   setMeasuredDimension(w, h);
}

public class ScaleGestureListener extends SimpleOnScaleGestureListener {

    @Override
    public boolean onScale(ScaleGestureDetector detector) {

        // Don't let the object get too small or too large.
        if(Deal.on == false) {
            mScaleFactor *= detector.getScaleFactor();
            mScaleFactor = Math.max(1f, Math.min(mScaleFactor, 20.0f));
        }

        invalidate();

        return true;
    }
}

2
你在使用画布进行任何绘制之前,尝试过对比例进行操作吗? - Gil Moshayof
1
问题是什么?为什么绘图没有缩放或者为什么绘图不能正常工作?显然,您每次都使用相同的画笔进行绘制,那为什么需要缩放呢? - sandrstar
你为什么在 doubleTap 时缩放,并且它是在哪里实现的?如果这样做,你必须相应地改变 scaleFactor。 - Tore Rudberg
我之所以关注双击操作,是因为我正在一个 WebView 上进行绘图。 - ono
由于您正在自己实现缩放功能,我认为您应该禁用默认的Webview缩放,实现自己的onTouchEvent,就像您现在所做的那样,但不要调用super。请查看此问题以获取更多信息。http://stackoverflow.com/questions/8049343/how-to-control-zoom-in-webview - Tore Rudberg
显示剩余3条评论
5个回答

3
我已经实现了这个功能,但是使用了一种稍微不同的方式。我使用矩阵来处理所有的缩放、滚动(以及旋转,在我的情况下)。它使代码整洁,而且运行非常稳定。不过,我不知道是什么导致了你奇怪的行为。
将一个矩阵和另一个路径作为类成员进行存储:
Matrix drawMatrix = new Matrix();
Path transformedPath = new Path();

替换您的onScale:

@Override
public boolean onScale(ScaleGestureDetector detector) {
    Matrix transformationMatrix = new Matrix();

    //Zoom focus is where the fingers are centered, 
    transformationMatrix.postTranslate(-detector.getFocusX(), -detector.getFocusY());

    transformationMatrix.postScale(detector.getScaleFactor(), detector.getScaleFactor());

/* Using getFocuShift allows for scrolling with two pointers down. Remove it to skip this functionality */
    transformationMatrix.postTranslate(detector.getFocusX() + detector.getFocusShiftX(), detector.getFocusY() + detector.getFocusShiftY());

    drawMatrix.postConcat(transformationMatrix);
    invalidate();
    return true;
}

在onDraw方法中,跳过保存画布的步骤,改为:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawBitmap(canvasBitmap, drawMatrix, canvasPaint);
    transformedPath.rewind();
    transformedMatrix.addPath(drawPath);
    transformedPath.transform(drawMatrix, null);
    canvas.drawPath(transformedPath, drawPaint);
}

愉快的编码!


实际上,这并不会真正缩放路径。它将在缩放后的位置绘制,但线宽将与原始线宽相同。这就是我做这个的原因,但也许不是你想要的。 - Tore Rudberg
线条宽度应随缩放比例而变化;如果放大,则应变粗。 - ono
好吧,这并不美观,但是您可以根据缩放级别更改线宽以实现此效果。 - Tore Rudberg
你真正想问的是如何获取缩放比例?如果你在 WebView 上双击,它会自动缩放。有没有办法获取这个缩放比例呢? - ono

1
一种简单的实现各种不同效果的方法是使用画布(canvas)方法。该方法允许您使用canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)函数,将整个源位图或其部分(矩形src)按照所需投影(矩形dst),自动进行缩放/平移矩阵计算,如下例所示:此示例仅对整个图像进行缩放/缩放操作...
     Canvas canvas = null;
     Bitmap elephantBmp = null;
     Rect src = new Rect();
     Rect postRect = new Rect();
     Paint paintIfAny = new Paint();

     //To scale the whole image, first take the whole source and then postRect with a bigger size... 
     src.top = src.left = 0;
     src.right = elephantBmp.getWidth();
     src.bottom = elephantBmp.getHeight();
     //Here you have the chance to translate / scale the image(in this case i will not translate it but just scale it...)
     postRect.top = postRect.left = 0;
     postRect.right = (int)(elephantBmp.getWidth() * 1.1F);
     postRect.bottom = (int)(elephantBmp.getHeight() * 1.1F);

     canvas.drawBitmap(elephantBmp, src, postRect, paintIfAny);

这个例子仅仅采用了图像的一部分,并展示了“内部缩放效果”。
     Canvas canvas = null;
     Bitmap elephantBmp = null;
     Rect src = new Rect();
     Rect postRect = new Rect();
     Paint paintIfAny = new Paint();

    //To shift the whole image, first take the part of the original source image you want to show 
     src.top = topPosition;//Top coordinate of piece of image to show
     src.left = leftPosition;//Left coordinate of piece of image to show
     src.right = rightPosition;//Right coordinate of piece of image to show
     src.bottom = bottomPosition;//Bottom coordinate of piece of image to show
     //Here you have the chance to show it as big as you want...
     postRect.top = postRect.left = 0;
     postRect.right = (int)(src.width() * 1.1F);
     postRect.bottom = (int)(src.height() * 1.1F);

     canvas.drawBitmap(elephantBmp, src, postRect, paintIfAny);

通过利用这些 src/dst 对象,您可以在Android上实现几乎任何想要的效果,它是一个简单而强大的工具。希望这有所帮助。祝好!

0
check below code will help you.

//CUSTOM IMAGEVIEW
import java.io.ByteArrayOutputStream;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.ImageView;

public class ScaleImageView extends ImageView implements OnTouchListener {

static final float STROKE_WIDTH = 10f;
static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2;

float lastTouchX;
float lastTouchY;
final RectF dirtyRect = new RectF();

private Context mContext;
private float MAX_SCALE = 2f;

private static Matrix mMatrix;
private final float[] mMatrixValues = new float[9];

// display width height.
private int mWidth;
private int mHeight;

private int mIntrinsicWidth;
private int mIntrinsicHeight;

private float mScale;
private float mMinScale;

private float mPrevDistance;
private boolean isScaling;

private int mPrevMoveX;
private int mPrevMoveY;
private GestureDetector mDetector;

Paint paint = new Paint();
public static Path path = new Path();

public static int imageheight, imagewidth;

String TAG = "ScaleImageView";

public ScaleImageView(Context context, AttributeSet attr) {
super(context, attr);
this.mContext = context;
initialize();
}

public ScaleImageView(Context context) {
super(context);
this.mContext = context;
initialize();
}

private void resetDirtyRect(float eventX, float eventY) {
dirtyRect.left = Math.min(lastTouchX, eventX);
dirtyRect.right = Math.max(lastTouchX, eventX);
dirtyRect.top = Math.min(lastTouchY, eventY);
dirtyRect.bottom = Math.max(lastTouchY, eventY);
}

@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
this.initialize();
}

@Override
public void setImageResource(int resId) {
super.setImageResource(resId);
this.initialize();
}

private void initialize() {
this.setScaleType(ScaleType.MATRIX);
this.mMatrix = new Matrix();
Drawable d = getDrawable();

paint.setAntiAlias(true);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(STROKE_WIDTH);

if (d != null) {
    mIntrinsicWidth = d.getIntrinsicWidth();
    mIntrinsicHeight = d.getIntrinsicHeight();
    setOnTouchListener(this);
}
mDetector = new GestureDetector(mContext,
        new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                maxZoomTo((int) e.getX(), (int) e.getY());
                cutting();
                return super.onDoubleTap(e);
            }
        });

}

@Override
protected boolean setFrame(int l, int t, int r, int b) {
mWidth = r - l;
mHeight = b - t;

mMatrix.reset();
int r_norm = r - l;
mScale = (float) r_norm / (float) mIntrinsicWidth;

int paddingHeight = 0;
int paddingWidth = 0;
// scaling vertical
if (mScale * mIntrinsicHeight > mHeight) {
    mScale = (float) mHeight / (float) mIntrinsicHeight;
    mMatrix.postScale(mScale, mScale);
    paddingWidth = (r - mWidth) / 2;
    paddingHeight = 0;
    // scaling horizontal
} else {
    mMatrix.postScale(mScale, mScale);
    paddingHeight = (b - mHeight) / 2;
    paddingWidth = 0;
}
mMatrix.postTranslate(paddingWidth, paddingHeight);

setImageMatrix(mMatrix);
mMinScale = mScale;
zoomTo(mScale, mWidth / 2, mHeight / 2);
cutting();
return super.setFrame(l, t, r, b);
}

protected float getValue(Matrix matrix, int whichValue) {
matrix.getValues(mMatrixValues);
return mMatrixValues[whichValue];
}

protected float getScale() {
return getValue(mMatrix, Matrix.MSCALE_X);
}

public float getTranslateX() {
return getValue(mMatrix, Matrix.MTRANS_X);
}

protected float getTranslateY() {
return getValue(mMatrix, Matrix.MTRANS_Y);
}

protected void maxZoomTo(int x, int y) {
if (mMinScale != getScale() && (getScale() - mMinScale) > 0.1f) {
    // threshold 0.1f
    float scale = mMinScale / getScale();
    zoomTo(scale, x, y);
} else {
    float scale = MAX_SCALE / getScale();
    zoomTo(scale, x, y);
}
}

public void zoomTo(float scale, int x, int y) {
if (getScale() * scale < mMinScale) {
    return;
}
if (scale >= 1 && getScale() * scale > MAX_SCALE) {
    return;
}
mMatrix.postScale(scale, scale);
// move to center
mMatrix.postTranslate(-(mWidth * scale - mWidth) / 2,
        -(mHeight * scale - mHeight) / 2);

// move x and y distance
mMatrix.postTranslate(-(x - (mWidth / 2)) * scale, 0);
mMatrix.postTranslate(0, -(y - (mHeight / 2)) * scale);
setImageMatrix(mMatrix);
}

public void cutting() {
int width = (int) (mIntrinsicWidth * getScale());
int height = (int) (mIntrinsicHeight * getScale());

imagewidth = width;
imageheight = height;

if (getTranslateX() < -(width - mWidth)) {
    mMatrix.postTranslate(-(getTranslateX() + width - mWidth), 0);
}
if (getTranslateX() > 0) {
    mMatrix.postTranslate(-getTranslateX(), 0);
}
if (getTranslateY() < -(height - mHeight)) {
    mMatrix.postTranslate(0, -(getTranslateY() + height - mHeight));
}
if (getTranslateY() > 0) {
    mMatrix.postTranslate(0, -getTranslateY());
}
if (width < mWidth) {
    mMatrix.postTranslate((mWidth - width) / 2, 0);
}
if (height < mHeight) {
    mMatrix.postTranslate(0, (mHeight - height) / 2);
}
setImageMatrix(mMatrix);
}

private float distance(float x0, float x1, float y0, float y1) {
float x = x0 - x1;
float y = y0 - y1;
return FloatMath.sqrt(x * x + y * y);
}

private float dispDistance() {
return FloatMath.sqrt(mWidth * mWidth + mHeight * mHeight);
}

public void clear() {
path.reset();
invalidate();
}

public static void save() {

Bitmap returnedBitmap = Bitmap.createBitmap(
        ScaleImageViewActivity.imageview.getWidth(),
        ScaleImageViewActivity.imageview.getHeight(),
        Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(returnedBitmap);

Drawable bgDrawable = ScaleImageViewActivity.imageview.getDrawable();

if (bgDrawable != null)
    bgDrawable.draw(canvas);
else
    canvas.drawColor(Color.WHITE);

ScaleImageViewActivity.imageview.draw(canvas);

ByteArrayOutputStream bs = new ByteArrayOutputStream();
returnedBitmap.compress(Bitmap.CompressFormat.PNG, 50, bs);

Bitmap FinalBitmap = BitmapFactory.decodeByteArray(bs.toByteArray(), 0,
        bs.toByteArray().length);

ScaleImageViewActivity.imageview.setImageBitmap(FinalBitmap);
path.reset();

// ScaleImageViewActivity.imageview.setImageMatrix(mMatrix);

}

@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!ScaleImageViewActivity.flag) {

    if (mDetector.onTouchEvent(event)) {
        return true;
    }
    int touchCount = event.getPointerCount();
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    case MotionEvent.ACTION_POINTER_1_DOWN:
    case MotionEvent.ACTION_POINTER_2_DOWN:
        if (touchCount >= 2) {
            float distance = distance(event.getX(0), event.getX(1),
                    event.getY(0), event.getY(1));
            mPrevDistance = distance;
            isScaling = true;
        } else {
            mPrevMoveX = (int) event.getX();
            mPrevMoveY = (int) event.getY();
        }
    case MotionEvent.ACTION_MOVE:
        if (touchCount >= 2 && isScaling) {
            float dist = distance(event.getX(0), event.getX(1),
                    event.getY(0), event.getY(1));
            float scale = (dist - mPrevDistance) / dispDistance();
            mPrevDistance = dist;
            scale += 1;
            scale = scale * scale;
            zoomTo(scale, mWidth / 2, mHeight / 2);
            cutting();
        } else if (!isScaling) {
            int distanceX = mPrevMoveX - (int) event.getX();
            int distanceY = mPrevMoveY - (int) event.getY();
            mPrevMoveX = (int) event.getX();
            mPrevMoveY = (int) event.getY();
            mMatrix.postTranslate(-distanceX, -distanceY);
            cutting();
        }
        break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_POINTER_UP:
    case MotionEvent.ACTION_POINTER_2_UP:
        if (event.getPointerCount() <= 1) {
            isScaling = false;
        }
        break;
    }
} else {
    float eventX = event.getX();
    float eventY = event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        path.moveTo(eventX, eventY);
        lastTouchX = eventX;
        lastTouchY = eventY;
        return true;

    case MotionEvent.ACTION_MOVE:

    case MotionEvent.ACTION_UP:

        resetDirtyRect(eventX, eventY);
        int historySize = event.getHistorySize();
        for (int i = 0; i < historySize; i++) {
            float historicalX = event.getHistoricalX(i);
            float historicalY = event.getHistoricalY(i);
            path.lineTo(historicalX, historicalY);
        }
        path.lineTo(eventX, eventY);
        break;
    }

    invalidate((int) (dirtyRect.left - HALF_STROKE_WIDTH),
            (int) (dirtyRect.top - HALF_STROKE_WIDTH),
            (int) (dirtyRect.right + HALF_STROKE_WIDTH),
            (int) (dirtyRect.bottom + HALF_STROKE_WIDTH));

    lastTouchX = eventX;
    lastTouchY = eventY;
}
return true;
}

@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
if (ScaleImageViewActivity.flag)
    canvas.drawPath(path, paint);
}

@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
return super.onTouchEvent(event);
}

}

//ACTIVITY

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class ScaleImageViewActivity extends Activity implements OnClickListener {

Button btndraw, btnzoom, btnsave;

public static ScaleImageView imageview;
public static boolean flag = true;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
initwidget();
}

private void initwidget() {

imageview = (ScaleImageView) findViewById(R.id.image);

btnsave = (Button) findViewById(R.id.activity_main_save);
btndraw = (Button) findViewById(R.id.activity_main_zoom_draw);
btnzoom = (Button) findViewById(R.id.activity_main_zoom_zoom);

btndraw.setOnClickListener(this);
btnzoom.setOnClickListener(this);
btnsave.setOnClickListener(this);
}

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
if (btndraw.equals(arg0)) {
    flag = true;
} else if (btnzoom.equals(arg0)) {
    flag = false;
} else if (btnsave.equals(arg0)) {
    ScaleImageView.save();
}
}
}
main.xml

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

<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content" >

<Button
    android:id="@+id/activity_main_zoom_zoom"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Zoom" />

<Button
    android:id="@+id/activity_main_zoom_draw"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Draw" />

<Button
    android:id="@+id/activity_main_save"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Save" />
</LinearLayout>

<com.matabii.dev.scaleimageview.ScaleImageView
android:id="@+id/image"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:src="@drawable/sample" />

</LinearLayout>

0
你能否尝试只绘制一次路径,将缩放的责任交给画布本身?
private boolean pathsDrawn;

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    clipBounds = canvas.getClipBounds();
    canvas.save();
    drawPaint.setStrokeWidth(8/mScaleFactor);
    canvas.scale(mScaleFactor, mScaleFactor, 0, 0);

    if(!pathsDrawn) {
       canvas.drawPath(drawPath, drawPaint);
       pathsDrawn = true;
    }
    canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
    canvas.restore();
}

这只是在我放开手指时绘制路径。它不会影响缩放。 - ono

0

看一下这个tutorial。在这里,作者使用了两个矩阵对象来管理翻译和缩放。我建议你使用矩阵,否则计算新坐标可能会很棘手和繁琐。你的问题是由于没有正确管理翻译而引起的。注意:在教程中,在OnDraw()函数内部,它缺少canvas.concat(matrix),请在canvas.drawBitmap(...)之后添加。


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