I'm working on a game that uses a 2D tile map. The map is rendered via a custom method doDraw() in a SurfaceView. It's a simple double for loop that gets the tile type from a 2D array, determines the corresponding bitmap for that type, then draws the tile to the canvas at the specified coordinates.
Previously I had placeholder .pngs in my drawable folder. These placeholder drawables were simple 100x100 .png files of solid colors that I created in ms paint. Things appeared to be working fine until I swapped out the placeholder drawables with some nice textured drawables. I then realized that the way I was referencing the coordinates to draw the tiles was incorrect and was actually causing them to overlap. This had not been obvious when the tiles were solid colors, but with textures it became pretty obvious that two sides of the tiles were getting covered by adjacent tiles.
I believe the problem is not actually in my doDraw() method but most likely in the way I am referencing the tile sizes. The .png files are 100x100 but they are probably getting converted to a different size based on the pixel density of my device. I think that's what's going on, and I've been reading up how to deal with that, but everything I'm finding out there will generally be talking about a single bitmap, as opposed to many small bitmaps that need to be drawn relative to each other.
Here's the relevant code from my SurfaceView:
public class MapView extends SurfaceView implements SurfaceHolder.Callback {
protected Context context;
public World world;
public Map<Integer,Bitmap> TILE_MAP;
public Bitmap SPRITE;
public Player player;
public Camera camera;
//hardcoded parameters for testing
private int tile_width = 50;
private int tile_height = 50;
public int screen_width = 12; //specifies how many tiles to draw in y direction
public int screen_height = 6; //specifies how many tiles to draw in x direction
public MapThread mapThread;
public MapView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
Log.d("LOGCAT", "MapView created");
//get the tile map
WorldFeatures worldFeatures = new WorldFeatures(context);
TILE_MAP = worldFeatures.TILE_MAP;
SPRITE = worldFeatures.SPRITE;
SurfaceHolder holder = getHolder();
holder.addCallback(this);
}
public void doDraw(Canvas canvas) {
/*the draw method looks a little muddled at the moment as my camera is referencing
the center tile instead of the top left tile. I'll be updating that soon*/
int screenX = 0; //reset screenX each loop
for (int worldX = (camera.x - screen_height/2 + 1); worldX < (camera.x + screen_height/2 + 1); worldX += 1, screenX += 1) {
int screenY = 0; //reset screenY each loop
for (int worldY = (camera.y - screen_width/2 + 1); worldY < (camera.y + screen_width/2 + 1); worldY += 1, screenY += 1) {
canvas.drawBitmap(TILE_MAP.get(world.world_map[worldX][worldY]), screenY*tile_height , screenX*tile_width, null);
if (player.x == worldX && player.y == worldY) { //if the player is standing here, draw the sprite
canvas.drawBitmap(SPRITE, screenY*tile_height + tile_height/5, screenX*tile_width + tile_width/5, null);
}
}
}
}
}
As you can see above, I've hardcoded the tile height and width. A means to abstract this would be nice, but first I'd like to understand what the correct numbers to use are. If I set the height and width for the tiles to 100, I get spaces between the tiles. If set them to 50, I get overlap. This tells me that the correct size to reference is something in between. I could keep guessing until I get it right, but I'd like to understand how to determine what size my drawables scale to for what pixel density.
Currently my drawables are residing in my drawable-hdpi folder, and nowhere else. I'm wondering if I put them in a generic "drawable" folder, will I be able to reference the tile_height and tile_width as 100x100? I've read up on getting intrinsic height and width but that doesn't help when the bitmaps are scaled for the view or device. Even if it's possible to somehow reference the tile sizes based on the original drawable size, I would think there should be a better way than referencing them by any specific number. Shouldn't I be able to find out what the scaled size is while inside my draw method? And then use that to determine what coordinates to draw the tiles to? Even better, is there a way to force what size I want the bitmap to scale to before I draw it to the canvas?
I've read up on pixel density, dpi, drawable folders, etc. at http://developer.android.com/guide/practices/screens_support.html but this doesn't really help me when I'm drawing tiles relative to each other and therefore needing coordinates.
I've also looked into using Rectangles to draw to the canvas since the drawbitmap() method of Canvas that takes Rectangles as an input is not dependent on pixel density. But I'm unsure how practical using Rectangles would be when I have a 12x6 set of tiles to draw. Would I need one Rectangle per tile location?
Any explanations on dealing with scaled bitmaps and/or a better method to draw what I'm drawing would be most helpful.
Answered my own question after getting some sleep. To get the height of a bitmap after android has scaled it automatically, use getWidth() and getHeight() on the Bitmap. For forcing a bitmap to scale to a specific size, use scaleBitmap()
Related
So this is my scenario:
I have an svg image that contains all the music notes on the staff (sort of sprite sheet)
I have two svg image that contains the key (maybe i can merge them together anyway)
All of them are converted to android Vector Drawable.
What i want to do is to select the note from the first svg, select the cleff, and then show them next to each other (the two images should be aligned).
So what i managed to achieve is to select the portion of svg for the note (i need to refine the rect size). But what i'm still having problem is to show both of them on the same line.
public MusicScore(Context context) {
super(context);
paint = new Paint();
paint.setColor(Color.BLACK);
Resources res = context.getResources();
musicClef = (VectorDrawable) res.getDrawable(R.drawable.ic_bassclef, null);
musicNotes = (VectorDrawable) res.getDrawable(R.drawable.ic_musicnotes, null);
int width = getWidth();
int height = getHeight();
Log.i("MUSIC", "H: " + musicClef.getMinimumHeight() + " W: " + musicClef.getMinimumWidth());
}
#Override
protected void onDraw(Canvas canvas){
Log.i("MUSIC", "Called");
int left = getWidth()/2;
int top = getHeight()/2;
musicClef.setBounds(0,0,musicClef.getIntrinsicWidth(), musicClef.getIntrinsicHeight() );
Bitmap source = Bitmap.createBitmap(musicNotes.getIntrinsicWidth(), musicNotes.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Bitmap clefSource = Bitmap.createBitmap(musicClef.getIntrinsicWidth(), musicClef.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas newcanvas = new Canvas(source);
Canvas clefCanvas = new Canvas(clefSource);
int notesLeft = musicClef.getIntrinsicWidth();
int notesTop = musicClef.getIntrinsicHeight();
musicNotes.setBounds(0, 0, musicNotes.getIntrinsicWidth(), musicNotes.getIntrinsicHeight());
musicNotes.draw(newcanvas);
musicClef.draw(clefCanvas);
Rect rect = new Rect(1150,0,1700, musicNotes.getIntrinsicHeight());
Rect rect2 = new Rect(notesLeft ,0, notesLeft + 450, musicNotes.getIntrinsicHeight());
Rect clefRect = new Rect(0, 0, musicClef.getIntrinsicWidth(), musicClef.getIntrinsicHeight());
canvas.drawBitmap(clefSource, null, clefRect, null);
canvas.drawBitmap(source, rect, rect2, null);
}
So with that code i can show a portion of the notes drawable, after converting it to a Bitmap. And i can also draw both of them, and the horizontal position is aligned. The problem is that the vertical position is not aligned, what i'm getting is:
So i know that my code is not correct (i'm pretty new to canvas in android and trying to figure out what to do), and what i learned so far is:
that in order to select a portion of the image i need to convert it to the Bitmap (i haven't found any way to do it directly with the VectorDrawable)
While drawing a Bitmap on the canvas with DrawBitmap, the first Rect represents the portion area i want to show, and the second one is the size and position of the area displayed.
In order to have the bitmap displayed on the canvas, i need to create a canvas from the Bitmap and draw it in it, in order to have it displayed. Is that correct?
So i have several questions:
I would like to understand how to vertically align the images, so they have to start from the same y (top).
I'm not sure if the vector image is the best idea, maybe is better to convert it into display dependant pngs? Or anyway the canvas size is relative to the screen? Or maybe i need to make my code screen-independent (i suppose that getIntrinsicWidth/Height are returning the real size of the image, so maybe i need to scale it?)
Why I need to explicitly setBounds of both images to have them displayed?
UPDATE #1
So i understood why probably my images are not aligned on the same line. It looks like that when creating a vector asset, for some reason the sizes are "adapted", not sure if is the android systems doing that or android studio. But anyway what i found after loading two images with the same height in pixels, is that for one i have an height, and for the other i have a different height (nearly double), and that explain why the image is shifted on the bottom.
So what i tried is to make a single image with both the notes, and the clefs, and it sort of works in the emulator. But when testing on a real device i get the error:
W/OpenGLRenderer: Bitmap too large to be uploaded into a texture (11732x1168, max=8192x8192)
Bitmap too large to be uploaded into a texture (11732x1168, max=8192x8192)
i can understand what the error means, but anyway the image size in pixel is: 2933x292 pixels. Why the getIntrinsicWidth and getIntrinsicHeight are returning that dimensions? what is their metrics?
I'm wondering that maybe the vector drawable is not the best choice? Maybe is better to cnvert it into screen-dependant pngs? and use them?
I have a little problem with my game. Let's assume that there's a game room of 1024x800 (px) and you can slide it to see more content.
When I place two objects (one next to other), based on the original dimensions, it looks OK on a mdpi device, but in hdpi (for example) they are "closer" (it must be because their bitmaps are larger but the "x" and "y" coordinates are the same).
Here's an example:
So, I think that I must make the game room larger in hdpi with a Rule of Three. First, I thought that I can do it just in the draw method (Canvas.drawBitmap) but it affected some collisions (touch events didn't match, for example). I can't make the rule in every single line where I used X&Y coordinates because I did all my levels and game physics considering the original unity (pixels).
Here is my draw method:
protected void drawSprite(int x, int y, Canvas canvas){
int xv = isHUD?0:lw.getXView();
int yv = isHUD?0:lw.getYView();
canvas.drawBitmap(sprite_index, x+xv, y+yv, paint);
}
getXView() and getYView() returns the coordinate of the actual slide of the view.
And this is a method that I used to check collisions:
public Rect getShape() {
int xv = isHUD?0:lw.getXView();
int yv = isHUD?0:lw.getYView();
return new Rect(x+xv,y+yv,x+width+xv,y+height+yv);
}
What should I do?
Considering that I'm going to put a "Level Editor" in which the players are going to share their creations, storing the "id" and XY coordinates on a database.
I know there are numerous other threads about this. I've been checking through them with no avail for the past 2 days and right now it's driving me nuts.
So basically I have a spritesheet, which I load into a Bitmap, cut the frames apart, perform scaling on them and load them into a list for later animation. Scaling looks good ranging from ldpi to xhdpi, so does animation.
But there's another thing that bugs me, and that's the positioning on different devices. I draw them with canvas.drawBitmap(bitmap, srcRect, destRect, paint). Well, I guess code tells more than thousand words.
//converting dps to pixels
int xCoord = convertToPixels(100);
int yCoord = convertToPixels(150);
//defining destination rectangle; sprite is a Bitmap object
Rect destRect = new Rect(xCoord, yCoord, xCoord+sprite.getWidth(), yCoord+sprite.getHeight());
//drawing
canvas.drawBitmap(sprite, null, destRect, null);
convertToPixels(float dp) just returns a number of pixels with formula Math.round(dp*DENSITY).
I believe that if I specify dps, the aspect ratio on all devices should be the same, e.g. if I put something on 10% of the screen size of one device, it should maintain those 10% on another smaller/bigger/less dense/more dense screen. But I guess my logic is flawed, because it doesn't work. It draws sprites on different devices on different places.
To summarize: drawing a Bitmap object with destRect with x and y coords 100 dps should maintain ratio on all devices with respect to canvas size or I ought to think this way.
I would kindly ask you to help me with this matter, for I'm lost currently lost, more literally than figuratively.
And please don't just give me a link to "Supporting Multiple Screens" topic on developer site or similar sites, because I've read them many times and yet it seems I don't understand them. Thank you!
Are you sure the dip to px calculation is correct? I wrote the following code to do this:
public class ViewHelper {
public static int getPxFromDip(int dips){
return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dips, Application.getContext().getResources().getDisplayMetrics());
}
}
Where Application.getContext() is the Application wide context, or provide the local Activity context.
I am writing a View that should show a drawable that seems to "never end".
It should be twice or third the displaysize and move slow through the display.
Therefore I studied some samplecode by Google and found the important Lines
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
canvasWidth = width;
canvasHeight = height;
float sf = backgroundImage.getWidth() / canvasWidth;
backgroundImage = Bitmap.createScaledBitmap(backgroundImage,
(int) (canvasWidth * sf), canvasHeight, true);
}
To rescale the image and than
// decrement the far background
backgroundXPos = backgroundXPos - DELTAMOVE;
// calculate the wrap factor for matching image draw
int newFarX = backgroundImage.getWidth() - (-backgroundXPos);
// if we have scrolled all the way, reset to start
if (newFarX <= 0) {
backgroundXPos = 0;
// only need one draw
canvas.drawBitmap(backgroundImage, backgroundXPos, 0, null);
} else {
// need to draw original and wrap
canvas.drawBitmap(backgroundImage, backgroundXPos, 0, null);
canvas.drawBitmap(backgroundImage, newFarX, 0, null);
}
To draw the moving image. The images is already moving, it's fine.
But, and this is the point of my question, the image looks very ugly. Its original is 960*190 pixels by 240ppi. It should be drawn inside a view with 80dip of height and "fill_parent" width.
It should look same (and good) on all devices. I have tried a lot but I don't know how to make the picture look nice.
Thanks for your help.
Best regards,
Till
Since you're saying that it's a never ending drawable, probably you're writing a game of some sort. If your image is a pixel-art type, then you don't want any scaling; pixel-art-type images cannot be scaled and keep its crisp look (you can try using nearest neighbor interpolation and scaling to an integer multiple of the original, which sometimes might work, but sometimes you will still need manual tweaks). This is the rare case where you actually would need to have different image resource for different screen resolutions.
Otherwise you might want to use a vector image, but if -- as you said -- your original is a high resolution image, then vector image probably won't help much here.
btw, you probably want to show some screenshot. "Looks ugly" is just as helpful as saying my code does not work.
Just a guess, but instead of passing a null paint to your drawBitmap() calls, try making a paint with bitmap filtering disabled:
Paint p = new Paint();
p.setFilterBitmap(false);
canvas.drawBitmap(backgroundImage, backgroundXPos, 0, p);
Hope that helps.
I would like to animate movement on a SurfaceView . Ideally I would like to also be notified once the animation had finished.
For example:
I might have a car facing North. If I wanted to animate it so that it faces South for a duration of 500ms, how could I do that?
I am using a SurfaceView so all animation must be handled manually, I don't think I can use XML or the android Animator classes.
Also, I would like to know the best way to animate something continuously inside a SurfaceView (ie. a walk cycle)
Rotating images manually can be a bit of a pain, but here's how I've done it.
private void animateRotation(int degrees, float durationOfAnimation){
long startTime = SystemClock.elapsedRealtime();
long currentTime;
float elapsedRatio = 0;
Bitmap bufferBitmap = carBitmap;
Matrix matrix = new Matrix();
while (elapsedRatio < 1){
matrix.setRotate(elapsedRatio * degrees);
carBitmap = Bitmap.createBitmap(bufferBitmap, 0, 0, width, height, matrix, true);
//draw your canvas here using whatever method you've defined
currentTime = SystemClock.elapsedRealtime();
elapsedRatio = (currentTime - startTime) / durationOfAnimation;
}
// As elapsed ratio will never exactly equal 1, you have to manually draw the last frame
matrix = new Matrix();
matrix.setRotate(degrees);
carBitmap = Bitmap.createBitmap(bufferBitmap, 0, 0, width, height, matrix, true);
// draw the canvas again here as before
// And you can now set whatever other notification or action you wanted to do at the end of your animation
}
This will rotate your carBitmap to whatever angle you specify in the time specified + the time to draw the last frame. However, there is a catch. This rotates your carBitmap without adjusting its position on screen properly. Depending on how you're drawing your bitmaps, you could end up with your carBitmap rotating while the top-left corner of the bitmap stays in place. As the car rotates, the bitmap will stretch and adjust to fit the new car size, filling the gaps around it with transparent pixels. It's hard to describe how this would look, so here's an example rotating a square:
The grey area represents the full size of the bitmap, and is filled with transparent pixels. To solve this problem, you need to use trigonometry. It's a bit complicated... if this ends up being a problem for you (I don't know how you're drawing your bitmaps to the canvas so it might not be), and you can't work out the solution, let me know and I'll post up how I did it.
(I don't know if this is the most efficient way of doing it, but it works smoothly for me as long as the bitmap is less than 300x300 or so. Perhaps if someone knows of a better way, they could tell us!)
Do you want multiple independent animated object? If so, then you should use a game loop. (One master while loop that incrementally updates all game objects.) Here's a good discussion on various loop implementations. (I'm currently using "FPS dependent on Constant Game Speed" for my Android game project.)
So then your Car will look something like this (lots of code missing):
class Car {
final Matrix transform = new Matrix();
final Bitmap image;
Car(Bitmap sprite) {
image = sprite; // Created by BitmapFactory.decodeResource in SurfaceView
}
void update() {
this.transform.preRotate(turnDegrees, width, height);
}
void display(Canvas canvas) {
canvas.drawBitmap(this.image, this.transform, null);
}
}
You only need to load your bitmap once. So if you have multiple Cars, you may want to give them each the same Bitmap object (cache the Bitmap in your SurfaceView).
I haven't got into walk animations yet, but the simplest solution is to have multiple Bitmaps and just draw a different bitmap each time display is called.
Have a look at lunarlander.LunarView in the Android docs if you haven't already.
If you want to be notified when the animation is complete, you should make a callback.
interface CompletedTurnCallback {
void turnCompleted(Car turningCar);
}
Have your logic class implement the callback and have your Car call it when the turn's complete (in update()). Note that you'll get a ConcurrentModificationException if you are iterating over a list of Cars in update_game() and you try to remove a Car from that list in your callback. (You can solve this with a command queue.)