Injury Injury - 3 months ago 21
Android Question

How does Matrix.postScale( sx, sy, px, py) work?

First read Taig's question

Taig said:


When calling Matrix.postScale( sx, sy, px, py ); the matrix gets
scaled and also translated (depending on the given point x, y). That
predestines this method to be used for zooming into images because I
can easily focus one specific point.
The android doc describes the method like this:

Postconcats the matrix with the specified scale. M' = S(sx, sy, px, py) * M


At a first glance this seems ridiculous because M is supposed to be a
3x3-Matrix. Digging around I've found out that android uses a
4x4-Matrix for its computations (while only providing 3x3 on its API).
Since this code is written in C I'm having a hard time trying to
understand what is actually happening.


I saw the visual transform at Wolfram

My question is same as Taig


What I actually want to know: How can I apply this kind of scaling
(with a focused point) to the 3x3 Matrix that I can access within my
Java-code?


Who can give me a example and 2d-scaling formula with 4 parameters (sx, sy, px, py) that a 10-year-old-kid would understand?

Answer

Look more closely at the Matrix methods. You will see getValue() and setValue(). The docs say they work with a float array with 9 values. There are also a bunch of constants: MSCALE_X, MSCALE_Y, MTRANS_X, MTRANS_Y etc. etc. Those constants are indices into the float[9] array.

Since we are only working in 2 dimensions, the matrix would actually be a 2x2 matrix. But because this matrix supports affine transforms, the matrix is extended to become a 3x3 matrix. 3x3 = 9, which corresponds to the float[9] array. That is, essentially, your 3x3 matrix.

The actual guts of Matrix are written in C++ and accessed through JNI because the operations have to be fast fast fast fast fast. They even use a special non-standard floating point number format ("16.16") that is optimized for calculation speed.

I don't know where you are getting the information about a 4x4 array. Here's a code snippet from the C++ JNI:

SkScalar         fMat[9];
mutable uint32_t fTypeMask;

void setScaleTranslate(SkScalar sx, SkScalar sy, SkScalar tx, SkScalar ty) {
    fMat[kMScaleX] = sx;
    fMat[kMSkewX]  = 0;
    fMat[kMTransX] = tx;

    fMat[kMSkewY]  = 0;
    fMat[kMScaleY] = sy;
    fMat[kMTransY] = ty;

    fMat[kMPersp0] = 0;
    fMat[kMPersp1] = 0;
    fMat[kMPersp2] = 1;

    unsigned mask = 0;
    if (sx != 1 || sy != 1) {
        mask |= kScale_Mask;
    }
    if (tx || ty) {
        mask |= kTranslate_Mask;
    }
    this->setTypeMask(mask | kRectStaysRect_Mask);
}

It's a 3x3 matrix for an affine transform.

When you call matrix.postScale(), you are modifying scaleX, scaleY, transX, and transY. (The pre..() and post...() methods preserve any transform that was in your matrix to start with.) The Matrix applies the new transform like this:

X' = X * scaleX + transX
Y' = Y * scaleY + transY

That's the simplified version of the entire matrix multiplication. If I have a figure with point (2,2) and I scale it 2x, the new point will be (4,4). To move along the X or Y axis, I just add a constant.

Because Matrix.postScale() actually takes a focus point, the method internally adjusts transX & transY as though you are translating in, scaling, then translating back out. This makes the scaling appear as though the expansion/shrinking is centered around a point px, py.

transX = (1 - scaleX) * px
transY = (1 - scaleY) * py

So for the focus point, I move the figure to (px,py) by adding px and py directly to the original x,y values. Then I do the scaling. But to undo the translation, I have to take into account that my original focus point is now scaled itself, so instead of subtracting px and py, I have to subtract scaleX * px and scaleY * py.

Skew or Shear is like scaling but with opposing axes:

X' = Y * skewX
Y' = X * skewY

Since you're scaling and translating without warping, skewX and skewY are set to zero. So they're still used in the matrix multiplication, they just don't affect the final outcome.

Rotation is done by adding in a little trig:

theta = angle of rotation
scaleX = cos(theta)
skewX = -sin(theta)
skewY = sin(theta)
scaleY = cos(theta)

Then there is the android.graphics.Camera (as opposed to android.hardware.Camera) that can take a 2D plane and rotate/translate it in 3D space. This is where MPERSP_0, MPERSP_1, & MPERSP_2 come into play. I'm not doing those equations; I'm a programmer, not a mathematician.

But I don't need to be a mathematician. I don't even need to know how Matrix does its calculations. I have been working on an ImageView subclass that supports pinch/zoom. So I use a ScaleGestureDetector to tell me when the user is zooming. It has methods getScaleFactor(), getFocusX() and getFocusY(). I plug those values into matrix.postScale(), and with my ImageView having a scale type set to MATRIX, I call ImageView.setImageMatrix() with my scaled matrix. VoilĂ , the image zooms exactly the way the user expects to see it based on their gestures.

So I don't understand all the angst about grokking how Matrix works under the hood. Still, I hope something I wrote here gives you the answers you are looking for.