blob: 8527241ce0e2dc42fd5eee719c5d0b43e70ca3f4 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.constraintlayout.core.state;
import static androidx.constraintlayout.core.state.ConstraintSetParser.parseColorString;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.constraintlayout.core.motion.CustomVariable;
import androidx.constraintlayout.core.motion.utils.TypedBundle;
import androidx.constraintlayout.core.motion.utils.TypedValues;
import androidx.constraintlayout.core.parser.CLArray;
import androidx.constraintlayout.core.parser.CLContainer;
import androidx.constraintlayout.core.parser.CLElement;
import androidx.constraintlayout.core.parser.CLKey;
import androidx.constraintlayout.core.parser.CLNumber;
import androidx.constraintlayout.core.parser.CLObject;
import androidx.constraintlayout.core.parser.CLParsingException;
/**
* Contains code for Parsing Transitions
*/
public class TransitionParser {
/**
* Parse a JSON string of a Transition and insert it into the Transition object
*
* @deprecated dpToPixel is unused now
* @param json Transition Object to parse.
* @param transition Transition Object to write transition to
*/
@Deprecated
public static void parse(CLObject json, Transition transition, CorePixelDp dpToPixel)
throws CLParsingException {
parse(json, transition);
}
/**
* Parse a JSON string of a Transition and insert it into the Transition object
*
* @param json Transition Object to parse.
* @param transition Transition Object to write transition to
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static void parse(@NonNull CLObject json, @NonNull Transition transition)
throws CLParsingException {
String pathMotionArc = json.getStringOrNull("pathMotionArc");
TypedBundle bundle = new TypedBundle();
boolean setBundle = false;
if (pathMotionArc != null) {
setBundle = true;
switch (pathMotionArc) {
// TODO use map
case "none":
bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 0);
break;
case "startVertical":
bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 1);
break;
case "startHorizontal":
bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 2);
break;
case "flip":
bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 3);
break;
case "below":
bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 4);
break;
case "above":
bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 5);
}
}
// TODO: Add duration
String interpolator = json.getStringOrNull("interpolator");
if (interpolator != null) {
setBundle = true;
bundle.add(TypedValues.TransitionType.TYPE_INTERPOLATOR, interpolator);
}
float staggered = json.getFloatOrNaN("staggered");
if (!Float.isNaN(staggered)) {
setBundle = true;
bundle.add(TypedValues.TransitionType.TYPE_STAGGERED, staggered);
}
if (setBundle) {
transition.setTransitionProperties(bundle);
}
CLContainer onSwipe = json.getObjectOrNull("onSwipe");
if (onSwipe != null) {
parseOnSwipe(onSwipe, transition);
}
parseKeyFrames(json, transition);
}
private static void parseOnSwipe(CLContainer onSwipe, Transition transition) {
String anchor = onSwipe.getStringOrNull("anchor");
int side = map(onSwipe.getStringOrNull("side"), Transition.OnSwipe.SIDES);
int direction = map(onSwipe.getStringOrNull("direction"),
Transition.OnSwipe.DIRECTIONS);
float scale = onSwipe.getFloatOrNaN("scale");
float threshold = onSwipe.getFloatOrNaN("threshold");
float maxVelocity = onSwipe.getFloatOrNaN("maxVelocity");
float maxAccel = onSwipe.getFloatOrNaN("maxAccel");
String limitBounds = onSwipe.getStringOrNull("limitBounds");
int autoCompleteMode = map(onSwipe.getStringOrNull("mode"), Transition.OnSwipe.MODE);
int touchUp = map(onSwipe.getStringOrNull("touchUp"), Transition.OnSwipe.TOUCH_UP);
float springMass = onSwipe.getFloatOrNaN("springMass");
float springStiffness = onSwipe.getFloatOrNaN("springStiffness");
float springDamping = onSwipe.getFloatOrNaN("springDamping");
float stopThreshold = onSwipe.getFloatOrNaN("stopThreshold");
int springBoundary = map(onSwipe.getStringOrNull("springBoundary"),
Transition.OnSwipe.BOUNDARY);
String around = onSwipe.getStringOrNull("around");
Transition.OnSwipe swipe = transition.createOnSwipe();
swipe.setAnchorId(anchor);
swipe.setAnchorSide(side);
swipe.setDragDirection(direction);
swipe.setDragScale(scale);
swipe.setDragThreshold(threshold);
swipe.setMaxVelocity(maxVelocity);
swipe.setMaxAcceleration(maxAccel);
swipe.setLimitBoundsTo(limitBounds);
swipe.setAutoCompleteMode(autoCompleteMode);
swipe.setOnTouchUp(touchUp);
swipe.setSpringMass(springMass);
swipe.setSpringStiffness(springStiffness);
swipe.setSpringDamping(springDamping);
swipe.setSpringStopThreshold(stopThreshold);
swipe.setSpringBoundary(springBoundary);
swipe.setRotationCenterId(around);
}
private static int map(String val, String... types) {
for (int i = 0; i < types.length; i++) {
if (types[i].equals(val)) {
return i;
}
}
return 0;
}
private static void map(TypedBundle bundle, int type, String val, String... types) {
for (int i = 0; i < types.length; i++) {
if (types[i].equals(val)) {
bundle.add(type, i);
}
}
}
/**
* Parses {@code KeyFrames} attributes from the {@link CLObject} into {@link Transition}.
*
* @param transitionCLObject the CLObject for the root transition json
* @param transition core object that holds the state of the Transition
*/
public static void parseKeyFrames(CLObject transitionCLObject, Transition transition)
throws CLParsingException {
CLContainer keyframes = transitionCLObject.getObjectOrNull("KeyFrames");
if (keyframes == null) return;
CLArray keyPositions = keyframes.getArrayOrNull("KeyPositions");
if (keyPositions != null) {
for (int i = 0; i < keyPositions.size(); i++) {
CLElement keyPosition = keyPositions.get(i);
if (keyPosition instanceof CLObject) {
parseKeyPosition((CLObject) keyPosition, transition);
}
}
}
CLArray keyAttributes = keyframes.getArrayOrNull("KeyAttributes");
if (keyAttributes != null) {
for (int i = 0; i < keyAttributes.size(); i++) {
CLElement keyAttribute = keyAttributes.get(i);
if (keyAttribute instanceof CLObject) {
parseKeyAttribute((CLObject) keyAttribute, transition);
}
}
}
CLArray keyCycles = keyframes.getArrayOrNull("KeyCycles");
if (keyCycles != null) {
for (int i = 0; i < keyCycles.size(); i++) {
CLElement keyCycle = keyCycles.get(i);
if (keyCycle instanceof CLObject) {
parseKeyCycle((CLObject) keyCycle, transition);
}
}
}
}
private static void parseKeyPosition(CLObject keyPosition,
Transition transition) throws CLParsingException {
TypedBundle bundle = new TypedBundle();
CLArray targets = keyPosition.getArray("target");
CLArray frames = keyPosition.getArray("frames");
CLArray percentX = keyPosition.getArrayOrNull("percentX");
CLArray percentY = keyPosition.getArrayOrNull("percentY");
CLArray percentWidth = keyPosition.getArrayOrNull("percentWidth");
CLArray percentHeight = keyPosition.getArrayOrNull("percentHeight");
String pathMotionArc = keyPosition.getStringOrNull("pathMotionArc");
String transitionEasing = keyPosition.getStringOrNull("transitionEasing");
String curveFit = keyPosition.getStringOrNull("curveFit");
String type = keyPosition.getStringOrNull("type");
if (type == null) {
type = "parentRelative";
}
if (percentX != null && frames.size() != percentX.size()) {
return;
}
if (percentY != null && frames.size() != percentY.size()) {
return;
}
for (int i = 0; i < targets.size(); i++) {
String target = targets.getString(i);
int pos_type = map(type, "deltaRelative", "pathRelative", "parentRelative");
bundle.clear();
bundle.add(TypedValues.PositionType.TYPE_POSITION_TYPE, pos_type);
if (curveFit != null) {
map(bundle, TypedValues.PositionType.TYPE_CURVE_FIT, curveFit,
"spline", "linear");
}
bundle.addIfNotNull(TypedValues.PositionType.TYPE_TRANSITION_EASING, transitionEasing);
if (pathMotionArc != null) {
map(bundle, TypedValues.PositionType.TYPE_PATH_MOTION_ARC, pathMotionArc,
"none", "startVertical", "startHorizontal", "flip", "below", "above");
}
for (int j = 0; j < frames.size(); j++) {
int frame = frames.getInt(j);
bundle.add(TypedValues.TYPE_FRAME_POSITION, frame);
set(bundle, TypedValues.PositionType.TYPE_PERCENT_X, percentX, j);
set(bundle, TypedValues.PositionType.TYPE_PERCENT_Y, percentY, j);
set(bundle, TypedValues.PositionType.TYPE_PERCENT_WIDTH, percentWidth, j);
set(bundle, TypedValues.PositionType.TYPE_PERCENT_HEIGHT, percentHeight, j);
transition.addKeyPosition(target, bundle);
}
}
}
private static void set(TypedBundle bundle, int type,
CLArray array, int index) throws CLParsingException {
if (array != null) {
bundle.add(type, array.getFloat(index));
}
}
private static void parseKeyAttribute(CLObject keyAttribute,
Transition transition) throws CLParsingException {
CLArray targets = keyAttribute.getArrayOrNull("target");
if (targets == null) {
return;
}
CLArray frames = keyAttribute.getArrayOrNull("frames");
if (frames == null) {
return;
}
String transitionEasing = keyAttribute.getStringOrNull("transitionEasing");
// These present an ordered list of attributes that might be used in a keyCycle
String[] attrNames = {
TypedValues.AttributesType.S_SCALE_X,
TypedValues.AttributesType.S_SCALE_Y,
TypedValues.AttributesType.S_TRANSLATION_X,
TypedValues.AttributesType.S_TRANSLATION_Y,
TypedValues.AttributesType.S_TRANSLATION_Z,
TypedValues.AttributesType.S_ROTATION_X,
TypedValues.AttributesType.S_ROTATION_Y,
TypedValues.AttributesType.S_ROTATION_Z,
TypedValues.AttributesType.S_ALPHA
};
int[] attrIds = {
TypedValues.AttributesType.TYPE_SCALE_X,
TypedValues.AttributesType.TYPE_SCALE_Y,
TypedValues.AttributesType.TYPE_TRANSLATION_X,
TypedValues.AttributesType.TYPE_TRANSLATION_Y,
TypedValues.AttributesType.TYPE_TRANSLATION_Z,
TypedValues.AttributesType.TYPE_ROTATION_X,
TypedValues.AttributesType.TYPE_ROTATION_Y,
TypedValues.AttributesType.TYPE_ROTATION_Z,
TypedValues.AttributesType.TYPE_ALPHA
};
// if true scale the values from pixels to dp
boolean[] scaleTypes = {
false,
false,
true,
true,
true,
false,
false,
false,
false,
};
TypedBundle[] bundles = new TypedBundle[frames.size()];
CustomVariable[][] customVars = null;
for (int i = 0; i < frames.size(); i++) {
bundles[i] = new TypedBundle();
}
for (int k = 0; k < attrNames.length; k++) {
String attrName = attrNames[k];
int attrId = attrIds[k];
boolean scale = scaleTypes[k];
CLArray arrayValues = keyAttribute.getArrayOrNull(attrName);
// array must contain one per frame
if (arrayValues != null && arrayValues.size() != bundles.length) {
throw new CLParsingException(
"incorrect size for " + attrName + " array, "
+ "not matching targets array!", keyAttribute);
}
if (arrayValues != null) {
for (int i = 0; i < bundles.length; i++) {
float value = arrayValues.getFloat(i);
if (scale) {
value = transition.mToPixel.toPixels(value);
}
bundles[i].add(attrId, value);
}
} else {
float value = keyAttribute.getFloatOrNaN(attrName);
if (!Float.isNaN(value)) {
if (scale) {
value = transition.mToPixel.toPixels(value);
}
for (int i = 0; i < bundles.length; i++) {
bundles[i].add(attrId, value);
}
}
}
}
// Support for custom attributes in KeyAttributes
CLElement customElement = keyAttribute.getOrNull("custom");
if (customElement != null && customElement instanceof CLObject) {
CLObject customObj = ((CLObject) customElement);
int n = customObj.size();
customVars = new CustomVariable[frames.size()][n];
for (int i = 0; i < n; i++) {
CLKey key = (CLKey) customObj.get(i);
String customName = key.content();
if (key.getValue() instanceof CLArray) {
CLArray arrayValues = (CLArray) key.getValue();
int vSize = arrayValues.size();
if (vSize == bundles.length && vSize > 0) {
if (arrayValues.get(0) instanceof CLNumber) {
for (int j = 0; j < bundles.length; j++) {
customVars[j][i] = new CustomVariable(customName,
TypedValues.Custom.TYPE_FLOAT,
arrayValues.get(j).getFloat());
}
} else { // since it is not a number switching to custom color parsing
for (int j = 0; j < bundles.length; j++) {
long color = parseColorString(arrayValues.get(j).content());
if (color != -1) {
customVars[j][i] = new CustomVariable(customName,
TypedValues.Custom.TYPE_COLOR,
(int) color);
}
}
}
}
} else {
CLElement value = key.getValue();
if (value instanceof CLNumber) {
float fValue = value.getFloat();
for (int j = 0; j < bundles.length; j++) {
customVars[j][i] = new CustomVariable(customName,
TypedValues.Custom.TYPE_FLOAT,
fValue);
}
} else {
long cValue = parseColorString(value.content());
if (cValue != -1) {
for (int j = 0; j < bundles.length; j++) {
customVars[j][i] = new CustomVariable(customName,
TypedValues.Custom.TYPE_COLOR,
(int) cValue);
}
}
}
}
}
}
String curveFit = keyAttribute.getStringOrNull("curveFit");
for (int i = 0; i < targets.size(); i++) {
for (int j = 0; j < bundles.length; j++) {
String target = targets.getString(i);
TypedBundle bundle = bundles[j];
if (curveFit != null) {
bundle.add(TypedValues.PositionType.TYPE_CURVE_FIT,
map(curveFit, "spline", "linear"));
}
bundle.addIfNotNull(TypedValues.PositionType.TYPE_TRANSITION_EASING,
transitionEasing);
int frame = frames.getInt(j);
bundle.add(TypedValues.TYPE_FRAME_POSITION, frame);
transition.addKeyAttribute(target, bundle, (customVars != null) ? customVars[j] :
null);
}
}
}
private static void parseKeyCycle(CLObject keyCycleData,
Transition transition) throws CLParsingException {
CLArray targets = keyCycleData.getArray("target");
CLArray frames = keyCycleData.getArray("frames");
String transitionEasing = keyCycleData.getStringOrNull("transitionEasing");
// These present an ordered list of attributes that might be used in a keyCycle
String[] attrNames = {
TypedValues.CycleType.S_SCALE_X,
TypedValues.CycleType.S_SCALE_Y,
TypedValues.CycleType.S_TRANSLATION_X,
TypedValues.CycleType.S_TRANSLATION_Y,
TypedValues.CycleType.S_TRANSLATION_Z,
TypedValues.CycleType.S_ROTATION_X,
TypedValues.CycleType.S_ROTATION_Y,
TypedValues.CycleType.S_ROTATION_Z,
TypedValues.CycleType.S_ALPHA,
TypedValues.CycleType.S_WAVE_PERIOD,
TypedValues.CycleType.S_WAVE_OFFSET,
TypedValues.CycleType.S_WAVE_PHASE,
};
int[] attrIds = {
TypedValues.CycleType.TYPE_SCALE_X,
TypedValues.CycleType.TYPE_SCALE_Y,
TypedValues.CycleType.TYPE_TRANSLATION_X,
TypedValues.CycleType.TYPE_TRANSLATION_Y,
TypedValues.CycleType.TYPE_TRANSLATION_Z,
TypedValues.CycleType.TYPE_ROTATION_X,
TypedValues.CycleType.TYPE_ROTATION_Y,
TypedValues.CycleType.TYPE_ROTATION_Z,
TypedValues.CycleType.TYPE_ALPHA,
TypedValues.CycleType.TYPE_WAVE_PERIOD,
TypedValues.CycleType.TYPE_WAVE_OFFSET,
TypedValues.CycleType.TYPE_WAVE_PHASE,
};
// type 0 the values are used as.
// type 1 the value is scaled from dp to pixels.
// type 2 are scaled if the system has another type 1.
int[] scaleTypes = {
0,
0,
1,
1,
1,
0,
0,
0,
0,
0,
2,
0,
};
// TODO S_WAVE_SHAPE S_CUSTOM_WAVE_SHAPE
TypedBundle[] bundles = new TypedBundle[frames.size()];
for (int i = 0; i < bundles.length; i++) {
bundles[i] = new TypedBundle();
}
boolean scaleOffset = false;
for (int k = 0; k < attrNames.length; k++) {
if (keyCycleData.has(attrNames[k]) && scaleTypes[k] == 1) {
scaleOffset = true;
}
}
for (int k = 0; k < attrNames.length; k++) {
String attrName = attrNames[k];
int attrId = attrIds[k];
int scale = scaleTypes[k];
CLArray arrayValues = keyCycleData.getArrayOrNull(attrName);
// array must contain one per frame
if (arrayValues != null && arrayValues.size() != bundles.length) {
throw new CLParsingException(
"incorrect size for $attrName array, "
+ "not matching targets array!", keyCycleData
);
}
if (arrayValues != null) {
for (int i = 0; i < bundles.length; i++) {
float value = arrayValues.getFloat(i);
if (scale == 1) {
value = transition.mToPixel.toPixels(value);
} else if (scale == 2 && scaleOffset) {
value = transition.mToPixel.toPixels(value);
}
bundles[i].add(attrId, value);
}
} else {
float value = keyCycleData.getFloatOrNaN(attrName);
if (!Float.isNaN(value)) {
if (scale == 1) {
value = transition.mToPixel.toPixels(value);
} else if (scale == 2 && scaleOffset) {
value = transition.mToPixel.toPixels(value);
}
for (int i = 0; i < bundles.length; i++) {
bundles[i].add(attrId, value);
}
}
}
}
String curveFit = keyCycleData.getStringOrNull(TypedValues.CycleType.S_CURVE_FIT);
String easing = keyCycleData.getStringOrNull(TypedValues.CycleType.S_EASING);
String waveShape = keyCycleData.getStringOrNull(TypedValues.CycleType.S_WAVE_SHAPE);
String customWave = keyCycleData.getStringOrNull(TypedValues.CycleType.S_CUSTOM_WAVE_SHAPE);
for (int i = 0; i < targets.size(); i++) {
for (int j = 0; j < bundles.length; j++) {
String target = targets.getString(i);
TypedBundle bundle = bundles[j];
if (curveFit != null) {
switch (curveFit) {
case "spline":
bundle.add(TypedValues.CycleType.TYPE_CURVE_FIT, 0);
break;
case "linear":
bundle.add(TypedValues.CycleType.TYPE_CURVE_FIT, 1);
break;
}
}
bundle.addIfNotNull(TypedValues.PositionType.TYPE_TRANSITION_EASING,
transitionEasing);
if (easing != null) {
bundle.add(TypedValues.CycleType.TYPE_EASING, easing);
}
if (waveShape != null) {
bundle.add(TypedValues.CycleType.TYPE_WAVE_SHAPE, waveShape);
}
if (customWave != null) {
bundle.add(TypedValues.CycleType.TYPE_CUSTOM_WAVE_SHAPE, customWave);
}
int frame = frames.getInt(j);
bundle.add(TypedValues.TYPE_FRAME_POSITION, frame);
transition.addKeyCycle(target, bundle);
}
}
}
}