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