1 /* 2 * Copyright (C) 2021 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 androidx.constraintlayout.core.motion.CustomAttribute; 20 import androidx.constraintlayout.core.motion.CustomVariable; 21 import androidx.constraintlayout.core.motion.utils.TypedBundle; 22 import androidx.constraintlayout.core.motion.utils.TypedValues; 23 import androidx.constraintlayout.core.parser.CLElement; 24 import androidx.constraintlayout.core.parser.CLKey; 25 import androidx.constraintlayout.core.parser.CLNumber; 26 import androidx.constraintlayout.core.parser.CLObject; 27 import androidx.constraintlayout.core.parser.CLParsingException; 28 import androidx.constraintlayout.core.widgets.ConstraintAnchor; 29 import androidx.constraintlayout.core.widgets.ConstraintWidget; 30 31 import org.jspecify.annotations.NonNull; 32 33 import java.util.HashMap; 34 import java.util.Set; 35 36 /** 37 * Utility class to encapsulate layout of a widget 38 */ 39 public class WidgetFrame { 40 public ConstraintWidget widget = null; 41 public int left = 0; 42 public int top = 0; 43 public int right = 0; 44 public int bottom = 0; 45 46 // transforms 47 48 public float pivotX = Float.NaN; 49 public float pivotY = Float.NaN; 50 51 public float rotationX = Float.NaN; 52 public float rotationY = Float.NaN; 53 public float rotationZ = Float.NaN; 54 55 public float translationX = Float.NaN; 56 public float translationY = Float.NaN; 57 public float translationZ = Float.NaN; 58 public static float phone_orientation = Float.NaN; 59 60 public float scaleX = Float.NaN; 61 public float scaleY = Float.NaN; 62 63 public float alpha = Float.NaN; 64 public float interpolatedPos = Float.NaN; 65 66 public int visibility = ConstraintWidget.VISIBLE; 67 68 private final HashMap<String, CustomVariable> mCustom = new HashMap<>(); 69 70 public String name = null; 71 72 TypedBundle mMotionProperties; 73 74 // @TODO: add description width()75 public int width() { 76 return Math.max(0, right - left); 77 } 78 79 // @TODO: add description height()80 public int height() { 81 return Math.max(0, bottom - top); 82 } 83 WidgetFrame()84 public WidgetFrame() { 85 } 86 WidgetFrame(ConstraintWidget widget)87 public WidgetFrame(ConstraintWidget widget) { 88 this.widget = widget; 89 } 90 WidgetFrame(WidgetFrame frame)91 public WidgetFrame(WidgetFrame frame) { 92 widget = frame.widget; 93 left = frame.left; 94 top = frame.top; 95 right = frame.right; 96 bottom = frame.bottom; 97 updateAttributes(frame); 98 } 99 100 // @TODO: add description updateAttributes(WidgetFrame frame)101 public void updateAttributes(WidgetFrame frame) { 102 if (frame == null) { 103 return; 104 } 105 pivotX = frame.pivotX; 106 pivotY = frame.pivotY; 107 rotationX = frame.rotationX; 108 rotationY = frame.rotationY; 109 rotationZ = frame.rotationZ; 110 translationX = frame.translationX; 111 translationY = frame.translationY; 112 translationZ = frame.translationZ; 113 scaleX = frame.scaleX; 114 scaleY = frame.scaleY; 115 alpha = frame.alpha; 116 visibility = frame.visibility; 117 setMotionAttributes(frame.mMotionProperties); 118 mCustom.clear(); 119 for (CustomVariable c : frame.mCustom.values()) { 120 mCustom.put(c.getName(), c.copy()); 121 } 122 } 123 isDefaultTransform()124 public boolean isDefaultTransform() { 125 return Float.isNaN(rotationX) 126 && Float.isNaN(rotationY) 127 && Float.isNaN(rotationZ) 128 && Float.isNaN(translationX) 129 && Float.isNaN(translationY) 130 && Float.isNaN(translationZ) 131 && Float.isNaN(scaleX) 132 && Float.isNaN(scaleY) 133 && Float.isNaN(alpha); 134 } 135 136 // @TODO: add description interpolate(int parentWidth, int parentHeight, WidgetFrame frame, WidgetFrame start, WidgetFrame end, Transition transition, float progress)137 public static void interpolate(int parentWidth, 138 int parentHeight, 139 WidgetFrame frame, 140 WidgetFrame start, 141 WidgetFrame end, 142 Transition transition, 143 float progress) { 144 int frameNumber = (int) (progress * 100); 145 int startX = start.left; 146 int startY = start.top; 147 int endX = end.left; 148 int endY = end.top; 149 int startWidth = start.right - start.left; 150 int startHeight = start.bottom - start.top; 151 int endWidth = end.right - end.left; 152 int endHeight = end.bottom - end.top; 153 154 float progressPosition = progress; 155 156 float startAlpha = start.alpha; 157 float endAlpha = end.alpha; 158 159 if (start.visibility == ConstraintWidget.GONE) { 160 // On visibility gone, keep the same size to do an alpha to zero 161 startX -= (int) (endWidth / 2f); 162 startY -= (int) (endHeight / 2f); 163 startWidth = endWidth; 164 startHeight = endHeight; 165 if (Float.isNaN(startAlpha)) { 166 // override only if not defined... 167 startAlpha = 0f; 168 } 169 } 170 171 if (end.visibility == ConstraintWidget.GONE) { 172 // On visibility gone, keep the same size to do an alpha to zero 173 endX -= (int) (startWidth / 2f); 174 endY -= (int) (startHeight / 2f); 175 endWidth = startWidth; 176 endHeight = startHeight; 177 if (Float.isNaN(endAlpha)) { 178 // override only if not defined... 179 endAlpha = 0f; 180 } 181 } 182 183 if (Float.isNaN(startAlpha) && !Float.isNaN(endAlpha)) { 184 startAlpha = 1f; 185 } 186 if (!Float.isNaN(startAlpha) && Float.isNaN(endAlpha)) { 187 endAlpha = 1f; 188 } 189 190 if (start.visibility == ConstraintWidget.INVISIBLE) { 191 startAlpha = 0f; 192 } 193 194 if (end.visibility == ConstraintWidget.INVISIBLE) { 195 endAlpha = 0f; 196 } 197 198 if (frame.widget != null && transition.hasPositionKeyframes()) { 199 Transition.KeyPosition firstPosition = 200 transition.findPreviousPosition(frame.widget.stringId, frameNumber); 201 Transition.KeyPosition lastPosition = 202 transition.findNextPosition(frame.widget.stringId, frameNumber); 203 204 if (firstPosition == lastPosition) { 205 lastPosition = null; 206 } 207 int interpolateStartFrame = 0; 208 int interpolateEndFrame = 100; 209 210 if (firstPosition != null) { 211 startX = (int) (firstPosition.mX * parentWidth); 212 startY = (int) (firstPosition.mY * parentHeight); 213 interpolateStartFrame = firstPosition.mFrame; 214 } 215 if (lastPosition != null) { 216 endX = (int) (lastPosition.mX * parentWidth); 217 endY = (int) (lastPosition.mY * parentHeight); 218 interpolateEndFrame = lastPosition.mFrame; 219 } 220 221 progressPosition = (progress * 100f - interpolateStartFrame) 222 / (float) (interpolateEndFrame - interpolateStartFrame); 223 } 224 225 frame.widget = start.widget; 226 227 frame.left = (int) (startX + progressPosition * (endX - startX)); 228 frame.top = (int) (startY + progressPosition * (endY - startY)); 229 int width = (int) ((1 - progress) * startWidth + (progress * endWidth)); 230 int height = (int) ((1 - progress) * startHeight + (progress * endHeight)); 231 frame.right = frame.left + width; 232 frame.bottom = frame.top + height; 233 234 frame.pivotX = interpolate(start.pivotX, end.pivotX, 0.5f, progress); 235 frame.pivotY = interpolate(start.pivotY, end.pivotY, 0.5f, progress); 236 237 frame.rotationX = interpolate(start.rotationX, end.rotationX, 0f, progress); 238 frame.rotationY = interpolate(start.rotationY, end.rotationY, 0f, progress); 239 frame.rotationZ = interpolate(start.rotationZ, end.rotationZ, 0f, progress); 240 241 frame.scaleX = interpolate(start.scaleX, end.scaleX, 1f, progress); 242 frame.scaleY = interpolate(start.scaleY, end.scaleY, 1f, progress); 243 244 frame.translationX = interpolate(start.translationX, end.translationX, 0f, progress); 245 frame.translationY = interpolate(start.translationY, end.translationY, 0f, progress); 246 frame.translationZ = interpolate(start.translationZ, end.translationZ, 0f, progress); 247 248 frame.alpha = interpolate(startAlpha, endAlpha, 1f, progress); 249 250 Set<String> keys = end.mCustom.keySet(); 251 frame.mCustom.clear(); 252 for (String key : keys) { 253 if (start.mCustom.containsKey(key)) { 254 CustomVariable startVariable = start.mCustom.get(key); 255 CustomVariable endVariable = end.mCustom.get(key); 256 CustomVariable interpolated = new CustomVariable(startVariable); 257 frame.mCustom.put(key, interpolated); 258 if (startVariable.numberOfInterpolatedValues() == 1) { 259 interpolated.setValue(interpolate(startVariable.getValueToInterpolate(), 260 endVariable.getValueToInterpolate(), 0f, progress)); 261 } else { 262 int n = startVariable.numberOfInterpolatedValues(); 263 float[] startValues = new float[n]; 264 float[] endValues = new float[n]; 265 startVariable.getValuesToInterpolate(startValues); 266 endVariable.getValuesToInterpolate(endValues); 267 for (int i = 0; i < n; i++) { 268 startValues[i] = interpolate(startValues[i], endValues[i], 0f, progress); 269 interpolated.setValue(startValues); 270 } 271 } 272 } 273 } 274 } 275 interpolate(float start, float end, float defaultValue, float progress)276 private static float interpolate(float start, float end, float defaultValue, float progress) { 277 boolean isStartUnset = Float.isNaN(start); 278 boolean isEndUnset = Float.isNaN(end); 279 if (isStartUnset && isEndUnset) { 280 return Float.NaN; 281 } 282 if (isStartUnset) { 283 start = defaultValue; 284 } 285 if (isEndUnset) { 286 end = defaultValue; 287 } 288 return (start + progress * (end - start)); 289 } 290 291 // @TODO: add description centerX()292 public float centerX() { 293 return left + (right - left) / 2f; 294 } 295 296 // @TODO: add description centerY()297 public float centerY() { 298 return top + (bottom - top) / 2f; 299 } 300 301 // @TODO: add description update()302 public WidgetFrame update() { 303 if (widget != null) { 304 left = widget.getLeft(); 305 top = widget.getTop(); 306 right = widget.getRight(); 307 bottom = widget.getBottom(); 308 WidgetFrame frame = widget.frame; 309 updateAttributes(frame); 310 } 311 return this; 312 } 313 314 // @TODO: add description update(ConstraintWidget widget)315 public WidgetFrame update(ConstraintWidget widget) { 316 if (widget == null) { 317 return this; 318 } 319 320 this.widget = widget; 321 update(); 322 return this; 323 } 324 325 /** 326 * Return whether this WidgetFrame contains a custom property of the given name. 327 */ containsCustom(@onNull String name)328 public boolean containsCustom(@NonNull String name) { 329 return mCustom.containsKey(name); 330 } 331 332 // @TODO: add description addCustomColor(String name, int color)333 public void addCustomColor(String name, int color) { 334 setCustomAttribute(name, TypedValues.Custom.TYPE_COLOR, color); 335 } 336 337 // @TODO: add description getCustomColor(String name)338 public int getCustomColor(String name) { 339 if (mCustom.containsKey(name)) { 340 return mCustom.get(name).getColorValue(); 341 } 342 return 0xFFFFAA88; 343 } 344 345 // @TODO: add description addCustomFloat(String name, float value)346 public void addCustomFloat(String name, float value) { 347 setCustomAttribute(name, TypedValues.Custom.TYPE_FLOAT, value); 348 } 349 350 // @TODO: add description getCustomFloat(String name)351 public float getCustomFloat(String name) { 352 if (mCustom.containsKey(name)) { 353 return mCustom.get(name).getFloatValue(); 354 } 355 return Float.NaN; 356 } 357 358 // @TODO: add description setCustomAttribute(String name, int type, float value)359 public void setCustomAttribute(String name, int type, float value) { 360 if (mCustom.containsKey(name)) { 361 mCustom.get(name).setFloatValue(value); 362 } else { 363 mCustom.put(name, new CustomVariable(name, type, value)); 364 } 365 } 366 367 // @TODO: add description setCustomAttribute(String name, int type, int value)368 public void setCustomAttribute(String name, int type, int value) { 369 if (mCustom.containsKey(name)) { 370 mCustom.get(name).setIntValue(value); 371 } else { 372 mCustom.put(name, new CustomVariable(name, type, value)); 373 } 374 } 375 376 // @TODO: add description setCustomAttribute(String name, int type, boolean value)377 public void setCustomAttribute(String name, int type, boolean value) { 378 if (mCustom.containsKey(name)) { 379 mCustom.get(name).setBooleanValue(value); 380 } else { 381 mCustom.put(name, new CustomVariable(name, type, value)); 382 } 383 } 384 385 // @TODO: add description setCustomAttribute(String name, int type, String value)386 public void setCustomAttribute(String name, int type, String value) { 387 if (mCustom.containsKey(name)) { 388 mCustom.get(name).setStringValue(value); 389 } else { 390 mCustom.put(name, new CustomVariable(name, type, value)); 391 } 392 } 393 394 /** 395 * Get the custom attribute given Nam 396 * @param name Name of the custom attribut 397 * @return The customAttribute 398 */ getCustomAttribute(String name)399 public CustomVariable getCustomAttribute(String name) { 400 return mCustom.get(name); 401 } 402 403 /** 404 * Get the known custom Attributes names 405 * @return set of custom attribute names 406 */ getCustomAttributeNames()407 public Set<String> getCustomAttributeNames() { 408 return mCustom.keySet(); 409 } 410 411 // @TODO: add description setValue(String key, CLElement value)412 public boolean setValue(String key, CLElement value) throws CLParsingException { 413 switch (key) { 414 case "pivotX": 415 pivotX = value.getFloat(); 416 break; 417 case "pivotY": 418 pivotY = value.getFloat(); 419 break; 420 case "rotationX": 421 rotationX = value.getFloat(); 422 break; 423 case "rotationY": 424 rotationY = value.getFloat(); 425 break; 426 case "rotationZ": 427 rotationZ = value.getFloat(); 428 break; 429 case "translationX": 430 translationX = value.getFloat(); 431 break; 432 case "translationY": 433 translationY = value.getFloat(); 434 break; 435 case "translationZ": 436 translationZ = value.getFloat(); 437 break; 438 case "scaleX": 439 scaleX = value.getFloat(); 440 break; 441 case "scaleY": 442 scaleY = value.getFloat(); 443 break; 444 case "alpha": 445 alpha = value.getFloat(); 446 break; 447 case "interpolatedPos": 448 interpolatedPos = value.getFloat(); 449 break; 450 case "phone_orientation": 451 phone_orientation = value.getFloat(); 452 break; 453 case "top": 454 top = value.getInt(); 455 break; 456 case "left": 457 left = value.getInt(); 458 break; 459 case "right": 460 right = value.getInt(); 461 break; 462 case "bottom": 463 bottom = value.getInt(); 464 break; 465 case "custom": 466 parseCustom(value); 467 break; 468 469 default: 470 return false; 471 } 472 return true; 473 } 474 475 // @TODO: add description getId()476 public String getId() { 477 if (widget == null) { 478 return "unknown"; 479 } 480 return widget.stringId; 481 } 482 parseCustom(CLElement custom)483 void parseCustom(CLElement custom) throws CLParsingException { 484 CLObject obj = ((CLObject) custom); 485 int n = obj.size(); 486 for (int i = 0; i < n; i++) { 487 CLElement tmp = obj.get(i); 488 CLKey k = ((CLKey) tmp); 489 CLElement v = k.getValue(); 490 String vStr = v.content(); 491 if (vStr.matches("#[0-9a-fA-F]+")) { 492 int color = Integer.parseInt(vStr.substring(1), 16); 493 setCustomAttribute(name, TypedValues.Custom.TYPE_COLOR, color); 494 } else if (v instanceof CLNumber) { 495 setCustomAttribute(name, TypedValues.Custom.TYPE_FLOAT, v.getFloat()); 496 } else { 497 setCustomAttribute(name, TypedValues.Custom.TYPE_STRING, vStr); 498 499 } 500 } 501 } 502 503 // @TODO: add description serialize(StringBuilder ret)504 public StringBuilder serialize(StringBuilder ret) { 505 return serialize(ret, false); 506 } 507 508 /** 509 * If true also send the phone orientation 510 */ serialize(StringBuilder ret, boolean sendPhoneOrientation)511 public StringBuilder serialize(StringBuilder ret, boolean sendPhoneOrientation) { 512 WidgetFrame frame = this; 513 ret.append("{\n"); 514 add(ret, "left", frame.left); 515 add(ret, "top", frame.top); 516 add(ret, "right", frame.right); 517 add(ret, "bottom", frame.bottom); 518 add(ret, "pivotX", frame.pivotX); 519 add(ret, "pivotY", frame.pivotY); 520 add(ret, "rotationX", frame.rotationX); 521 add(ret, "rotationY", frame.rotationY); 522 add(ret, "rotationZ", frame.rotationZ); 523 add(ret, "translationX", frame.translationX); 524 add(ret, "translationY", frame.translationY); 525 add(ret, "translationZ", frame.translationZ); 526 add(ret, "scaleX", frame.scaleX); 527 add(ret, "scaleY", frame.scaleY); 528 add(ret, "alpha", frame.alpha); 529 add(ret, "visibility", frame.visibility); 530 add(ret, "interpolatedPos", frame.interpolatedPos); 531 if (widget != null) { 532 for (ConstraintAnchor.Type side : ConstraintAnchor.Type.values()) { 533 serializeAnchor(ret, side); 534 } 535 } 536 if (sendPhoneOrientation) { 537 add(ret, "phone_orientation", phone_orientation); 538 } 539 if (sendPhoneOrientation) { 540 add(ret, "phone_orientation", phone_orientation); 541 } 542 543 if (frame.mCustom.size() != 0) { 544 ret.append("custom : {\n"); 545 for (String s : frame.mCustom.keySet()) { 546 CustomVariable value = frame.mCustom.get(s); 547 ret.append(s); 548 ret.append(": "); 549 switch (value.getType()) { 550 case TypedValues.Custom.TYPE_INT: 551 ret.append(value.getIntegerValue()); 552 ret.append(",\n"); 553 break; 554 case TypedValues.Custom.TYPE_FLOAT: 555 case TypedValues.Custom.TYPE_DIMENSION: 556 ret.append(value.getFloatValue()); 557 ret.append(",\n"); 558 break; 559 case TypedValues.Custom.TYPE_COLOR: 560 ret.append("'"); 561 ret.append(CustomVariable.colorString(value.getIntegerValue())); 562 ret.append("',\n"); 563 break; 564 case TypedValues.Custom.TYPE_STRING: 565 ret.append("'"); 566 ret.append(value.getStringValue()); 567 ret.append("',\n"); 568 break; 569 case TypedValues.Custom.TYPE_BOOLEAN: 570 ret.append("'"); 571 ret.append(value.getBooleanValue()); 572 ret.append("',\n"); 573 break; 574 } 575 } 576 ret.append("}\n"); 577 } 578 579 ret.append("}\n"); 580 return ret; 581 } 582 serializeAnchor(StringBuilder ret, ConstraintAnchor.Type type)583 private void serializeAnchor(StringBuilder ret, ConstraintAnchor.Type type) { 584 ConstraintAnchor anchor = widget.getAnchor(type); 585 if (anchor == null || anchor.mTarget == null) { 586 return; 587 } 588 ret.append("Anchor"); 589 ret.append(type.name()); 590 ret.append(": ['"); 591 String str = anchor.mTarget.getOwner().stringId; 592 ret.append(str == null ? "#PARENT" : str); 593 ret.append("', '"); 594 ret.append(anchor.mTarget.getType().name()); 595 ret.append("', '"); 596 ret.append(anchor.mMargin); 597 ret.append("'],\n"); 598 599 } 600 add(StringBuilder s, String title, int value)601 private static void add(StringBuilder s, String title, int value) { 602 s.append(title); 603 s.append(": "); 604 s.append(value); 605 s.append(",\n"); 606 } 607 add(StringBuilder s, String title, float value)608 private static void add(StringBuilder s, String title, float value) { 609 if (Float.isNaN(value)) { 610 return; 611 } 612 s.append(title); 613 s.append(": "); 614 s.append(value); 615 s.append(",\n"); 616 } 617 618 /** 619 * For debugging only 620 */ printCustomAttributes()621 void printCustomAttributes() { 622 StackTraceElement s = new Throwable().getStackTrace()[1]; 623 String ss = ".(" + s.getFileName() + ":" + s.getLineNumber() + ") " + s.getMethodName(); 624 ss += " " + (this.hashCode() % 1000); 625 if (widget != null) { 626 ss += "/" + (widget.hashCode() % 1000) + " "; 627 } else { 628 ss += "/NULL "; 629 } 630 if (mCustom != null) { 631 for (String key : mCustom.keySet()) { 632 System.out.println(ss + mCustom.get(key).toString()); 633 } 634 } 635 } 636 637 /** 638 * For debugging only 639 */ logv(String str)640 void logv(String str) { 641 StackTraceElement s = new Throwable().getStackTrace()[1]; 642 String ss = ".(" + s.getFileName() + ":" + s.getLineNumber() + ") " + s.getMethodName(); 643 ss += " " + (this.hashCode() % 1000); 644 if (widget != null) { 645 ss += "/" + (widget.hashCode() % 1000); 646 } else { 647 ss += "/NULL"; 648 } 649 650 System.out.println(ss + " " + str); 651 } 652 653 // @TODO: add description setCustomValue(CustomAttribute valueAt, float[] mTempValues)654 public void setCustomValue(CustomAttribute valueAt, float[] mTempValues) { 655 } 656 setMotionAttributes(TypedBundle motionProperties)657 void setMotionAttributes(TypedBundle motionProperties) { 658 mMotionProperties = motionProperties; 659 } 660 661 /** 662 * get the property bundle associated with MotionAttributes 663 * 664 * @return the property bundle associated with MotionAttributes or null 665 */ getMotionProperties()666 public TypedBundle getMotionProperties() { 667 return mMotionProperties; 668 } 669 } 670