for the sake of simplicity let's assume that I'm making a simple Pong clone game for Android. Let's assume that it would only be playable in the landscape mode. (ignore square phones for now).
I'd like the game to look in the same scale on each phone, to the extent that if you took a screenshot of the game on QVGA phone, and resized the screenshot to WVGA size, it would look almost the same as would the game look on WVGA phone. In other words, the paddle's width should always be 1/32 of the screen width, the ball's diameter should always be 1/16 of the screen width.
What would be the proper way to paint the application? It would run in standard SurfaceView that would be drawn onto a Canvas.
Let's say that I have a high-resolution PNGs for the paddle, ball and for the game font (scoreboard, main menu).
Do I find out the physical resolution, then scale the Canvas via scale(float sx, float sy) to make all my Canvases (on QVGA and WVGA) have the same virtual resolution, and then draw exactly the same primitives on each position on each screen size?
Or can I use density-independent pixels (dip) somehow in the Canvas?
I only played once with the draw canvas functions and then switched all to opengl but the logic stays the same (I think).
first issue you'll want to keep a ratio constant form one phone to the other.
in my app I add a " black band" effect on each side.
in onSurfaceChanged, you'll want to calculate a ratio, this ratio will allow you to determine how much space you have to remove on each side to keep a consistent aspect to your drawing. this will give you a delta X or Y to apply to all your draws
the following code is something I adapted from the ogl version so it might need to be tweeked a bit
#Override
public void onSurfaceChanged(int w, int h){
float ratioPhysicScreen = nativeScreenResoltionW/nativeScreenResoltionH;
float ratioWanted = GameView.getWidth()/GameView.getHeight();
if(ratioWanted>ratioPhysicScreen){
newHeight = (int) (w*GameView.getHeight()/GameView.getWidth());
newWidth = w;
deltaY = (int) ((h-newHeight)/2);
deltaX = 0;
}else{
newWidth = (int) (h/GameView.getHeight()*GameView.getWidth());
newHeight = h;
deltaX = (int) ((w-newWidth)/2);
deltaY = 0;
}
then you'll also want to be able to draw your pictures on the canvas by knowing there size as well on the canvas than there real size and that where the difference in between image.getWidth() (actual size of the picture) and a image.getScaledWidth(canvas) which give you the size of the element in dp which means how big it will appear on the screen) is important. look at the example underneath.
public class MainMenuView extends PixelRainView{
private Bitmap bmpPlay = null;
private float playLeft = 0;
private float playRight = 0;
private float playBottom = 0;
private float playTop = 0;
public MainMenuView(Context context, AttributeSet attrs) {
super(context,attrs);
}
#Override
public void unLoadBitmaps() {
super.unLoadBitmaps();
if(bmpPlay != null){
bmpPlay.recycle();
bmpPlay = null;
}
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(bmpPlay == null){
bmpPlay = getBitmapFromRessourceID(R.drawable.play_bt);
playLeft = (this.getWidth()-bmpPlay.getScaledWidth(canvas))/2;
playRight = playLeft + bmpPlay.getScaledWidth(canvas);
playTop = (this.getHeight()-bmpPlay.getScaledHeight(canvas))/2;
playBottom = playTop+bmpPlay.getScaledHeight(canvas);
}
canvas.drawBitmap(bmpPlay,playLeft, playTop, null);
}
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN){
float x = event.getX();
float y = event.getY();
//test central button
if(x>playLeft && x<playRight && y<playBottom && y>playTop){
Log.e("jason", "touched play");
PixelRainView.changeView(R.layout.composedlayoutgroup);
}
return super.onTouchEvent(event);
}
}
This should solve all your ratio problem cross plateforms
I would suggest opengl because it will simplify your need for keeping a constant aspect but I guess its not an option ^^
Hope this helps you enough
Use dips and paint the paddles instead of using PNGs
I think that there are only 3 standard resolutions available (ldip, mdip, hdip) and you should have a resource folder for each of them.
http://developer.android.com/guide/practices/screens_support.html
If you manually scale your PNG's to fit each of the 3 available resolutions, the phone should load the specific resource folder in a transparent way
Related
For cropping images I am using CropImageView of joshholtz (https://github.com/joshdholtz/CropImageView). But I am getting IllegalArgumentException exception (saying: x + width must be <= bitmap.width()) at CreateBitmap function at crop function in the last line right before the return line.
public Bitmap crop(Context context) throws IllegalArgumentException {
// Weird padding cause image size
int weirdSidePadding = this.getWeirdSideMargin();
int weirdVerticalPadding = this.getWeirdVerticalMargin();
FrameLayout.LayoutParams params = (LayoutParams) mDaBox.getLayoutParams();
// Getting crop dimensions
float d = context.getResources().getDisplayMetrics().density;
int x = (int)((params.leftMargin - weirdSidePadding) * d);
int y = (int)((params.topMargin - weirdVerticalPadding) * d);
int width = (int)((this.getWidth() - params.leftMargin - params.rightMargin) * d);
int height = (int)((this.getHeight() - params.topMargin - params.bottomMargin) * d);
Bitmap crooopppppppppppppppeed = Bitmap.createBitmap(mBitmap, x, y, width, height);
return crooopppppppppppppppeed;
}
Actually I had a look at potentially same questions, but unluckily they are not same with my situation to the degree to help me.
Can you please help me to come over this barrier?
So in createBitmap, the function you're using creates a bitmap from a subsection of the original bitmap. For it to work, x+width may not be bigger than the width of the original bitmap (same for y+height) and x and y must both be >=0. This isn't the case here.
I think I get what this function is trying to do, but its just wrong. It seems to confuse the idea of cropping and scaling. If you want to scale and crop a bitmap at once, you should use the createBitmap(Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter) with the scale factor in the matrix m. And I'm not sure you want to be scaling here at all- I don't know why the code is looking at the density, but its probably wrong to be doing so.
I was working on a "draw with mask" app. When the user drag on the screen , it cleans part of the mask.
I implemented it through cavans and with setXfermode Clear
// Specify that painting will be with fat strokes:
drawPaint.setStyle(Paint.Style.STROKE);
drawPaint.setStrokeWidth(canvas.getWidth() / 15);
// Specify that painting will clear the pixels instead of paining new ones:
drawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
cv.drawPath(path, drawPaint);
The problem is , how can I get the percentage of space cleaned?, it doesn't necessary to be accurate, just roughly detect when more than half of screen size is clean. Thanks for helping
What you need to do is to convert your canvas in to bitmap and count the number of black pixels in it. Using simple math you can divide the number of black pixels to the number of pixels in the canvas which will give you the percentage of black pixels.
sample taken from this post:
public float percentTransparent(Bitmap bm) { //pass the converted bitmap of canvas
final int width = bm.getWidth();
final int height = bm.getHeight();
int totalBlackPixels = 0;
for(int x = 0; x < width; x++) {
for(int y = 0; y < height; y++) {
if (bm.getPixel(x, y) == Color.BLACK) {
totalBlackPixels ++;
}
}
}
return ((float)totalBlackPixels )/(width * height); //returns the percentage of black pixel on screen
}
I am developing game with libgdx and i got stuck with aspect ratio on different devices.
After a lot of thinking i figured that following is best solution for the problem:
I want to have camera always fixed to 16:9 aspect ratio and draw everything using that camera
If a device aspect is for example 4:3 i want to show only part of the view, not to strech it.
it should look something like this
blue is virtual screen(camera viewport) and red is device screen(visible area to 4:3 devices)
If for example device screen is also 16:9 full view should be visible.
Problem is i don't know how to achieve this.
I've done this for portrait screens, leaving some blank spaces at the top and bottom. As you can see in the following picture, the game stays at a 4:3 ratio and leaves whatever is leftover blank.
The content is always centered, and keeps its aspect ratio by not stretching the content unevenly. So here is some sample code Im using to achieve it.
public static final float worldW = 3;
public static final float worldH = 4;
public static OrthographicCamera camera;
...
//CALCULATING THE SCREENSIZE
float tempCalc = Gdx.graphics.getHeight() * GdxGame.worldW / Gdx.graphics.getWidth();
if(tempCalc < GdxGame.worldH){
//Adjust width
camera.viewportHeight = GdxGame.worldH;
camera.viewportWidth = Gdx.graphics.getWidth() * GdxGame.worldH / Gdx.graphics.getHeight();
worldWDiff = camera.viewportWidth - GdxGame.worldW;
}else{
//Adjust Height
camera.viewportHeight = tempCalc;
camera.viewportWidth = GdxGame.worldW;
worldHDiff = camera.viewportHeight - GdxGame.worldH;
}
camera.position.set(camera.viewportWidth/2f - worldWDiff/2f, camera.viewportHeight/2f - worldHDiff/2f, 0f);
camera.zoom = 1f;
camera.update();
I'm sure im not proposing the perfect solution, but you can play with the values on how the camera position and viewports are calculated so you can achieve the desired effect. Better than nothing I guess.
Also, Clash Of The Olympians Developers talk about how to achieve something like it and still make it look good on different devices (it is really interesting, but there is no code though): Our solution to handle multiple screen sizes in Android - Part one
The newer versions of libGDX provides a wrapper for glViewport called Viewport.
Camera camera = new OrthographicCamera();
//the viewport object will handle camera's attributes
//the aspect provided (worldWidth/worldHeight) will be kept
Viewport viewport = new FitViewport(worldWidth, worldHeight, camera);
//your resize method:
public void resize(int width, int height)
{
//notice that the method receives the entire screen size
//the last argument tells the viewport to center the camera in the screen
viewport.update(width, height, true);
}
I have written a function which works with different sizes of screen and images. The image might be zoomed and translated if it's possible.
public void transformAndDrawImage(SpriteBatch spriteBatch, Texture background, float scl, float offsetX, float offseY){
float width;
float height;
float _offsetX;
float _offsetY;
float bh2sh = 1f*background.getHeight()/Gdx.graphics.getHeight();
float bw2sw = 1f*background.getWidth()/Gdx.graphics.getWidth();
float aspectRatio = 1f*background.getHeight()/background.getWidth();
if(bh2sh>bw2sw){
width = background.getWidth() / bw2sw * scl;
height = width * aspectRatio;
}
else{
height = background.getHeight() / bh2sh * scl;
width = height / aspectRatio;
}
_offsetX = (-width+Gdx.graphics.getWidth())/2;
_offsetX += offsetX;
if(_offsetX-Gdx.graphics.getWidth()<=-width) _offsetX=-width+Gdx.graphics.getWidth();
if(_offsetX>0) _offsetX=0;
_offsetY = (-height+Gdx.graphics.getHeight())/2;
_offsetY += offseY;
if(_offsetY-Gdx.graphics.getHeight()<=-height) _offsetY=-height+Gdx.graphics.getHeight();
if(_offsetY>0) _offsetY=0;
spriteBatch.draw(background, _offsetX, _offsetY, width, height);
}
How to use it? It's easy:
#Override
public void render(float delta) {
Gdx.graphics.getGL10().glClearColor( 0.1f, 0.1f, 0.1f, 1 );
Gdx.graphics.getGL10().glClear( GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT );
spriteBatch.setProjectionMatrix(cam.combined);
spriteBatch.begin();
//zoom the image by 20%
float zoom = 1.2f; //Must be not less than 1.0
//translating the image depends on a few parameters such as zooming, aspect ratio of screen and image
offsetX+=0.1f; //offset the image to the left, if it's possible
offsetY+=0.1f; //offset the image to the bottom, if it's possible
transformAndDrawImage(spriteBatch, background, zoom, offsetX, offsetY);
//draw something else...
spriteBatch.end();
}
I'm working on an app where we hand rolled some star maps. So I don't have to talk in the abstract, here is the app in question: https://play.google.com/store/apps/details?id=com.tw.fireballs
If you look on the store listing, you can see we use an image just above the horizon...mostly because it looks super pretty. It's not an issue on lower resolution devices, they don't seem to suffer a huge issue rendering it due to lower number of pixels involved. On a higher resolution device like my HTC One or my wifes Nexus 5, there is a big framerate drop when it's on the screen. It's not bad enough to prevent me releasing it, but I'd like to improve it if possible.
I'm currently just using a SurfaceView, and drawing to a canvas acquired by mSurfaceHolder.lockCanvas(null), so it's not hardware accelerated. I tried implementing it as a regular view which is, but overall it actually went slower, so before I take the plunge into OpenGL land I want to explore if there is something I can do to speed this up.
Essentially, we start with a thin (opaque) image "slice" through the horizon, scale it up to the size of the diagonal of the screen and rotate it around to match the orientation of the device.
The drawing code is as follows:
private void loadResources() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
mHorizonImage = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.star_map_background, options);
}
#Override
public void updateDimensions(int width, int height) {
super.updateDimensions(width, height);
int horizonHeight = mCanvasHeight / 3;
int horizonWidth = (int) Math.sqrt(Math.pow(mCanvasHeight, 2) + Math.pow(mCanvasWidth, 2));
mHorizonImage = Bitmap.createScaledBitmap(mHorizonImage, horizonWidth, horizonHeight, false);
}
#Override
public void drawOn(Canvas canvas) {
double upScreen = -mProjection.distanceFromScreen * Math.tan(Math.toRadians(mOrientation.elevation));
double rightWithTiltCheat = upScreen * Math.sin(Math.toRadians(mOrientation.tilt));
double upWithTiltCheat = upScreen * Math.cos(Math.toRadians(mOrientation.tilt));
float x = (float) (mCanvasWidth / 2 * (1 + rightWithTiltCheat / mProjection.screenWidthInCm));
float y = (float) (mCanvasHeight / 2 * (1 - upWithTiltCheat / mProjection.screenHeightInCm));
canvas.save();
canvas.translate(x, y);
canvas.rotate((float) mOrientation.tilt);
canvas.drawBitmap(mHorizonImage, -mHorizonImage.getWidth() / 2, -mHorizonImage.getHeight(), null);
canvas.restore();
}
Is there a more efficient way to do this that won't be so affected by screen resolution? We've bandied around a few ideas, but I'd be keen to hear from someone who knows this stuff better than I. If you have any questions, please ask, I'd be grateful for any assistance.
Cheers,
Nathan
#pskink hit the nail on the head. Drawing the bitmap with a Matrix param is much more performant than the scaled bitmap and canvas rotation I was experiencing. On 1080P devices like my HTC One I used to watch the frame rate drop from 50+ to ~10 if I held the phone on a diagonal. Now I'd be lucky if it drops 5.
The modified code now looks like:
#Override
public void updateDimensions(int width, int height) {
super.updateDimensions(width, height);
mHorizonHeight = mCanvasHeight / 3;
mHeightScale = (float)mHorizonHeight / (float)mHorizonImage.getHeight();
mHorizonWidth = (int) Math.sqrt(Math.pow(mCanvasHeight, 2) + Math.pow(mCanvasWidth, 2));
mWidthScale = (float)mHorizonWidth / (float)mHorizonImage.getWidth();
}
#Override
public void drawOn(Canvas canvas) {
double upScreen = -mProjection.distanceFromScreen * Math.tan(Math.toRadians(mOrientation.elevation));
double rightWithTiltCheat = upScreen * Math.sin(Math.toRadians(mOrientation.tilt));
double upWithTiltCheat = upScreen * Math.cos(Math.toRadians(mOrientation.tilt));
float x = (float) (mCanvasWidth / 2 * (1 + rightWithTiltCheat / mProjection.screenWidthInCm));
float y = (float) (mCanvasHeight / 2 * (1 - upWithTiltCheat / mProjection.screenHeightInCm));
mDrawMatrix.reset();
mDrawMatrix.postScale(mWidthScale, mHeightScale);
mDrawMatrix.postTranslate(x - mHorizonWidth / 2, y - mHorizonHeight);
mDrawMatrix.postRotate((float) mOrientation.tilt, x, y);
canvas.drawBitmap(mHorizonImage, mDrawMatrix, null);
canvas.save();
}
I'm trying to find information on how to change the coordinate system for the canvas. I have some vector data I'd like to draw to a canvas using things like circles and lines, but the data's coordinate system doesn't match the canvas coordinate system.
Is there a way to map the units I'm using to the screen's units?
I'm drawing to an ImageView which isn't taking up the entire display.
If I have to do my own calculations prior to each drawing call, how to I find the width and height of my ImageView?
The getWidth() and getHeight() calls I tried seem to be returning the entire canvas size and not the size of the ImageView which isn't helpful.
I see some matrix stuff, is that something that will work for me?
I tried to use the "public void scale(float sx, float sy)", but that works more like a pixel level zoom rather than a vector scale function by expanding each pixel. This means if the dimensions are increased to fit the screen, the line thickness is also increased.
Update:
After some research I'm starting to think there's no way to change coordinate systems to something else. I'll need to map all my coordinates to the screen's pixel coordinates and do so by modifying each vector. The getWidth() and getHeight() seem to be working better for me now. I can say what was wrong, but I suspect I can't use these methods inside the constructor.
Thanks for the reply. I have pretty much given up on getting this to work in the way I think it should. Of course how I think things should happen isn't how they do happen. :)
Here's basically how it works, but it seems to be off by a pixel in some cases and the circles seem to be missing sections when things land on some boundary conditions I have yet to figure out. Personally I think this is unacceptable to be inside application code and should be in the Android libraries... wink wink, nudge nudge if you work for Google. :)
private class LinearMapCanvas
{
private final Canvas canvas_; // hold a wrapper to the actual canvas.
// scaling and translating values:
private double scale_;
private int translateX_;
private int translateY_;
// linear mapping from my coordinate system to the display's:
private double mapX(final double x)
{
final double result = translateX_ + scale_*x;
return result;
}
private double mapY(final double y)
{
final double result = translateY_ - scale_*y;
return result;
}
public LinearMapCanvas(final Canvas canvas)
{
canvas_ = canvas;
// Find the extents of your drawing coordinates:
final double minX = extentArray_[0];
final double minY = extentArray_[1];
final double maxX = extentArray_[2];
final double maxY = extentArray_[3];
// deltas:
final double dx = maxX - minX;
final double dy = maxY - minY;
// The display's available pixels, accounting for margins and pen stroke width:
final int width = width_ - strokeWidth_ - 2*margin_;
final int height = height_ - strokeWidth_ - 2*margin_;
final double scaleX = width / dx;
final double scaleY = height / dy;
scale_ = Math.min(scaleX , scaleY); // Pick the worst case, so the drawing fits
// Translate so the image is centered:
translateX_ = (int)((width_ - (int)(scale_*dx))/2.0 - scale_*minX);
translateY_ = (int)((height_ - (int)(scale_*dy))/2.0 + scale_*maxY);
}
// wrappers around the canvas functions you use. These are only two of many that would need to be wrapped. Annoying and error prone, but beats any alternative I can find.
public void drawCircle(final float cx, final float cy, final float radius, final Paint paint)
{
canvas_.drawCircle((float)mapX(cx), (float)mapY(cy), (float)(scale_*radius), paint);
}
public void drawLine(final float startX, final float startY, final float stopX, final float stopY, final Paint paint)
{
canvas_.drawLine((float)mapX(startX), (float)mapY(startY), (float)mapX(stopX), (float)mapY(stopY), paint);
}
...
}
You can scale the canvas co-ordinates to your own units using the preScale() method of the canvas' matrix. Be aware though that this also scales the Paint's stroke width, which may not be what you want.
The only way I know of to do custom vector graphics on the fly in Android is to draw everything into an image file and then put that into an ImageView. I'm not sure I understand exactly what you are asking, but if the only issue is scaling, the ImageView will scale whatever image it is given to its own size using android:scaleType="center" as a property of the ImageView.
As far as changing the coordinate system, I doubt that is possible (although I haven't researched it). It might be fairly trivial to write a function that would map your data's system to the standard Android coordinate system, though.