I have seen how to draw a shape in Android, but what I want to know is how to rescale the shape when the user touches over the shape.
Imagine a square into a screen corner, so when you touch it, it grows until fitting the whole screen. I'd like to make that with a transition, animated, not instant.
Any idea of how to do that, or any known resource?
Android has built-in support for Animations. You can find many examples by searching the Web. This one is a good start.
In order to make your shapes touchable, you can implement them by overriding the View class (a nice example can be found here). Then you can use View.OnTouchListener.
The built in Animations are nice in Android but they aren't the most efficient by any means. When performance is a must I would recommend creating your own method. What I would do is create a class that extends View and give it a bounding box (Rect/RectF) and a circle. Then you can use the bounding box to detect when the circle is touched.
public class Circle extends View {
public static final float SCALE_AMOUNT = 1.0f;
public RectF boundingBox;
private Paint paint;
private float circleCenterX, circleCenterY, circleRadius;
private float x, y;
public Circle(Context context) {
super(context);
// Create paint
paint = new Paint();
paint.setColor(Color.BLACK);
paint.setAntiAlias(true);
// Set circle start radius
circleRadius = 50.0f;
// Set start x and y (this is the upper left hand corner)
x = 100.0f;
y = 100.0f;
// Create boundingBox
boundingBox = new RectF();
boundingBox.left = x;
boundingBox.top = y;
boundingBox.right = x + (circleRadius*2);
boundingBox.bottom = y + (circleRadius*2);
// Set circleCenterX and circleCenterY (the center of the bounding box and circle)
circleCenterX = x + circleRadius;
circleCenterY = y + circleRadius;
}
public void scale(boolean scaleUp) {
float scaleBy = (scaleUp) ? SCALE_AMOUNT : -SCALE_AMOUNT;
// Update circleRadius
circleRadius += scaleBy;
// Update the bounding box
boundingBox.left = x;
boundingBox.top = y;
boundingBox.right = x + (circleRadius*2);
boundingBox.bottom = y + (circleRadius*2);
// Update the circle center positions
circleCenterX = x + circleRadius;
circleCenterY = y + circleRadius;
}
#Override
public void onDraw(Canvas canvas) {
canvas.drawCircle(circleCenterX, circleCenterY, circleRadius, paint);
}
}
... Then in your Activity class override the onTouchEvent() method and check if your Circle is touched.
Circle circle = new Circle(this);
#Override
public void onDraw(Canvas canvas) {
circle.onDraw(canvas);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float x = event.getX();
float y = event.getY();
// Detect if pointer goes down on screen
if(action == MotionEvent.ACTION_DOWN) {
if(circle.boundingBox.contains(x, y) == true) {
// Circle was touched so scale it
circle.scale(true); // true is scale up, false is scale down
}
}
return true;
}
... This will scale your circle/rectangle every time you touch it. If you wanted to make it continually grow you could have a boolean variable that gets set to true when you touch the shape and grows until you pick your finger up. I haven't tried this code, just typed it up real quick so it may not compile but this is going to be you're best bet. It is really easy to add many shapes and detect touches on all of the shapes. Add different effects to each one... etc. I didn't want to do all of it for you but this should point you in the right direction.
Maybe this github project could help you: https://github.com/markushi/android-circlebutton
Related
I am working on android studio and I want to create the effect of throwing an object, in this case a circle drawn on the canvas. But I'm having problems. Can someone guide me?
I take the "X" and the "Y" value where the user touch but how can i make that the circle move in that direction?
Thanks
Game class:
public class Game extends SurfaceView implements View.OnTouchListener {
Paint paint;
int x, y, radius = 100, speedX = 5, speedY = 5, touchY, touchX;
boolean move = false;
boolean one_time = true;
public Game(Context context, AttributeSet attrs) {
super(context, attrs);
this.setOnTouchListener(this);
setFocusable(true);
paint = new Paint();
}
public void onDraw(Canvas canvas){
paint.setColor(Color.WHITE);
canvas.drawRect(0,0,canvas.getWidth(),canvas.getHeight(),paint);
if (one_time == true){
x = canvas.getWidth()/2;
y = canvas.getHeight()/2;
one_time = false;
}
paint.setColor(Color.BLACK);
canvas.drawCircle(x, y, radius, paint);
if (move == true){
if (x >= canvas.getWidth() - radius) {
speedX = -5;
}
if (x <= radius) {
speedX = 5;
}
if (y >= canvas.getHeight() - radius) {
speedY = -5;
}
if (y <= radius) {
speedY = 5;
}
x = x + speedX;
y = y + speedY;
}
invalidate();
}
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case (MotionEvent.ACTION_DOWN):
touchX = (int) motionEvent.getX();
touchY = (int) motionEvent.getY();
move = true;
return true;
default:
return super.onTouchEvent(motionEvent);
}
}
}
Gonna need to be an x+=speedX or y+=speedY in there somewhere so that the x or y value changes causing the ball to move. If your trying to implement some physics in there ur gonna need some maths.
You need a way to change the (x, y) values at a certain interval. You can do this with the Timer class, but then you get into some difficult multithreading code. I suggest looking at using OpenGL ES or a higher level library such as LibGDX. Both of these provide an event loop which allow you to update objects that will be drawn.
Basically you want to move the circle to the place the user touched the screen, using some type of constant speed?
If all you want is to move a circle the Timer can do it - or you could use something like: https://github.com/MasayukiSuda/FPSAnimator
Finally, are you always looking for a linear straight line constant speed movement? Then your math is fine. If your looking for something with gravity etc. then you can reference this: https://developer.android.com/guide/topics/graphics/physics-based-animation.html and consider this: https://github.com/google/liquidfun
This will move the circle exactly to the point the user touched:
theta = atan2(touchY - y,touchX - x)
speedX = cos(theta)
speedY = sin(theta)
x += speedX
y += speedY
What happens once the circle reaches the point depends on how the calculation is implemented. If you want the circle to continue on its path and travel the direction infinitely, you must only calculate the velocity of x and y once. That way the same x and y velocity will always be used to update the position of the circle.
If the velocities are recalculated every time the position of the circle is to be updated, the circle will continuously move to the point even once it has technically reached it and must manually be stopped.
The velocities can be increased by multiplying them by a value greater than 1. To maintain the correct direction, the value should be the same for both velocities.
Example:
theta = atan2(touchY - y,touchX - x)
speedX = maxSpeed*cos(theta)
speedY = maxSpeed*sin(theta)
x += speedX
y += speedY
I need to get the touch x and y with respect to the canvas to check for collisions and things like that after I have moved and scaled the canvas.
I already managed to get the touch coordinate whenever I translate the canvas or scale it around the origin (0,0) by using the following code:
private float convertToCanvasCoordinate(float touchx, float touchy) {
float newX=touchx/scale-translatex;
float newY=touchy/scale-translatey
}
But if I scale the canvas around another point like for example canvas.scale(scale,scale,50,50), it doesn't work .
I know it shouldn't work but I just couldn't figure out how to solve it. I already looked at other questions but none of the answers talks about how to get the coordinate if I scale according to a specific point.
The most basic way to properly do a scene in android is to use a matrix to modify the view and the inverse of that matrix to modify your touches. Here's a simplified answer. Kept very short.
public class SceneView extends View {
Matrix viewMatrix = new Matrix(), invertMatrix = new Matrix();
Paint paint = new Paint();
ArrayList<RectF> rectangles = new ArrayList<>();
RectF moving = null;
public SceneView(Context context) { super(context); }
public SceneView(Context context, AttributeSet attrs) { super(context, attrs); }
public SceneView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
#Override
public boolean onTouchEvent(MotionEvent event) {
//transform touch. (inverted matrix)
event.transform(invertMatrix);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
moving = null;
//collision detection
for (RectF f : rectangles) {
if (f.contains(event.getX(), event.getY())) {
moving = f;
return true;
}
}
// adding arbitrary transforms.
viewMatrix.postTranslate(50,50);
viewMatrix.postScale(.99f,.99f);
viewMatrix.postRotate(5);
// inverse matrix is needed for touches.
invertMatrix = new Matrix(viewMatrix);
invertMatrix.invert(invertMatrix);
break;
case MotionEvent.ACTION_MOVE:
if (moving != null) {
moving.set(event.getX() - 50, event.getY() - 50, event.getX() + 50, event.getY() + 50);
}
break;
case MotionEvent.ACTION_UP:
if (moving == null) {
rectangles.add(new RectF(event.getX() - 50, event.getY() - 50, event.getX() + 50, event.getY() + 50));
}
break;
}
invalidate();
return true;
}
#Override
protected void onDraw(Canvas canvas) {
// transform the view by the matrix.
canvas.concat(viewMatrix);
// draw objects
for (RectF f : rectangles) {
canvas.drawRect(f,paint);
}
}
This is rather minimalist, but it shows all the relevant aspects.
Moving the view
Touch modification
Collision detection.
Each time you touch the screen it will move diagonally, zoomout, and rotate (basically moves in a spiral), and create a black rectangle. If you touch the rectangles you can move them around to your heart's content. When you click the background, more spiraling the view, dropping black rectangles.
See: https://youtu.be/-XSjanaAdWA
The other way does not work. You could, in theory, take the scene we want and convert that via the View class rather than in the canvas. This would make the touch events occur in the same space as the screen. But Android will void out touch events that occur outside of the view, So MotionEvents that begin outside of the original clipped part of the view will be discarded. So this is a non-starter. You want to transform the canvas, and counter transform the MotionEvents.
private float convertToCanvasXCoordinate(float touchx,float offsetx,float viewportVisibleWidth){
float newx=(touchx*viewportVisibleWidth)/getWidth()+offsetx;
return newx;
}
private float convertToCanvasYCoordinate(float touchy,float offsety,float viewportVisibleHeight){
float newy=(touchy*viewportVisibleHeight)/getHeight()+offsety;
return newy;
}
i just found out there is a function canvas.getClipBound() which is a rectangle representing the visible viewport that includes the offsetx offset y (the left and top of the rectangle respectively) and the viewport width and height
simply call these functions and it will get you touchx and touchy with respect to canvas
This should help:
float px = e.getX() / mScaleFactorX;
float py = e.getY() / mScaleFactorY;
int ipy = (int) py;
int ipx = (int) px;
Rect r = new Rect(ipx, ipy, ipx+2, ipy+2);
And where the canvas is:
final float scaleFactorX = getWidth()/(WIDTH*1.f);
final float scaleFactorY = getHeight()/(HEIGHT*1.f);
if(mScaleFactorX == INVALID){
mScaleFactorX = scaleFactorX;
mScaleFactorY = scaleFactorY;
}
This is a really simple way, and it works because it scales down the onTouch coordinates to be the same min/max as the canvas, causing them to be scaled. Do NOT use getRawX and getRawY because that will return wrong coordinates if you are using a custom layout with the view added and other elements around it. getX and getY returns the accurate coordinates scaled to your view.
This is really simple and does not take up a lot of code. scaleFactor can be used elsewhere to handle zoom(you take care of that code) but what this code does is to handle the issue of getting the pointer coordinates to match the scaled canvas
Im using below code to draw line on bitmap canvas while finger touch move... here i posted partial code and it is working fine..
As shown in below image, the black and white bitmap erased on touch drag.. I made canvas transparent so the parent layout background(color image) is getting visible.
I want to know , how much area is erased(like 50% or 60% of bitmap ).. is there any way to find that?
//Erasing paint
mDrawPaint = new Paint();
mDrawPaint.setAntiAlias(true);
mDrawPaint.setDither(true);
mDrawPaint.setStyle(Paint.Style.STROKE);
mDrawPaint.setStrokeJoin(Paint.Join.ROUND);
mDrawPaint.setStrokeCap(Paint.Cap.ROUND);
mDrawPaint.setStrokeWidth(50);
mDrawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
BlurMaskFilter mBlur = new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL);
mDrawPaint.setMaskFilter(mBlur);
private void doDraw(Canvas c) {
c.drawBitmap(mBitmap, 0, 0,null );
}
private float mX, mY;
private static final float TOUCH_TOLERANCE = 1;
void touch_start(float x, float y) {
mPath.reset();
mPath.moveTo(x, y);
mX = x;
mY = y;
}
void touch_move(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
mX = x;
mY = y;
}
canvas.drawPath(mPath, mDrawPaint ); //Erasing Black and white image
}
void touch_up() {
mPath.lineTo(mX, mY);
// commit the path to our offscreen
mCanvas.drawPath(mPath, mDrawPaint);
// kill this so we don't double draw
mPath.reset();
}
Try to use Monte Carlo method to estimate percentage of transparent area. I think it is a fastest and easiest way to do this. Take about 50 (depends on accuracy you need) random pixels on your transparency mask and check their color. Then calc ans = TransparentPixelsCount/TestPixelCount.
It is very hard to calculate square of user's drawings using path coordinates. And it's quite long to iterate over all pixels. So, IMHO Monte Carlo is your choise.
To get an exact (and slow) answer, you need to inspect every pixel and count the number are transparent and divide by the total number of pixels. If your requirements allow for some estimation, it is probably best to sample the image.
You could downsize the image and run and the above procedure on the smaller image. That has the disadvantage that the scaling operation might be going through all the pixels making it slow. I would recommend a grid sampling, it is similar to downsizing, but skips over pixels. Basically, we evenly space x sample points on a grid over the image. Then count the number of sample points that are transparent. The estimate of transparent percentage is the total transparent samples/number of transparent samples. You can get reasonable accuracy (usually within 5%) with a small number, say 100, samples. Here is a code function that implements this method -- bm is the Bitmap and scale is the number of samples per axis, so setting scale = 10 gives 100 total samples (10x10 sampling grid over the image).
static public float percentTransparent(Bitmap bm, int scale) {
final int width = bm.getWidth();
final int height = bm.getHeight();
// size of sample rectangles
final int xStep = width/scale;
final int yStep = height/scale;
// center of the first rectangle
final int xInit = xStep/2;
final int yInit = yStep/2;
// center of the last rectangle
final int xEnd = width - xStep/2;
final int yEnd = height - yStep/2;
int totalTransparent = 0;
for(int x = xInit; x <= xEnd; x += xStep) {
for(int y = yInit; y <= yEnd; y += yStep) {
if (bm.getPixel(x, y) == Color.TRANSPARENT) {
totalTransparent++;
}
}
}
return ((float)totalTransparent)/(scale * scale);
}
For reference, the slow method that would give you the results by counting every pixel is below. It can be used for reference on testing the above estimator.
static public float percentTransparent(Bitmap bm) {
final int width = bm.getWidth();
final int height = bm.getHeight();
int totalTransparent = 0;
for(int x = 0; x < width; x++) {
for(int y = 0; y < height; y++) {
if (bm.getPixel(x, y) == Color.TRANSPARENT) {
totalTransparent++;
}
}
}
return ((float)totalTransparent)/(width * height);
}
A different approach on this: you can calculate the size of each path using ComputeBounds. Then it should be simple to compare this with the size of your view and decide the % of the drawing.
Jus you need to keep in mind that the path can be drawn over itself, so you need to be careful and handle that in the calculation.
Store all point x and y value in two different sorted sets, one for x value of point and other for y value of point.
The final value of your bound will be point(min_x,min_y) and point(max_x,max_y).
You need to detect the points lying inside the drawn polygon.
Here is the functions which takes array that contains all the drawn point, and second parameter are the points itself i.e. x ,y.
// Return true if the dot { x,y } is within any of the polygons in the list
function pointInPolygons( polygons, dot )
for (i=1, [polygons count] i++)
{
if (pointInPolygon( polygons[i], dot ))
return true
}
return false
end
// Returns true if the dot { x,y } is within the polygon
//defined by points table { {x,y},- --{x,y},{x,y},... }
function pointInPolygon( points, dot )
local i, j = #points, #points
local oddNodes = false
for i=1, #points do
if ((points[i].y < dot.y and points[j].y>=dot.y
or points[j].y< dot.y and points[i].y>=dot.y) and (points[i].x<=dot.x
or points[j].x<=dot.x)) then
if (points[i].x+(dot.y-points[i].y)/(points[j].y-points[i].y)*(points[j].x-points[i].x)<dot.x) then
oddNodes = not oddNodes
end
end
j = i
end
return oddNodes
end
I've been working on a very simple little application using an extended view.
The problem is that i can't find what been pressed in my onTouchEvent.
I've understood that i've to compare the pressure-points (x and y) to the elements and see which one it could be.
But...
I've declared a rectangle using:
Paint color= new Paint();
color.setColor(Color.BLACK);
rectF = new RectF(0.0f, 0.0f, 1.0f, 0.5f) ;
canvas.drawRect(rectF, color);
So far so good, but in the onTouchEvent function
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction() ;
float x = event.getX() ;
float y = event.getY() ;
switch(action) {
case MotionEvent.ACTION_DOWN:
if(rectF().contains(x, y)) {
// Something should happen
}
}
invalidate();
return true ;
}
I set the rectF with some kind of relative points ranging from 0-1, but the X and Y i get from the event ranges from 0 and upwards depending on screen-size.
Can I easily convert either of the values?
Edit
Thought someone was interested in the final solution...
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction() ;
float x = event.getX() ;
float y = event.getY() ;
int width = this.getWidth() ;
int height = this.getHeight() ;
switch(action) {
case MotionEvent.ACTION_DOWN:
if(rectF().contains(x/width, y/height)) {
// Something should happen
}
}
return true ;
}
The values of the touch point and the RectF have to be on the same reference scale in oreder to be compared properly. Meaning that if you declare your RectF to use relative sizes (0-1) you will either need to :
1: normalize the MotionEvent position by your screen size
OR 2: "denormalize" the RectF dimensions by your actual screen size.
I think 1 is generally preferrable as it lets you define your rects in terms of relative layout, independent of the actual screen size and convert the event positions on-the-fly.
like this : rectF.contains(x/screenWidth, y/screenHeight);
You can retrieve the screen dimensions by calling Display display = getWindowManager().getDefaultDisplay() in your Activity and then calling display.getWidth() or display.getHeight()
So I have an ImageView using a Matrix to scale the Bitmap I'm displaying. I can double-tap to zoom to full-size, and my ScaleAnimation handles animating the zoom-in, it all works fine.
Now I want to double-tap again to zoom out, but when I animate this with ScaleAnimation, the ImageView does not draw the newly exposed areas of the image (as the current viewport shrinks), instead you see the portion of visible image shrinking in. I have tried using ViewGroup.setClipChildren(false), but this only leaves the last-drawn artifacts from the previous frame - leading to an trippy telescoping effect, but not quite what I was after.
I know there are many zoom-related questions, but none cover my situation - specifically animating the zoom-out operation. I do have the mechanics working - ie aside from the zoom-out animation, double-tapping to zoom in and out works fine.
Any suggestions?
In the end I decided to stop using the Animation classes offered by Android, because the ScaleAnimation applies a scale to the ImageView as a whole which then combines with the scale of the ImageView's image Matrix, making it complicated to work with (aside from the clipping issues I was having).
Since all I really need is to animate the changes made to the ImageView's Matrix, I implemented the OnDoubleTapListener (at the end of this post - I leave it as an "exercise to the reader" to add the missing fields and methods - I use a few PointF and Matrix fields to avoid excess garbage creation). Basically the animation itself is implemented by using View.post to keep posting a Runnable that incrementally changes the ImageView's image Matrix:
public boolean onDoubleTap(MotionEvent e) {
final float x = e.getX();
final float y = e.getY();
matrix.reset();
matrix.set(imageView.getImageMatrix());
matrix.getValues(matrixValues);
matrix.invert(inverseMatrix);
doubleTapImagePoint[0] = x;
doubleTapImagePoint[1] = y;
inverseMatrix.mapPoints(doubleTapImagePoint);
final float scale = matrixValues[Matrix.MSCALE_X];
final float targetScale = scale < 1.0f ? 1.0f : calculateFitToScreenScale();
final float finalX;
final float finalY;
// assumption: if targetScale is less than 1, we're zooming out to fit the screen
if (targetScale < 1.0f) {
// scaling the image to fit the screen, we want the resulting image to be centred. We need to take
// into account the shift that is applied to zoom on the tapped point, easiest way is to reuse
// the transformation matrix.
RectF imageBounds = new RectF(imageView.getDrawable().getBounds());
// set up matrix for target
matrix.reset();
matrix.postTranslate(-doubleTapImagePoint[0], -doubleTapImagePoint[1]);
matrix.postScale(targetScale, targetScale);
matrix.mapRect(imageBounds);
finalX = ((imageView.getWidth() - imageBounds.width()) / 2.0f) - imageBounds.left;
finalY = ((imageView.getHeight() - imageBounds.height()) / 2.0f) - imageBounds.top;
}
// else zoom around the double-tap point
else {
finalX = x;
finalY = y;
}
final Interpolator interpolator = new AccelerateDecelerateInterpolator();
final long startTime = System.currentTimeMillis();
final long duration = 800;
imageView.post(new Runnable() {
#Override
public void run() {
float t = (float) (System.currentTimeMillis() - startTime) / duration;
t = t > 1.0f ? 1.0f : t;
float interpolatedRatio = interpolator.getInterpolation(t);
float tempScale = scale + interpolatedRatio * (targetScale - scale);
float tempX = x + interpolatedRatio * (finalX - x);
float tempY = y + interpolatedRatio * (finalY - y);
matrix.reset();
// translate initialPoint to 0,0 before applying zoom
matrix.postTranslate(-doubleTapImagePoint[0], -doubleTapImagePoint[1]);
// zoom
matrix.postScale(tempScale, tempScale);
// translate back to equivalent point
matrix.postTranslate(tempX, tempY);
imageView.setImageMatrix(matrix);
if (t < 1f) {
imageView.post(this);
}
}
});
return false;
}