During the past weeks I was looking for appropriate source code showing how to enable zoom and pan functionality on a custom view. All solutions that I found had some problems. For example the movement/zoom was not smooth enough or the view jumped around when releasing one finger after a scaling (two-finger) operation. So I came up with a modified solution that I want to share with you. Suggestions and enhancements are welcomed.
What’s different?
It is bad practice to calculate the difference (distance vector) on any interaction (pan, zoom) between each single events and use it to set new values. If you do so, the action does not look smooth and the view might flicker (jump around in some pixels). A better approach is to remember values when the action starts (onScaleBegin, touch-down) and calculate distances for each event in comparison to those start values.
You could handle finger indices in onTouchEvent to better distinguish between pan/move and zoom/scale interaction.
public class CanvasView extends SurfaceView implements SurfaceHolder.Callback {
final static String TAG = "CanvasView";
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
ScaleGestureDetector mScaleDetector;
InteractionMode mode;
Matrix mMatrix = new Matrix();
float mScaleFactor = 1.f;
float mTouchX;
float mTouchY;
float mTouchBackupX;
float mTouchBackupY;
float mTouchDownX;
float mTouchDownY;
Rect boundingBox = new Rect();
public CanvasView(Context context) {
super(context);
// we need to get a call for onSurfaceCreated
SurfaceHolder sh = this.getHolder();
sh.addCallback(this);
// for zooming (scaling) the view with two fingers
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
boundingBox.set(0, 0, 1024, 768);
paint.setColor(Color.GREEN);
paint.setStyle(Style.STROKE);
setFocusable(true);
// initial center/touch point of the view (otherwise the view would jump
// around on first pan/move touch
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
mTouchX = metrics.widthPixels / 2;
mTouchY = metrics.heightPixels / 2;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
mScaleDetector.onTouchEvent(event);
if (!this.mScaleDetector.isInProgress()) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
// similar to ScaleListener.onScaleEnd (as long as we don't
// handle indices of touch events)
mode = InteractionMode.None;
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "Touch down event");
mTouchDownX = event.getX();
mTouchDownY = event.getY();
mTouchBackupX = mTouchX;
mTouchBackupY = mTouchY;
// pan/move started
mode = InteractionMode.Pan;
break;
case MotionEvent.ACTION_MOVE:
// make sure we don't handle the last move event when the first
// finger is still down and the second finger is lifted up
// already after a zoom/scale interaction. see
// ScaleListener.onScaleEnd
if (mode == InteractionMode.Pan) {
Log.d(TAG, "Touch move event");
// get current location
final float x = event.getX();
final float y = event.getY();
// get distance vector from where the finger touched down to
// current location
final float diffX = x - mTouchDownX;
final float diffY = y - mTouchDownY;
mTouchX = mTouchBackupX + diffX;
mTouchY = mTouchBackupY + diffY;
CalculateMatrix(true);
}
break;
}
}
return true;
}
#Override
public void onDraw(Canvas canvas) {
int saveCount = canvas.getSaveCount();
canvas.save();
canvas.concat(mMatrix);
canvas.drawColor(Color.BLACK);
canvas.drawRect(boundingBox, paint);
canvas.restoreToCount(saveCount);
}
#Override
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
}
#Override
public void surfaceCreated(SurfaceHolder arg0) {
// otherwise onDraw(Canvas) won't be called
this.setWillNotDraw(false);
}
#Override
public void surfaceDestroyed(SurfaceHolder arg0) {
}
void CalculateMatrix(boolean invalidate) {
float sizeX = this.getWidth() / 2;
float sizeY = this.getHeight() / 2;
mMatrix.reset();
// move the view so that it's center point is located in 0,0
mMatrix.postTranslate(-sizeX, -sizeY);
// scale the view
mMatrix.postScale(mScaleFactor, mScaleFactor);
// re-move the view to it's desired location
mMatrix.postTranslate(mTouchX, mTouchY);
if (invalidate)
invalidate(); // re-draw
}
private class ScaleListener extends
ScaleGestureDetector.SimpleOnScaleGestureListener {
float mFocusStartX;
float mFocusStartY;
float mZoomBackupX;
float mZoomBackupY;
public ScaleListener() {
}
#Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
mode = InteractionMode.Zoom;
mFocusStartX = detector.getFocusX();
mFocusStartY = detector.getFocusY();
mZoomBackupX = mTouchX;
mZoomBackupY = mTouchY;
return super.onScaleBegin(detector);
}
#Override
public void onScaleEnd(ScaleGestureDetector detector) {
mode = InteractionMode.None;
super.onScaleEnd(detector);
}
#Override
public boolean onScale(ScaleGestureDetector detector) {
if (mode != InteractionMode.Zoom)
return true;
Log.d(TAG, "Touch scale event");
// get current scale and fix its value
float scale = detector.getScaleFactor();
mScaleFactor *= scale;
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
// get current focal point between both fingers (changes due to
// movement)
float focusX = detector.getFocusX();
float focusY = detector.getFocusY();
// get distance vector from initial event (onScaleBegin) to current
float diffX = focusX - mFocusStartX;
float diffY = focusY - mFocusStartY;
// scale the distance vector accordingly
diffX *= scale;
diffY *= scale;
// set new touch position
mTouchX = mZoomBackupX + diffX;
mTouchY = mZoomBackupY + diffY;
CalculateMatrix(true);
return true;
}
}
}
Related
First of all this is a follow up question originally asked here, Pan, Zoom and Scale a custom View for Canvas drawing in Android
Since there was no answer yet, I finally solved my issue using the android-gesture-detectors
After applying the zoom/scale gesture I found that, the canvas drawing coordinates are still pointing to the old position (before applying the zoom) and not drawing on the exact same touch coordinates. Basically, I can't get the correct canvas coordinates after scaling or dragging the canvas.
Before zooming,
After zooming out the touch points are drawing on the previous location. I want it to draw on the current touch location,
Sample code,
public class DrawingView extends View {
private void setupDrawing() {
mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
mgd = new MoveGestureDetector(ctx, mgl);
sgd = new ScaleGestureDetector(ctx, sgl);
rgd = new RotateGestureDetector(ctx, rgl);
}
class ScaleListener extends ScaleGestureDetector.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;
}
}
MoveGestureDetector.SimpleOnMoveGestureListener mgl = new MoveGestureDetector.SimpleOnMoveGestureListener() {
#Override
public boolean onMove(MoveGestureDetector detector) {
PointF delta = detector.getFocusDelta();
matrix.postTranslate(delta.x, delta.y);
invalidate();
return true;
}
};
ScaleGestureDetector.SimpleOnScaleGestureListener sgl = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
#Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = detector.getScaleFactor();
matrix.postScale(scale, scale, detector.getFocusX(), detector.getFocusY());
invalidate();
return true;
}
};
RotateGestureDetector.SimpleOnRotateGestureListener rgl = new RotateGestureDetector.SimpleOnRotateGestureListener() {
#Override
public boolean onRotate(RotateGestureDetector detector) {
matrix.postRotate(-detector.getRotationDegreesDelta(), detector.getFocusX(), detector.getFocusY());
invalidate();
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);
}
private void touch_start(float x, float y) {
undonePaths.clear();
drawPath.reset();
drawPath.moveTo(x, y);
mX = x;
mY = y;
}
private void touch_move(float x, float y, float x2, float y2) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
/* QUad to curves using a quadratic line (basically an ellipse of some sort).
LineTo is a straight line. QuadTo will smooth out jaggedies where they turn.
*/
drawPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
mX = x;
mY = y;
}
}
private void touch_up() {
drawPath.lineTo(mX, mY);
// commit the path to our offscreen
drawCanvas.drawPath(drawPath, drawPaint);
// kill this so we don't double draw
paths.add(drawPath);
drawPath = new Path();
drawPath.reset();
invalidate();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (isZoomable) {
mgd.onTouchEvent(event);
sgd.onTouchEvent(event);
rgd.onTouchEvent(event);
}
if (!isTouchable) {
return super.onTouchEvent(event);
} else {
//detect user touch
float x = event.getX();
float y = event.getY();
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
if (!isZoomable) {
touch_start(x, y);
}
invalidate();
break;
case MotionEvent.ACTION_MOVE:
if (!isZoomable) {
//mPositions.add(new Vector2(x - mBitmapBrushDimensions.x / 2, y - mBitmapBrushDimensions.y / 2));
if (isCustomBrush && mBitmapBrushDimensions != null) {
mPositions = new Vector2(x - mBitmapBrushDimensions.x / 2, y - mBitmapBrushDimensions.y / 2);
touch_move(x, y, x - mBitmapBrushDimensions.x / 2, y - mBitmapBrushDimensions.y / 2);
} else {
touch_move(x, y, 0, 0);
}
}
invalidate();
break;
case MotionEvent.ACTION_UP:
if (!isZoomable) {
touch_up();
}
invalidate();
break;
}
mScaleDetector.onTouchEvent(event);
return true;
}
}
#Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.setMatrix(matrix);
for (Path p : paths) {
canvas.drawPath(p, drawPaint);
drawPaint.setColor(selectedColor);
drawPaint.setStrokeWidth(brushSize);
canvas.drawPath(drawPath, drawPaint);
}
canvas.restore();
}
}
PS: MoveGestureDetector(), ScaleGestureDetector() & RotateGestureDetector() are custom classes inherited from android-gesture-detectors
Here's what I did. Basically, you have to find the difference between the "old" and new points. Skip to the bottom for the important lines...
#Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
float xDiff = initialFocalPoints[0] - currentFocalPoints[0];
float yDiff = initialFocalPoints[1] - currentFocalPoints[1];
transformMatrix.setScale(scaleFactor, scaleFactor,
currentFocalPoints[0], currentFocalPoints[1]);
transformMatrix.postTranslate(xDiff, yDiff);
child.setImageMatrix(transformMatrix);
return true;
}
#Override
public boolean onScaleBegin(ScaleGestureDetector detector){
float startX = detector.getFocusX() + getScrollX();
float startY = detector.getFocusY() + getScrollY();
initialFocalPoints = new float[]{startX, startY};
if(transformMatrix.invert(inverseTransformMatrix))
inverseTransformMatrix.mapPoints(currentFocalPoints, initialFocalPoints);
return true;
}
The lines that made the difference were the following:
float xDiff = initialFocalPoints[0] - currentFocalPoints[0];
float yDiff = initialFocalPoints[1] - currentFocalPoints[1];
transformMatrix.postTranslate(xDiff, yDiff);
The answer was as simple as figuring out the difference between the two points and translating the imageview everytime the image is scaled.
To apply any transformation you have to understand the rules of mathematics. It works both for 2dim and 3 dimensions graphics.
That is, if you work with translation (T), rotation (R), scale (S) matrices to apply any transformation, you have scale object at first (multiply coordinates xyz by this matrix S) then rotate (mult. by R) then shift the object by T.
So, you apply rotation over some point you have to move the object to zero and scale then return to base point.
That is namely in your case, before applying scale, you have to shift (decrease) all coordinates by touch position then apply scale matrix by multiplication then shift by increasing all positions by this touch against.
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 am trying to implement pinch zoom and drag using Android's gesture listener and scale listener. The problem is that when I perform pinch zoom, the image (which I am trying to zoom) bounces to a particular location. Also the zoom position is not centered.
The following code demonstrates what I am trying to achieve. Any idea why the image is jumping (and how to correct it) ?
public class CustomView extends View {
Bitmap image;
int screenHeight;
int screenWidth;
Paint paint;
GestureDetector gestures;
ScaleGestureDetector scaleGesture;
float scale = 1.0f;
float horizontalOffset, verticalOffset;
int NORMAL = 0;
int ZOOM = 1;
int DRAG = 2;
boolean isScaling = false;
float touchX, touchY;
int mode = NORMAL;
public CustomView(Context context) {
super(context);
//initializing variables
image = BitmapFactory.decodeResource(getResources(),
R.drawable.image_name);
//This is a full screen view
screenWidth = getResources().getDisplayMetrics().widthPixels;
screenHeight = getResources().getDisplayMetrics().heightPixels;
paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
paint.setColor(Color.WHITE);
scaleGesture = new ScaleGestureDetector(getContext(),
new ScaleListener());
gestures = new GestureDetector(getContext(), new GestureListener());
mode = NORMAL;
initialize();
}
//Best fit image display on canvas
private void initialize() {
float imgPartRatio = image.getWidth() / (float) image.getHeight();
float screenRatio = (float) screenWidth / (float) screenHeight;
if (screenRatio > imgPartRatio) {
scale = ((float) screenHeight) / (float) (image.getHeight()); // fit height
horizontalOffset = ((float) screenWidth - scale
* (float) (image.getWidth())) / 2.0f;
verticalOffset = 0;
} else {
scale = ((float) screenWidth) / (float) (image.getWidth()); // fit width
horizontalOffset = 0;
verticalOffset = ((float) screenHeight - scale
* (float) (image.getHeight())) / 2.0f;
}
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.drawColor(0, Mode.CLEAR);
canvas.drawColor(Color.WHITE);
if(mode == DRAG || mode == NORMAL) {
//This works perfectly as expected
canvas.translate(horizontalOffset, verticalOffset);
canvas.scale(scale, scale);
canvas.drawBitmap(image, getMatrix(), paint);
}
else if (mode == ZOOM) {
//PROBLEM AREA - when applying pinch zoom,
//the image jumps to a position abruptly
canvas.scale(scale, scale, touchX, touchY);
canvas.drawBitmap(image, getMatrix(), paint);
}
canvas.restore();
}
public class ScaleListener implements OnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactorNew = detector.getScaleFactor();
if (detector.isInProgress()) {
touchX = detector.getFocusX();
touchY = detector.getFocusY();
scale *= scaleFactorNew;
invalidate(0, 0, screenWidth, screenHeight);
}
return true;
}
#Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
isScaling = true;
mode=ZOOM;
return true;
}
#Override
public void onScaleEnd(ScaleGestureDetector detector) {
mode = NORMAL;
isScaling = false;
}
}
public class GestureListener implements GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener {
#Override
public boolean onDown(MotionEvent e) {
isScaling = false;
return true;
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
if (!isScaling) {
mode = DRAG;
isScaling = false;
horizontalOffset -= distanceX;
verticalOffset -= distanceY;
invalidate(0, 0, screenWidth, screenHeight);
} else {
mode = ZOOM;
isScaling = true;
}
return true;
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
scaleGesture.onTouchEvent(event);
gestures.onTouchEvent(event);
return true;
}
}
Thanks in advance.
I have implemented this behaviour, and I used a matrix to handle all the zooming and scrolling (and rotation, in my case). It makes for neat code and works like clockwork.
Store a Matrix as a class member:
Matrix drawMatrix;
Edit: Store old focus point, used to get the focus shift during scaling.
float lastFocusX;
float lastFocusY;
Edit: Set lastFocus variables in onScaleBegin
#Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
lastFocusX = detector.getFocusX();
lastFocusY = detector.getFocusY();
return true;
}
Replace your onScale:
#Override
public boolean onScale(ScaleGestureDetector detector) {
Matrix transformationMatrix = new Matrix();
float focusX = detector.getFocusX();
float focusY = detector.getFocusY();
//Zoom focus is where the fingers are centered,
transformationMatrix.postTranslate(-focusX, -focusY);
transformationMatrix.postScale(detector.getScaleFactor(), detector.getScaleFactor());
/* Adding focus shift to allow for scrolling with two pointers down. Remove it to skip this functionality. This could be done in fewer lines, but for clarity I do it this way here */
//Edited after comment by chochim
float focusShiftX = focusX - lastFocusX;
float focusShiftY = focusY - lastFocusY;
transformationMatrix.postTranslate(focusX + focusShiftX, focusY + focusShiftY);
drawMatrix.postConcat(transformationMatrix);
lastFocusX = focusX;
lastFocusY = focusY;
invalidate();
return true;
}
Similarly in onScroll:
#Override
public boolean onScroll(MotionEvent downEvent, MotionEvent currentEvent,
float distanceX, float distanceY) {
drawMatrix.postTranslate(-distanceX, -distanceY);
invalidate();
return true;
}
in onDraw; Draw with your drawMatrix:
canvas.drawBitmap(image, drawMatrix, paint);
Happy coding!
All I want rotate image in particular angle as like below image. I have code for rotation but it rotate 360 degree but I want it only for particular degrees and get the selected number which is upper side of dial.
below is my code.
My custom View this work fine but lake of perfomance.
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector.OnGestureListener;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
public class MyDialView extends View implements OnGestureListener{
private static Bitmap bimmap;
private static Paint paint;
private static Rect bounds;
private int totalNicks = 100;
private int currentNick = 0;
private GestureDetector gestureDetector;
private float dragStartDeg = Float.NaN;
float dialerWidth = 0,dialerHeight = 0;
private static Paint createDefaultPaint() {
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
return paint;
}
private float xyToDegrees(float x, float y) {
float distanceFromCenter = PointF.length((x - 0.5f), (y - 0.5f));
if (distanceFromCenter < 0.1f
|| distanceFromCenter > 0.5f) { // ignore center and out of bounds events
return Float.NaN;
} else {
return (float) Math.toDegrees(Math.atan2(x - 0.5f, y - 0.5f));
}
}
public final float getRotationInDegrees() {
return (360.0f / totalNicks) * currentNick;
}
public final void rotate(int nicks) {
currentNick = (currentNick + nicks);
if (currentNick >= totalNicks) {
currentNick %= totalNicks;
} else if (currentNick < 0) {
currentNick = (totalNicks + currentNick);
}
Log.e("Current nick", String.valueOf(currentNick));
if((currentNick > 80 || currentNick < 20)){
invalidate();
}
}
public MyDialView(Context context, AttributeSet attrs) {
super(context, attrs);
bimmap = BitmapFactory.decodeResource(context.getResources(),R.drawable.out_round);
paint = createDefaultPaint();
gestureDetector = new GestureDetector(getContext(), this);
dialerWidth = bimmap.getWidth() /2.0f;
dialerHeight = bimmap.getHeight() / 2.0f;
bounds = new Rect();
}
#Override
protected void onDraw(Canvas canvas) {
canvas.getClipBounds(bounds);
canvas.save(Canvas.MATRIX_SAVE_FLAG);
//{
canvas.translate(bounds.left, bounds.top);
float rotation = getRotationInDegrees();
canvas.rotate(rotation, dialerWidth, dialerHeight);
canvas.drawBitmap(bimmap, 0,0,null);
//canvas.rotate(- rotation, dialerWidth, dialerHeight);
//}
canvas.restore();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (gestureDetector.onTouchEvent(event)) {
return true;
} else {
return super.onTouchEvent(event);
}
}
//Gesture detector methods
#Override
public boolean onDown(MotionEvent e) {
float x = e.getX() / ((float) getWidth());
float y = e.getY() / ((float) getHeight());
dragStartDeg = xyToDegrees(x, y);
//Log.d("deg = " , ""+dragStartDeg);
if (! Float.isNaN(dragStartDeg)) {
return true;
} else {
return false;
}
}
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
return false;
}
#Override
public void onLongPress(MotionEvent e) {
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
if (! Float.isNaN(dragStartDeg)) {
float currentDeg = xyToDegrees(e2.getX() / getWidth(),
e2.getY() / getHeight());
if (! Float.isNaN(currentDeg)) {
float degPerNick = 360.0f / totalNicks;
float deltaDeg = dragStartDeg - currentDeg;
final int nicks = (int) (Math.signum(deltaDeg)
* Math.floor(Math.abs(deltaDeg) / degPerNick));
if (nicks != 0) {
dragStartDeg = currentDeg;
rotate(nicks);
}
}
return true;
} else {
return false;
}
}
#Override
public void onShowPress(MotionEvent e) {
}
#Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
}
I want 0-9 according to user selection & also allow user rotation to 0-9 not more rotation.
I have also check another code this is below.
dialer = (ImageView) findViewById(R.id.imageView_ring);
dialer.setOnTouchListener(new MyOnTouchListener());
dialer.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// method called more than once, but the values only need to be initialized one time
if (dialerHeight == 0 || dialerWidth == 0) {
dialerHeight = dialer.getHeight();
dialerWidth = dialer.getWidth();
// resize
Matrix resize = new Matrix();
resize.postScale((float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getWidth(), (float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getHeight());
imageScaled = Bitmap.createBitmap(imageOriginal, 0, 0, imageOriginal.getWidth(), imageOriginal.getHeight(), resize, false);
// translate to the image view's center
float translateX = dialerWidth / 2 - imageScaled.getWidth() / 2;
float translateY = dialerHeight / 2 - imageScaled.getHeight() / 2;
matrix.postTranslate(translateX, translateY);
dialer.setImageBitmap(imageScaled);
dialer.setImageMatrix(matrix);
Log.e("Rotation degree :"+rotationDegrees, String.valueOf(tickNumber));
}
}
});
int tickNumber = 0;
private void rotateDialer(float degrees) {
//System.out.println("Rotation Done :: "+rotationDone);
// if(!rotationDone) {
this.rotationDegrees += degrees;
this.rotationDegrees = this.rotationDegrees % 360;
tickNumber = (int)this.rotationDegrees*100/360;
// It could be negative
if (tickNumber > 0) tickNumber = 100 - tickNumber;
//this.rotationDegrees = Math.abs(rotationDegrees);
this.tickNumber = Math.abs(tickNumber);
if(tickNumber < 20 || tickNumber > 80){
Log.e("Rotation degree :"+rotationDegrees, String.valueOf(tickNumber));
matrix.postRotate(degrees, dialerWidth / 2, dialerHeight / 2);
dialer.setImageMatrix(matrix);
}
// }
}
/**
* #return The angle of the unit circle with the image view's center
*/
private double getAngle(double xTouch, double yTouch) {
double delta_x = xTouch - (dialerWidth) /2;
double delta_y = (dialerHeight) /2 - yTouch;
double radians = Math.atan2(delta_y, delta_x);
double dx = xTouch - dWidth;
double dy = (dHeight - ((dialerHeight) /2)) - yTouch;
double dRadi = Math.atan2(dy, dx);
//Log.e("MY degree", String.valueOf( Math.toDegrees(dRadi)));
//return Math.toDegrees(dRadi);
return Math.toDegrees(radians);
}
/**
* Simple implementation of an {#link OnTouchListener} for registering the dialer's touch events.
*/
private class MyOnTouchListener implements OnTouchListener {
private double startAngle;
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// reset the touched quadrants
/*for (int i = 0; i < quadrantTouched.length; i++) {
quadrantTouched[i] = false;
}*/
//allowRotating = false;
startAngle = getAngle(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
/*double rotationAngleRadians = Math.atan2(event.getX() - (dialer.getWidth() / 2 ), ( (dialer.getHeight() / 2 ) - event.getY()));
double angle = (int) Math.toDegrees(rotationAngleRadians);
Log.i("gg", "rotaion angle"+angle);*/
double currentAngle = getAngle(event.getX(), event.getY());
//if(currentAngle < 130 || currentAngle < 110){
//Log.e("Start angle :"+startAngle, "Current angle:"+currentAngle);
rotateDialer((float) (startAngle - currentAngle));
startAngle = currentAngle;
//}
//Log.e("MOVE start Degree:"+startAngle, "Current Degree :"+currentAngle);
break;
case MotionEvent.ACTION_UP:
//allowRotating = true;
break;
}
// set the touched quadrant to true
//quadrantTouched[getQuadrant(event.getX() - (dialerWidth / 2), dialerHeight - event.getY() - (dialerHeight / 2))] = true;
//detector.onTouchEvent(event);
return true;
}
}
I do not understand your problem. The code below rotate the image 48 degrees.
ImageView dialer = (ImageView) findViewById(R.id.imageView_ring);
int degrees = 48;
Matrix matrix = new Matrix();
matrix.setRotate(degrees);
Bitmap bmpBowRotated = Bitmap.createBitmap(imageOrginal, 0, 0, imageOrginal.getWidth(),imageOrginal.getHeight(), matrix, false);
dialer.setImageBitmap(bmpBowRotated);
Hi Girish there is a class Named RotateAnimation by using this class u can easily do it
look Example like
RotateAnimation r = new RotateAnimation(0f, -90f,200,200); // HERE
r.setStartOffset(1000);
r.setDuration(1000);
r.setFillAfter(true); //HERE
animationSet.addAnimation(r);
I would like to first know what will be there for deployment? Does it allow manipulation Evenets? if yes then you get handle ManipulationStatring and ManipulationDelta Event to rotate the element.
If the same is not the case where Manipulation is not available then you can try RenderTransformation property with RorateTransform of the element is you are working with WPf.
I was able to achieve this by doing few of the following tweaks on your code
Making user click exactly on the arrow always to get the initial angle at which the arrow is placed, in your case 90 degree, else return false
Also save the angle at which the user removed his finger and use that angle as the initial value for his next touch ,like if he placed arrow at 100 deg make that his initial touch position to activate rotation again
Now for checking his answer take the angle at which your numbers 0 to 9 are placed ,im guessing your values take 120 deg from 0 to 9, divide that angle by 10, you can easily find out what angle represents what value and get your result
Also touching exactly at 90deg to begin rotation is very irritating, so always check for value which is bw 90+4 and 90-4 to begin, but always use the 90 as your start angle
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));
}
}
}