I am drawing a complicated shape using canvas.drawPath. The result of these drawPath methods is something like a map which is bigger than the screen. I would like the user to do the following: move the map using one finger. Or rotate it around the center of the screen using 2 fingers. Basically it should behave exactly like Google Maps only without scaling. So far I was able to get the desired movement and rotation:
private void handleTouch(MotionEvent event)
{
switch(event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_POINTER_DOWN:
{
rotate=true;
move=false;
a=Math.toDegrees(-Math.atan2(event.getX(0)-event.getX(1),event.getY(0)-event.getY(1)));
if(a>360)a=a-360;
if(a<-360)a=a+360;
da=a-angle;
if(da>360)da=da-360;
if(da<-360)da=da+360;
}
break;
case MotionEvent.ACTION_POINTER_UP:
{
rotate=false;
move=false;
x = (int)event.getX();
y = (int)event.getY();
dx = x - translate.x;
dy = y - translate.y;
}
break;
case MotionEvent.ACTION_DOWN:
{
move=true;
x = (int)event.getX();
y = (int)event.getY();
dx = x - translate.x;
dy = y - translate.y;
}
break;
case MotionEvent.ACTION_MOVE:
{
if(rotate && event.getPointerCount()==2)
{
angle=Math.toDegrees((-Math.atan2(event.getX(0)-event.getX(1),event.getY(0)-event.getY(1))))-da;
if(angle>360)angle=angle-360;
if(angle<-360)angle=angle+360;
vmap.invalidate();
}
else if(move==true)
{
translate.set((int)event.getX() - dx,(int)event.getY() - dy);
//trans.setTranslate(translate.x,translate.y);
vmap.invalidate();
}
}
break;
case MotionEvent.ACTION_UP:
{
//your stuff
}
break;
}
The problem is correctly applying them together. I can easily make the rotate around its own center if I first translate and then rotate around the center coordinate of the map. However if I try to rotate around the center of the screen things start to get complicated. Lets say I moved the canvas 20 pixels left and then rotated it 30 degrees, then moved 50 pixels down and then rotated it another 50 degrees. If I try to add the two angles together during the second rotation, I will now be rotating around a different coordinate, meaning that as soon as I do the new rotation the map is going to suddenly jump.
I looked into using matrices, but so far that didn't help.
Take a look at the Matrix class. It lets you transform ImageViews in many ways. For this problem, I created a test project that included the members:
ImageView imageView;
Matrix imageMatrix = new Matrix();
PointF screenCenter = new PointF();
Then, in onCreate():
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
screenCenter.x = metrics.widthPixels/2;
screenCenter.y = metrics.heightPixels/2;
imageView = (ImageView) findViewById(R.id.image);
imageView.setScaleType(ImageView.ScaleType.MATRIX);
imageMatrix.set(imageView.getImageMatrix());
}
The screenCenter object now contains the coordinates of the center of the screen, which will be used later. The imageView scale type has been set to MATRIX, and the imageMatrix has been set with the starting matrix that was applied to the view when we changed the scale type.
Now, when you translate you modify the imageMatrix, then apply the new matrix to the view with setImageMatrix():
void translate(float x, float y) {
imageMatrix.postTranslate(x, y);
imageView.setImageMatrix(imageMatrix);
}
Rotation works the same way, but we need the coordinates of the center of the screen:
void rotate(float angle) {
imageMatrix.postRotate(angle, screenCenter.x, screenCenter.y);
imageView.setImageMatrix(imageMatrix);
}
This works on my emulator, translate moves the expected direction and rotate always moves around the center of the screen, no matter how it's translated. Admittedly, it's a test project and it doesn't look very pretty.
OpenGL ES is included in the Android framework, and you might get better graphics performance if you go that route. If you do, then this answer might be helpful.
Related
I am developing a Gdx game but I have got stuck on some part and I will explain briefly about it:
I have 9 balls organized on 3*3 and I need to detect the ball that I'm touching as shown in the image below:
enter image description here
and I typed this code:
for (int i : listBalls) {
touchPoint = new Vector3(Gdx.input.getX(), Gdx.input.getY(), 0);
rectangle = new Rectangle(sprite[i].getX(), sprite[i].getY(), spriteSize, spriteSize);
if (rectangle.contains(touchPoint.x, touchPoint.y)) {
Gdx.app.log("Test", "Touched dragged " + String.valueOf(i));
pointer = i;
}
}
In case of touching any ball of the above row, it detects the opposite ball in the bottom row. For example in the above image, if I touch ball no 2 on the top it will point to ball no 8, and same if touching any of the bottom balls.
In case of touching any ball of the middle ball, it gives the right on.
I hope I could explain clearly my issue. Please help.
As explained here: LibGDX input y-axis flipped the coordinate system of the input is inverted on the y-axis, try substracting the screen height of your device or camera
int y = screenHeight - Gdx.input.getY();
Keep in mind that using a Camera and unprojecting the input coordinates is the most recommended way to go about detecting input in LibGDX. Example:
#Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
Vector3 unprojected = camera.unproject(new Vector3(screenX, screenY,0));
}
You don't even have to invert the y-coordinate, unproject() does this automatically for you. To use the correct x and y coordinates you then could use:
float x = unprojected.x;
float y = unprojected.y;
I'm trying to rotate my Bitmap using a readymade solution I found somewhere. The code is below:
public void onDraw(Canvas canvas) {
float x = ship.Position.left;
float y = ship.Position.top;
canvas.drawBitmap(ship.ship, x,y,null);
invalidate();
}
However, when I do it, the X and Y axii change their direction - if I increase the Y the image goes towards the top of the screen, not towards the bottom. Same happens to X if I rotate by 90 degrees.
I need to rotate it but without changing the Y and X axii directions.
Even rotated, I still want the Bitmap to go towards the bottom if I increase Y and to the right if I increase the X.
public void update()
{
if(!moving)
{
fall();
}
else //moving
{
move();
faceDirection();
}
Position.top += Speed;
}
private void move() {
if(Speed < MAXSPEED)
Speed -= 0.5f;
}
private void fall() {
if(Speed > MAXSPEED*-1)
Speed += 0.2f;
}
private void faceDirection() {
double OldDiretion = Direction;
Direction = DirectionHelper.FaceObject(Position, ClickedDiretion);
if (Direction != OldDiretion)
{
Matrix matrix = new Matrix();
matrix.postRotate((float)Direction);
ship = Bitmap.createBitmap(ship, 0, 0, ship.getWidth(),ship.getHeight(), matrix, false);
}
I tried the code above, but it's still changing the Y direction, It's going to bottom of the BitMap, not bottom of the screen.
Here is the project: https://docs.google.com/file/d/0B8V9oTk0eiOKOUZJMWtsSmUtV3M/edit?usp=sharing
You should first rotate, than translate:
matrix.postTranslate(x, y);
matrix.postRotate(degree);
alternative would be to try to use preRotate() instead of postRotate().
I also strongly recommend to translate/rotate the original while drawing. So your createBitmap() call shouldn't modify the orientation. At least not when you change it dynamically on user interaction. Otherwise you would create a lot of bitmaps to represent rotations over and over again which would impact the performance.
The problem is that you don't actually rotate the bitmap - you just draw it rotated. So, the next time you redraw it, you first push it towards the bottom or right by incrementing x/y and then rotate it.
You have to actually rotate the bitmap itself. You could use the following code:
ship.ship = Bitmap.createBitmap(ship.ship, 0, 0, ship.ship.getWidth(), ship.ship.getHeight(), matrix, false);
Here you create a new rotated bitmap and set your reference to point to it.
Note! You must do this only once! So you can't do it in the onDraw method, since then it will get rotated every time it's redrawn. You have to do it somewhere else and then draw it as usual in onDraw (without the matrix rotations).
I am trying to gain some more familiarity with the Android SurfaceView class, and in doing so am attempting to create a simple application that allows a user to move a Bitmap around the screen. The troublesome part of this implementation is that I am also including the functionality that the user may drag the image again after it has been placed. In order to do this, I am mapping the bitmap to a simple set of coordinates that define the Bitmap's current location. The region I am mapping the image to, however, does not match up with the image.
The Problem
After placing an image on the SurfaceView using canvas.drawBitmap(), and recording the coordinates of the placed image, the mapping system that I have set up misinterprets the Bitmap's coordinates somehow and does not display correctly. As you can see in this image, I have simply used canvas.drawLine() to draw lines representing the space of my touch region, and the image is always off and to the right:
The Code
Here, I shall provide the relevant code excerpts to help answer my question.
CustomSurface.java
This method encapsulates the drawing of the objects onto the canvas. The comments clarify each element:
public void onDraw(Canvas c){
//Simple black paint
Paint paint = new Paint();
//Draw a white background
c.drawColor(Color.WHITE);
//Draw the bitmap at the coordinates
c.drawBitmap(g.getResource(), g.getCenterX(), g.getCenterY(), null);
//Draws the actual surface that is receiving touch input
c.drawLine(g.left, g.top, g.right, g.top, paint);
c.drawLine(g.right, g.top, g.right, g.bottom, paint);
c.drawLine(g.right, g.bottom, g.left, g.bottom, paint);
c.drawLine(g.left, g.bottom, g.left, g.top, paint);
}
This method encapsulates how I capture touch events:
public boolean onTouchEvent(MotionEvent e){
switch(e.getAction()){
case MotionEvent.ACTION_DOWN:{
if(g.contains((int) e.getX(), (int) e.getY()))
item_selected = true;
break;
}
case MotionEvent.ACTION_MOVE:{
if(item_selected)
g.move((int) e.getX(), (int) e.getY());
break;
}
case MotionEvent.ACTION_UP:{
item_selected = false;
break;
}
default:{
//Do nothing
break;
}
}
return true;
}
Graphic.java
This method is used to construct the Graphic:
//Initializes the graphic assuming the coordinate is in the upper left corner
public Graphic(Bitmap image, int start_x, int start_y){
resource = image;
left = start_x;
top = start_y;
right = start_x + image.getWidth();
bottom = start_y + image.getHeight();
}
This method detects if a user is clicking inside the image:
public boolean contains(int x, int y){
if(x >= left && x <= right){
if(y >= top && y <= bottom){
return true;
}
}
return false;
}
This method is used to move the graphic:
public void move(int x, int y){
left = x;
top = y;
right = x + resource.getWidth();
bottom = y + resource.getHeight();
}
I also have 2 methods that determine the center of the region (used for redrawing):
public int getCenterX(){
return (right - left) / 2 + left;
}
public int getCenterY(){
return (bottom - top) / 2 + top;
}
Any help would be greatly appreciated, I feel as though many other StackOverflow users could really benefit from a solution to this issue.
There's a very nice and thorough explanation of touch/multitouch/gestures on Android Developers blog, that includes free and open source code example at google code.
Please, take a look. If you don't need gestures -- just skip that part, read about touch events only.
This issue ended up being much simpler than I had thought, and after some tweaking I realized that this was an issue of image width compensation.
This line in the above code is where the error stems from:
c.drawBitmap(g.getResource(), g.getCenterX(), g.getCenterY(), null);
As you can tell, I manipulated the coordinates from within the Graphic class to produce the center of the bitmap, and then called canvas.drawBitmap() assuming that it would draw from the center outward.
Obviously, this would not work because the canvas always drops from the top left of an image downwards and to the right, so the solution was simple.
The Solution
Create the touch region with regards to the touch location, but draw it relative to a distance equal to the image width subtracted from the center location in the x and y directions. I basically changed the architecture of the Graphic class to implement a getDrawX() and getDrawY() method that would return the modified x and y coordinates of where it should be drawn in order to have the center_x and center_y values (determined in the constructor) actually appear to be at the center of the region.
It all comes down to the fact that in an attempt to compensate for the way the canvas draws bitmaps, I unfortunately incorporated some bad behaviors and in the end had to handle the offset in a completely different way.
In my app, when the user touches the screen, a circle is drawn, and if a circle has already been drawn at point a, then another circle cannot be drawn there.
when I use canvas.drawCircle(...), it's innacurate. if I tap close to the top of the screen, then it is just very minutely missing where I tapped it (slightly to the left or right). The farther I go down the farther up my circle is from where I touched. If I'm touching the left side of the screen, the circle goes to the right of the touch point, and if I touch the right side of the screen, the circle is on the left.
Here's my code:
public void onCreate(...){
super.onCreate();
setcontentView(...);
drawnX = new ArrayList<Float>();
drawnY = new ArrayList<Float>();
layout = (ImageView)findViewById(R.id.layout);
layout.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent me) {
newY = me.getY();
newX = me.getX();
int action = me.getAction();
if (action==0){
Log.v(tag, "New Touch");
Log.i(tag, String.valueOf("Action " + action +
" happened at X,Y: " + "("+newX+","+newY + ")"));
getDistance(newX, newY);
}
return true;
}
});
#Override
public void onResume(){
super.onResume();
display = getWindowManager().getDefaultDisplay();
screenHeight = display.getHeight();
screenWidth = display.getWidth();
bitmap = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.ARGB_8888);
canvas = new Canvas(bitmap);
paint = new Paint();
circleRadius = screenHeight/50;
}
public void getDistance(float xNew, float yNew){
// here I compare the new touch x and y values with the x and y values saved
// in the Array Lists I made above. If distance is > circleRadius, then:
drawPoint(xNew, yNew)
}
public void drawPoint(final float x, final float y){
minDistance = Float.NaN;
new Thread(new Runnable(){
#Override
public void run(){
paint.setColor(Color.RED);
canvas.drawCircle(x, y, circleRadius, paint);
layout.post(new Runnable(){
#Override
public void run(){
layout.setImageDrawable(new BitmapDrawable(bitmap));
//I have also tried setImageBitmap and setBackgroundDrawable
}
});
}
}).start();
}
Int the process of all of this, I also do some logging in the drawPoint, and it shows that the x and y coordinates match that which are gotten in the onTouch.
I've also tried maknig it a LinearLayout, but that had the same effect.
And, the bitmap size matches the screen size and canvas size. also, for testing I did
canvas.drawARGB(...)
and it covered the whole screen, so I know it's not just that the bitmap is not stretching to the bottom. The layout's height is less than the height of everything else, and the width of the layout is the same as everything else, but when I use layout.getHeight and layout.getWidth in my onResume, they always return 0, so I can't really use that.
I really have no clue what's causing it. I also tried it on two emulators and got the same response. Any help would be greatly appreciated.
Also, if I tap on the drawn circle (toward the bottom of the screen), another circle will then be drawn above that one, but if I tap where I previously tapped, then a new circle will not be drawn, which really is what's suppose to happen. the circle is just not showing on the screen correctly.
I found the answer.
Oddly enough, even though the width of the bitmap matched the width of the layout, when I did a little more testing, I found the dimensions didn't actually fit together on the left and right either.
I wasn't using the layout.getWidth() and layout.getHeight() due to it returning 0 in onResume()
But, I thought the different height from display.getHeight() very well may be causing the problem, so I did essentially:
new Handler().postDelayed(getLayoutSizeRunnable, 5000);
where getLayoutSizeRunnable was essentially:
screenWidth = layout.getWidth();
screenHeight = layout.getHeight();
Then everything worked perfectly.
And for the usage in my app, I'll use an if statement so the first time a touch point is made, the layout size will be calculated, and the bitmap will be created based on it.
I've drawn 5 bitmaps from .png files on a canvas - a head, a body and two arms and legs.
How can I detect which of these has been touched on an OnTouch? And, more specifically, can I detect if the OnTouch was within the actual shape of the body part touched?
What I mean is, obviously, the .pngs themselves are rectangular, but does Android know, or can I tell it, to ignore the transparency within the image?
You could get the colour of pixel touched and compare it to the colour of pixel on the background at those co-ords.
EDIT: ok, ignore that, you can't get the colour of a pixel on the canvas, so instead, get the x,y of the touch, check if any of the body part images have been touched, if so, take the x,y of the image from the touch x,y, then get the pixel of the image, which should be transparent or colour.
public boolean onTouchEvent(MotionEvent event)
{
int x = (int) event.getX();
int y = (int) event.getY();
int offsetx, offsety;
for(int i = 0;i<NUM_OF_BODY_PARTS;i++)
{
if(bodyPartRect[i].intersects(x,y,x+1,y+1))
{
offsetx = x - bodyPartRect[i].left;
offsety = y - bodyPartRect[i].top;
if(bodyPartBMP[i].getPixel(offsetx,offsety) == TRANSPARENT)
{
//whatever
}
}
}
}