Continuously scroll one large seamless image in any direction
The issue is gaming related, but I think it could be applied to other use cases.
I have a seamless image larger than the screen. About twice as big. The goal is to have this image scrollable in any direction on a surface. This is for Android.
I've seen a few questions/answers regarding images in general, but not seamless images.
The difference here, and the question I have is how to handle scrolling when at some points, there can be 4 separate pieces of the image on display (when the user scrolls halfway through the image diagonally).
It's pretty obvious how to do it when you want to scroll an image edge to edge. Use a world value, and use a rectangle to display a sort of 'viewport' of where you are.
Something like:
Rect oRect1 = new Rect(dWorldX, dWorldY, dWorldX + canvas.getWidth(), dWorldY + canvas.getHeight());
Rect oRect2 = new Rect(0, 0, canvas.getWidth(), canvas.getHeight());
canvas.drawBitmap(largeBitmapTexture, oRect1, oRect2, new Paint());
But when the bitmap is seamless and continuously scrolling, this presents a programming challenge for me. Do I use a bunch of if statements? Is there a more efficient way of doing something like this?
edit: A few more thoughts. One complication here is that the world (x,y) is infinitely large. Because of this, I have to use divisions of the screen width and height to display what needs to be displayed. I was originally considering just having 4 iterations of Rect since there would never be more than 4 pieces of the image on the screen. If that makes any sense. I guess I'm just a little fuzzy about how to calculate.
edit: Here is the code I have now, with suggestions from @glowcoder.
public void draw(Canvas canvas){
int iImagesOverX=0;
int iImagesOverY=0;
iImagesOverX = (int)dX / mCloud.getIntrinsicWidth();
iImagesOverY = (int)dY / mCloud.getIntrinsicHeight();
mCloud.setBounds(iImagesOverX * (int)mCloud.getIntrinsicWidth() , iImagesOverY * (int)mCloud.getIntrinsicHeight() , (iImagesOverX * (int)mCloud.getIntrinsicWidth()) + mCloud.getIntrinsicWidth() , (iImagesOverY * (int)mCloud.getIntrinsicHeight()) + mCloud.getIntrinsicHeight() );
Log.d(TAG, "bounds:"+ mCloud.getBounds());
mCloud.draw(canvas);
mCloud.setBounds((iImagesOverX + 1) * (int)mCloud.getIntrinsicWidth() , iImagesOverY * (int)mCloud.getIntrinsicHeight() , ((iImagesOverX + 1) * (int)mCloud.getIntrinsicWidth()) + mCloud.getIntrinsicWidth() , (iImagesOverY * (int)mCloud.getIntrinsicHeight()) + mCloud.getIntrinsicHeight() );
mCloud.draw(canvas);
mCloud.setBounds((iImagesOverX + 1) * (int)mCloud.getIntrinsicWidth() , (iImagesOverY + 1)* (int)mCloud.getIntrinsicHeight() , ((iImagesOverX + 1) * (int)mCloud.getIntrinsicWidth()) + mCloud.getIntrinsicWidth() , ((iImagesOverY + 1) * (int)mCloud.getIntrinsicHeight()) + mCloud.getIntrinsicHeight() );
mCloud.draw(canvas);
mCloud.setBounds((iImagesOverX) * (int)mCloud.getIntrinsicWidth() , (iImagesOverY + 1)* (int)mCloud.getIntrinsicHeight() , ((iImagesOverX) * (int)mCloud.getIntrinsicWidth()) + mCloud.getIntrinsicWidth() , ((iImagesOverY + 1) * (int)mCloud.getIntrinsicHeigh开发者_运维知识库t()) + mCloud.getIntrinsicHeight() );
mCloud.draw(canvas);
Log.d(TAG, "imagesoverx:"+ iImagesOverX);
}
I had to add dX and dY to the bounds to get the images to move. However, once you go one 'tile' over to the right, the third tile does not appear.
So this is partly working, but not quite where I need to be. There are 4 panels, but only because of the 4 instances of bounds and draw. I need these 4 panels to draw where they need to be no matter where we are with x and y.
The basic idea is that you have an infinite plane, which contains your image repeated over and over in all directions. And then you have a "viewport" - which you can think of as a window that limits your view of the infinite plane. In other words, the area of the infinite plane that is "under" the viewport is what is actually shown on the screen.
So when the top left of your viewport is at (0,0), you see the part of the infinite plane from (0,0) to (50,50) (assuming your viewport is 50 pixels in width and height). Similarly, if your viewport is at (238,753), then you see the part of the infinite plane from (238, 753) to (288, 803).
If your images are 100x100, then your infinite plane would look something like:
(-100,-100) (0,-100) (100,-100) (200,-100)
******************************************
* * * *
* * * *
* * * *
* * * *
*(-100,0) *(0,0) *(100,0) *(200,0)
******************************************
* * * *
* * * *
* * * *
* * * *
*(-100,100) *(0,100) *(100,100) *(200,100)
******************************************
* * * *
* * * *
* * * *
* * * *
*(-100,200) *(0,200) *(100,200) *(200,200)
******************************************
Now, let's say that the top left corner of the viewport is at 75,75. Graphically, it would look something like:
(-100,-100) (0,-100) (100,-100) (200,-100)
******************************************
* * * *
* * * *
* * * *
* * * *
*(-100,0) *(0,0) *(100,0) *(200,0)
******************************************
* * * *
* * * *
* * (75,75) * (125,75) *
* * ####### *
*(-100,100) *(0,100) # * # *(200,100)
************************#*****#***********
* * # * # *
* * ####### *
* * (75,125) * (125,125) *
* * * *
*(-100,200) *(0,200) *(100,200) *(200,200)
******************************************
In this case, your viewport has corners at (75,75), (75,125), (125,75) and (125,125). So you see the bottom right corner of the image that is at (0,0), the bottom left corner of the image that is at (100,0), and so forth.
Now, let's say that you implement a function that calculates which grid a particular point is in. Specifically, it returns the top left corner of the grid that contains that point. A few examples:
gridContainingPoint(0,0) -> (0,0)
gridContainingPoint(50,50) -> (0,0)
gridContainingPoint(100,100) -> (100,100)
gridContainingPoint(123, -27) -> (100,-100)
The implementation of this function is fairly simple:
Point gridContainingPoint(Point pt) {
int newX = ((int)Math.floor(pt.x/100f)) * 100;
int newY = ((int)Math.floor(pt.y/100f)) * 100;
return new Point(newX, newY);
}
Now, to determine which images that you need to draw, you call the gridContainingPoint()
method for each corner of the viewport. In this example, you would get:
gridContainingPoint(75,75) -> (0,0)
gridContainingPoint(75,125) -> (0,100)
gridContainingPoint(125,75) -> (100, 0)
gridContainingPoint(125,125) -> (100, 100)
Now, you know specifically which images that you need to draw in order to entirely cover the viewport.
Before you draw the images, you have to set the viewport correctly. By default, when you start drawing on the canvas, the viewport is positioned at (0,0). So you would only see things that are drawn onto the canvas in the area of (0,0)x(50,50). However, we want the viewport to be positioned at (75,75), so that we see things in the area (75,75)x(125,125). In order to do this, you call the Canvas.translate()
method. For example, canvas.translate(-75,-75)
. It has to be negative, because it is conceptually moving the canvas underneath the viewport, rather than moving the viewport.
Now, using the information from the 4 calls to gridContainingPoint()
, you draw your image at (0,0), at (0,100), at (100, 0) and at (100, 100). And you are done :)
A few things to keep in mind - The numbers used in this example won't be the actual numbers that you want to use.
For one thing, the size of your viewport won't be 50x50, but it will be the actual size of your view at the time that it is drawn. You can get this with View.getWidth()
and View.getHeight()
.
Secondly, you'll need to adjust the grid sizing and related calculation based on your image size - I doubt you're using an image that is 100x100.
And finally, keep in mind that when you call the gridContainingPoint()
method for each of the corners of the viewport, it may return the same grid for multiple corners. For example, if your viewport was instead at (25,25), then it would return the (0,0) image for every corner, because (25,25), (25,75), (75,25) and (75,75) are all within the image that is from (0,0) to (100,100). So that would be the only image that you have to draw in order to completely cover the viewport.
I think Google Map does something similar when you pan around. They break their MAP into many smaller tiles (128x128 or 196x196). If you want smoother scroll, you need to pre-load some BITMAPs in advance. These are all Bitmap Virtualization Techniques.
Since you know your image is significantly larger than your canvas, you know you will only ever have a single corner of the image on your screen at a time. This is useful.
Let your current position be x,y, with an origin 0,0. Let your image be of dimensions w,h
Calculate how many complete images over you are from the origin. This is simply x/w and y/h in integer division. Let the result of this be i images in the horizontal and j images in the vertical.
Always draw your image at (i*w,j*h) ((i+1)*w,j*h) (i*w,(j+1)*h) and ((i+1)*w,(j+1)*h). Drawing 4 images to your canvas won't be a big performance hit. Most of it will end up off the screen and ignored. Then you can continue to draw your screen as normal. This will guarantee you always have a full background no matter where the "seam" lies (Yes I know it's seamless but that just means we can't see the boundary. It's still there of course.)
精彩评论