Technical: Java
Advanced Search
Apple Developer Connection
Member Login Log In | Not a Member? Contact ADC

title


Previous Section Table of Contents Next Section


Module 5- Custom Media Controllers in QTJ


Contents

Overview

Introduction to QuickTime Controllers

  1. Setting up the Project

  2. Designing the Buttons

  3. Laying the Foundation

  4. Modifying AnimalPane's Inheritance

  5. Further Organization

  6. Adding Image Loading Utility Code

  7. Loading the Controller Media

  8. Creating a Custom Button Class

  9. Creating the Buttons

  10. Creating a Button Listener Class

  11. Registering the Action Listeners

  12. Creating a Controller Object

  13. Registering the Buttons with the Controller

  14. Implementing the Start Method

  15. Implementing the Stop Method

  16. Final Modifications to the Zoo Class

Summary

Further Exploration


Overview
Module4In the first few modules, we learned how to create a window with a QTCanvas and use a compositor to display an image and a movie. We learned how to draw text using QuickTime's text services, and entered the world of audio via the QTPlayer class.

In this module, we will learn how to QuickTime's new button classes to offer the user an alternative to the standard QuickTime controller. This will allow us to present our own custom UI to control the playback of our zebra movie.

Introduction to QuickTime Controllers

Quicktime has a standard controller that allows the user to interact with the movie and perform tasks such as adjusting the volume, starting and stopping the movie, and navigating to points within the movie. This standard controller looks like this:

Standard Controller

Using the standard controller is very simple. If you use a class such as QTPlayer, you get standard controller without any additional effort. Some applications, however, do not want to use the standard controller, either because it presents more functionality than is needed or because the developer wants to present an interface that is more consistent with the rest of the application.

In either case, it is possible to create a custom controller that controls the playback of a movie. In this module, we will examine the ReleaseButton class and use it to present our own playback controls.

Back to top

Setting up the Project

This project is based on the previous module and assumes that you have completed it. If you would like to download a completed copy of the module 4, you may do so. We have renamed the main class to "Zoo5" and changed all instances of previous module classes to prevent confusion. Other than that, we do not need to modify the main class file.

Click here to see the main source file Zoo5.java.

Back to top

Designing the Buttons

In this example, we will be creating three different buttons, a play button, a stop button, and a rewind button. These buttons function in a very specific manner. If your browser supports JavaScript, you my experiment with the functionality of the these buttons using the controller mockup below:

Our Button Custom Controller

As we recall from previous modules, when the user launches our program, the movie starts playing immediately. In order for the controller to be synchronized with the movie, we need to ensure that the initial state of the play button indicates that the movie is playing. This is indicated by a blue highlight color in the play triangle.

Each button has a pressed state that is shown when the button is clicked. When the stop button is pressed, the movie will stop playing. We need to indicate this by adding a blue highlight color in the stop button square. We also need to make sure that we remove the active indicator from the play button.

Thus, the button has the following states:

 
Inactive
Depressed
Active
Rewind  
Play
Stop

Note that the Rewind button does not have an active state. It rewinds the movie to the beginning of the media with a single click. If the movie was playing before the rewind button is pressed, the movie will begin playback at the beginning of the movie. If the movie was stopped, the movie will rewind and remain stopped.

Back to top

Laying the Foundation

Before we start implementing our controller, we need to modify our underlying architecture. Currently, we have been doing most of our work in our AnimalPane class. This has been acceptable for previous modules, but before we add more functionality, we need to start working on making our foundation solid.

We create a new file and save it as ZooPane.java. In this file, we will define a new abstract class called ZooPane that enforces a specific interface:

ZooPane.java
import quicktime.app.anim.Compositor;

public abstract class ZooPane
{
    public Compositor getCompositor()
    {
        return compositor;
    }

    abstract public void start();

    abstract public void stop();
	
    protected Compositor compositor;
}

This class will be the superclass of AnimalPane. In future modules, we will have multiple classes that use this interface- not just AnimalPane.

We move the getCompositor( ) routine and the declaration of the compositor data member down to this class, and add two additional methods start( ) and stop( ). These methods are like master control switches that can start and stop all activity in a given compositor as well as associate or disassociate the controller with the movie.

Now we need to make the appropriate changes to AnimalPane.

Back to top

Modifying AnimalPane's Inheritance

Since we have created a base class for AnimalPane, we now need to modify that file to implement the new interface:

AnimalPane.java
  
  ...
public class AnimalPane extends ZooPane
{
    public AnimalPane() { ... }
    public void start()
    {
    }
	
    public void stop()
    {
    }
    
    public void playSound( ) { ... } 
    public Compositor getCompositor( ) { return compositor; }
    protected void addText(String textPath, int layer, int x, 
                           int y, int width, int height)
    { 

       ... 

    protected QTPlayer player;
    protected Compositor compositor;
    protected MoviePresenter md;
}

First, we add the keyword "extends" and the class ZooPane after the declaration of the AnimalPane class. Now that we have inserted the ZooPane base class, we must implement its two abstract methods, start( ) and stop( ), We place these after our constructor, and before the playSound( ) method. We leave these two routines stubbed out for now. We will provide an implementation later as we get farther along with this module.

Finally, we remove the getCompositor( ) call and the compositor data member since we moved these to our base class.

Now that we have made these small modifications, we are ready to start making our controller.

Back to top

Further Organization

Now we are nearly ready to start working on our controller. First, we must do some additional housekeeping. We have a lot of new code to write, and no where to put it except in our constructor which is starting to get a little bit unruly.

AnimalPane.java
  
  ...

public class AnimalPane extends ZooPane
{
    public AnimalPane()
    {
        QDRect size = new QDRect(Zoo4.WIDTH, Zoo4.HEIGHT);
        try
        {
            loadSound("data/zebra/Zebra.au");
            QDGraphics gw = new QDGraphics( size );	
            compositor = new Compositor( gw, 
                             QDColor.white, 30, 1 );
                ...
			
            addText( "data/zebra/Zebra.txt", 2, 15, 160, 
                     415, 220 );
            Movie m = makeMovie( new QTFile( QTFactory.
               findAbsolutePath( "data/zebra/Zebra.mov" )));
            md = new MoviePresenter( m );			
            compositor.addMember( md, 1 );			
            compositor.getTimer().setRate(1);	
            md.setRate(1);

            addMovie( "data/zebra/Zebra.mov", 1, 0, 0 );
 
           PlaySound();
 
              ...
    }
    
    public void start()
    {
    }
	
    public void stop()
    {
    }

    public void playSound() { ...
    protected void addMovie( String moviePath, int layer, 
        int x, int y )
    {
        Movie m = makeMovie( new QTFile( QTFactory.
            findAbsolutePath( moviePath )));
        md = new MoviePresenter( m );			
        compositor.addMember( md, 1ayer );			
        compositor.getTimer().setRate(1);	
        md.setRate(1);
    }
        ...


We cut the code (highlighted above in red) in the constructor that creates the movie and loads it into the compositor. We will move this block of code into a new routine called addMovie( ). Next, we will load our movie into the compositor as well as create our custom controller in this method. We make a call to this new method and pass the path to the movie, the layer number, and the x and y position of the movie (similar to our addText( ) method, but without the height and width properties).

Next, we declare our addMovie( ) method. This method is protected and takes five parameters, the string URL to the movie, the layer of the compositor in which the movie will be displayed, and the x and y coordinate positions of the movie within the compositor.

Once this method is declared, we paste the source code in the clipboard from our constructor into the body of this method. We modify the hard-coded path of the movie file and replace it with the moviePath variable.

Back to top

Adding Image Loading Utility Code

Previous versions of this module drew the Zebra image directly within the AnimalPane constructor. In this step, we will be replacing this code with a utility function which we will also use to draw the images associated with our custom controller:

AnimalPane.java
  
  ...

public class AnimalPane extends ZooPane
{
    public AnimalPane()
    {
        QDRect size = new QDRect(Zoo4.WIDTH, Zoo4.HEIGHT);
        try
        {
            loadSound("data/zebra/Zebra.au");
            QDGraphics gw = new QDGraphics( size );	
            compositor = new Compositor( gw, 
                             QDColor.white, 30, 1 );

            compositor.getTimer().setRate(1);
... addText( "data/zebra/Zebra.txt", 2, 15, 160, 415, 220 );
            QTFile imageFile = new QTFile(
QTFactory.findAbsolutePath(
"data/zebra/ZebraBackground.jpg")); GraphicsImporterDrawer drawer = new
GraphicsImporterDrawer(imageFile); ImagePresenter presenter = ImagePresenter.
fromGraphicsImporterDrawer(drawer);
            ImagePresenter presenter = makePresenterFromFile(
                "data/zebra/ZebraBackground.jpg");
            
            presenter.setLocation( 110, 110 );
            compositor.addMember( presenter, 3 );

           addMovie( "data/zebra/Zebra.mov", 1, 0, 0 ); 
           PlaySound();
 
              ...
    }
    public ImagePresenter makePresenterFromFile( String name )
    {
        try
        {
            QTFile file = new QTFile( QTFactory.
findAbsolutePath(name)); GraphicsImporterDrawer drawer = new
GraphicsImporterDrawer(file); return ImagePresenter. fromGraphicsImporterDrawer(drawer); } catch(IOException e) { e.printStackTrace(); } catch(QTException e) { e.printStackTrace(); } return null; }

public void start() { } public void stop() { } ...

In order to move the image loading and drawing code into its own method, we select the code in the constructor (highlighted above in red) and cut it to the clipboard. We then replace our code with a call to our new utility class we will write called makePresenterFromFile( ). Our utility method will take a string parameter representing the url of the file to be presented and return an ImagePresenter object that is capable of drawing the image.

We declare a ImagePresenter data member, and assign it to the result of our utility function, passing the URL of our zebra image file to the procedure.

Next, we declare our utility function immediately following our constructor. We declare our function with the following signature:

public ImagePresenter makePresenterFromFile( String name )

The body of our method is in a try / catch block. We then paste the code that we copied from the constructor and modify it slightly to work in our method. We change the parameter of the GraphicsImporterDrawer constructor from a static string to the parameter passed in our method. We also modify the code to return the ImagePresenter object from the method.

Back to top

Loading the Controller Media

Now that we have made a specific method for our custom controller code, it is time to load all of our media for our controller via our utility function:

AnimalPane.java
    ...

public class AnimalPane extends ZooPane
{
    ...
public void playSound() { ... }
protected void addMovie( String moviePath, int layer, int x, int y ) { Movie m = makeMovie( new QTFile( QTFactory. findAbsolutePath( moviePath ))); md = new MoviePresenter( m );
        md.setLocation(x, y);
playRel = makePresenterFromFile( "data/Play.jpg" ); playPress = makePresenterFromFile( "data/PlayPressed.jpg" ); playPlaying = makePresenterFromFile( "data/Playing.jpg");
stopRel = makePresenterFromFile( "data/Stop.jpg" ); stopPress = makePresenterFromFile( "data/StopPressed.jpg" ); stopDeactive= makePresenterFromFile("data/Stopped.jpg"); rewRel = makePresenterFromFile( "data/Rewind.jpg" ); rewPress = makePresenterFromFile( "data/RewindPressed.jpg" ); rewDeactive = rewRel;
        compositor.addMember( md, 1ayer );			
        compositor.getTimer().setRate(1);	
        md.setRate(1);
    }

        ...
 
    protected QTPlayer player;
    protected MoviePresenter md;
    protected ImagePresenter playRel;
    protected ImagePresenter playPress;
    protected ImagePresenter playPlaying;
 			
    protected ImagePresenter stopRel;
    protected ImagePresenter stopPress;
    protected ImagePresenter stopDeactive;
	
    protected ImagePresenter rewRel;
    protected ImagePresenter rewPress;
    protected ImagePresenter rewDeactive;
}


Our first task is to call setLocation( ) from the MoviePresenter object with the x and y positions that are passed in to our method. This will offset the movie within the compositor based on the values passed to our constructor. Next we call makePresenterFromFile( ) with each of the 8 images used by our controller. Individual data members for each of the ImagePresenter objects are declared at the end of the source file. This loads each of the images we will be using in our buttons into an individual ImagePresenter object. This is necessary for creating the custom buttons as we will shortly see.

In the next section, we will create our custom buttons.


Back to top



Previous Section Table of Contents Next Section