1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.constraintlayout.core.state; 18 19 import static androidx.constraintlayout.core.state.ConstraintSetParser.parseColorString; 20 21 import androidx.annotation.RestrictTo; 22 import androidx.constraintlayout.core.motion.CustomVariable; 23 import androidx.constraintlayout.core.motion.utils.TypedBundle; 24 import androidx.constraintlayout.core.motion.utils.TypedValues; 25 import androidx.constraintlayout.core.parser.CLArray; 26 import androidx.constraintlayout.core.parser.CLContainer; 27 import androidx.constraintlayout.core.parser.CLElement; 28 import androidx.constraintlayout.core.parser.CLKey; 29 import androidx.constraintlayout.core.parser.CLNumber; 30 import androidx.constraintlayout.core.parser.CLObject; 31 import androidx.constraintlayout.core.parser.CLParsingException; 32 33 import org.jspecify.annotations.NonNull; 34 35 /** 36 * Contains code for Parsing Transitions 37 */ 38 public class TransitionParser { 39 /** 40 * Parse a JSON string of a Transition and insert it into the Transition object 41 * 42 * @deprecated dpToPixel is not necessary, use {@link #parse(CLObject, Transition)} instead. 43 * @param json Transition Object to parse. 44 * @param transition Transition Object to write transition to 45 */ 46 @Deprecated parse(CLObject json, Transition transition, CorePixelDp dpToPixel)47 public static void parse(CLObject json, Transition transition, CorePixelDp dpToPixel) 48 throws CLParsingException { 49 parse(json, transition); 50 } 51 52 /** 53 * Parse a JSON string of a Transition and insert it into the Transition object 54 * 55 * @param json Transition Object to parse. 56 * @param transition Transition Object to write transition to 57 */ 58 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) parse(@onNull CLObject json, @NonNull Transition transition)59 public static void parse(@NonNull CLObject json, @NonNull Transition transition) 60 throws CLParsingException { 61 transition.resetProperties(); 62 String pathMotionArc = json.getStringOrNull("pathMotionArc"); 63 TypedBundle bundle = new TypedBundle(); 64 boolean setBundle = false; 65 if (pathMotionArc != null) { 66 setBundle = true; 67 switch (pathMotionArc) { 68 // TODO use map 69 case "none": 70 bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 0); 71 break; 72 case "startVertical": 73 bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 1); 74 break; 75 case "startHorizontal": 76 bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 2); 77 break; 78 case "flip": 79 bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 3); 80 break; 81 case "below": 82 bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 4); 83 break; 84 case "above": 85 bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 5); 86 } 87 88 } 89 // TODO: Add duration 90 String interpolator = json.getStringOrNull("interpolator"); 91 if (interpolator != null) { 92 setBundle = true; 93 bundle.add(TypedValues.TransitionType.TYPE_INTERPOLATOR, interpolator); 94 } 95 96 float staggered = json.getFloatOrNaN("staggered"); 97 if (!Float.isNaN(staggered)) { 98 setBundle = true; 99 bundle.add(TypedValues.TransitionType.TYPE_STAGGERED, staggered); 100 } 101 if (setBundle) { 102 transition.setTransitionProperties(bundle); 103 } 104 105 CLContainer onSwipe = json.getObjectOrNull("onSwipe"); 106 107 if (onSwipe != null) { 108 parseOnSwipe(onSwipe, transition); 109 } 110 parseKeyFrames(json, transition); 111 } 112 parseOnSwipe(CLContainer onSwipe, Transition transition)113 private static void parseOnSwipe(CLContainer onSwipe, Transition transition) { 114 String anchor = onSwipe.getStringOrNull("anchor"); 115 int side = map(onSwipe.getStringOrNull("side"), Transition.OnSwipe.SIDES); 116 int direction = map(onSwipe.getStringOrNull("direction"), 117 Transition.OnSwipe.DIRECTIONS); 118 float scale = onSwipe.getFloatOrNaN("scale"); 119 float threshold = onSwipe.getFloatOrNaN("threshold"); 120 float maxVelocity = onSwipe.getFloatOrNaN("maxVelocity"); 121 float maxAccel = onSwipe.getFloatOrNaN("maxAccel"); 122 String limitBounds = onSwipe.getStringOrNull("limitBounds"); 123 int autoCompleteMode = map(onSwipe.getStringOrNull("mode"), Transition.OnSwipe.MODE); 124 int touchUp = map(onSwipe.getStringOrNull("touchUp"), Transition.OnSwipe.TOUCH_UP); 125 float springMass = onSwipe.getFloatOrNaN("springMass"); 126 float springStiffness = onSwipe.getFloatOrNaN("springStiffness"); 127 float springDamping = onSwipe.getFloatOrNaN("springDamping"); 128 float stopThreshold = onSwipe.getFloatOrNaN("stopThreshold"); 129 int springBoundary = map(onSwipe.getStringOrNull("springBoundary"), 130 Transition.OnSwipe.BOUNDARY); 131 String around = onSwipe.getStringOrNull("around"); 132 133 Transition.OnSwipe swipe = transition.createOnSwipe(); 134 swipe.setAnchorId(anchor); 135 swipe.setAnchorSide(side); 136 swipe.setDragDirection(direction); 137 swipe.setDragScale(scale); 138 swipe.setDragThreshold(threshold); 139 swipe.setMaxVelocity(maxVelocity); 140 swipe.setMaxAcceleration(maxAccel); 141 swipe.setLimitBoundsTo(limitBounds); 142 swipe.setAutoCompleteMode(autoCompleteMode); 143 swipe.setOnTouchUp(touchUp); 144 swipe.setSpringMass(springMass); 145 swipe.setSpringStiffness(springStiffness); 146 swipe.setSpringDamping(springDamping); 147 swipe.setSpringStopThreshold(stopThreshold); 148 swipe.setSpringBoundary(springBoundary); 149 swipe.setRotationCenterId(around); 150 } 151 152 map(String val, String... types)153 private static int map(String val, String... types) { 154 for (int i = 0; i < types.length; i++) { 155 if (types[i].equals(val)) { 156 return i; 157 } 158 } 159 return 0; 160 } 161 map(TypedBundle bundle, int type, String val, String... types)162 private static void map(TypedBundle bundle, int type, String val, String... types) { 163 for (int i = 0; i < types.length; i++) { 164 if (types[i].equals(val)) { 165 bundle.add(type, i); 166 } 167 } 168 } 169 170 /** 171 * Parses {@code KeyFrames} attributes from the {@link CLObject} into {@link Transition}. 172 * 173 * @param transitionCLObject the CLObject for the root transition json 174 * @param transition core object that holds the state of the Transition 175 */ parseKeyFrames(CLObject transitionCLObject, Transition transition)176 public static void parseKeyFrames(CLObject transitionCLObject, Transition transition) 177 throws CLParsingException { 178 CLContainer keyframes = transitionCLObject.getObjectOrNull("KeyFrames"); 179 if (keyframes == null) return; 180 CLArray keyPositions = keyframes.getArrayOrNull("KeyPositions"); 181 if (keyPositions != null) { 182 for (int i = 0; i < keyPositions.size(); i++) { 183 CLElement keyPosition = keyPositions.get(i); 184 if (keyPosition instanceof CLObject) { 185 parseKeyPosition((CLObject) keyPosition, transition); 186 } 187 } 188 } 189 CLArray keyAttributes = keyframes.getArrayOrNull("KeyAttributes"); 190 if (keyAttributes != null) { 191 for (int i = 0; i < keyAttributes.size(); i++) { 192 CLElement keyAttribute = keyAttributes.get(i); 193 if (keyAttribute instanceof CLObject) { 194 parseKeyAttribute((CLObject) keyAttribute, transition); 195 } 196 } 197 } 198 CLArray keyCycles = keyframes.getArrayOrNull("KeyCycles"); 199 if (keyCycles != null) { 200 for (int i = 0; i < keyCycles.size(); i++) { 201 CLElement keyCycle = keyCycles.get(i); 202 if (keyCycle instanceof CLObject) { 203 parseKeyCycle((CLObject) keyCycle, transition); 204 } 205 } 206 } 207 } 208 209 parseKeyPosition(CLObject keyPosition, Transition transition)210 private static void parseKeyPosition(CLObject keyPosition, 211 Transition transition) throws CLParsingException { 212 TypedBundle bundle = new TypedBundle(); 213 CLArray targets = keyPosition.getArray("target"); 214 CLArray frames = keyPosition.getArray("frames"); 215 CLArray percentX = keyPosition.getArrayOrNull("percentX"); 216 CLArray percentY = keyPosition.getArrayOrNull("percentY"); 217 CLArray percentWidth = keyPosition.getArrayOrNull("percentWidth"); 218 CLArray percentHeight = keyPosition.getArrayOrNull("percentHeight"); 219 String pathMotionArc = keyPosition.getStringOrNull("pathMotionArc"); 220 String transitionEasing = keyPosition.getStringOrNull("transitionEasing"); 221 String curveFit = keyPosition.getStringOrNull("curveFit"); 222 String type = keyPosition.getStringOrNull("type"); 223 if (type == null) { 224 type = "parentRelative"; 225 } 226 if (percentX != null && frames.size() != percentX.size()) { 227 return; 228 } 229 if (percentY != null && frames.size() != percentY.size()) { 230 return; 231 } 232 for (int i = 0; i < targets.size(); i++) { 233 String target = targets.getString(i); 234 int pos_type = map(type, "deltaRelative", "pathRelative", "parentRelative"); 235 bundle.clear(); 236 bundle.add(TypedValues.PositionType.TYPE_POSITION_TYPE, pos_type); 237 if (curveFit != null) { 238 map(bundle, TypedValues.PositionType.TYPE_CURVE_FIT, curveFit, 239 "spline", "linear"); 240 } 241 bundle.addIfNotNull(TypedValues.PositionType.TYPE_TRANSITION_EASING, transitionEasing); 242 243 if (pathMotionArc != null) { 244 map(bundle, TypedValues.PositionType.TYPE_PATH_MOTION_ARC, pathMotionArc, 245 "none", "startVertical", "startHorizontal", "flip", "below", "above"); 246 } 247 248 for (int j = 0; j < frames.size(); j++) { 249 int frame = frames.getInt(j); 250 bundle.add(TypedValues.TYPE_FRAME_POSITION, frame); 251 set(bundle, TypedValues.PositionType.TYPE_PERCENT_X, percentX, j); 252 set(bundle, TypedValues.PositionType.TYPE_PERCENT_Y, percentY, j); 253 set(bundle, TypedValues.PositionType.TYPE_PERCENT_WIDTH, percentWidth, j); 254 set(bundle, TypedValues.PositionType.TYPE_PERCENT_HEIGHT, percentHeight, j); 255 256 transition.addKeyPosition(target, bundle); 257 } 258 } 259 } 260 set(TypedBundle bundle, int type, CLArray array, int index)261 private static void set(TypedBundle bundle, int type, 262 CLArray array, int index) throws CLParsingException { 263 if (array != null) { 264 bundle.add(type, array.getFloat(index)); 265 } 266 } 267 parseKeyAttribute(CLObject keyAttribute, Transition transition)268 private static void parseKeyAttribute(CLObject keyAttribute, 269 Transition transition) throws CLParsingException { 270 CLArray targets = keyAttribute.getArrayOrNull("target"); 271 if (targets == null) { 272 return; 273 } 274 CLArray frames = keyAttribute.getArrayOrNull("frames"); 275 if (frames == null) { 276 return; 277 } 278 String transitionEasing = keyAttribute.getStringOrNull("transitionEasing"); 279 // These present an ordered list of attributes that might be used in a keyCycle 280 String[] attrNames = { 281 TypedValues.AttributesType.S_SCALE_X, 282 TypedValues.AttributesType.S_SCALE_Y, 283 TypedValues.AttributesType.S_TRANSLATION_X, 284 TypedValues.AttributesType.S_TRANSLATION_Y, 285 TypedValues.AttributesType.S_TRANSLATION_Z, 286 TypedValues.AttributesType.S_ROTATION_X, 287 TypedValues.AttributesType.S_ROTATION_Y, 288 TypedValues.AttributesType.S_ROTATION_Z, 289 TypedValues.AttributesType.S_ALPHA 290 }; 291 int[] attrIds = { 292 TypedValues.AttributesType.TYPE_SCALE_X, 293 TypedValues.AttributesType.TYPE_SCALE_Y, 294 TypedValues.AttributesType.TYPE_TRANSLATION_X, 295 TypedValues.AttributesType.TYPE_TRANSLATION_Y, 296 TypedValues.AttributesType.TYPE_TRANSLATION_Z, 297 TypedValues.AttributesType.TYPE_ROTATION_X, 298 TypedValues.AttributesType.TYPE_ROTATION_Y, 299 TypedValues.AttributesType.TYPE_ROTATION_Z, 300 TypedValues.AttributesType.TYPE_ALPHA 301 }; 302 // if true scale the values from pixels to dp 303 boolean[] scaleTypes = { 304 false, 305 false, 306 true, 307 true, 308 true, 309 false, 310 false, 311 false, 312 false, 313 }; 314 TypedBundle[] bundles = new TypedBundle[frames.size()]; 315 CustomVariable[][] customVars = null; 316 317 for (int i = 0; i < frames.size(); i++) { 318 bundles[i] = new TypedBundle(); 319 } 320 321 for (int k = 0; k < attrNames.length; k++) { 322 323 String attrName = attrNames[k]; 324 int attrId = attrIds[k]; 325 boolean scale = scaleTypes[k]; 326 CLArray arrayValues = keyAttribute.getArrayOrNull(attrName); 327 // array must contain one per frame 328 if (arrayValues != null && arrayValues.size() != bundles.length) { 329 throw new CLParsingException( 330 "incorrect size for " + attrName + " array, " 331 + "not matching targets array!", keyAttribute); 332 } 333 if (arrayValues != null) { 334 for (int i = 0; i < bundles.length; i++) { 335 float value = arrayValues.getFloat(i); 336 if (scale) { 337 value = transition.mToPixel.toPixels(value); 338 } 339 bundles[i].add(attrId, value); 340 } 341 } else { 342 float value = keyAttribute.getFloatOrNaN(attrName); 343 if (!Float.isNaN(value)) { 344 if (scale) { 345 value = transition.mToPixel.toPixels(value); 346 } 347 for (int i = 0; i < bundles.length; i++) { 348 bundles[i].add(attrId, value); 349 } 350 } 351 } 352 } 353 // Support for custom attributes in KeyAttributes 354 CLElement customElement = keyAttribute.getOrNull("custom"); 355 if (customElement != null && customElement instanceof CLObject) { 356 CLObject customObj = ((CLObject) customElement); 357 int n = customObj.size(); 358 customVars = new CustomVariable[frames.size()][n]; 359 for (int i = 0; i < n; i++) { 360 CLKey key = (CLKey) customObj.get(i); 361 String customName = key.content(); 362 if (key.getValue() instanceof CLArray) { 363 CLArray arrayValues = (CLArray) key.getValue(); 364 int vSize = arrayValues.size(); 365 if (vSize == bundles.length && vSize > 0) { 366 if (arrayValues.get(0) instanceof CLNumber) { 367 for (int j = 0; j < bundles.length; j++) { 368 customVars[j][i] = new CustomVariable(customName, 369 TypedValues.Custom.TYPE_FLOAT, 370 arrayValues.get(j).getFloat()); 371 } 372 } else { // since it is not a number switching to custom color parsing 373 for (int j = 0; j < bundles.length; j++) { 374 long color = parseColorString(arrayValues.get(j).content()); 375 if (color != -1) { 376 customVars[j][i] = new CustomVariable(customName, 377 TypedValues.Custom.TYPE_COLOR, 378 (int) color); 379 } 380 } 381 } 382 } 383 } else { 384 CLElement value = key.getValue(); 385 if (value instanceof CLNumber) { 386 float fValue = value.getFloat(); 387 for (int j = 0; j < bundles.length; j++) { 388 customVars[j][i] = new CustomVariable(customName, 389 TypedValues.Custom.TYPE_FLOAT, 390 fValue); 391 } 392 } else { 393 long cValue = parseColorString(value.content()); 394 if (cValue != -1) { 395 for (int j = 0; j < bundles.length; j++) { 396 customVars[j][i] = new CustomVariable(customName, 397 TypedValues.Custom.TYPE_COLOR, 398 (int) cValue); 399 400 } 401 } 402 } 403 } 404 405 } 406 } 407 String curveFit = keyAttribute.getStringOrNull("curveFit"); 408 for (int i = 0; i < targets.size(); i++) { 409 for (int j = 0; j < bundles.length; j++) { 410 String target = targets.getString(i); 411 TypedBundle bundle = bundles[j]; 412 if (curveFit != null) { 413 bundle.add(TypedValues.PositionType.TYPE_CURVE_FIT, 414 map(curveFit, "spline", "linear")); 415 } 416 bundle.addIfNotNull(TypedValues.PositionType.TYPE_TRANSITION_EASING, 417 transitionEasing); 418 int frame = frames.getInt(j); 419 bundle.add(TypedValues.TYPE_FRAME_POSITION, frame); 420 transition.addKeyAttribute(target, bundle, (customVars != null) ? customVars[j] : 421 null); 422 } 423 } 424 } 425 parseKeyCycle(CLObject keyCycleData, Transition transition)426 private static void parseKeyCycle(CLObject keyCycleData, 427 Transition transition) throws CLParsingException { 428 CLArray targets = keyCycleData.getArray("target"); 429 CLArray frames = keyCycleData.getArray("frames"); 430 String transitionEasing = keyCycleData.getStringOrNull("transitionEasing"); 431 // These present an ordered list of attributes that might be used in a keyCycle 432 String[] attrNames = { 433 TypedValues.CycleType.S_SCALE_X, 434 TypedValues.CycleType.S_SCALE_Y, 435 TypedValues.CycleType.S_TRANSLATION_X, 436 TypedValues.CycleType.S_TRANSLATION_Y, 437 TypedValues.CycleType.S_TRANSLATION_Z, 438 TypedValues.CycleType.S_ROTATION_X, 439 TypedValues.CycleType.S_ROTATION_Y, 440 TypedValues.CycleType.S_ROTATION_Z, 441 TypedValues.CycleType.S_ALPHA, 442 TypedValues.CycleType.S_WAVE_PERIOD, 443 TypedValues.CycleType.S_WAVE_OFFSET, 444 TypedValues.CycleType.S_WAVE_PHASE, 445 }; 446 int[] attrIds = { 447 TypedValues.CycleType.TYPE_SCALE_X, 448 TypedValues.CycleType.TYPE_SCALE_Y, 449 TypedValues.CycleType.TYPE_TRANSLATION_X, 450 TypedValues.CycleType.TYPE_TRANSLATION_Y, 451 TypedValues.CycleType.TYPE_TRANSLATION_Z, 452 TypedValues.CycleType.TYPE_ROTATION_X, 453 TypedValues.CycleType.TYPE_ROTATION_Y, 454 TypedValues.CycleType.TYPE_ROTATION_Z, 455 TypedValues.CycleType.TYPE_ALPHA, 456 TypedValues.CycleType.TYPE_WAVE_PERIOD, 457 TypedValues.CycleType.TYPE_WAVE_OFFSET, 458 TypedValues.CycleType.TYPE_WAVE_PHASE, 459 }; 460 // type 0 the values are used as. 461 // type 1 the value is scaled from dp to pixels. 462 // type 2 are scaled if the system has another type 1. 463 int[] scaleTypes = { 464 0, 465 0, 466 1, 467 1, 468 1, 469 0, 470 0, 471 0, 472 0, 473 0, 474 2, 475 0, 476 }; 477 478 // TODO S_WAVE_SHAPE S_CUSTOM_WAVE_SHAPE 479 TypedBundle[] bundles = new TypedBundle[frames.size()]; 480 for (int i = 0; i < bundles.length; i++) { 481 bundles[i] = new TypedBundle(); 482 } 483 boolean scaleOffset = false; 484 for (int k = 0; k < attrNames.length; k++) { 485 if (keyCycleData.has(attrNames[k]) && scaleTypes[k] == 1) { 486 scaleOffset = true; 487 } 488 } 489 for (int k = 0; k < attrNames.length; k++) { 490 String attrName = attrNames[k]; 491 int attrId = attrIds[k]; 492 int scale = scaleTypes[k]; 493 CLArray arrayValues = keyCycleData.getArrayOrNull(attrName); 494 // array must contain one per frame 495 if (arrayValues != null && arrayValues.size() != bundles.length) { 496 throw new CLParsingException( 497 "incorrect size for $attrName array, " 498 + "not matching targets array!", keyCycleData 499 ); 500 } 501 if (arrayValues != null) { 502 for (int i = 0; i < bundles.length; i++) { 503 float value = arrayValues.getFloat(i); 504 if (scale == 1) { 505 value = transition.mToPixel.toPixels(value); 506 } else if (scale == 2 && scaleOffset) { 507 value = transition.mToPixel.toPixels(value); 508 } 509 bundles[i].add(attrId, value); 510 } 511 } else { 512 float value = keyCycleData.getFloatOrNaN(attrName); 513 if (!Float.isNaN(value)) { 514 if (scale == 1) { 515 value = transition.mToPixel.toPixels(value); 516 } else if (scale == 2 && scaleOffset) { 517 value = transition.mToPixel.toPixels(value); 518 } 519 for (int i = 0; i < bundles.length; i++) { 520 bundles[i].add(attrId, value); 521 } 522 } 523 } 524 } 525 String curveFit = keyCycleData.getStringOrNull(TypedValues.CycleType.S_CURVE_FIT); 526 String easing = keyCycleData.getStringOrNull(TypedValues.CycleType.S_EASING); 527 String waveShape = keyCycleData.getStringOrNull(TypedValues.CycleType.S_WAVE_SHAPE); 528 String customWave = keyCycleData.getStringOrNull(TypedValues.CycleType.S_CUSTOM_WAVE_SHAPE); 529 for (int i = 0; i < targets.size(); i++) { 530 for (int j = 0; j < bundles.length; j++) { 531 String target = targets.getString(i); 532 TypedBundle bundle = bundles[j]; 533 534 535 if (curveFit != null) { 536 switch (curveFit) { 537 case "spline": 538 bundle.add(TypedValues.CycleType.TYPE_CURVE_FIT, 0); 539 break; 540 case "linear": 541 bundle.add(TypedValues.CycleType.TYPE_CURVE_FIT, 1); 542 break; 543 } 544 } 545 bundle.addIfNotNull(TypedValues.PositionType.TYPE_TRANSITION_EASING, 546 transitionEasing); 547 if (easing != null) { 548 bundle.add(TypedValues.CycleType.TYPE_EASING, easing); 549 } 550 if (waveShape != null) { 551 bundle.add(TypedValues.CycleType.TYPE_WAVE_SHAPE, waveShape); 552 } 553 if (customWave != null) { 554 bundle.add(TypedValues.CycleType.TYPE_CUSTOM_WAVE_SHAPE, customWave); 555 } 556 557 int frame = frames.getInt(j); 558 bundle.add(TypedValues.TYPE_FRAME_POSITION, frame); 559 transition.addKeyCycle(target, bundle); 560 561 } 562 } 563 } 564 } 565