Dissecting Google’s Mobile Animation Engine — Part 2

Continued from part 1

Now that we have our main view or “stage” created we can begin to add our animated child views. Let’s continue by examining the file format again.


{
"version": 1,
"comment": "’20150313T13:08:29-OOB attract loop V2' exported by AE Butterfly Format Exporter v0.37",
"type": "stage",
"aspectRatio": 0.5625,
"size": {
“width": 720,
“height": 1280
},
"animations": [
{
"type": "animationGroup",
"id": "BG-gooBlue-3",
"duration": 0,
"initialValues": {
"size": {
"width": 2,
"height": 1
},
"anchorPoint": {
"x": 0.5,
"y": 0.5
},
"scale": {
"sx": 1,
"sy": 1
},
"rotation": 0,
"opacity": 1,
"position": {
"x": 0.5,
"y": 0.5
},
"backgroundColor": {
"r": 0.258823543787,
"g": 0.52156865596771,
"b": 0.95686274766922,
"a": 1
}
},
"shape": {
"name": "rectangle"
},
"animations": [

]
},
...
}

After our stage section properties there is a single “animations” property which holds a list of “animationGroup” objects. Creating a model class to hold this data is straight forward.


public class BtfyAnimationGroup {
public final BtfyAnimationGroup.Type type;
public final String id;
public final String parentId;
public final Float duration;
public final BtfyInitialValues initialValues;
public final String text;
public final String backgroundImage;
public final BtfyShape shape;
public final ArrayList<BtfyAnimationElement> animationList;
}

We can infer a lot by looking at this animation group.

The color is blue


"backgroundColor": {
"r": 0.258823543787,
"g": 0.52156865596771,
"b": 0.95686274766922,
"a": 1
}

It is a rectangle shape

"shape": {
"name": "rectangle"
},

The initial values for the child view.


"size": {
"width": 2,
"height": 1
},
"anchorPoint": {
"x": 0.5,
"y": 0.5
},
"scale": {
"sx": 1,
"sy": 1
},
"rotation": 0,
"opacity": 1,
"position": {
"x": 0.5,
"y": 0.5
}

NOTE: since most of the values are less than 1 let’s assume they are percentages of the overall width and height of the stage. So for size:

 "size" : { "width": 2, "height": 1 } 

it will mean 2 * width of the BtfyAnimationView and 1 * the height of the BtfyAnimationView.


To begin with we will need to convert the percentage based measurements into actual screen coordinates, so let’s create a helper class to do that. The helper class multiplies the percentage values against the width/height of the BtfyAnimationView

public class BtfyAnimationViewTranslator {
private final View view;
private final BtfyAnimationGroup animationGroup;
private float width;
private float height;
private float x;
private float y;
private float pivotX;
private float pivotY;
private float scaleX;
private float scaleY;
private float rotation;
private BtfyAnimationView btfyAnimationView;

public BtfyAnimationViewTranslator(BtfyAnimationView butterflyView, View view, BtfyAnimationGroup animationGroup) {
this.btfyAnimationView = butterflyView;
this.view = view;
this.animationGroup = animationGroup;
init();
}

public final void init() {
BtfyInitialValues initialValues = this.animationGroup.initialValues;
this.view.setBackgroundColor(initialValues.backgroundColor.toARGBInt());
setWidth(initialValues.size.width);
setHeight(initialValues.size.height);
setTranslationX(initialValues.position.x);
setTranslationY(initialValues.position.y);
setPivotX(initialValues.anchorPoint.x);
setPivotY(initialValues.anchorPoint.y);
setScaleX(initialValues.scale.sx);
setScaleY(initialValues.scale.sy);
setRotation(initialValues.rotation);
setAlpha(initialValues.opacity);
}

public float getWidth() {
return width;
}

private void setWidth(float width) {
this.width = width;
}

public float getHeight() {
return height;
}

private void setHeight(float height) {
this.height = height;
}

BtfyAnimationGroup getAnimationGroup() {
return animationGroup;
}

private float getRelativeToWidth(float x) {
return x * ((float) this.btfyAnimationView.width);
}

private float getRelativeToHeight(float y) {
return y * ((float) this.btfyAnimationView.height);
}

private float getRelativeWidth() {
return getRelativeToWidth(this.width);
}

private float getRelativeHeight() {
return getRelativeToHeight(this.height);
}

// region X
public final void setTranslationX(float newx) {
this.x = newx;
updateViewTranslationX();
}

private final void updateViewTranslationX() {
this.view.setTranslationX(getRelativeToWidth(this.x));
}
//endregion


// region Y

public final void setTranslationY(float newy) {
this.y = newy;
updateViewTranslationY();
}

private final void updateViewTranslationY() {
this.view.setTranslationY(getRelativeToHeight(this.y));
}


//endregion

//region pivot
private final void setPivotX(float newPivotX) {
this.pivotX = newPivotX;
this.view.setPivotX(getRelativeWidth() * newPivotX);
}

public final float getPivotX() {
return this.pivotX;
}

private final void setPivotY(float newPivotY) {
this.pivotY = newPivotY;
this.view.setPivotY(getRelativeHeight() * newPivotY);
}

public final float getPivotY() {
return this.pivotY;
}

//endregion

//region scale

public final void setScaleX(float newScaleX) {
this.scaleX = newScaleX;
updateViewScaleX();
}

private void updateViewScaleX() {
this.view.setScaleX(this.scaleX);
}

public final void setScaleY(float newScaleY) {
this.scaleY = newScaleY;
updateViewScaleY();
}

private void updateViewScaleY() {
this.view.setScaleY(this.scaleY);
}
//endregion

//region rotation

public final void setRotation(float newRotation) {
this.rotation = newRotation;
updateViewRotation();
}

private void updateViewRotation() {
this.view.setRotation(this.rotation);
}

//endregion

public final void setAlpha(float newAlpha) {
this.view.setAlpha(newAlpha);
}

public View getView() {
return this.view;
}
}

Now let’s construct a view to represent this animationGroup. It will be a rectangle shape so we can just use a simple View as our base.


public final class BtfyRectangleView extends View {
public BtfyRectangleView(Context context) {
super(context);
}
}

We can add it to our stage like so

private void initStage(BtfyStage stage, BtfyImageProvider btfyImageProvider) {
removeAllViews();
this.btfyStage = stage;
for (BtfyAnimationGroup animationGroup : stage.animationGroups) {
View view = null;
if (BtfyShape.Name.RECTANGLE.equals(animationGroup.shape.name)) {
view = new BtfyRectangleView(getContext());
}
if (view == null) continue;
BtfyAnimationViewTranslator btfyViewTranslator = new BtfyAnimationViewTranslator(this, view, animationGroup);
addView(view);
}
}

If we run the app now we will get a blank screen. That is because we have not implemented onLayout yet, so let’s do that now.

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);

int measuredWidth = childAt.getMeasuredWidth();
int measuredHeight = childAt.getMeasuredHeight();
childAt.layout(0, 0, measuredWidth, measuredHeight);
}
}

Still a blank screen, we need to revisit our onMeasure method and measure all of our children.

    ...
setMeasuredDimension(measuredWidth, resolveSizeWidth);
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);

childAt.measure(MeasureSpec.makeMeasureSpec(Math.round(childAt.getWidth() * ((float) measuredWidth)),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(Math.round(childAt.getHeight() * ((float) resolveSizeWidth)),
MeasureSpec.EXACTLY));
}
}

Still blank!? childAt.getWidth() and childAt.getHeight() are 0. We need to use the values from our helper class BtfyAnimationViewTranslator to get the expected width and height. In order to access the BtfyAnimationViewTranslator from onMeasure we’ll need to stash it some where. The view’s tag is ideal for this.

private void initStage(BtfyStage stage, BtfyImageProvider btfyImageProvider) {
removeAllViews();
this.btfyStage = stage;
for (BtfyAnimationGroup animationGroup : stage.animationGroups) {
View view = null;
if (BtfyShape.Name.RECTANGLE.equals(animationGroup.shape.name)) {
view = new BtfyRectangleView(getContext());
}
if (view == null) continue;
BtfyAnimationViewTranslator btfyViewTranslator = new BtfyAnimationViewTranslator(this, view, animationGroup);
view.setTag(R.id.VIEW_TAG_KEY, btfyViewTranslator);
addView(view);
}
}

Now we can retrieve the BtfyAnimationViewTranslator in onMeasure

setMeasuredDimension(measuredWidth, resolveSizeWidth);
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
BtfyAnimationViewTranslator btfyViewTranslator = (BtfyAnimationViewTranslator) childAt.getTag(R.id.VIEW_TAG_KEY);
childAt.measure(MeasureSpec.makeMeasureSpec(Math.round(btfyViewTranslator.getWidth() * ((float) measuredWidth)),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(Math.round(btfyViewTranslator.getHeight() * ((float) resolveSizeWidth)),
MeasureSpec.EXACTLY));
}

And if we run it you should see a blue screen.

A blue screen means we’ve successfully created our BtfyRectangleView

From here we can easily create additional views to support Text, and Images, and other shapes like Ellipses

private void initStage(BtfyStage stage, BtfyImageProvider btfyImageProvider) {
removeAllViews();
this.btfyStage = stage;
for (BtfyAnimationGroup animationGroup : stage.animationGroups) {
View view = null;
if (animationGroup.text != null) {
view = new TextView(getContext());
((TextView) view).setText(animationGroup.text);
} else {
if (animationGroup.backgroundImage != null) {
view = new ImageView(getContext());
btfyImageProvider.loadInto(animationGroup.backgroundImage, (ImageView) view);
((ImageView) view).setScaleType(ImageView.ScaleType.FIT_CENTER);
} else if (BtfyShape.Name.ELLIPSE.equals(animationGroup.shape.name)) {
view = new BtfyEllipseView(getContext());
} else {
view = new BtfyRectangleView(getContext());
}
}
if (view == null) continue;
BtfyAnimationViewTranslator btfyViewTranslator = new BtfyAnimationViewTranslator(this, view, animationGroup);
view.setTag(R.id.VIEW_TAG_KEY, btfyViewTranslator);
addView(view);
}
}