Whilst animations are supported in Kuix through the use of the Picture class, sometimes you want more control i.e. a portion of screen's real estate to draw to using the graphics context (to render images, primitives, text etc.). This can be achieved by creating custom widgets, overriding the widget's paint methods. However, this approach may be a bit heavy if you just want to customise the background, but not change widget behaviour, or if you want to apply a background animation across different widgets. The following code provides a flexible, pluggable approach to creating widget containers with a custom animated (or static) backgrounds.
Install this jar file to try this method implemented with the Kuix demo sources, or see a video of the demo on YouTube.

The first step to creating widgets supporting animated backgrounds is to create an interface, called PainterTask, that describes the drawing operation:
public interface PainterTask { public void paint(Graphics g, int width, int height, int frame); }
Then we need to create our own widget type that will support custom draw operations (by calling a class instance that implements PainterTask in the paintBackground method):
public class AnimatedBackgroundWidget extends Widget { public static final String WIDGET_TAG = "anibgcont"; private static final int DEFAULT_DELAY = 500; private static final String PAINTER_ATTRIBUTE = "painter"; private static final String FRAME_WIDTH_ATTRIBUTE = "framewidth"; private static final String FRAME_HEIGHT_ATTRIBUTE = "frameheight"; private static final String FRAME_DELAY_ATTRIBUTE = "framedelay"; private PainterTask painter; private WorkerTask animationWorkerTask; private int frameCount = 0; private int frameWidth = -1; // Max private int frameHeight = -1; // Max private int frameDelay = DEFAULT_DELAY; private long lastFrameTime; public AnimatedBackgroundWidget() { super(WIDGET_TAG); } public boolean setAttribute(String name, String value) { if (FRAME_WIDTH_ATTRIBUTE.equals(name)) { frameWidth = Integer.parseInt(value); return true; } else if (FRAME_HEIGHT_ATTRIBUTE.equals(name)) { frameHeight = Integer.parseInt(value); return true; } else if (FRAME_DELAY_ATTRIBUTE.equals(name)) { frameDelay = Integer.parseInt(value); return true; } return super.setAttribute(name, value); } public boolean setObjectAttribute(String name, Object value) { if (PAINTER_ATTRIBUTE.equals(name)) { if (value instanceof PainterTask) { setPainterTask((PainterTask) value); } else { setPainterTask(null); } return true; } return super.setObjectAttribute(name, value); } public boolean isObjectAttribute(String name) { if (PAINTER_ATTRIBUTE.equals(name)) { return true; } return super.isObjectAttribute(name); } public Object getAttribute(String name) { if (PAINTER_ATTRIBUTE.equals(name)) { return painter; } return super.getAttribute(name); } public void setPainterTask(PainterTask painter) { this.painter = painter; invalidate(); } public void paintBackground(Graphics g) { super.paintBackground(g); if (painter != null) { int fw = frameWidth == -1 ? getWidth() : frameWidth; int fh = frameHeight == -1 ? getHeight() : frameHeight; if (animationWorkerTask == null && frameDelay > 0) { animationWorkerTask = new WorkerTask() { public boolean run() { if (isVisible()) { frameCount++; if (lastFrameTime == 0) { lastFrameTime = System.currentTimeMillis(); } else if (System.currentTimeMillis() - lastFrameTime > frameDelay) { lastFrameTime = System.currentTimeMillis(); invalidateAppearance(); } } if (!isInWidgetTree()) { animationWorkerTask = null; return true; } return false; } }; Worker.instance.pushTask(animationWorkerTask); } painter.paint(g, fw, fh, frameCount); } } }
This class is similar to the Picture class (check the Kuix source code) as it supports animation by using the Worker. As with all custom widget types, you'll need to create a custom convertor in order for Kuix to recognise this widget tag through XML - see here for a complete guide to creating custom widgets. The "painter" attribute requires a class that implements the PainterTask interface (returned through the data provider associated with the widget).The tag <anibgcont/> (perhaps you could think of a better label!) is used to add the component in the relevant XML UI file:
<anibgcont framedelay="60" style="..."> <_painter>@{painter}</_painter> ... </anibgcont>
Here's a simple class that renders some animated balls to the background:
public class BallBackground implements PainterTask { private static final int DIAMETER = 16; private static final int RADIUS = DIAMETER / 2; private static final int MIN_SPEED = 2; private static final int MAX_SPEED = 6; private static final int NUM_BALLS = 20; private Random rnd = new Random(System.currentTimeMillis()); private Ball[] balls; private class Ball { int x; int y; int dx; int dy; int clr; public Ball(int x, int y, int dx, int dy, int clr) { this.x = x; this.y = y; this.dx = dx; this.dy = dy; this.clr = clr; } } private Ball[] createBalls(int num, int w, int h) { Ball[] bs = new Ball[num]; for (int i = 0; i < num; i++) { bs[i] = new Ball( RADIUS + rnd.nextInt(w - DIAMETER), RADIUS + rnd.nextInt(h - DIAMETER), MIN_SPEED + rnd.nextInt(MAX_SPEED - MIN_SPEED), MIN_SPEED + rnd.nextInt(MAX_SPEED - MIN_SPEED), rnd.nextInt(0xFFFFFF)); } return bs; } private void moveBall(Ball b, int w, int h) { b.x += b.dx; b.y += b.dy; if (b.x + RADIUS >= w) { b.x = w - RADIUS; b.dx = -(MIN_SPEED + rnd.nextInt(MAX_SPEED - MIN_SPEED)); } else if (b.x - RADIUS < 0) { b.x = RADIUS; b.dx = MIN_SPEED + rnd.nextInt(MAX_SPEED - MIN_SPEED); } if (b.y + RADIUS >= h) { b.y = h - RADIUS; b.dy = -(MIN_SPEED + rnd.nextInt(MAX_SPEED - MIN_SPEED)); } else if (b.y - RADIUS < 0) { b.y = RADIUS; b.dy = MIN_SPEED + rnd.nextInt(MAX_SPEED - MIN_SPEED); } } public void paint(Graphics g, int width, int height, int frame) { if (balls == null) { balls = createBalls(NUM_BALLS, width, height); } Ball b; for (int i = 0; i < balls.length; i++) { b = balls[i]; moveBall(b, width, height); g.setColor(b.clr); g.fillArc(b.x - RADIUS, b.y - RADIUS, DIAMETER, DIAMETER, 0, 360); } } }
Download complete source code here. |