I'm writing an app which displays map. User can zoom and pan. Map is rotated according to magnetometer's value (map is rotated in opposite direction of device rotation).
For scaling I'm using ScaleGestureDetector and passing scale factor to Matrix.scaleM.
For panning I'm using this code:
GlSurfaceView side:
private void handlePanAndZoom(MotionEvent event) {
int action = MotionEventCompat.getActionMasked(event);
// Get the index of the pointer associated with the action.
int index = MotionEventCompat.getActionIndex(event);
int xPos = (int) MotionEventCompat.getX(event, index);
int yPos = (int) MotionEventCompat.getY(event, index);
mScaleDetector.onTouchEvent(event);
switch (action) {
case MotionEvent.ACTION_DOWN:
mRenderer.handleStartPan(xPos, yPos);
break;
case MotionEvent.ACTION_MOVE:
if (!mScaleDetector.isInProgress()) {
mRenderer.handlePan(xPos, yPos);
}
break;
}
}
Renderer side:
private static final PointF mPanStart = new PointF();
public void handleStartPan(final int x, final int y) {
runOnGlThread(new Runnable() {
#Override
public void run() {
windowToWorld(x, y, mPanStart);
}
});
}
private static final PointF mCurrentPan = new PointF();
public void handlePan(final int x, final int y) {
runOnGlThread(new Runnable() {
#Override
public void run() {
windowToWorld(x, y, mCurrentPan);
float dx = mCurrentPan.x - mPanStart.x;
float dy = mCurrentPan.y - mPanStart.y;
mOffsetX += dx;
mOffsetY += dy;
updateModelMatrix();
mPanStart.set(mCurrentPan);
}
});
}
windowToWorld function uses gluUnProject and works because I'm using it for many other tasks. UpdateModelMatrix:
private void updateModelMatrix() {
Matrix.setIdentityM(mScaleMatrix,0);
Matrix.scaleM(mScaleMatrix, 0, mScale, mScale, mScale);
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, 1.0f);
Matrix.setIdentityM(mTranslationMatrix,0);
Matrix.translateM(mTranslationMatrix, 0, mOffsetX, mOffsetY, 0);
// Model = Scale * Rotate * Translate
Matrix.multiplyMM(mIntermediateMatrix, 0, mScaleMatrix, 0, mRotationMatrix, 0);
Matrix.multiplyMM(mModelMatrix, 0, mIntermediateMatrix, 0, mTranslationMatrix, 0);
}
Same mModelMatrix is used in gluUnproject of windowToWorld function for point translation.
So my problem two-fold:
The panning occurs twice slower than the movement of finger on the device's screen
At some point when panning continuously for a few seconds (making circles on screen, for example) the map starts to 'shake'. The amplitude of this shaking is getting bigger and bigger. Looks like some value adds up in handlePan iterations and causes this effect.
Any idea why these happen?
Thank you in advance, Greg.
Well, the problem with my code is this line:
mPanStart.set(mCurrentPan);
Simply because I drag in world coordinates, and update offset, but current location stays the same. This was my bug.
Removing this line will fix everything.
Related
I have a Canvas that is scaled so everything fits better:
#Override
public void draw(Canvas c){
super.draw(c);
final float scaleFactorX = getWidth()/(WIDTH*1.f);
final float scaleFactorY = getHeight()/(HEIGHT*1.f);
if(c!=null) {
final int savedState = c.save();
c.scale(scaleFactorX, scaleFactorY);
(rendering)
c.restoreToCount(savedState);
}
}
It scales based on these two:
public static final int WIDTH = 856;
public static final int HEIGHT = 1050;
Which causes the problem that the coordinates of the MotionEvent that handles touch events is not equal to the coordinates that is created with the Canvas. This causes problems when I try to check collision between the MotionEvent Rect and the Rect of a class that is based on the rendering scale. This causes the class SuperCoin's X coordinate to not be equal to MotionEvent X coordinates.
Usually, MotionEvent's coordinates, both X and Y is way bigger than the screen's max size(defined by WIDTH and HEIGHT)
#Override
public boolean onTouchEvent(MotionEvent e) {
super.onTouchEvent(e);
switch (MotionEventCompat.getActionMasked(e)) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
(...)
Rect r = new Rect((int)e.getX(), (int)e.getY(), (int)e.getX() + 3, (int)e.getY() + 3);
if(superCoins.size() != 0) {
for (SuperCoin sc : superCoins) {
if (sc.checkCollision(r)) {
progress++;
superCoins.remove(sc);
}
}
}
break;
}
return true;
}
And the SuperCoin:
public class SuperCoin {
private Bitmap bm;
public int x, y, orgY;
Clicker cl;
private Long startTime;
Random r = new Random();
public SuperCoin(Bitmap bm, int x, int y, Clicker c){
this.x = x;
this.y = y;
this.orgY = y;
this.bm = bm;
this.cl = c;
startTime = System.nanoTime();
bounds = new Rect(x, y, x + bm.getWidth(), y + bm.getHeight());
}
private Rect bounds;
public boolean checkCollision(Rect second){
if(second.intersect(bounds)){
return true;
}
return false;
}
private int velX = 0, velY = 0;
public void render(Canvas c){
long elapsed = (System.nanoTime()-startTime)/1000000;
if(elapsed>50) {
int cx;
cx = r.nextInt(2);
if(cx == 0){
velX = r.nextInt(4);
}else if(cx == 1){
velX = -r.nextInt(4);
}
velY = r.nextInt(10) + 1;
startTime = System.nanoTime();
}
if(x < 0) velX = +2;
if(x > Clicker.WIDTH) velX = -2;
x += velX;
y -= velY;
c.drawBitmap(bm, x, y, null);
}
}
How can I check collision between the two different when the MotionEvent X coordinate is bigger than the screen's scaled max coordinates?
Honestly, I am not completly sure why the Rect defined in the SuperCoin class is different from the one defined in the onTouchEvent method. I'm guessing because the X and Y is permanently different between the one defined by MotionEvent and the ones defined by the scaled canvas. The Rect in the SuperCoin class goes by the width of the Bitmap it has been passed. It scales it with the width and height of the Bitmap.
After looking through StackOverflow and Google for the past 2 days looking for something that comes close to a solution, I came over this: Get Canvas coordinates after scaling up/down or dragging in android Which solved the problem. It was really hard to find because the title was slightly misleading(of the other question)
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);
I added this as an answer and accepting it so it no longer will be an unanswered question as it is solved. The code above converts the coordinates to integers so they can be used for checking collision between the finger and the object I'm checking with
Don't scale the canvas directly. Make a Matrix object, scale that once. Then concat that to the canvas. Then you can make an inverted matrix for your touch events.
And just make the invert matrix whenever you change the view matrix:
viewMatrix = new Matrix();
viewMatrix.scale(scalefactor,scalefactor);
invertMatrix = new Matrix(viewMatrix);
invertMatrix.invert(invertMatrix);
Then apply these two matrices to the relevant events.
#Override
public boolean onTouchEvent(MotionEvent event) {
event.transform(invertMatrix);
And then on the draw events, concat the matrix.
#Override
protected void onDraw(Canvas canvas) {
canvas.concat(viewMatrix);
And you're done. everything is taken care of for you. Whatever modifications you do to the view matrix will change your viewbox and your touch events will be translated into that same scene too.
If you want to add panning or rotation, or even skew the view, it's all taken care of. Just apply that to the matrix, get the inverted matrix, and the view will look that way and the touch events will respond as you expect.
I'm new to Android, and I'm trying to get the hang of multi touch input. I've begun with a simple app that allows the user to create rectangles on a Canvas by dragging and releasing with one finger, which I have working. To expand upon that, I now want a user to be able to rotate the rectangle they are drawing using a second finger, which is where my problems begin. As it stands, adding a second finger will cause multiple rectangles to rotate, instead of just the current one, but they will revert to their default orientation as soon as the second finger is released.
I've been working at it for a while, and I think my core problem is that I'm mishandling the multiple MotionEvents that come with two (or more fingers). Logging statements I left to display the coordinates on the screen for each event stay tied to the first finger touching the screen, instead of switching to the second. I've tried multiple configurations of accessing and changing the event pointer ID, and still no luck. If anyone could provide some guidance in the right direction, I would be extremely grateful.
My code is as follows:
public class BoxDrawingView extends View {
private static final String TAG = "BoxDrawingView";
private static final int INVALID_POINTER_ID = -1;
private int mActivePointerId = INVALID_POINTER_ID;
private Box mCurrentBox;
private List<Box> mBoxen = new ArrayList<>();
private Float mLastTouchX;
private Float mLastTouchY;
...
#Override
public boolean onTouchEvent(MotionEvent event) {
switch(MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = MotionEventCompat.getPointerId(event, 0);
current = new PointF(MotionEventCompat.getX(event, mActivePointerId),
MotionEventCompat.getY(event, mActivePointerId));
action = "ACTION_DOWN";
// Reset drawing state
mCurrentBox = new Box(current);
mBoxen.add(mCurrentBox);
mLastTouchX = MotionEventCompat.getX(event, MotionEventCompat.getPointerId(event, 0));
mLastTouchY = MotionEventCompat.getY(event, MotionEventCompat.getPointerId(event, 0));
break;
case MotionEvent.ACTION_POINTER_DOWN:
action = "ACTION_POINTER_DOWN";
mActivePointerId = MotionEventCompat.getPointerId(event, 0);
mLastTouchX = MotionEventCompat.getX(event, MotionEventCompat.getPointerId(event, 0));
mLastTouchY = MotionEventCompat.getY(event, MotionEventCompat.getPointerId(event, 0));
break;
case MotionEvent.ACTION_MOVE:
action = "ACTION_MOVE";
current = new PointF(MotionEventCompat.getX(event, mActivePointerId),
MotionEventCompat.getY(event, mActivePointerId));
if (mCurrentBox != null) {
mCurrentBox.setCurrent(current);
invalidate();
}
if(MotionEventCompat.getPointerCount(event) > 1) {
int pointerIndex = MotionEventCompat.findPointerIndex(event, mActivePointerId);
float currX = MotionEventCompat.getX(event, pointerIndex);
float currY = MotionEventCompat.getY(event, pointerIndex);
if(mLastTouchX < currX) {
// simplified: only use x coordinates for rotation for now.
// +X for clockwise, -X for counter clockwise
Log.d(TAG, "Clockwise");
mRotationAngle = 30;
}
else if (mLastTouchX > getX()) {
Log.d(TAG, "Counter clockwise");
mRotationAngle = -30;
}
}
break;
case MotionEvent.ACTION_UP:
action = "ACTION_UP";
mCurrentBox = null;
mLastTouchX = null;
mLastTouchY = null;
mActivePointerId = INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_UP:
action = "ACTION_POINTER_UP";
int pointerIndex = event.getActionIndex();
int pointerId = event.getPointerId(pointerIndex);
if(pointerId == mActivePointerId){
mActivePointerId = INVALID_POINTER_ID;
}
break;
case MotionEvent.ACTION_CANCEL:
action = "ACTION_CANCEL";
mCurrentBox = null;
mActivePointerId = INVALID_POINTER_ID;
break;
}
return true;
}
#Override
protected void onDraw(Canvas canvas){
// Fill the background
canvas.drawPaint(mBackgroundPaint);
for(Box box : mBoxen) {
// Box is a custom object. Origin is the origin point,
// Current is the point of the opposite diagonal corner
float left = Math.min(box.getOrigin().x, box.getCurrent().x);
float right = Math.max(box.getOrigin().x, box.getCurrent().x);
float top = Math.min(box.getOrigin().y, box.getCurrent().y);
float bottom = Math.max(box.getOrigin().y, box.getCurrent().y);
if(mRotationAngle != 0) {
canvas.save();
canvas.rotate(mRotationAngle);
canvas.drawRect(left, top, right, bottom, mBoxPaint);
canvas.rotate(-mRotationAngle);
canvas.restore();
mRotationAngle = 0;
} else {
canvas.drawRect(left, top, right, bottom, mBoxPaint);
}
}
}
}
There are several ways to draw things, not just in android, but in Java as well. The thing is that you are trying to draw the rectangles by rotating the Canvas. That's a way, but in my personal experience I think that is only a good choice if you want to rotate the whole picture. If not, that may get a little tricky because you need to place a rotation axis, which it seems you are not using, so Android will asume that you want to rotate from the left top corner or the center of the view (I don't remember).
If you are opting for that choice, you may try to do it like this:
Matrix matrix = new Matrix();
matrix.setRotate(angle, rectangleCenterX, rectangleCenterY);
canvas.setMatrix(matrix);
But I recommend you to try a different approach. Do the rotation directly on the rectangle that you are moving, by calculating the axes of the polygon. This you can do it using Java Math operations:
public void formShape(int cx[], int cy[], double scale) {
double xGap = (width / 2) * Math.cos(angle) * scale;
double yGap = (width / 2) * Math.sin(angle) * scale;
cx[0] = (int) (x * scale + xGap);
cy[0] = (int) (y * scale + yGap);
cx[1] = (int) (x * scale - xGap);
cy[1] = (int) (y * scale - yGap);
cx[2] = (int) (x * scale - xGap - length * Math.cos(radians) * scale);
cy[2] = (int) (y * scale - yGap - length * Math.sin(radians) * scale);
cx[3] = (int) (x * scale + xGap - length * Math.cos(radians) * scale);
cy[3] = (int) (y * scale + yGap - length * Math.sin(radians) * scale);
}
So (x,y) is the center of your rectangle and with, height tell you how big is it. In the formShape(int[], int[], double) method cx and cy are going to be used to draw your shape and scale is the value to use if you want to do zoom in or zoom out later, if not just use scale = 1;
Now for drawing your rectangles, this is how you do it:
Paint paint = new Paint();
paint.setColor(Color.GRAY);
paint.setStyle(Style.FILL);
int[] cx = new int[4];
int[] cy = new int[4];
Box box = yourBoxHere;
box.formShape(cx, cy, 1);
Path path = new Path();
path.reset(); // only needed when reusing this path for a new build
path.moveTo(cx[0], cy[0]); // used for first point
path.lineTo(cx[1], cy[1]);
path.lineTo(cx[2], cy[2]);
path.lineTo(cx[3], cy[3]);
path.lineTo(cx[0], cy[0]); // repeat the first point
canvas.drawPath(wallpath, paint);
For multitouch rotation listener you should override 2 methods in your Activity or View:
#Override
public boolean onTouch(View v, MotionEvent event) {
if(event.getId() == MotionEvent.ACTION_UP)
this.points = null;
}
}
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
if(event.getPointerCount() >= 2) {
float newPoints[][] = new float[][] {
{event.getX(0), event.getY(0)},
{event.getX(1), event.getY(1)}
};
double angle = angleBetweenTwoPoints(newPoints[0][0], newPoints[0][1], newPoints[1][0], newPoints[1][1]);
if(points != null) {
double difference = angle - initialAngle;
if(Math.abs(difference) > rotationSensibility) {
listener.onGestureListener(GestureListener.ROTATION, Math.toDegrees(difference));
this.initialAngle = angle;
}
} else {
this.initialAngle = angle;
}
this.points = newPoints;
}
}
public static double angleBetweenTwoPoints(double xHead, double yHead, double xTail, double yTail) {
if(xHead == xTail) {
if(yHead > yTail)
return Math.PI/2;
else
return (Math.PI*3)/2;
} else if(yHead == yTail) {
if(xHead > xTail)
return 0;
else
return Math.PI;
} else if(xHead > xTail) {
if(yHead > yTail)
return Math.atan((yHead-yTail)/(xHead-xTail));
else
return Math.PI*2 - Math.atan((yTail-yHead)/(xHead-xTail));
} else {
if(yHead > yTail)
return Math.PI - Math.atan((yHead-yTail)/(xTail-xHead));
else
return Math.PI + Math.atan((yTail-yHead)/(xTail-xHead));
}
}
Sorry, but this answer is getting long, if you have further questions about any of those operations and you want to change the approach of your solution, please ask again and tell me in the comments.
I hope this was helpful.
I'm trying to develop a simple game. My problem is I am trying to rotate an arrow image in the bottom of the screen by following the "x" event from Touch Listener.
Below is my Arrow class:
public class Arrow extends GameObject{
boolean show = true;
Bitmap bmp;
Game game;
Matrix matrix;
int i = 0;
public Arrow(Handler handler, int x, int y, int xSpeed, int ySpeed , Game game) {
super(handler, x, y, xSpeed, ySpeed);
this.game = game;
bmp = BitmapFactory.decodeResource(game.getResources(), R.drawable.arrow);
matrix = new Matrix();
}
#Override
public void tick() {
}
#Override
public void render(Canvas c) {
matrix.postRotate(x);
c.drawBitmap(bmp,matrix, null);
}
}
and this is the Event Listener
#Override
public boolean onTouch(View v, MotionEvent event) {
x = (int) event.getX();
y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
arrow.setX(x);
break;
case MotionEvent.ACTION_MOVE :
touch.move();
break;
case MotionEvent.ACTION_UP:
break;
}
}
}
}
return true;
}
How do I write code which rotates the bitmap?
Here is an example.
Matrix matrix = new Matrix();
//Calculate the rotation of the bitmap.
rotation += 10;
matrix.postRotate(rotation); // or matrix.postRotate(rotation,cx,cy);
canvas.drawBitmap(bitmap, matrix, null);
As an optimization, create the Matrix once outside this method and replace the creation with a call to matrix.reset().This way the canvas stays directed as before, and you can do more stuff with your Matrix like translating, scaling etc. and the matrix's content encapsulates the real meaning of your manipulation.
Calculating the angle :
For calculating the angle first you need to know the total progress length(Min value of progress - Max value of progress) of the Seekbar that can and the change in the value after seek.
Consider
Min of seek bar = 0
Max value of seekbar = 30
Now calculate the angle per unit that is,
1 unit = 360/30 = 12.
Suppose if the user has changed the seekbar position from 10 to 20. Now calculate the diff
int diff = 10-20 = -10.
rotation angle = (-10 * 12) = -120;
Example 2 :
if the user has changed the seekbar position from 15 to 10. Now calculate the diff
int diff = 15-10 = 5.
rotation angle = (5 * 12) = 60;
Here is the answer to my own question, it rotates the image according to the x input data:
float zeroPoint =MainGame.SCREEN_W / 2;
zeroPoint-=x;
float lala = MainGame.SCREEN_W/(180-y);
matrix.setRotate(zeroPoint/=lala, bmp.getWidth() /2, bmp.getHeight());
I intend to create an application that can take photos in the following way:
When the user touches the screen, it starts to take photos
It takes several photos within a few microseconds, each with different focus
In pseudocode:
Camera camera = getAndroidCamera();
for(i<10)
{
camera.setFocus(i*0.1);
camera.takePhoto(path, pictureName+i);
}
So basically I intend to take photos of the same object with different values of focus.
According to this, it is not possible, only assisted autofocus is viable.
Can you confirm it?
If possible, how should I do it? Should I set autofocus to different areas?
Answer -- Android setFocusArea and Auto Focus
All I had to do is cancel previously called autofocus. Basically the correct order of actions is this:
protected void focusOnTouch(MotionEvent event) {
if (camera != null) {
camera.cancelAutoFocus();
Rect focusRect = calculateTapArea(event.getX(), event.getY(), 1f);
Rect meteringRect = calculateTapArea(event.getX(), event.getY(), 1.5f);
Parameters parameters = camera.getParameters();
parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO);
parameters.setFocusAreas(Lists.newArrayList(new Camera.Area(focusRect, 1000)));
if (meteringAreaSupported) {
parameters.setMeteringAreas(Lists.newArrayList(new Camera.Area(meteringRect, 1000)));
}
camera.setParameters(parameters);
camera.autoFocus(this);
}}
..... update
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
...
Parameters p = camera.getParameters();
if (p.getMaxNumMeteringAreas() > 0) {
this.meteringAreaSupported = true;
}
...
}
/**
* Convert touch position x:y to {#link Camera.Area} position -1000:-1000 to 1000:1000.
*/
private Rect calculateTapArea(float x, float y, float coefficient) {
int areaSize = Float.valueOf(focusAreaSize * coefficient).intValue();
int left = clamp((int) x - areaSize / 2, 0, getSurfaceView().getWidth() - areaSize);
int top = clamp((int) y - areaSize / 2, 0, getSurfaceView().getHeight() - areaSize);
RectF rectF = new RectF(left, top, left + areaSize, top + areaSize);
matrix.mapRect(rectF);
return new Rect(Math.round(rectF.left), Math.round(rectF.top), Math.round(rectF.right), Math.round(rectF.bottom));
}
private int clamp(int x, int min, int max) {
if (x > max) {
return max;
}
if (x < min) {
return min;
}
return x;
}
I am developing a game for Android using LibGDX. I have added pinch zoom and pan. My issue is how to keep from going outside of the play area. As it is, you can pan outside of the play area into blackness. When zoomed out fully I know how to deal with it, I just said:
if(camera.zoom == 1.0f) ;
else {
}
But, if zoomed in, how do I accomplish this. I know this is not that complicated, I just can't seem to figure it out. Upon creation I set the camera to the middle of the screen. I know how to pan, I am using camera.translate(-input.deltaX, -input.deltaY, 0), I just need to test before this call to see if the position is outside of the play area. When I am zoomed in, how do I test if I am at the edge of the screen?
You can use one of
camera.frustum.boundsInFrustum(BoundingBox box)
camera.frustum.pointInFrustum(Vector3 point)
camera.frustum.sphereInFrustum(Vector3 point, float radius)
to check if a point/box/sphere is within your camera's view.
What I normally do is define 4 boxes around my world where the player should not be allowed to see. If the camera is moved and one of the boxes is in the frustum, I move the camera back to the previous position.
Edit: AAvering has implemented this in code below.
Credit goes to Matsemann for idea, here is the implementation I used.
Make a custom MyCamera class extending OrthographicCamera and add the following code:
BoundingBox left, right, top, bottom = null;
public void setWorldBounds(int left, int bottom, int width, int height) {
int top = bottom + height;
int right = left + width;
this.left = new BoundingBox(new Vector3(left - 2, 0, 0), new Vector3(left -1, top, 0));
this.right = new BoundingBox(new Vector3(right + 1, 0, 0), new Vector3(right + 2, top, 0));
this.top = new BoundingBox(new Vector3(0, top + 1, 0), new Vector3(right, top + 2, 0));
this.bottom = new BoundingBox(new Vector3(0, bottom - 1, 0), new Vector3(right, bottom - 2, 0));
}
Vector3 lastPosition = new Vector3();
#Override
public void translate(float x, float y) {
lastPosition.set(position.x, position.y, 0);
super.translate(x, y);
}
public void translateSafe(float x, float y) {
translate(x, y);
update();
ensureBounds();
update();
}
public void ensureBounds() {
if (frustum.boundsInFrustum(left) || frustum.boundsInFrustum(right) || frustum.boundsInFrustum(top) || frustum.boundsInFrustum(bottom)) {
position.set(lastPosition);
}
}
Now, in you custom sceene or whathever you use (in my case it was a custom Board class) call:
camera.setWorldBounds()
and in your GestureListener.pan method you can call
camera.translateSafe(x, y);
it should keep your camera in bounds
Here's the code I call after the position of the camera is updated due to panning or zooming in my 2D game using an orthographic camera. It corrects the camera position so that it doesn't show anything outside the borders of the play area.
float camX = camera.position.x;
float camY = camera.position.y;
Vector2 camMin = new Vector2(camera.viewportWidth, camera.viewportHeight);
camMin.scl(camera.zoom/2); //bring to center and scale by the zoom level
Vector2 camMax = new Vector2(borderWidth, borderHeight);
camMax.sub(camMin); //bring to center
//keep camera within borders
camX = Math.min(camMax.x, Math.max(camX, camMin.x));
camY = Math.min(camMax.y, Math.max(camY, camMin.y));
camera.position.set(camX, camY, camera.position.z);
camMin is the lowest left corner that the camera can be without showing anything outside of the play area and is also the offset from a corner of the camera to the center.
camMax is the opposite highest right location the camera can be in.
The key part I'm guessing you're missing is scaling the camera size by the zoom level.
Here's my solution:
float minCameraX = camera.zoom * (camera.viewportWidth / 2);
float maxCameraX = worldSize.x - minCameraX;
float minCameraY = camera.zoom * (camera.viewportHeight / 2);
float maxCameraY = worldSize.y - minCameraY;
camera.position.set(Math.min(maxCameraX, Math.max(targetX, minCameraX)),
Math.min(maxCameraY, Math.max(targetY, minCameraY)),
0);
Where:
targetX and targetY are world coordinates of where your target is.
worldSize is a Vector2 of the size of the world.
I don't have enough reputation to write comments, so I'll point to some previous answers.
AAverin's solution with bounding box that's made with Matsemann's idea isn't good because it annoyingly slows when you are near the one edge (boundary) and trying to translate diagonally in which case you are panning to one side out of bounds and other in proper direction.
I strongly suggest that you try solution from the bottom of handleInput method presented at
https://github.com/libgdx/libgdx/wiki/Orthographic-camera
That one works smoothly, and some of the previous answers look like that one but this one uses MathUtils.clamp wihch is a straight forward and much cleaner.
Perfect class for this, (partly thanks to AAverin)
This class not only sticks into the bounds it also snaps into the bounds when you zoom.
Call these for setting bounds and moving the camera.
camera.setWorldBounds()
camera.translateSafe(x, y);
When zooming call
camera.attemptZoom();
And here's the class:
public class CustomCamera extends OrthographicCamera
{
public CustomCamera() {}
public CustomCamera(float viewportWidth, float viewportHeight)
{
super(viewportWidth, viewportHeight);
}
BoundingBox left, right, top, bottom = null;
public void setWorldBounds(int left, int bottom, int width, int height) {
int top = bottom + height;
int right = left + width;
this.left = new BoundingBox(new Vector3(left - 2, 0, 0), new Vector3(left -1, top, 0));
this.right = new BoundingBox(new Vector3(right + 1, 0, 0), new Vector3(right + 2, top, 0));
this.top = new BoundingBox(new Vector3(0, top + 1, 0), new Vector3(right, top + 2, 0));
this.bottom = new BoundingBox(new Vector3(0, bottom - 1, 0), new Vector3(right, bottom - 2, 0));
}
Vector3 lastPosition;
#Override
public void translate(float x, float y) {
lastPosition = new Vector3(position);
super.translate(x, y);
}
public void translateSafe(float x, float y) {
translate(x, y);
update();
ensureBounds();
update();
}
public void ensureBounds()
{
if(isInsideBounds())
{
position.set(lastPosition);
}
}
private boolean isInsideBounds()
{
if(frustum.boundsInFrustum(left) || frustum.boundsInFrustum(right) || frustum.boundsInFrustum(top) || frustum.boundsInFrustum(bottom))
{
return true;
}
return false;
}
public void attemptZoom(float newZoom)
{
this.zoom = newZoom;
this.snapCameraInView();
}
private void snapCameraInView()
{
float halfOfCurrentViewportWidth = ((viewportWidth * zoom) / 2f);
float halfOfCurrentViewportHeight = ((viewportHeight * zoom) / 2f);
//Check the vertical camera.
if(position.x - halfOfCurrentViewportWidth < 0f) //Is going off the left side.
{
//Snap back.
float amountGoneOver = position.x - halfOfCurrentViewportWidth;
position.x += Math.abs(amountGoneOver);
}
else if(position.x + halfOfCurrentViewportWidth > viewportWidth)
{
//Snap back.
float amountGoneOver = (viewportWidth - (position.x + halfOfCurrentViewportWidth));
position.x -= Math.abs(amountGoneOver);
}
//Check the horizontal camera.
if(position.y + halfOfCurrentViewportHeight > viewportHeight)
{
float amountGoneOver = (position.y + halfOfCurrentViewportHeight) - viewportHeight;
position.y -= Math.abs(amountGoneOver);
}
else if(position.y - halfOfCurrentViewportHeight < 0f)
{
float amountGoneOver = (position.y - halfOfCurrentViewportHeight);
position.y += Math.abs(amountGoneOver);
}
}
}
The CustomCamera class given doesn't work very well. I used it to map a pinch gesture to zoomSafe and the camera would bounce/flash from left to right constantly when on the edge of the bounds. The camera also doesnt work properly with panning. If you try to pan along the edge of the bounds it doesnt pan anywhere as if the edges are "sticky". This is because it just translates back to the last position instead of just adjusting the coordinate that it outside the bounds.