Japan

Draw the flag of Japan on a two-dimensional canvas. What you see (the big white area and the red disk) is an object of class JapanView, which is a subview of class View that I created myself. The toast displays the dimensions of the JapanView in pixels. On a Nexus 5X API 23, the hights in pixels are as follows.

portrait orientation landscape orientation
63 height of status bar
147 height of app bar
1584 height of JapanView
 + 126 height of navigation bar
1920 total height of screen
63 height of app bar
126 height of app bar
891 height of JapanView
   + 0 height of navigation bar (not displayed)
1080 total height of screen

At right, the app runs without the three bars (status, app, and navigation), and the circle has anti-aliasing (a smoothed edge).

The JapanView’s constructor is called by the onCreate method of the MainActivity. We never get to see who calls the JapanView’s onDraw method, and we should never try to call this method ourselves. The JapanView’s getWidth and getHeight return zero before onDraw is called. (More precisely, they return zero until onLayout is called.) That means we can’t call getWidth and getHeight in the JapanView’s constructor.

Source code in Japan.zip

  1. MainActivity.java. I commented out the call to setContentView in onCreate and replaced it with a call to another method with the same name.
  2. JapanView.java. I edited out all the methods except the one-argument constructor and onDraw.
  3. R.java
  4. activity_main.xml: unused and ignored. We should have removed it from the project to save space.
  5. strings.xml
  6. styles.xml
  7. attrs_japan_view.xml: attribute resources used only by the code we will edit out of the newborn JapanView.java file.
  8. sample_japan_view.xml: shows how to create one of your JapanView objects in an .xml file, if you’re interested.
  9. AndroidManifest.xml. The full name of the subclass of class Activity that will be automatically instantiated when the app is launched is edu.nyu.sps.japan.MainActivity.
  10. build.gradle (Module: app)

Create the project

In the Android Studio project view, select the folder app/java/edu.nyu.sps.japan. This folder already contains the Java file MainActivity.java. We will put a new Java file named JapanView.java there too.
File → New… → UI Component → Custom View
Customize the Activity
Creates a new custom view that extends android.view.View and exposes custom attributes
Package name: edu.nyu.sps.japan
View Class: JapanView     (no space; by convention ends in “View”)
Finish

The folder edu.nyu.sps.japan should now contain two .java files, JapanView.java and MainActivity.java. (You also have two new files that we don’t much care about now: attrs_japan_view.xml in the app/res/values folder, and sample_japan_view.xml in the app/res/layouts folder.)

▼ app
  ► manifests
  ▼ java
    ▼ edu.nyu.sps.japan
      © JapanView
      © MainActivity

Then edit JapanView.java. You can remove all of the methods except the one-argument constructor and onDraw.

Things to try

  1. In the onCreate method of class MainActivity, change
            JapanView japanView = new JapanView(this);
            setContentView(japanView);
    
    to the following. After all, if a variable (japanView) is going to be mentioned only once after it is created, why bother giving it a name? Let the variable be an anonymous temporary.
            setContentView(new JapanView(this));
    

  2. Replace the
            Color.RED                   //Opaque red.  Can also be written 0xFFFF0000
    
    with
            Color.rgb(255, 0, 0)       //Opaque red.  Can also be written 0xFFFF0000
    
    or
            Color.argb(128, 255, 0, 0) //Semi-opaque red.  Can also be written 0x80FF0000
    
    Each parameter must be a whole number in the range 0 (minimum) to 255 (maximum) inclusive. Create your own color.

  3. What are the dimensions of the entire display (screen), including the status and app bars (but not the navigation bar)? Insert the following code into the onCreate method of class MainActivity, immediately after the call to super.onCreate.
    		//getSystemService returns a general-purpose Object, which we must downcast to a
    		//WindowManager before we can store it into the variable windowManager.
    
    		WindowManager manager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
    		Display defaultDisplay = manager.getDefaultDisplay();
    
    		DisplayMetrics displayMetrics = new DisplayMetrics(); //Create an empty DisplayMetrics.
    		defaultDisplay.getMetrics(displayMetrics);            //Put values into the DisplayMetrics.
    
    		String s = displayMetrics.widthPixels + " \u00D7 " + displayMetrics.heightPixels;
    		Toast toast = Toast.makeText(this, s, Toast.LENGTH_LONG);
                    toast.show();
    
    On my AVD (a Nexus 5X API 23 in portrait orientation), the window was 1080 × 1794 pixels. On my Amazon Fire HD 6 in portrait orientation, it was 800 × 1208. On my pathetic Azpen A727 tablet in portrait orientation, it was 480 × 764, but you can’t go wrong for $14. You can create another AVD with a different window size.
    Tools → Android → AVD Manager
    and click on the pencil. Or if you’re using Genymotion, click on the Genymotion plugin icon in Android Studio (it has a pink rectangle) and press the New… button in the Genymotion Device Manager. The Virtual device creation wizard window will open.

  4. Verify the heights of the three bars directly. Insert the following code into the onCreate method of the MainActivity before creating the JapanView.

    I wanted to pass the int variable android.R.dimen.status_bar_height directly to getDimensionPixelSize, but unfortunately this variable does not exist. Neither does android.R.dimen.navigation_bar_height. (See the list of members of class android.R.dimen here.) But by calling getIdentifier, we can get the values that these variables would have had.

            String s = "";
    
            Resources resources = getResources();
            int id = resources.getIdentifier("status_bar_height", "dimen", "android");
            if (id > 0) {
                int statusBarHeight = resources.getDimensionPixelSize(id);
                s = "status bar height = " + statusBarHeight + " pixels";
            }
    
            //an array containing only one int
            int[] appBarSize = {R.attr.actionBarSize};
            TypedArray typedArray = obtainStyledAttributes(new TypedValue().data, appBarSize);
            int appBarHeight = (int)typedArray.getDimension(0, -1f);
            if (appBarHeight != -1) {
                s += "\napp bar height = " + appBarHeight + " pixels";
            }
            typedArray.recycle();
    
            id = resources.getIdentifier("navigation_bar_height", "dimen", "android");
            if (id > 0) {
                int navigationBarHeight = resources.getDimensionPixelSize(id);
                s += "\nnavigation bar height = " + navigationBarHeight + " pixels";
            }
    
            Toast toast = Toast.makeText(this, s, Toast.LENGTH_LONG);
            toast.show();
    

    On Nexus 5X API 23 in portrait orientation,

    status bar height = 63 pixels
    app bar height = 147 pixels
    navigation bar height = 126 pixels
    

    Amazon Fire HD 6 in portrait orientation,

    status bar height = 36 pixels
    app bar height = 84 pixels
    navigation bar height = 72 pixels
    

    Excerpt from the file ~/Library/Android/sdk/platforms/android-24/data/res/values/dimens.xml. The Nexus 5X has 420 pixels per inch, and every device has approximately 160 dp (or dip) per inch, so the Nexus 5X has 2⅝ pixels per dp. Therefore the 24 dp equal the 63 pixels of the status bar.

    <resources>
        <!-- Height of the status bar -->
        <dimen name="status_bar_height">24dp</dimen>
    
        <!-- Height of the bottom navigation / system bar. -->
        <dimen name="navigation_bar_height">48dp</dimen>
    </resources>
    

    Excerpt from the file ~/Library/Android/sdk/platforms/android-24/data/res/values/dimens_material.xml.

    <resources>
        <!-- Default height of an app bar. -->
        <dimen name="action_bar_default_height_material">56dp</dimen>
    </resources>
    

  5. Hide the status and app bars. (When the status bar is hidden, the app bar must be hidden too.) In the onCreate method of class MainActivity, insert the following code immediately after the call to super.onCreate. The height of the JapanView will now be the full height of the screen.
            Window window = getWindow();
    
            if (Build.VERSION.SDK_INT < 16) {
                //Hide the app bar (used to be called the action bar or title bar).
                requestWindowFeature(Window.FEATURE_NO_TITLE);
    
                //Hide the status bar.
                window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            } else {
                //Hide the app bar.
                //import android.support.v7.app.ActionBar;
                ActionBar appBar = getSupportActionBar();
                appBar.hide();
    
                //Hide the status and navigation bars.  Requires at least
                //minSdkVersion 16 in Gradle Scripts -> build.gradle (app).
                View decorView = window.getDecorView();
                int options = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
                decorView.setSystemUiVisibility(options);
            }
    
    I got two Toasts from the onDraw method of the JapanView on Nexus 5X: 1080 × 1857 after the action and navigation bars were hidden, and then 1080 × 1920 after the status bar was hidden. Tap the flag to get the status and naviagtion bars back.

  6. By default, the origin of the JapanView is in the upper left corner, with the X axis pointing to the right and the Y axis pointing down.

    We can center the circle by brute force. In the onDraw method of class JapanView, change
    		canvas.drawCircle(0f, 0f, radius, paint);
    
    to the following. The return type of getWidth and getHeight is int, but the first two arguments of drawCircle are float.
    		canvas.drawCircle(width / 2f, height / 2f, radius, paint);
    

  7. Center the circle by applying a transformation. In the onDraw method of class JapanView, change the first two arguments of drawCircle back to 0f, 0f. Insert a call to translate immediately before the call to drawCircle.
    		//Let the center of the JapanView be the origin.
    		canvas.translate(width / 2f, height / 2f);
    
    		canvas.drawCircle(0f, 0f, radius, paint);
    

  8. Scale (stretch) the circle:
    		//Let the center of the JapanView be the origin.
    		canvas.translate(width / 2f, height / 2f);
    
    		//Don't change the width.  Double the height.
    		canvas.scale(1f, 2f);
    
    		canvas.drawCircle(0f, 0f, radius, paint);
    

    What would go wrong if we did the scale before the translate?


  9. To demonstrate a negative scale factor, let’s draw an isoceles right triangle. Since the X axis points to the right and the Y axis points down, the triangle will appear to the lower right of the origin.
    		//Let the center of the JapanView be the origin.
    		canvas.translate(width / 2f, height / 2f);
    
    		Path path = new Path();    //import android.graphics.Path;
    		path.moveTo(0f, 0f);       //right angle at origin
    		path.lineTo(radius, 0f);   //the horizontal leg
    		path.lineTo(0f, radius);   //the hypotenuse
    		path.close();              //the vertical leg
    
    		canvas.drawPath(path, paint);
    

    But everyone expects the Y axis to point up. Here’s how we can accomplish that:

    		//My favorite pair of transformations:
    		//let the center of the JapanView be the origin,
    		//with the X axis pointing right, Y axis pointing up.
    		canvas.translate(width / 2f, height / 2f);
    		canvas.scale(1f, -1f);
    
    		Path path = new Path();    //import android.graphics.Path;
    		path.moveTo(0f, 0f);       //right angle at origin
    		path.lineTo(radius, 0f);   //the horizontal leg
    		path.lineTo(0f, radius);   //the hypotenuse
    		path.close();              //the vertical leg
    
    		canvas.drawPath(path, paint);
    

  10. The rotate and skew transformations will be easier to demonstrate if we draw a square.
    		//Let the center of the JapanView be the origin.
    		canvas.translate(width / 2f, height / 2f);
    
    		//left, top, right, bottom
    		canvas.drawRect(-radius, -radius, radius, radius, paint);
    

    You could draw the single pixel at coördinates (0, 0) with

    		//left, top, right, bottom
    		canvas.drawRect(0f, 0f, 1f, 1f, paint);
    
    but it’s simpler to say
    		canvas.drawPoint(0f, 0f, paint);
    

  11. Rotate the square 15° counterclockwise.
    		//Let the center of the JapanView be the origin.
    		canvas.translate(width / 2f, height / 2f);
    
    		//Degrees, not radians.  Negative is counterclockwise.
    		canvas.rotate(-15f);
    
    		//left, top, right, bottom
    		canvas.drawRect(-radius, -radius, radius, radius, paint);
    

    What would go wrong if we did the rotate before the translate?


  12. Let’s apply the skew transformation.
    		//Origin at center, X axis points right, Y axis points up.
    		canvas.translate(width / 2f, height / 2f);
    		canvas.scale(1f, -1f);
    
    		//Move top edge of square to the right, bottom edge to the left.
    		//The point (0, 1) is moved to (.5, 1).
    		canvas.skew(.5f, 0f);
    
    		canvas.drawRect(-radius, -radius, radius, radius, paint);
    

  13. Remove the skew. Go back to the original circle and smooth out its jagged edge. In class JapanView, change
    		private Paint paint = new Paint();
    
    to
    		private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);	//no jagged edges
    

  14. A drop shadow is the poor man’s (or poor woman’s) 3D graphics. Add a drop shadow to the circle and to anything drawn with the paint. The first argument of setShadowLayer is the sharpness of the shadow (.1f for very sharp). The second and third arguments are an (x, y) vector giving the shadow’s length and direction.
    		//in the JapanView's constructor
    
    		//The variable LAYER_TYPE_SOFTWARE was invented only in version 11
    		//(Honeycomb) of the Android SDK.
    
    		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    			//Draw the JapanView without using hardware acceleration,
    			//because hardware acceleration would prevent the shadow
    			//from being drawn.
    			setLayerType(LAYER_TYPE_SOFTWARE, paint);
    		}
    		paint.setShadowLayer(2f, 10f, 20f, Color.GRAY);
    

  15. Remove the shadow and add a linear gradient (vs. a radial gradient) to the circle (and to anything drawn with the paint).
    		//in onDraw
    		float radius = .3f * Math.min(width, height);
    
    		//The two parallel arrays must have the same length.
    
    		int[] colors = {
    			Color.RED,
    			Color.rgb(255, 80, 0),	//There is no Color.ORANGE.
    			Color.YELLOW,
    			Color.GREEN,
    			Color.BLUE
    		};
    
    		float[] positions = {	//These are the default positions.
    			0f / (colors.length - 1),
    			1f / (colors.length - 1),
    			2f / (colors.length - 1),
    			3f / (colors.length - 1),
    			4f / (colors.length - 1)
    		};
    
    		LinearGradient linearGradient = new LinearGradient(
    			-radius, 0f,	//starting point
    			 radius, 0f,	//ending point
    			colors,
    			positions,
    			Shader.TileMode.CLAMP //vs. Shader.TileMode.REPEAT, etc.
    		);
    
    		paint.setShader(linearGradient);
    

  16. In addition to drawColor, drawCircle, drawRect, drawPoint, and drawLine, try some of the other drawing methods of class Canvas: drawLines, drawPoints, and drawText.

  17. Create the JapanView in activity_main.xml. Replace the unused TextView element in that file with the following. By default, activity_main.xml. thinks that each class belongs to the package android.view. If activity_main.xml had said JapanView instead of edu.nyu.sps.japan.JapanView, the computer would have looked for android.view.JapanView. See sample_japan_view.xml.
        <edu.nyu.sps.japan.JapanView
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
    In JapanView.java, add a two-argument constructor. Click on the word JapanView in that file and pull down
    Code → Generate… → Constructor
    Select the two-argument constructor and press OK.
    Choose Fields to Initialize by Constructor.
    Press Select None.
        public JapanView(Context context, AttributeSet attributeSet) {
            super(context, attributeSet);
            paint.setColor(Color.RED);
            paint.setStyle(Paint.Style.FILL);	//vs. STROKE
    
            //If you want to know what layout_width was fed to the JapanView.  match_parent is -1.
            String s = attributeSet.getAttributeValue("http://schemas.android.com/apk/res/android", "layout_width");
        }
    
    In MainActivity.java, reinstate the original
            setContentView(R.layout.activity_main);
    
    and get rid of the japanView object.