Scaled Graphics

From Bill's Tutorials at btutor.com

Scaled Shapes

We can write a simple app to draw a rectangle with the onDraw() code,

public void onDraw(Canvas canvas){
float width=8;
float x=200,y=100, a=120, b=160;
this.setBackgroundColor(Color.YELLOW);
paint.setStrokeWidth(width);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(x, y, x+a,y+b, paint);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
canvas.drawRect(x, y, x+a,y+b, paint);
}

Because the position coordinates, x and y, and the length and width, a and b, of the rectangle are defined in fixed values the rectangle will be drawn at different relative positions and relative sizes in different screens.

For example, the above code executed on the screen of a Samsung Mega 6.3 smartphone which as a screen resolution of 720 by 1280 pixels, gives the output shown on the left of Figure 1. The right image is the same app shown on a smaller smartphone with a screen resolution of 480 by 800 pixels

Small rectangle
large rectangle
Figure 1. The rectangle drawn with fixed parameters on two different screens.

Scale

The problem with this shape is that it looks different on different screen sizes and we would really like to have our graphics look the same on all mobile screens. In both of the images shown above, the rectangle is 200 pixels in from the left and 100 pixels down from the top. In both cases the rectangle is 120 by 160 pixels. If we add more shapes to make a complete scene, some screens will show all of the picture and others may show only part of it. We would not like to see this on a television screen and it is just as unsatisfactory on a mobile screen.

And this is only in the portrait mode. If the device is rotated to the landscape mode the pictures change again.

So we have to scale the positions and sizes of our graphics if we want them to look the same on all screens. We can only do that by using values of x, y a, and b that are relative to the width and height of the screen.

At this point we might mention another feature of these mobile screens that complicates the issue in some cases, although not here. The pixel numbers are actually the product of the screen size and the pixel density. If you are using the layout code to produce graphic effects, you need to consider both. But in this case, with Java programming, we only need to know the total number of pixels to get relative values.

There are several ways to scale our graphics to the screen sizes. For simple drawings we can use relative graphics parameters where we use position and shape coordinates that are fractions of the screen width and height. Another more generally useful approach for more complicated drawings is to define graphics units in which to draw. These might be 1/100 of the screen width or perhaps 1/1000 for more detailed pictures. This works quite well but it does mean adding another unit variable to the drawing routine. A third method is to draw to a memory buffer instead of the screen. The buffer has a fixed size and shape so drawing is at least standardized. Then we scale and copy the buffer to the screen. Although more conceptually difficult this is probably the best approach to drawing complex graphics.

We will have a look at the first two methods in this tutorial and save the buffer approach for later.

Relative Parameters

We can get the width and height of the screen in pixels by adding a couple of lines to the ShapeView constructor, namely,

width = getResources().getDisplayMetrics().widthPixels;
height = getResources().getDisplayMetrics().heightPixels;

 

Then we can set the values of x, y, a, and b relative to the width and height values. For example, to draw the rectangle in the middle of the screen and make it one third of the width and height, we can set the rectangle parameters to

float a=width/3, b=height/3;
float x=width/2-a/2, y=height/2-b/2;

This sets x to the mid-point of the screen less half the width of the rectangle and does the same for the vertical parameters to center the rectangle. So with these new coordinates the app code becomes

package com.androidjavaapps.shape;

import android.app.*;
import android.os.*;
import android.view.*;
import android.content.*;
import android.graphics.*;

public class MainActivity extends Activity {
ShapeView shapeView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
shapeView = new ShapeView(this);
setContentView(shapeView);
}

public class ShapeView extends View{
Paint paint= new Paint();
float width, height;

public ShapeView(Context context){
super(context);
width = getResources().getDisplayMetrics().widthPixels;
height = getResources().getDisplayMetrics().heightPixels;
}

public void onDraw(Canvas canvas){
float w=width/200;
float a=width/3, b=height/3;
float x=width/2-a/2, y=height/2-b/2;
this.setBackgroundColor(Color.YELLOW);
paint.setStrokeWidth(w);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(x, y, x+a,y+b, paint);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
canvas.drawRect(x, y, x+a,y+b, paint);
}
}
}

The result is shown in Figure 2 and looks the same on all screens, whatever their size and pixel density.

Scaled rectangle
Figure 2. The Shape app with display coordinates scaled to the screen dimensions.

Drawing Units

A more flexible way to draw scaled graphics is to define a drawing unit as a small fraction of the screen dimension and draw in terms of that. This allows us to use sizes that are not just simple fractions of the width and height of the screen but anything on the chosen scale. For example if we define a drawing unit as 1000 of the screen width we can position and scale our drawing to 1/1000 of the screen size.

To illustrate the method, the previous example can be drawn in this way by defining a unit variable, u, in onDraw(). However, this time the rectangle will be at a different position and a different size to utilize our new found flexibility. The onDraw() method now becomes,

public void onDraw(Canvas canvas){
float u=width/1000;    // resolution of 1 in 1000
float w=5*u;           // line width same as in previous example
float a=200*u,  b=650*u;
float x=300*u, y=300*u;
// the next 8 statements are unchanged
this.setBackgroundColor(Color.YELLOW);
paint.setStrokeWidth(w);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(x, y, x+a,y+b, paint);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
canvas.drawRect(x, y, x+a,y+b, paint);
// just for fun we can add a circle
a=160*u; x=600*u; y=800*u;
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(x, y, a, paint);
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(x, y, a, paint);
}

We have added some code to draw a circle shape as well as the rectangle. So the output is as shown in Figure 3. It shows a red rectangle and a blue circle, each with a black outline. The rectangle is drawn first so the circle overlaps it slightly.

Scaled rctangle and circle
Figure 3. A scaled rectangle and circle.

 

Rotating the Mobile

If the mobile device is rotated from portrait to landscape mode, or vice versa, the app will stop and restart in its new orientation. This can be prevented by adding code to the manifest file but if the intention is to allow the user to work in either mode then the scaling will have to be adjusted.

We have been using the screen width to scale the app, on the assumption of a portrait mode in which the width is smaller than the height. In landscape mode the opposite is true and our code will produce an undesirable scaling in which the shapes may not even be completely visible. This is because they are now scaled to the larger screen dimension. The result is illustrated in Figure 4.

 

image rotated to landscape
Figure 4. The shapes displayed when the device is rotated to landscape mode

 

We can deal with this problem quite easily by using a decision statement to scale our drawing to whatever is the smaller dimension. So onDraw() now becomes,

public void onDraw(Canvas canvas){
float u;
if (width<height) u=width/1000;    // new code
else u=height/1000;
float w=5*u;           // line width same as in previous example
float a=200*u,  b=650*u;
float x=300*u, y=200*u;
// the next 8 statements are unchanged
this.setBackgroundColor(Color.YELLOW);
paint.setStrokeWidth(w);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(x, y, x+a,y+b, paint);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
canvas.drawRect(x, y, x+a,y+b, paint);
// just for fun we can add a circle
a=160*u; x=600*u; y=700*u;
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(x, y, a, paint);
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(x, y, a, paint)
}

Now, when the device is rotated to landscape mode the result is as shown in Figure 5.

 

scaled rectangle and circle
Figure 5. The result of scaling to the smallest screen dimension and rotating