Views, camera, action! …on hitting your users in the face

Everyone loves a nice, animated UI. Lots of rotations, things flipping, etc. Except, Android likes to flip things in a way that kinda.. well, jumps at you.

This is a short post on one way to change the field of view (focal length) of the camera drawing your rotated views.

If anyone can show me a better way, I would really, really appreciate it!

Demo

First, a demo! Source code is available here. (it’s a quick demo, not exactly production code)


The first animation uses the default parameters and as you can see it tries to come out of the screen and slap you in the face. Changing the camera distance shows a more modest perspective but now everything is scaled down. Changing the secret FOV factor fixes the perspective without scaling things down.

Views and projections

Unfortunately, the sample code below uses old-style animations, since I needed a quick-n-dirty demo. To make the code work with ViewProperty animations, take a look at View.getMatrix(). You actually need updateMatrix() but it’s private; getMatrix might work too (haven’t tried), as long as you remember to re-apply the change every time rotation/translation/etc changes.

What we’re looking to change is called the focal length. However, I searched and searched and couldn’t for the life of me find the focal length anywhere in the code. In fact, I couldn’t find anything resembling the canonical perspective projection matrix either.


A confession

I have no clue how Android does its 3D projection. It’s not using homogeneous coordinates and everything is in 3x3 matrices. I have a guess but I’ll shut up because 1) I haven’t actually investigated this long enough and 2) linear algebra and I didn’t part ways on the most amicable of terms. It’s still mad at me.

If anyone can spare me the effort and maths, feel free to share with the class. :) (it must be something obvious, I have just never come across it before; also, skia lacks any sort of documentation or comments)


Anyway, short story slightly shorter, if you get your hands on the 3x3 Matrix that describes the 3D projection (say, by using android.graphics.Camera and implementing your own Animation), you can modify the 9-th element (x3,1, helpfully called MPERSP_0) by scaling it down (by a factor of 2-5, empirically determined) and that will fix the crazy FOV.

@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
    mCamera.save();

    mCamera.translate(0, mHeight/2, sCameraDistance); // middle of left edge

    if (interpolatedTime <= 0.5f) {
        // First half rotates from 0 to MAX_ANGLE
        mCamera.rotateY(interpolatedTime * 2f * MAX_ANGLE);
    } else {
        // Second half rotates back from MAX_ANGLE to 0
        mCamera.rotateY((1f - (interpolatedTime - 0.5f) * 2f) * MAX_ANGLE);
    }

    mCamera.getMatrix(mMatrix);
    mMatrix.postTranslate(mWidth / 2, 0); // put the left edge in the middle of the View rectangle                                       

    // YOU CAN IGNORE EVERYTHING UNTIL HERE. HERE BE DRAGONS
                                                                                                                                         
    mMatrix.getValues(mMatrixVals);                                                                                                      
    mMatrixVals[Matrix.MPERSP_0] /= sFOVFactor; // magic happens here
    mMatrix.setValues(mMatrixVals);

    t.getMatrix().set(mMatrix);

    mCamera.restore();
}


And that’s that.

The bug that isn’t

This isn’t just an in-your-face problem. At some point in the past, I got the animated View to clip really badly and “hit” the camera plane. This caused artefacts and a vanishing View mid-animation.

However, in writing this [pst, I couldn’t get it to do that again. I suspect it was a non-rectangular clip all along and Android 4.3 fixed those. I might retest on an older version to make sure I wasn’t imagining things.


← (Teaser) Lightly poking Dalvik with a stick On the importance of case-insensitivity_→