Implementing the drawing functionality

The Paint MIDlet is a good demonstration of how to circumvent hardware and platform limitations. For smooth drawing, a lot of touch events are needed. However, drawing is time consuming, so the events need to be buffered in order to prevent dropping any events. In this MIDlet, lines are drawn from the previous drag event to the next one by drawing dots along the drag path to create the impression of a continuous line. This way the line width can also be easily adjusted.

To implement the drawing functionality:

  1. Since drag events are not generated for every single pixel you touch, if you tried to draw just by making a dot every time a drag event occurs, you would get a series of dots separated by varying lengths of empty space, depending on the velocity of the drag action. In addition, on Series 40 devices with a smaller screen you get more drag events than on Symbian devices with a much larger screen (also, more time is spent on the platform screening the display for events). To create a solid line, you need to fill in the gaps by drawing more dots.

    Why draw dots when you could use lines? Because Java ME only supports 1-pixel-wide lines. Thicker lines need to be created by other means, for example by using dots. In the Paint MIDlet, line drawing is implemented by drawing overlapping filled circles along the drag path. (The diameter of the circles is determined by the selected brush size.) This is achieved by dividing the line between the drag events into small segments with a length of 1/3 of the brush width. The 1/3 length presents a good trade-off for not generating too much unnecessary drawing load while still producing a decent line.

    The following code snippet shows how to draw a line segment between two drag events.

        private Graphics dg;
    
        // ...
    
            float divider = brushWidth / 3;
    
        // ...
    
                if (startX  > 0 && startY  > 0 && endX > 0 && endY > 0) {
                    float d = calcDistance(startX , startY , endX, endY);
                    if (d > divider) {
                        int steps = (int) (d / divider + 0.5);
                        float dx = (endX - startX ) / (float) steps;
                        float dy = (endY - startY ) / (float) steps;
                        for (int i = 0; i < steps; i++) {
                            int x = (int) (startX  + dx * i + 0.5);
                            int y = (int) (startY  + dy * i + 0.5);
                            dg.fillArc(x, y - ToolBar.HEIGHT, brushWidth, brushWidth, 0, 360);
                        }
                    }
                    dg.fillArc((int) endX, (int) endY - ToolBar.HEIGHT, brushWidth, brushWidth, 0, 360);
                }
    
        // ...
    
        private float calcDistance(float x1, float y1, float x2, float y2) {
            return (float) (Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)));
        }
  2. Do not draw on the screen immediately when a drag event occurs, since it is not very efficient. Drawing requires time, and in the meanwhile subsequent events would be lost.

    The most efficient way to actually draw on the screen is to buffer the events. This MIDlet pushes all the drag events to a buffer, which is read every 60 ms while the UI thread runs. The buffering allows the MIDlet to get all the drag events without complicated threading. The same UI thread that draws everything else also draws the buffer on the screen.

    The pointerDragged method adds line segments to the buffer, and the renderLines method reads and renders them.

        public void pointerDragged(int x, int y, int id) {
            if (!drawingAllowed && colorpicker.isVisible()) {
                colorpicker.dragged(x, y);
            }
            else if (y > Toolbar.HEIGHT) {
                buffers[id].addElement(new DrawableLine(previousX[id], previousY[id], x, y));
                previousX[id] = x;
                previousY[id] = y;
            }
        }
    
        // ...
    
        private void renderLines(Vector buffer) {
            int l = buffer.size();
            DrawableLine[] currentLines = new DrawableLine[buffer.size()];
            for (int i = 0; i < l; i++) {
                currentLines[i] = (DrawableLine) buffer.elementAt(i);
            }
    
            int s = currentLines.length;
            float divider = brushWidth / 3;
            for (int j = 0; j < s; j++) {
                DrawableLine line = currentLines[j];
    
                buffer.removeElement(line);
                int startX  = line.startX;
                int startY  = line.startY;
                int endX = line.endX;
                int endY = line.endY;
                // Draw a line segment (see previous code snippet)
            }
        }
  3. The current lines are moved from the buffer to a local array, which is iterated in the drawing algorithm. The move is needed because new events can arrive into the buffer while the buffer is being read, and this way no events are lost. Basically, events are gathered from the buffer at a specific moment, and all events that arrive to the buffer after that moment are handled in the next UI cycle. When a line is actually being drawn, that line object is also removed from the buffer.

    The DrawableLine class encapsulates the drag event by forming a line whose starting coordinate is the previous drag coordinate and the ending coordinate is the current drag coordinate. The buffer works like a queue where the drag events are drawn in the order they arrive to the buffer.

    The render method renders the UI by calling the renderLines method. When drawing is allowed and the buffer is not empty, calculations are performed and the drag events in the buffer are drawn. Note that if drawing is not allowed, everything that has been drawn to the screen so far is drawn again, because otherwise the transparency of the UI elements does not work.

        protected void render() {
            g.setClip(0, 0, getWidth(), getHeight());
            toolbar.render(g);
            if (renderDrawing) {
                g.drawImage(drawing, 0, Toolbar.HEIGHT, Graphics.LEFT | Graphics.TOP);
                renderDrawing = false;
            }
            else {
                if (drawingAllowed) {
                    boolean changed = false;
                    for (int i = 0; i < TOUCH_POINTS; i++) {
                        Vector buffer = buffers[i];
                        if (!buffer.isEmpty()) {
                            renderLines(buffer);
                            changed = true;
                        }
                    }
                    if (changed) {
                        g.drawImage(drawing, 0, Toolbar.HEIGHT, Graphics.LEFT | Graphics.TOP);
                    }
                }
                else {
                    g.drawImage(drawing, 0, Toolbar.HEIGHT, Graphics.LEFT | Graphics.TOP);
                }
                colorpicker.render(g);
                savedialog.render(g);
            }
        }