Implementing the UI

The MIDlet entry point is a simple Main class that contains the standard MIDlet lifecycle methods. When the startApp method is called, it launches the Splash class, which shows basic information about the MIDlet. When the user taps the Start Jamming button, the Drumkit view opens, containing the main UI of the MIDlet.

The Drumkit view consists of a single GameCanvas-based view. The view background is a static 240 x 400 pixel PNG image, where the drum pads are displayed. The background is designed to also work on a 240 x 320 screen resolution. The pads themselves are drawn as circular, but, for easier touch event handling, the actual areas where taps are listened to are rectangular elements. The same applies to the buttons on the main view (play, record, and exit).

Touch events

For maximum responsiveness, touch events are handled with the pointerPressed method instead of pointerReleased, which is generally more common in UI design. When the events are handled immediately after the tap has been received, the corresponding sample can be played instantly.

    protected void pointerPressed(int x, int y) {
        pressed = true;
        if (kitEnabled) {
            for (int i = 0; i < 4; i++) {
                Pad pad = pads[i];
                if (pad.contains(x, y)) {
                    if (recording) {
                        // Record sample
                        tape.addElement(new Sample(pad));
                    }
                    // Play sound assigned to the pad
                    SoundManager.playSound(pad.getSound());
                    pad.setHit(true);

                    // Set timer for monitoring long tap
                    if (!recording) {
                        acivePad = i;
                        monitorLongTap(x, y);
                    }
                    break;
                }
            }
        }

        playBtn.pointerPressed(x, y);
        recordBtn.pointerPressed(x, y);
        exitBtn.pointerPressed(x, y);
        wheel.pointerPressed(x, y);
    }

Animations

The record button is animated using the AnimatedButton class, which uses the Sprite class for the animation. The animated sprites are all within a single PNG file from where they are read when the sprite is active. The following code snippet shows how the AnimatedButton class starts and stops the animation.

public class AnimatedButton
        extends Button {

    private long interval;
    private Timer animator;
    private Sprite animation;

    public AnimatedButton(String unpressed_image_url, String pressed_image_url,
                          String disabled_image_url, long interval, Listener listener) {
        super(unpressed_image_url, pressed_image_url, disabled_image_url, listener);
        this.interval = interval;
        this.animation = new Sprite(getPressedImage(), getWidth(), getHeight());
    }

    // ...

    /**
     * Start animation
     */
    private void startAnimation(long interval) {
        if (animator != null) {
            stopAnimation();
        }
        animator = new Timer();
        animator.schedule(new TimerTask() {

            public void run() {
                animation.nextFrame();

            }
        }, 0, interval);
    }

    /**
     * Stop animation
     */
    private void stopAnimation() {
        if (animator != null) {
            animator.cancel();
            animator = null;
            animation.setFrame(0);
        }
    }
}

Wheel menu

The wheel menu has a selection of samples for pads. It opens with a long tap and has a simple hide and show animation. The menu consists of 12 PNG icons, which are positioned in a circle for selecting different drum samples. The animations are done by varying the circle radius and the angle passed to sine and cosine functions.

Animations use Timer and TimerTasks. The following update function is called after lengthening or shortening the radius. The loop calculates new positions for the wheel buttons:

    public void updateWheel() {
        float ratio = (float) radius / MAX_RADIUS;
        int newX = originX + (int) ((float) (destinationX - originX) * ratio);
        int newY = originY + (int) ((float) (destinationY - originY) * ratio);
        int btnX = 0;
        int btnY = 0;

        // AngleExtra is used to get the spinning effect
        float angleExtra = 8 * ratio;
        for (int i = 0; i < BUTTON_COUNT; i++) {
            btnX = (int) (Math.cos(i * ANGLE + angleExtra) * radius);
            btnY = (int) (Math.sin(i * ANGLE + angleExtra) * radius);
            buttons[i].setPosition(btnX - btnWidth / 2 + newX,
                                   btnY - btnHeight / 2 + newY);
        }
    }

Long tap detection also uses a Timer. When the user touches the screen, a new Timer with a new TimerTask is initialized. If a long tap timer already exists, it is cancelled. Also, the boolean flag pressed turns true, and if the user lifts his/her finger from the screen, pressed turns false. The timer task simply checks if the pressed flag is still true after a given period, which in this case is one second. The following code snipped shows how the long tap events are handled:

    private boolean pressed = false;

    // ...

    private int acivePad = -1;

    // ...

    private Timer longTapTimer;

    // ...

    protected void pointerPressed(int x, int y) {
        pressed = true;
        if (kitEnabled) {
            for (int i = 0; i < 4; i++) {
                Pad pad = pads[i];
                if (pad.contains(x, y)) {
                    // ...

                    // Set timer for monitoring long tap
                    if (!recording) {
                        acivePad = i;
                        monitorLongTap(x, y);
                    }
                    break;
                }
            }
        }

        // ...
        wheel.pointerPressed(x, y);
    }

    // ...

    protected void pointerReleased(int x, int y) {
        // ...
        wheel.pointerReleased(x, y);

        // Stop monitoring long taps
        if (longTapTimer != null) {
            longTapTimer.cancel();
            longTapTimer = null;
        }
        pressed = false;
    }

    // ...

    private void monitorLongTap(final int x, final int y) {
        // Reset long tap timer
        if (longTapTimer != null) {
            longTapTimer.cancel();
        }
        longTapTimer = new Timer();

        final int pad = acivePad;

        // Task that shows prepares wheel menu to show up in the right place
        longTapTimer.schedule(
                new TimerTask() {

                    public void run() {
                        if (pressed) {
                            wheel.setOrigin(x, y);

                            // Calculate destination coordinates for the wheel menu
                            centerX = getWidth() / 2;
                            centerY = getHeight() / 2;
                            int destinationX = centerX - (int) (0.2 * (centerX - x));
                            int destinationY = centerY - (int) (0.5 * (centerY - y));
                            wheel.setDestination(destinationX, destinationY);
                            stopTape();

                            // ...

                            // Preselect a percussion and open the menu
                            wheel.select(pads[pad].getSound());
                            wheel.show();
                            kitEnabled = false;
                        }
                    }
                }, 1000);
    }