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 | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
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.
MainActivity.java
.
I commented out the call to
setContentView
in
onCreate
and replaced it with a call to
another
method
with the same name.
JapanView.java
.
I edited out all the methods except the
one-argument
constructor
and
onDraw
.
R.java
activity_main.xml
:
unused and ignored.
We should have removed it from the project to save space.
strings.xml
styles.xml
attrs_japan_view.xml
:
attribute resources used only by the code we will edit out of the newborn
JapanView.java
file.
sample_japan_view.xml
:
shows how to create one of your
JapanView
objects in an
.xml
file, if you’re interested.
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
.
build.gradle
(Module: app)
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
.
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));
Color.RED //Opaque red. Can also be written 0xFFFF0000with
Color.rgb(255, 0, 0) //Opaque red. Can also be written 0xFFFF0000or
Color.argb(128, 255, 0, 0) //Semi-opaque red. Can also be written 0x80FF0000Each parameter must be a whole number in the range 0 (minimum) to 255 (maximum) inclusive. Create your own color.
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.
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>
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
Toast
s
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.
JapanView
is in the upper left corner,
with the X axis pointing to the right and the Y axis pointing down.
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);
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);
//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
?
//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);
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);
//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
?
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);
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
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);
//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);
drawColor
,
drawCircle
,
drawRect
,
drawPoint
,
and
drawLine
,
try some of the other drawing methods of class
Canvas
:
drawLines
,
drawPoints
,
and
drawText
.
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
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.