Remember that LiveDrawingView
cannot see the variables in LiveDrawingActivity
. By using the constructor, LiveDrawingActivity
is providing LiveDrawingView
with a reference to itself (this
) as well as the screen size in pixels contained in size.x
and size.y
. Add this constructor to LiveDrawingView
. The code must go within the opening and closing curly braces of the class. It is convention, but not mandatory, to place constructors above other methods but after member variable declarations:
// The LiveDrawingView constructor // Called when this line: // mLiveDrawingView = new LiveDrawingView(this, size.x, size.y); // is executed from LiveDrawingActivity public LiveDrawingView(Context context, int x, int y) { // Super... calls the parent class // constructor of SurfaceView // provided by the Android API super(context); }
To import the Context
class, do the following:
Context
in the new constructor's signature.The previous steps will import the Context
class.
Now, we have no errors in our LiveDrawingView
class or the LiveDrawingActivity
class that initializes it.
At this stage, we could run the app and see that using LiveDrawingView
as the View
in setContentView
has worked and that we have a beautiful blank screen, ready to draw our particle systems on. Try this if you like, but we will be coding the LiveDrawingView
class so that it does something, including adding code to the constructor, next.
We will be returning to this class constantly over the course of this project. What we will do right now is get the fundamentals set up ready so that we can add the ParticleSystem
instances after we have coded them in the next chapter.
To achieve this, first, we will add a bunch of member variables, and then we will add some code inside the constructor to set the class up when it is instantiated/created by LiveDrawingActivity
.
Following on, we get to code the draw
method, which will reveal the new steps that we need to take to draw to the screen 60 times per second, and we will also see some familiar code that uses our old friends, Canvas
, Paint
, and drawText
, from the previous chapter.
At this point, we need to discuss some more theory items, such as how we will time the animations of the particles, and how we lock these timings without interfering with the smooth running of Android. These last two topics, the game loop and threads, will then allow us to add the final code of this chapter and witness our particle system painting app in action, albeit with just a bit of text.
Add the variables, as shown in the following code, after the LiveDrawingView
declaration, but before the constructor, and then import the necessary extra classes:
// Are we debugging? private final boolean DEBUGGING = true; // These objects are needed to do the drawing private SurfaceHolder mOurHolder; private Canvas mCanvas; private Paint mPaint; // How many frames per second did we get? private long mFPS; // The number of milliseconds in a second private final int MILLIS_IN_SECOND = 1000; // Holds the resolution of the screen private int mScreenX; private int mScreenY; // How big will the text be? private int mFontSize; private int mFontMargin; // The particle systems will be declared here later // These will be used to make simple buttons
Be sure to study the code, and then we can talk about it.
We are using the naming convention of adding m
before the member variable names. As we add local variables to the methods, this will help distinguish them from each other.
Also, notice that all the variables are declared private
. You could happily delete all the private
access specifiers and the code will still work but, as we have no need to access any of these variables from outside of this class, it is sensible to guarantee that this can never happen by declaring them private
.
The first member variable is DEBUGGING
. We have declared this as final
because we don't want to change its value during the app's execution. Note that declaring it final
does not preclude us from switching its value manually when we wish to switch between debugging and not debugging.
The next three classes we declared instances of will handle the drawing on the screen. Observe the new one we have not seen before that I have highlighted:
// These objects are needed to do the drawing
private SurfaceHolder mOurHolder;
private Canvas mCanvas;
private Paint mPaint;
The SurfaceHolder
class is required to enable drawing to take place. It literally is the object that holds the drawing surface. We will see the methods it allows us to use to draw to the screen when we code the draw
method in a minute.
The next two variables give us a bit of insight into what we will need to achieve our smooth and consistent animation. Here they are again:
// How many frames per second did we get? private long mFPS; // The number of milliseconds in a second private final int MILLIS_IN_SECOND = 1000;
Both are of the type long
because they will be holding a huge number. Computers measure time in milliseconds since 1970. More on that when we talk about the game loop, but for now, we need to know that monitoring and measuring the speed of each frame of animation is how we will make sure that the particles move exactly as they should.
The first mFPS
will be reinitialized every frame of animation, around 60 times per second. It will be passed into each of the particle objects (every frame of animation) so that they know how much time has elapsed and can then calculate how far to move, or not.
The MILLIS_IN_SECOND
variable is initialized to 1000
. There are indeed 1000
milliseconds in a second. We will use this variable in calculations as it will make our code clearer than if we used the literal value, 1000. It is declared final
because the number of milliseconds in a second will obviously never change.
The next piece of the code we just added is shown again here for convenience:
// Holds the resolution of the screen private int mScreenX; private int mScreenY; // How big will the text be? private int mFontSize; private int mFontMargin;
The variables mScreenX
and mScreenY
will hold the horizontal and vertical resolution of the screen. Remember that they are being passed in from LiveDrawingActivity
into the constructor.
The next two, mFontSize
and mMarginSize
, will be initialized, based on the screen size in pixels, to hold a value in pixels to make formatting of our text neat and more concise than constantly doing calculations for each bit of text.
Just to be clear before we move on, these are the import
statements that you should currently have at the top of the LiveDrawingView.java
code file:
import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.view.SurfaceHolder; import android.view.SurfaceView;
Now, we can begin to initialize some of these variables in the constructor.
Add the following highlighted code to the constructor. Be sure to study the code as well and then we can discuss it:
public LiveDrawingView(Context context, int x, int y) { // Super... calls the parent class // constructor of SurfaceView // provided by Android super(context); // Initialize these two members/fields // With the values passed in as parameters mScreenX = x; mScreenY = y; // Font is 5% (1/20th) of screen width mFontSize = mScreenX / 20; // Margin is 1.5% (1/75th) of screen width mFontMargin = mScreenX / 75; // getHolder is a method of SurfaceView mOurHolder = getHolder(); mPaint = new Paint(); // Initialize the two buttons // Initialize the particles and their systems }
The code we just added to the constructor begins by using the values passed in as parameters (x
and y
) to initialize mScreenX
and mScreenY
. Our entire LiveDrawingView
class now has access to the screen resolution whenever it needs it. Here are the two lines again:
// Initialize these two members/fields // With the values passed in as parameters mScreenX = x; mScreenY = y;
Next, we initialize mFontSize
and mFontMargin
as a fraction of the screen width in pixels. These values are a bit arbitrary, but they work, and we will use various multiples of these variables to align text on the screen neatly. Here are the two lines of code I am referring to:
// Font is 5% (1/20th) of screen width mFontSize = mScreenX / 20; // Margin is 1.5% (1/75th) of screen width mFontMargin = mScreenX / 75;
Moving on, we initialize our Paint
and SurfaceHolder
objects. Paint
uses the default constructor, as we have done previously, but mHolder
uses the getHolder
method, which is a method of the SurfaceView
class. The getHolder
method returns a reference that is initialized to mHolder
, so mHolder
is now that reference. In short, mHolder
is now ready to be used. We have access to this handy method because LiveDrawingView
is a SurfaceView
:
// getHolder is a method of SurfaceView mOurHolder = getHolder(); mPaint = new Paint();
We will need to do more preparation in the draw
method before we can use our Paint
and Canvas
classes like we have done before. We will see exactly what this preparation entails very soon. Notice the comments indicating where we will eventually get around to initializing the particle systems, as well as two control buttons.
Let's get ready to draw.
Add the draw
method, which is shown immediately after the constructor method. There will be a couple of errors in the code. We will first deal with them, and then we will go into detail about how the draw
method will work in relation to SurfaceView
because there are some completely alien-looking lines of code in there, as well as some familiar ones. This is the code to add:
// Draw the particle systems and the HUD private void draw() { if (mOurHolder.getSurface().isValid()) { // Lock the canvas (graphics memory) ready to draw mCanvas = mOurHolder.lockCanvas(); // Fill the screen with a solid color below //Fill...... mCanvas.drawColor(Color.argb(255, 0, 0, 0)); // Choose a color to paint with mPaint.setColor(Color.argb(255, 255, 255, 255)); // Choose the font size mPaint.setTextSize(mFontSize); // Draw the particle systems // Draw the buttons // Draw the HUD if(DEBUGGING){ printDebuggingText(); } // Display the drawing on screen // unlockCanvasAndPost is a method of SurfaceHolder mOurHolder.unlockCanvasAndPost(mCanvas); } }
We have two errors. One is that the Color
class needs importing. You can fix this in the usual way, or add the next line of code manually.
Whichever method you choose, the following extra line needs to be added to the code at the top of the file:
import android.graphics.Color;
Let's deal with the other error.
The second error is the call to printDebuggingText
. This method doesn't exist yet. Let's add that now.
Add the following code after the draw
method, as follows:
private void printDebuggingText(){ int debugSize = mFontSize / 2; int debugStart = 150; mPaint.setTextSize(debugSize); mCanvas.drawText("FPS: " + mFPS , 10, debugStart + debugSize, mPaint); // We will add more code here in the next chapter }
The previous code uses the local variable debugSize
to hold a value that is half that of the member variable mFontSize
. This means that as mFontSize
(which is used for the HUD) is initialized dynamically based on the screen resolution, debugSize
will always be half that. The debugSize
variable is then used to set the size of the font before we start drawing the text. The debugStart
variable is just a guess at a good position vertically to start printing the debugging text.
These two values are then used to position a line of text on the screen that shows the current frames per second. As this method is called from draw
, which, in turn, will be called from the game loop, this line of text will be constantly refreshed up to sixty times per second.
Let's explore the new lines of code in the draw
method and exactly how we can use SurfaceView
, from which our LiveDrawingView
class is derived, to handle all of our drawing requirements.
Starting in the middle of the method and working outward for a change, we have a few familiar things, such as the calls to drawColor
, setTextSize
, and drawText
. We can also see the comment that indicates where we will eventually add code to draw the particle systems and the HUD:
drawColor
code clears the screen with a solid color.setTextSize
method sets the size of the text for drawing the HUD.What is totally new, however, is the code at the very start of the draw
method. Here it is again:
if (mOurHolder.getSurface().isValid()) { // Lock the canvas (graphics memory) ready to draw mCanvas = mOurHolder.lockCanvas(); … …
The if
statement contains a call to getSurface
and chains it with a call to isValid
. If this line returns true, it confirms that if the area of memory that we want to manipulate to represent our frame of drawing is available, the code continues inside the if
statement.
What goes on inside those methods (especially the first) is quite complex. They are necessary because all of our drawing and other processing (such as moving the objects) will take place asynchronously with the code that detects the user input and listens to the operating system for messages. This wasn't an issue in the previous project because our code just drew a single frame.
Now that we want to execute the code 60 times a second, we are going to need to confirm that we have access to the memory before we access it.
This raises more questions about how this code runs asynchronously. This will be answered when we discuss threads shortly. For now, just know that the line of code checks whether some other part of our code or Android itself is currently using the required portion of memory. If it is free, then the code inside the if
statement executes.
Furthermore, the first line of code to execute inside the if
statement calls lockCanvas
, so that if another part of the code tries to access the memory while our code is accessing it, it won't be able to.
Then, we do all of our drawing.
Finally, in the draw
method, there is this following line (plus comments) right at the end:
// Display the drawing on screen // unlockCanvasAndPost is a method of SurfaceHolder mOurHolder.unlockCanvasAndPost(mCanvas);
The unlockCanvasAndPost
method sends our newly decorated Canvas
object (mCanvas
) for drawing to the screen and releases the lock so that other areas of code can use it again, albeit very briefly before the whole process starts again. This process happens every single frame of animation.
We now understand the code in the draw
method. However, we still don't have the mechanism that calls the draw
method over and over. In fact, we don't even call the draw
method once. Next, we need to talk about game loops and threads.
3.16.76.43