Search code examples
javaandroidcanvascarouselandroid-drawable

How to Make an Android 3D Carousel Drawable


I'm trying to create a 3D carousel in Android (Java) but I can't quite get it to work. This is the kind of carousel I'm aiming for:

enter image description here

I've tried several long and complicated ways of doing this, including drawing paths by hand that use a ton of math, but it's never quite right.

The idea that feels closest is to draw a carousel of images in the x-y plane by calculating their positions on a circle, and then "flipping" that plane so the carousel is now in the x-z direction. That seems to almost work (though the corners don't quite touch), but the squares are lying flat and I need them to stand up on their outermost edge. I can't figure out how to do that final rotation though in a way that doesn't undo the previous rotations by the camera.

Here is what my result looks like visually currently:

enter image description here

Am I going about this complete the wrong way? Am I close but need to just change a couple of things? I'm honestly not sure. Please help me figure out how to do this better!

Here is my drawable code:

public class CarouselCard extends Drawable {

    Paint paintBackground = null;
    
    /* CONSTRUCTOR */

    public CarouselCard() {
        super();
        invalidateSelf();
    }

    @Override
    public void draw(Canvas canvas) {

        if (paintBackground == null) {    // Just starting
            paintBackground = new Paint();
            paintBackground.setStyle(Paint.Style.FILL);
            paintBackground.setColor(Color.BLUE);
        }

        int HALF_CHORD_WIDTH = 150;
        float RADIUS = 200;

        float INTERIOR_ANGLE = 30;

        INTERIOR_ANGLE %= 360;
        int numObjects = (int) (360 / INTERIOR_ANGLE);

        for (int i = 0; i < numObjects; i++) {

            float theta = INTERIOR_ANGLE * i;
            double rad = Math.toRadians(theta);

            float circleX = (float) (RADIUS * Math.sin(rad));
            float circleY = (float) (RADIUS * Math.cos(rad));

            canvas.save();
            canvas.translate(canvas.getWidth() / 2, canvas.getHeight() / 2);

            Camera camera = new Camera();
            camera.save();

            camera.rotateX(80); // Approximates 90 but keeps it visible since flat
            camera.rotateZ(theta);

            camera.applyToCanvas(canvas);
            camera.restore();

            if (i % 4 == 0) paintBackground.setColor(Color.YELLOW);
            if (i % 4 == 1) paintBackground.setColor(Color.GREEN);
            if (i % 4 == 2) paintBackground.setColor(Color.BLUE);
            if (i % 4 == 3) paintBackground.setColor(Color.GRAY);

            paintBackground.setAlpha(128);

            canvas.drawRect(circleX-HALF_CHORD_WIDTH, circleY-HALF_CHORD_WIDTH,
                    circleX+HALF_CHORD_WIDTH, circleY+HALF_CHORD_WIDTH, paintBackground);

            canvas.restore();
        }
    }

    @Override
    public void setAlpha(int alpha) {}

    @Override
    public void setColorFilter(ColorFilter colorFilter) {}

    @Override
    public int getOpacity() {
        return PixelFormat.UNKNOWN;
    }
}

Alternatively, I can get everything facing the right way and oriented correctly, but only if they're all co-located at the center of the carousel. I'm not sure how to shift them appropriately while still maintaining the transformations that make this picture possible:

enter image description here

This image was produced by adjusting the above for-loop like so:

for (int i = 0; i < numObjects; i++) {

    if (i % 4 == 0) paintBackground.setColor(Color.YELLOW);
    if (i % 4 == 1) paintBackground.setColor(Color.GREEN);
    if (i % 4 == 2) paintBackground.setColor(Color.BLUE);
    if (i % 4 == 3) paintBackground.setColor(Color.GRAY);
    paintBackground.setAlpha(128);

    float theta = INTERIOR_ANGLE * i;

    canvas.save();
    canvas.translate(canvas.getWidth() / 2, canvas.getHeight() / 2);

    Camera camera = new Camera();
    camera.save();
    
    camera.rotateY(theta);

    camera.applyToCanvas(canvas);
    camera.restore();

    canvas.drawRect(
            -HALF_CHORD_WIDTH,
            -HALF_CHORD_WIDTH,
            HALF_CHORD_WIDTH,
            HALF_CHORD_WIDTH,
            paintBackground);

    canvas.restore();
}

Solution

  • I was able to create the following carousel:

    enter image description here

    The code for it is:

    @Override
    public void draw(Canvas canvas) {
    
        if (paintBackground == null) {    // Just starting
            paintBackground = new Paint();
            paintBackground.setStyle(Paint.Style.FILL);
            paintBackground.setColor(Color.BLUE);
        }
    
        // ######## CRUCIAL - START - Modify constants with care!
    
        // Decreasing chord denominator makes carousel bigger, while increasing it
        // makes front side contain more squares (but gets smaller)
    
        float HALF_CHORD_WIDTH = canvas.getWidth()/31f; // Width of each square
        float RADIUS = HALF_CHORD_WIDTH; // Radius of carousel
        float CAMERA_MULT = HALF_CHORD_WIDTH/10f;
    
        // ######## CRUCIAL - END
    
        float INTERIOR_ANGLE = 30;
    
        INTERIOR_ANGLE %= 360;
        int numObjects = (int) (360 / INTERIOR_ANGLE);
    
        for (int i = 0; i < numObjects; i++) {
    
            if (i % 4 == 0) paintBackground.setColor(Color.YELLOW);
            if (i % 4 == 1) paintBackground.setColor(Color.GREEN);
            if (i % 4 == 2) paintBackground.setColor(Color.BLUE);
            if (i % 4 == 3) paintBackground.setColor(Color.GRAY);
            paintBackground.setAlpha(128);
    
            float theta = INTERIOR_ANGLE * i;
            double rad = Math.toRadians(theta);
    
            float circleX = (float) (RADIUS * Math.sin(rad));
            float circleZ = (float) (RADIUS * Math.cos(rad));
    
            canvas.save();
            canvas.translate(canvas.getWidth() / 2, canvas.getHeight() / 2);
    
            Camera camera = new Camera();
            camera.save();
    
            camera.translate(circleX*CAMERA_MULT, 0, -circleZ*CAMERA_MULT);
            camera.rotateY(theta);
    
            camera.applyToCanvas(canvas);
            camera.restore();
    
            canvas.drawRect(
                    -HALF_CHORD_WIDTH,
                    -HALF_CHORD_WIDTH,
                    HALF_CHORD_WIDTH,
                    HALF_CHORD_WIDTH,
                    paintBackground);
    
            canvas.restore();
        }
    }