I'm trying to create a SurfaceView that can be zoomed and dragged. It implements an HTTP image stream that draws directly into the canvas
I've tried the following code and it kinda work... but it gives me problems in the borders. No idea of the reason why. Any help?
Full stream:
Zoomed image:
In the second image you can see multiple green lines that doesn't need to be there.
This is the class that handles this stream:
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Display;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.SurfaceView;
import android.view.WindowManager;
/**
* Created by fil on 07/12/15.
*/
public class ZoomSurfaceView extends SurfaceView {
//These two constants specify the minimum and maximum zoom
private static float MIN_ZOOM = 1f;
private static float MAX_ZOOM = 5f;
private float scaleFactor = 1.f;
private ScaleGestureDetector detector;
//These constants specify the mode that we're in
private static int NONE = 0;
private static int DRAG = 1;
private static int ZOOM = 2;
private boolean dragged = false;
private float displayWidth;
private float displayHeight;
private int mode;
//These two variables keep track of the X and Y coordinate of the finger when it first
//touches the screen
private float startX = 0f;
private float startY = 0f;
//These two variables keep track of the amount we need to translate the canvas along the X
//and the Y coordinate
private float translateX = 0f;
private float translateY = 0f;
//These two variables keep track of the amount we translated the X and Y coordinates, the last time we
//panned.
private float previousTranslateX = 0f;
private float previousTranslateY = 0f;
private final Paint p = new Paint();
private void init(Context context){
detector = new ScaleGestureDetector(getContext(), new ScaleListener());
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
displayWidth = display.getWidth();
displayHeight = display.getHeight();
}
public ZoomSurfaceView(Context context) {
super(context);
init(context);
}
public ZoomSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ZoomSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public ZoomSurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
public void resetZoom() {
}
public void drawBitmap(Canvas canvas, Bitmap b, Rect rect){
canvas.save();
//If translateX times -1 is lesser than zero, letfs set it to zero. This takes care of the left bound
if((translateX * -1) > (scaleFactor - 1) * displayWidth)
{
translateX = (1 - scaleFactor) * displayWidth;
}
if(translateY * -1 > (scaleFactor - 1) * displayHeight)
{
translateY = (1 - scaleFactor) * displayHeight;
}
//We need to divide by the scale factor here, otherwise we end up with excessive panning based on our zoom level
//because the translation amount also gets scaled according to how much we've zoomed into the canvas.
canvas.translate(translateX / scaleFactor, translateY / scaleFactor);
//We're going to scale the X and Y coordinates by the same amount
canvas.scale(scaleFactor, scaleFactor);
canvas.drawBitmap(b, null, rect, p);
/* The rest of your canvas-drawing code */
canvas.restore();
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener
{
#Override
public boolean onScale(ScaleGestureDetector detector)
{
scaleFactor *= detector.getScaleFactor();
scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
return true;
}
}
#Override
public boolean onTouchEvent(MotionEvent event)
{
switch (event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
mode = DRAG;
//We assign the current X and Y coordinate of the finger to startX and startY minus the previously translated
//amount for each coordinates This works even when we are translating the first time because the initial
//values for these two variables is zero.
startX = event.getX() - previousTranslateX;
startY = event.getY() - previousTranslateY;
break;
case MotionEvent.ACTION_MOVE:
translateX = event.getX() - startX;
translateY = event.getY() - startY;
//We cannot use startX and startY directly because we have adjusted their values using the previous translation values.
//This is why we need to add those values to startX and startY so that we can get the actual coordinates of the finger.
double distance = Math.sqrt(Math.pow(event.getX() - (startX + previousTranslateX), 2) +
Math.pow(event.getY() - (startY + previousTranslateY), 2));
if(distance > 0)
{
dragged = true;
distance *= scaleFactor;
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
break;
case MotionEvent.ACTION_UP:
mode = NONE;
dragged = false;
//All fingers went up, so letfs save the value of translateX and translateY into previousTranslateX and
//previousTranslate
previousTranslateX = translateX;
previousTranslateY = translateY;
break;
case MotionEvent.ACTION_POINTER_UP:
mode = DRAG;
//This is not strictly necessary; we save the value of translateX and translateY into previousTranslateX
//and previousTranslateY when the second finger goes up
previousTranslateX = translateX;
previousTranslateY = translateY;
break;
}
detector.onTouchEvent(event);
//We redraw the canvas only in the following cases:
//
// o The mode is ZOOM
// OR
// o The mode is DRAG and the scale factor is not equal to 1 (meaning we have zoomed) and dragged is
// set to true (meaning the finger has actually moved)
if ((mode == DRAG && scaleFactor != 1f && dragged) || mode == ZOOM)
{
invalidate();
}
return true;
}
}
The code for adding the frames to the surface is the following:
if (!b.isRecycled()){
try {
Rect rect = new Rect(0, 0, frame.getWidth(), frame.getHeight());
Canvas canvas = frame.getHolder().lockCanvas();
synchronized (frame.getHolder()) {
if (!b.isRecycled()) {
frame.drawBitmap(canvas, b, rect);
b.recycle();
}
}
frame.getHolder().unlockCanvasAndPost(canvas);
} catch (java.lang.RuntimeException exc){
Dbg.d("ERROR", exc);
}
lastBitmap = b;
}
The code you posted is incomplete so its difficult to say what the problem is. I did drop your code into a quick demo project and didn't see any issues with the borders.
Just by looking at the screenshots: any chance that your image data is somehow wrapping? The 2nd screenshot looks like the bottom border is being drawn at the top of the image. Again tough to say without reproducible code.
Might try repainting the background before redrawing the bitmap
canvas.drawRect(rect, backgroundPaint);
frame.drawBitmap(canvas, b, rect);
Related
I'm working on a drawing app, where the user can pan & zoom to a specific portion of the screen and start drawing with zoom applied. To be more specific, I'm looking for a way to implement zoom, pinch & pan gesture in Canvas (horizontal and vertical scrolling with moveable x, y coordinates).
Right now, I've successfully developed the zoom only feature but it's not accurate and the pan option is not working.
See my sample code here,
public class DrawingView extends View {
//canvas
private Canvas drawCanvas;
//canvas bitmap
private Bitmap canvasBitmap;
private boolean dragged = false;
private float displayWidth;
private float displayHeight;
//These two constants specify the minimum and maximum zoom
private static float MIN_ZOOM = 1f;
private static float MAX_ZOOM = 5f;
private float scaleFactor = 1.f;
private ScaleGestureDetector detector;
//These constants specify the mode that we're in
private static int NONE = 0;
private static int DRAG = 1;
private static int ZOOM = 2;
private int mode;
public DrawingView(Context context, AttributeSet attrs) {
super(context, attrs);
ctx = context;
setupDrawing();
}
private void setupDrawing() {
detector = new ScaleGestureDetector(getContext(), new ScaleListener());
WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
displayWidth = metrics.widthPixels;
displayHeight = metrics.heightPixels;
setFocusable(true);
setFocusableInTouchMode(true);
}
class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
return true;
}
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
//view given size
super.onSizeChanged(w, h, oldw, oldh);
canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
drawCanvas = new Canvas(canvasBitmap);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
//detect user touch
float x = event.getX();
float y = event.getY();
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
//
downx = event.getX();
downy = event.getY();
mode = DRAG;
//We assign the current X and Y coordinate of the finger to startX and startY minus the previously translated
//amount for each coordinates This works even when we are translating the first time because the initial
//values for these two variables is zero.
startX = event.getX() - previousTranslateX;
startY = event.getY() - previousTranslateY;
break;
case MotionEvent.ACTION_MOVE:
translateX = event.getX() - startX;
translateY = event.getY() - startY;
//We cannot use startX and startY directly because we have adjusted their values using the previous translation values.
//This is why we need to add those values to startX and startY so that we can get the actual coordinates of the finger.
double distance = Math.sqrt(Math.pow(event.getX() - (startX + previousTranslateX), 2) + Math.pow(event.getY() - (startY + previousTranslateY), 2));
if (distance > 0) {
dragged = true;
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
mode = ZOOM;
break;
ase MotionEvent.ACTION_UP:
//
upx = event.getX();
upy = event.getY();
mode = NONE;
dragged = false;
//All fingers went up, so let's save the value of translateX and translateY into previousTranslateX and
//previousTranslate
previousTranslateX = translateX;
previousTranslateY = translateY;
break;
case MotionEvent.ACTION_POINTER_UP:
mode = DRAG;
//This is not strictly necessary; we save the value of translateX and translateY into previousTranslateX
//and previousTranslateY when the second finger goes up
previousTranslateX = translateX;
previousTranslateY = translateY;
break;
detector.onTouchEvent(event);
//We redraw the canvas only in the following cases:
//
// o The mode is ZOOM
// OR
// o The mode is DRAG and the scale factor is not equal to 1 (meaning we have zoomed) and dragged is
// set to true (meaning the finger has actually moved)
if ((mode == DRAG && scaleFactor != 1f && dragged) || mode == ZOOM) {
invalidate();
}
}
#Override
protected void onDraw(Canvas canvas) {
canvas.save();
//If translateX times -1 is lesser than zero, let's set it to zero. This takes care of the left bound
if ((translateX * -1) < 0) {
translateX = 0;
}
//This is where we take care of the right bound. We compare translateX times -1 to (scaleFactor - 1) * displayWidth.
//If translateX is greater than that value, then we know that we've gone over the bound. So we set the value of
//translateX to (1 - scaleFactor) times the display width. Notice that the terms are interchanged; it's the same
//as doing -1 * (scaleFactor - 1) * displayWidth
else if ((translateX * -1) > (scaleFactor - 1) * displayWidth) {
translateX = (1 - scaleFactor) * displayWidth;
}
if (translateY * -1 < 0) {
translateY = 0;
}
//We do the exact same thing for the bottom bound, except in this case we use the height of the display
else if ((translateY * -1) > (scaleFactor - 1) * displayHeight) {
translateY = (1 - scaleFactor) * displayHeight;
}
//We need to divide by the scale factor here, otherwise we end up with excessive panning based on our zoom level
//because the translation amount also gets scaled according to how much we've zoomed into the canvas.
canvas.translate(translateX / scaleFactor, translateY / scaleFactor);
}
//We're going to scale the X and Y coordinates by the same amount
//canvas.scale(scaleFactor, scaleFactor);
canvas.scale(this.scaleFactor, this.scaleFactor, this.detector.getFocusX(), this.detector.getFocusY());
}
I would like to add the ability to zoom the canvas in my application. I explain a bit my code:
When I draw I get the bitmap of the canvas and save it to an ArrayList (this is for undo the draws, I tried with drawing paths and saving to a path arraylist but I cannot draw text, so I used that method).
I have a boolean variable to check if the zoom is enabled or not. This is due because while you press the screen it will be draw something (a line, a cicle...) and I use this variable at onDraw and onTouchEvent for disable the drawing and enable zoom.
When I draw there is no problem but when I tried to use the zoom it does nothing. I was searching a lot about zoom but I cannot implement it into my application. The following code is my last code I tried to implement zoom. I get it from here.
private boolean zoomenable=false;
private ArrayList<Bitmap> bmps=new ArrayList<Bitmap>();
private static float MIN_ZOOM=1f;
private static float MAX_ZOOM=5f;
private float scaleFactor=1.f;
private ScaleGestureDetector detector;
private static int NONE=0;
private static int DRAG=1;
private static int ZOOM=2;
private int mode;
private float startX=0f;
private float startY=0f;
private float translateX=0f;
private float translateY=0f;
private float previusTranslateX=0f;
private float previusTranslateY=0f;
private boolean dragged=false;
private float displayWidth;
private float displayHeight;
public DrawingView(Context context, AttributeSet attrs){
super(context, attrs);
detector=new ScaleGestureDetector(getContext(), new ScaleListener());
WindowManager wm=(WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
Display display=wm.getDefaultDisplay();
displayHeight=display.getHeight();
displayWidth=display.getWidth();
//Draw setup (create path, paint, set color, stroke...)
}
protected void onDraw(Canvas canvas){
if(zoomenable==true) {
canvas.save();
if(bmps.size()==1){
bmps.add(canvasBitmap);
}
canvas.drawBitmap(bmps.get(bmps.size() - 1), 0, 0, canvasPaint);
canvas.drawPath(drawPath, drawPaint);
canvas.scale(scaleFactor, scaleFactor);
if (translateX * -1 < 0) {
translateX = 0;
} else if ((translateX * -1) > (scaleFactor - 1) * displayWidth) {
translateX = (1 - scaleFactor) * displayWidth;
}
if (translateY * -1 < 0) {
translateY = 0;
} else if ((translateY * -1) > (scaleFactor - 1) * displayHeight) {
translateY = (1 - scaleFactor) * displayHeight;
}
canvas.translate(translateX / scaleFactor, translateY / scaleFactor);
canvas.restore();
}else {
//Draw shapes preview
}
}
public boolean onTouchEvent(MotionEvent event){
if(zoomenable==false) {
//touch events for drawing
}
if(zoomenable==true){
switch (event.getAction()&MotionEvent.ACTION_MASK){
case MotionEvent.ACTION_DOWN:
mode=DRAG;
startX=event.getX()-previusTranslateX;
startY=event.getY()-previusTranslateY;
break;
case MotionEvent.ACTION_MOVE:
translateX=event.getX()-startX;
translateY=event.getY()-startY;
double distance=Math.sqrt(Math.pow(event.getX()-(startX+previusTranslateX),2)+Math.pow(event.getY()-(startY+previusTranslateY),2));
if(distance>0){
dragged=true;
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
mode=ZOOM;
break;
case MotionEvent.ACTION_UP:
mode=NONE;
dragged=false;
previusTranslateY=translateY;
previusTranslateX=translateX;
break;
case MotionEvent.ACTION_POINTER_UP:
mode=DRAG;
previusTranslateX=translateX;
previusTranslateY=translateY;
break;
}
detector.onTouchEvent(event);
if((mode==DRAG&&scaleFactor!=1f&&dragged)||mode==ZOOM){
invalidate();
}
}
return true;
}
I created custom view where you can touch and scale it.
Most of work was created with the help of this post
Next i observe that if i want to zoom in image it is always zooming to top left corner.
Here is my onDraw() method:
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.translate(mPosX, mPosY);
canvas.scale(mScaleFactor, mScaleFactor);
// actual drawing
canvas.getClipBounds(mRect);
canvas.restore();
}
So this is normal behaviour for canvas.scale(px, py); I want from my view be scaling to center point(later i will take focalX, focalY coordinates, but now just to center). I decided to replace
canvas.scale(mScaleFactor, mScaleFactor); to
canvas.scale(mScaleFactor, mScaleFactor, getWidth()/2-mPosX, getHeight()/2-mPosY);
And now it is scaling perfectly at the mid of the image. But it somehow affects my mPosX, mPosY coordinates. I attach an image for better understanding.
Top left corner is eqivalent mPosX = 0, mPosY=0, and that's OK.
Next if i am zooming out next happens:
Now it decides that top left corner is eqivalent to mPosX=0,mPosY=0(top left corner). But in my logic it should be somehow like (mPosX=100, mPosY=130) or something like that.
So my problem is that i am sliding out from my actual content view. What should i do to prevent such behaviour?
I had same question regarding touch points calculation and referring this answer, I solved it my own way using metrics. Above answer provides better pinch zooming and dragging functionality but not correct touch points. Here is the full code and ignore my own variables.
package axis.nbapp;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import java.io.IOException;
public class NewView extends ViewGroup {
float dx;
float dy;
float scaleFac;
Region r;
private Path mPath;
private Paint mPaint;
private Path mTransformedPath;
Stencil stencil;
private Paint cColor;
boolean scaling;
Canvas bmcanvas;
SVG svg = null;
int viewWidth;
int viewHeight;
float svgWidth;
float svgHeight;
// 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 NewView(Context context) {
super(context);
init(context);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
public NewView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public NewView(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);
dx = event.getX() - start.x;
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);
scaleFac = newDist;
matrix.postScale(scale, scale, mid.x, mid.y);
matrix.invert(matrixInverse);
}
} else {
matrix.set(savedMatrix);
float scale = event.getY() / start.y;
scaleFac = scale;
matrix.postScale(scale, scale, mid.x, mid.y);
matrix.invert(matrixInverse);
}
}
break;
}
float[] values = new float[9];
matrix.getValues(values);
//int x = ((int)(event.getX() - values[2]*values[Matrix.MSCALE_X]))/(int)values[0];
//int y = ((int)(event.getY() - values[5]*values[Matrix.MSCALE_Y]))/(int)values[4];
int x = ((int)(event.getX() / values[Matrix.MSCALE_X] - (values[Matrix.MTRANS_X]/values[Matrix.MSCALE_X])));
int y = ((int)(event.getY() / values[Matrix.MSCALE_Y] - (values[Matrix.MTRANS_Y]/values[Matrix.MSCALE_Y])));
invalidate();
return true;
}
}
Check onTouchEvent you can find transformed x, y touch points from following calculation.
int x = ((int)(event.getX() / values[Matrix.MSCALE_X] - (values[Matrix.MTRANS_X]/values[Matrix.MSCALE_X])));
int y = ((int)(event.getY() / values[Matrix.MSCALE_Y] - (values[Matrix.MTRANS_Y]/values[Matrix.MSCALE_Y])));
Did you try just using getWidth()/2?
As I remember the pivot point parameter always ask for a point from 0 to width. And 0 is always on top left of the image.
So I would try just:
canvas.scale(mScaleFactor, mScaleFactor, getWidth()/2, getHeight()/2);
I'm drawing bitmap and trying to do some drawing over it using Canvas.for drawing i'm adding
path whenever user draw something on it surface. I have implemented pinch Zoom and drag features over bitmap but when user Zoom the bitmap and draw something on it then Path doesn't get draw appropriately. I'm using Matrix for pinch zoom and drag features.
My problem is using the Path with with Matrix.
This is a working example where you can see how to implement a draggable and zoomable Path:
MainActivity
import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
public class DrawActivity extends Activity {
DrawView drawView;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set full screen view
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
requestWindowFeature(Window.FEATURE_NO_TITLE);
drawView = new DrawView(DrawActivity.this);
setContentView(drawView);
}
}
DrawView
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.WindowManager;
public class DrawView extends View {
Context ctx;
static final String TAG = "DrawView";
Paint paint = new Paint();
//These two constants specify the minimum and maximum zoom
private static float MIN_ZOOM = 3f;
private static float MAX_ZOOM = 10f;
private float scaleFactor = 3.f;
private ScaleGestureDetector detector;
//These two variables keep track of the X and Y coordinate of the finger when it first
//touches the screen
private float startX = 0f;
private float startY = 0f;
//These two variables keep track of the amount we need to translate the canvas along the X
//and the Y coordinate
private float translateX = 0f;
private float translateY = 0f;
//These two variables keep track of the amount we translated the X and Y coordinates, the last time we
//panned.
private float previousTranslateX = 0f;
private float previousTranslateY = 0f;
private boolean dragged = false;
// Used for set first translate to a quarter of screen
private float displayWidth;
private float displayHeight;
public DrawView(Context context)
{
super(context);
ctx = context;
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
displayWidth = metrics.widthPixels;
displayHeight = metrics.heightPixels;
translateX = displayWidth/4;
translateY = displayHeight/4;
previousTranslateX = displayWidth/4;
previousTranslateY = displayHeight/4;
detector = new ScaleGestureDetector(context, new ScaleListener());
setFocusable(true);
setFocusableInTouchMode(true);
// Path's color
paint.setColor(Color.GRAY);
paint.setAntiAlias(false);
}
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
//We're going to scale the X and Y coordinates by the same amount
canvas.scale(scaleFactor, scaleFactor, 0, 0);
//We need to divide by the scale factor here, otherwise we end up with excessive panning based on our zoom level
//because the translation amount also gets scaled according to how much we've zoomed into the canvas.
canvas.translate((translateX) / scaleFactor, (translateY) / scaleFactor);
canvas.drawRect(0, 0, 100, 100, paint);
canvas.restore();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
// First finger is on screen
//We assign the current X and Y coordinate of the finger to startX and startY minus the previously translated
//amount for each coordinates This works even when we are translating the first time because the initial
//values for these two variables is zero.
startX = event.getX() - previousTranslateX;
startY = event.getY() - previousTranslateY;
break;
case MotionEvent.ACTION_MOVE:
// first finger is moving on screen
// update translation value to apply on Path
translateX = event.getX() - startX;
translateY = event.getY() - startY;
break;
case MotionEvent.ACTION_UP:
// No more fingers on screen
// All fingers went up, so let's save the value of translateX and translateY into previousTranslateX and
//previousTranslate
previousTranslateX = translateX;
previousTranslateY = translateY;
break;
// All touch events are sended to ScaleListener
detector.onTouchEvent(event);
return true;
}
class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
invalidate();
return true;
}
}
}
I've got this canvas which the user can add images/text etc to. If the user drags one of the items to either side of the canvas, it should expand if needed. I googled, but couldn't find any reasonable solution. Also, the canvas is about 90% of the screen width, and 70% of the height.. I'm not asking for an entire solution.. I just need a tip on how to do this (Links, docs, whatever)
Well, it's difficult to guess what you're trying to achieve. When you say "it should expand if needed", what do you mean? Expand to fill the parent view? Expand to it's intrinsic size?
Here's some (incomplete) code I use in a custom view class. Most of it is gleaned from multiple solutions on here and I give thanks to the original authors. The onDraw is the most interesting one. When you want to draw (where it says custom drawing here), you don't need to worry about translation or scaling as the canvas itself is translated and scaled. In other words, your x and y co-ords are relative to the view size - simply multiply them by scale.
public class LightsViewer extends ImageView {
private float scale;
// minimum and maximum zoom
private float MIN_ZOOM = 1f;
private float MAX_ZOOM = 5f;
// mid point between fingers to centre scale
private PointF mid = new PointF();
private float scaleFactor = 1.f;
private ScaleGestureDetector detector;
// drag/zoom mode
private final static int NONE = 0;
// current mode
private int mode ;
private float startX = 0f;
private float startY = 0f;
private float translateX = 0f;
private float translateY = 0f;
private float previousTranslateX = 0f;
private float previousTranslateY = 0f;
public LightsViewer(Context context) {
super(context);
detector = new ScaleGestureDetector(getContext(), new ScaleListener());
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
displayWidth = display.getWidth();
displayHeight = display.getHeight();
}
public LightsViewer(Context context, AttributeSet attrs) {
super(context,attrs);
detector = new ScaleGestureDetector(getContext(), new ScaleListener());
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
displayWidth = display.getWidth();
displayHeight = display.getHeight();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
int ZOOM = 2;
int DRAG = 1;
if (!allowZooming){return true;}
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mode = DRAG;
//We assign the current X and Y coordinate of the finger to startX and startY minus the previously translated
//amount for each coordinates This works even when we are translating the first time because the initial
//values for these two variables is zero.
startX = event.getX() - previousTranslateX;
startY = event.getY() - previousTranslateY;
break;
case MotionEvent.ACTION_MOVE:
if (mode == ZOOM){
Log.d("LIGHTS","ACTION_MOVE:Move but ZOOM, breaking");
break;}
translateX = event.getX() - startX;
translateY = event.getY() - startY;
//We cannot use startX and startY directly because we have adjusted their values using the previous translation values.
//This is why we need to add those values to startX and startY so that we can get the actual coordinates of the finger.
double distance = Math.sqrt(Math.pow(event.getX() - (startX + previousTranslateX), 2) +
Math.pow(event.getY() - (startY + previousTranslateY), 2)
);
if(distance > 0) {
dragged = true;
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
midPoint(mid, event);
mode = ZOOM;
break;
case MotionEvent.ACTION_UP:
mode = NONE;
dragged = false;
//All fingers went up, so let's save the value of translateX and translateY into previousTranslateX and
//previousTranslate
previousTranslateX = translateX;
previousTranslateY = translateY;
break;
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
//This is not strictly necessary; we save the value of translateX and translateY into previousTranslateX
//and previousTranslateY when the second finger goes up
previousTranslateX = translateX;
previousTranslateY = translateY;
break;
}
detector.onTouchEvent(event);
//We redraw the canvas only in the following cases:
//
// o The mode is ZOOM
// OR
// o The mode is DRAG and the scale factor is not equal to 1 (meaning we have zoomed) and dragged is
// set to true (meaning the finger has actually moved)
if ((mode == DRAG && scaleFactor != 1f && dragged) || mode == ZOOM) {
this.invalidate();
}
return true;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (this.getImageMatrix().isIdentity()){return;}
if (allowZooming){
this.applyMatrix(this.getImageMatrix());
}
}
#Override
public void onDraw(Canvas canvas) {
canvas.save();
// scale the canvas
canvas.scale(scaleFactor, scaleFactor, mid.x, mid.y);
canvas.translate(translateX / scaleFactor, translateY / scaleFactor);
super.onDraw(canvas);
// do custom drawing here...e.g.
canvas.drawCircle(100,100, 3 / scaleFactor,light.paint);
canvas.restore();
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
return true;
}
}
// 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);
Log.d("LIGHTS", x/2 + "," + y/2);
}
public void setMinZoom(float minZoom){
this.MIN_ZOOM = minZoom;
}
public void applyMatrix(Matrix matrix){
float[] matrixValues = new float[9];
matrix.getValues(matrixValues);
int x = (int)matrixValues[Matrix.MTRANS_X];
int y = (int)matrixValues[Matrix.MTRANS_Y];
float scale = matrixValues[Matrix.MSCALE_X];
if (lights!=null){
for (Light light:lights){
light.setX((int)((light.originalX * scale) + x));
light.setY((int)((light.originalY * scale) + y));
}
}
// if either the x or y translations are less than 0, then the image has been cropped
// so set the min zoom to the ratio of the displayed size and the actual size of the image
if (matrixValues[Matrix.MTRANS_X] < 0 || matrixValues[Matrix.MTRANS_Y] <0){
MIN_ZOOM = displayWidth / this.getDrawable().getIntrinsicWidth();
}else{
MIN_ZOOM = 1;
}
}
public void enableZooming(boolean enable){
allowZooming = enable;
}
public void setScale(float scale){
for (Light light:lights){
light.setX((int)(light.originalX * scale));
light.setY((int)(light.originalY * scale));
}
}
}