| /* |
| * 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); |
| |
| } |
| } |
| } |
| } |