Coding the LiveDrawingView class

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:

  1. Place the mouse pointer on the red colored Context in the new constructor's signature.
  2. Hold the ALT key and tap the Enter key. Choose Import Class from the pop-up options.

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.

Tip

A game loop is a concept that describes allowing virtual systems to update and draw themselves at the same time as allowing them to be altered/interacted with by the user.

Adding the member variables

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.

Coding the LiveDrawingView 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.

Coding the draw method

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.

Adding the printDebuggingText method

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.

Note

It is possible that on very high or very low resolution screens, you might need to experiment with this value to find something more appropriate for your screen.

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.

Understanding the draw method and the SurfaceView class

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:

  • The drawColor code clears the screen with a solid color.
  • The setTextSize method sets the size of the text for drawing the HUD.
  • We will code the process of drawing the HUD once we have explored particle systems a little more. We will let the player know how many particles and systems their drawing is comprised of.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.76.43