18

Since Android 3.0, we have the possibility to set basic 3D transformations to Views using setRotationX and setRotationY, for instance.

In a specific case, I would need to achieve a complex transformation that is rotating around a distant pivot point then scaling on a different pivot point. This is not possible simply by using setScaleX/Y and setRotationX/Y since they share the same pivot point.

Does any simple way exist to provide the desired matrix to the view for display?

For now, I have found two possible workarounds:

  • Set up a rotation matrix then use it to map a {0,0} point; apply the resulting point to the View's translation then use the View's setScaleX/Y to set the scaling

  • Hack the View's onDraw and apply the transformation on the canvas. Add the logic to reverse map touch events, too.

Both are not really convenient since I am building an AdapterView which displays its items using a configurable effect; for example, it can be possible to switch the effect to have the items arrranged in a circle or like a "cover flow".

DavGin
  • 8,205
  • 2
  • 19
  • 26

3 Answers3

10

I, too, am wracking my brain over this. This is the best I've come up with (super hacky):

 MyAnimation animation = new MyAnimation(matrix);
 animation.setDuration(0);
 animation.setFillAfter(true);
 view.setAnimation(animation);
xtravar
  • 1,321
  • 11
  • 24
7

The animation solution above works but it results in infinite calls to applyTransformation which leaves a little to be desired from an efficiency standpoint. In addition if you kill the animation the matrix applied isn't permanent to the view so you're forced into letting it run forever. Here's an alternative less hacky approach that results in only a single call to setMatrix each time invalidate() on the view is called:

In the parents view constructor:

setStaticTransformationsEnabled(true);

Then in the body:

    @Override
    protected boolean getChildStaticTransformation(View child, Transformation t) {

    // apply transform to child view - child triggers this call by call to `invalidate()`
    t.getMatrix().set(someMatrix);
    return true;
}

Simply call invalidate() on the child anytime you want to update it's matrix.

Travis
  • 141
  • 1
  • 5
  • This worked for me, with two caveats. Every update to the transformation Matrix requires iterating through the child views to call invalidate on each one - parent.invalidate() didn't do it. Also, I had to disable hardware acceleration on the underlying Activity - setting the parent's layer type to Software (in code/constructor, or in XML) was insufficient. It's possible that hardware acceleration could be successfully disabled somewhere else in the view hierarchy though... – MandisaW Aug 09 '16 at 14:31
  • ETA: Revising my earlier comment - it's an either-or situation. *If* hardware acceleration is disabled/off, then parent.invalidate() is fine. *If* hardware acceleration is enabled/on, then I found I needed to call child.invalidate() after updating the transformation. Seems to be related to the drawing order-of-operations, as described here: https://developer.android.com/guide/topics/graphics/hardware-accel.html#model – MandisaW Aug 09 '16 at 15:36
4

xtravar's answer is great, and MyAnimation implementation is here

    private class MyAnimation extends Animation {
    private Matrix matrix;

    public MyAnimation(Matrix matrix) {
        this.matrix = matrix;
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        super.applyTransformation(interpolatedTime, t);
        t.getMatrix().set(matrix);
    }
}
Community
  • 1
  • 1
mes
  • 3,581
  • 29
  • 28