diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java new file mode 100644 index 000000000..4e4fb2424 --- /dev/null +++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/LogJson.java @@ -0,0 +1,765 @@ +/* + * Copyright (C) 2023 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.helper.widget; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import static androidx.constraintlayout.widget.ConstraintSet.Layout.UNSET_GONE_MARGIN; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Environment; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.constraintlayout.motion.widget.Debug; +import androidx.constraintlayout.widget.ConstraintAttribute; +import androidx.constraintlayout.widget.ConstraintHelper; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintSet; +import androidx.constraintlayout.widget.R; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This is a class is a debugging/logging utility to write out the constraints in JSON + * This is used for debugging purposes + * + * logJsonTo supports: + * + * logJsonMode modes are: + * + * + * The defaults are: + * + * Usage: + *

+ *
+ *  {@code
+ *      
+ *  }
+ * 
+ *

+ */ +public class LogJson extends ConstraintHelper { + private static final String TAG = "JSON5"; + private int mDelay = 1000; + private int mMode = LOG_DELAYED; + private String mLogToFile = null; + private boolean mLogConsole = true; + + public static final int LOG_PERIODIC = 1; + public static final int LOG_DELAYED = 2; + public static final int LOG_LAYOUT = 3; + public static final int LOG_API = 4; + private boolean mPeriodic = false; + + public LogJson(@androidx.annotation.NonNull Context context) { + super(context); + } + + public LogJson(@androidx.annotation.NonNull Context context, + @androidx.annotation.Nullable AttributeSet attrs) { + super(context, attrs); + initLogJson(attrs); + + } + + public LogJson(@androidx.annotation.NonNull Context context, + @androidx.annotation.Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initLogJson(attrs); + } + + private void initLogJson(AttributeSet attrs) { + + if (attrs != null) { + TypedArray a = getContext().obtainStyledAttributes(attrs, + R.styleable.LogJson); + final int count = a.getIndexCount(); + for (int i = 0; i < count; i++) { + int attr = a.getIndex(i); + if (attr == R.styleable.LogJson_logJsonDelay) { + mDelay = a.getInt(attr, mDelay); + } else if (attr == R.styleable.LogJson_logJsonMode) { + mMode = a.getInt(attr, mMode); + } else if (attr == R.styleable.LogJson_logJsonTo) { + TypedValue v = a.peekValue(attr); + if (v.type == TypedValue.TYPE_STRING) { + String value = a.getString(attr); + } else { + int value = a.getInt(attr, 0); + mLogConsole = value == 2; + } + } + } + a.recycle(); + } + setVisibility(GONE); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + switch (mMode) { + case LOG_PERIODIC: + mPeriodic = true; + this.postDelayed(this::periodic, mDelay); + break; + case LOG_DELAYED: + this.postDelayed(this::writeLog, mDelay); + break; + case LOG_LAYOUT: + ConstraintLayout cl = (ConstraintLayout) getParent(); + cl.addOnLayoutChangeListener((v, a, b, c, d, e, f, g, h) -> logOnLayout()); + } + } + + private void logOnLayout() { + if (mMode == LOG_LAYOUT) { + writeLog(); + } + } + + /** + * Set the duration of periodic logging of constraints + * + * @param duration the time in ms between writing files + */ + public void setPeriodicDuration(int duration) { + mDelay = duration; + } + + /** + * Start periodic sampling + */ + public void periodicStart() { + mPeriodic = true; + this.postDelayed(this::periodic, mDelay); + } + + /** + * Stop periodic sampling + */ + public void periodicStop() { + mPeriodic = false; + } + + private void periodic() { + if (mPeriodic) { + writeLog(); + this.postDelayed(this::periodic, mDelay); + } + } + + /** + * This writes a JSON5 representation of the constraintSet + */ + public void writeLog() { + String str = asString((ConstraintLayout) this.getParent()); + if (mLogToFile == null) { + if (mLogConsole) { + System.out.println(str); + } else { + logBigString(str); + } + } else { + String name = toFile(str, mLogToFile); + Log.v("JSON", "\"" + name + "\" written!"); + } + } + + /** + * This writes the JSON5 description of the constraintLayout to a file named fileName.json5 + * in the download directory which can be pulled with: + * "adb pull "/storage/emulated/0/Download/" ." + * + * @param str String to write as a file + * @param fileName file name + * @return full path name of file + */ + private static String toFile(String str, String fileName) { + FileOutputStream outputStream; + if (!fileName.endsWith(".json5")) { + fileName += ".json5"; + } + try { + File down = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + File file = new File(down, fileName); + outputStream = new FileOutputStream(file); + outputStream.write(str.getBytes()); + outputStream.close(); + return file.getCanonicalPath(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + @SuppressLint("LogConditional") + private void logBigString(String str) { + int len = str.length(); + for (int i = 0; i < len; i++) { + int k = str.indexOf("\n", i); + if (k == -1) { + Log.v(TAG, str.substring(i)); + break; + } + Log.v(TAG, str.substring(i, k)); + i = k; + } + } + + /** + * Get a JSON5 String that represents the Constraints in a running ConstraintLayout + * + * @param constraintLayout its constraints are converted to a string + * @return JSON5 string + */ + private static String asString(ConstraintLayout constraintLayout) { + JsonWriter c = new JsonWriter(); + return c.constraintLayoutToJson(constraintLayout); + } + + // ================================== JSON writer============================================== + + private static class JsonWriter { + public static final int UNSET = ConstraintLayout.LayoutParams.UNSET; + ConstraintSet mSet; + Writer mWriter; + ConstraintLayout mLayout; + Context mContext; + int mUnknownCount = 0; + final String mLEFT = "left"; + final String mRIGHT = "right"; + final String mBASELINE = "baseline"; + final String mBOTTOM = "bottom"; + final String mTOP = "top"; + final String mSTART = "start"; + final String mEND = "end"; + private static final String INDENT = " "; + private static final String SMALL_INDENT = " "; + HashMap mIdMap = new HashMap<>(); + private static final String LOG_JSON = LogJson.class.getSimpleName(); + private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1); + HashMap mNames = new HashMap<>(); + + private static int generateViewId() { + final int max_id = 0x00FFFFFF; + for (;;) { + final int result = sNextGeneratedId.get(); + int newValue = result + 1; + if (newValue > max_id) { + newValue = 1; + } + if (sNextGeneratedId.compareAndSet(result, newValue)) { + return result; + } + } + } + + private String constraintLayoutToJson(ConstraintLayout constraintLayout) { + StringWriter writer = new StringWriter(); + + int count = constraintLayout.getChildCount(); + for (int i = 0; i < count; i++) { + View v = constraintLayout.getChildAt(i); + String name = v.getClass().getSimpleName(); + int id = v.getId(); + if (id == -1) { + if (android.os.Build.VERSION.SDK_INT + >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + id = View.generateViewId(); + } else { + id = generateViewId(); + } + v.setId(id); + if (!LOG_JSON.equals(name)) { + name = "noid_" + name; + } + mNames.put(id, name); + } else if (LOG_JSON.equals(name)) { + mNames.put(id, name); + } + } + writer.append("{\n"); + + writeWidgets(writer, constraintLayout); + writer.append(" ConstraintSet:{\n"); + ConstraintSet set = new ConstraintSet(); + set.clone(constraintLayout); + String name = + (constraintLayout.getId() == -1) ? "cset" : Debug.getName(constraintLayout); + try { + writer.append(name + ":"); + setup(writer, set, constraintLayout); + writeLayout(); + writer.append("\n"); + } catch (IOException e) { + throw new RuntimeException(e); + } + writer.append(" }\n"); + writer.append("}\n"); + return writer.toString(); + } + + private void writeWidgets(StringWriter writer, ConstraintLayout constraintLayout) { + writer.append("Widgets:{\n"); + int count = constraintLayout.getChildCount(); + + for (int i = -1; i < count; i++) { + View v = (i == -1) ? constraintLayout : constraintLayout.getChildAt(i); + int id = v.getId(); + if (LOG_JSON.equals(v.getClass().getSimpleName())) { + continue; + } + String name = (mNames.containsKey(id)) ? mNames.get(id) + : ((i == -1) ? "parent" : Debug.getName(v)); + String cname = v.getClass().getSimpleName(); + String bounds = ", bounds: [" + v.getLeft() + ", " + v.getTop() + + ", " + v.getRight() + ", " + v.getBottom() + "]},\n"; + writer.append(" " + name + ": { "); + if (i == -1) { + writer.append("type: '" + v.getClass().getSimpleName() + "' , "); + + try { + ViewGroup.LayoutParams p = (ViewGroup.LayoutParams) v.getLayoutParams(); + String wrap = "'WRAP_CONTENT'"; + String match = "'MATCH_PARENT'"; + String w = p.width == MATCH_PARENT ? match : + (p.width == WRAP_CONTENT) ? wrap : p.width + ""; + writer.append("width: " + w + ", "); + String h = p.height == MATCH_PARENT ? match : + (p.height == WRAP_CONTENT) ? wrap : p.height + ""; + writer.append("height: ").append(h); + } catch (Exception e) { + } + } else if (cname.contains("Text")) { + if (v instanceof TextView) { + writer.append("type: 'Text', label: '" + + escape(((TextView) v).getText().toString()) + "'"); + } else { + writer.append("type: 'Text' },\n"); + } + } else if (cname.contains("Button")) { + if (v instanceof Button) { + writer.append("type: 'Button', label: '" + ((Button) v).getText() + "'"); + } else + writer.append("type: 'Button'"); + } else if (cname.contains("Image")) { + writer.append("type: 'Image'"); + } else if (cname.contains("View")) { + writer.append("type: 'Box'"); + } else { + writer.append("type: '" + v.getClass().getSimpleName() + "'"); + } + writer.append(bounds); + } + writer.append("},\n"); + } + + private static String escape(String str) { + return str.replaceAll("'", "\\'"); + } + + JsonWriter() { + + } + + void setup(Writer writer, + ConstraintSet set, + ConstraintLayout layout) throws IOException { + this.mWriter = writer; + this.mLayout = layout; + this.mContext = layout.getContext(); + this.mSet = set; + set.getConstraint(2); + } + + private int[] getIDs() { + return mSet.getKnownIds(); + } + + private ConstraintSet.Constraint getConstraint(int id) { + return mSet.getConstraint(id); + } + + private void writeLayout() throws IOException { + mWriter.write("{\n"); + for (Integer id : getIDs()) { + ConstraintSet.Constraint c = getConstraint(id); + String idName = getSimpleName(id); + if (LOG_JSON.equals(idName)) { // skip LogJson it is for used to log + continue; + } + mWriter.write(SMALL_INDENT + idName + ":{\n"); + ConstraintSet.Layout l = c.layout; + if (l.mReferenceIds != null) { + StringBuilder ref = + new StringBuilder("type: '_" + idName + "_' , contains: ["); + for (int r = 0; r < l.mReferenceIds.length; r++) { + int rid = l.mReferenceIds[r]; + ref.append((r == 0) ? "" : ", ").append(getName(rid)); + } + mWriter.write(ref + "]\n"); + } + if (l.mReferenceIdString != null) { + StringBuilder ref = + new StringBuilder(SMALL_INDENT + "type: '???' , contains: ["); + String[] rids = l.mReferenceIdString.split(","); + for (int r = 0; r < rids.length; r++) { + String rid = rids[r]; + ref.append((r == 0) ? "" : ", ").append("`").append(rid).append("`"); + } + mWriter.write(ref + "]\n"); + } + writeDimension("height", l.mHeight, l.heightDefault, l.heightPercent, + l.heightMin, l.heightMax, l.constrainedHeight); + writeDimension("width", l.mWidth, l.widthDefault, l.widthPercent, + l.widthMin, l.widthMax, l.constrainedWidth); + + writeConstraint(mLEFT, l.leftToLeft, mLEFT, l.leftMargin, l.goneLeftMargin); + writeConstraint(mLEFT, l.leftToRight, mRIGHT, l.leftMargin, l.goneLeftMargin); + writeConstraint(mRIGHT, l.rightToLeft, mLEFT, l.rightMargin, l.goneRightMargin); + writeConstraint(mRIGHT, l.rightToRight, mRIGHT, l.rightMargin, l.goneRightMargin); + writeConstraint(mBASELINE, l.baselineToBaseline, mBASELINE, UNSET, + l.goneBaselineMargin); + writeConstraint(mBASELINE, l.baselineToTop, mTOP, UNSET, l.goneBaselineMargin); + writeConstraint(mBASELINE, l.baselineToBottom, + mBOTTOM, UNSET, l.goneBaselineMargin); + + writeConstraint(mTOP, l.topToBottom, mBOTTOM, l.topMargin, l.goneTopMargin); + writeConstraint(mTOP, l.topToTop, mTOP, l.topMargin, l.goneTopMargin); + writeConstraint(mBOTTOM, l.bottomToBottom, mBOTTOM, l.bottomMargin, + l.goneBottomMargin); + writeConstraint(mBOTTOM, l.bottomToTop, mTOP, l.bottomMargin, l.goneBottomMargin); + writeConstraint(mSTART, l.startToStart, mSTART, l.startMargin, l.goneStartMargin); + writeConstraint(mSTART, l.startToEnd, mEND, l.startMargin, l.goneStartMargin); + writeConstraint(mEND, l.endToStart, mSTART, l.endMargin, l.goneEndMargin); + writeConstraint(mEND, l.endToEnd, mEND, l.endMargin, l.goneEndMargin); + + writeVariable("horizontalBias", l.horizontalBias, 0.5f); + writeVariable("verticalBias", l.verticalBias, 0.5f); + + writeCircle(l.circleConstraint, l.circleAngle, l.circleRadius); + + writeGuideline(l.orientation, l.guideBegin, l.guideEnd, l.guidePercent); + writeVariable("dimensionRatio", l.dimensionRatio); + writeVariable("barrierMargin", l.mBarrierMargin); + writeVariable("type", l.mHelperType); + writeVariable("ReferenceId", l.mReferenceIdString); + writeVariable("mBarrierAllowsGoneWidgets", + l.mBarrierAllowsGoneWidgets, true); + writeVariable("WrapBehavior", l.mWrapBehavior); + + writeVariable("verticalWeight", l.verticalWeight); + writeVariable("horizontalWeight", l.horizontalWeight); + writeVariable("horizontalChainStyle", l.horizontalChainStyle); + writeVariable("verticalChainStyle", l.verticalChainStyle); + writeVariable("barrierDirection", l.mBarrierDirection); + if (l.mReferenceIds != null) { + writeVariable("ReferenceIds", l.mReferenceIds); + } + writeTransform(c.transform); + writeCustom(c.mCustomConstraints); + + mWriter.write(" },\n"); + } + mWriter.write("},\n"); + } + + private void writeTransform(ConstraintSet.Transform transform) throws IOException { + if (transform.applyElevation) { + writeVariable("elevation", transform.elevation); + } + writeVariable("rotationX", transform.rotationX, 0); + writeVariable("rotationY", transform.rotationY, 0); + writeVariable("rotationZ", transform.rotation, 0); + writeVariable("scaleX", transform.scaleX, 1); + writeVariable("scaleY", transform.scaleY, 1); + writeVariable("translationX", transform.translationX, 0); + writeVariable("translationY", transform.translationY, 0); + writeVariable("translationZ", transform.translationZ, 0); + } + + private void writeCustom(HashMap cset) throws IOException { + if (cset != null && cset.size() > 0) { + mWriter.write(INDENT + "custom: {\n"); + for (String s : cset.keySet()) { + ConstraintAttribute attr = cset.get(s); + if (attr == null) { + continue; + } + String custom = INDENT + SMALL_INDENT + attr.getName() + ": "; + switch (attr.getType()) { + case INT_TYPE: + custom += attr.getIntegerValue(); + break; + case COLOR_TYPE: + custom += colorString(attr.getColorValue()); + break; + case FLOAT_TYPE: + custom += attr.getFloatValue(); + break; + case STRING_TYPE: + custom += "'" + attr.getStringValue() + "'"; + break; + case DIMENSION_TYPE: + custom = custom + attr.getFloatValue(); + break; + case REFERENCE_TYPE: + case COLOR_DRAWABLE_TYPE: + case BOOLEAN_TYPE: + custom = null; + } + if (custom != null) { + mWriter.write(custom + ",\n"); + } + } + mWriter.write(SMALL_INDENT + " } \n"); + } + } + + private static String colorString(int v) { + String str = "00000000" + Integer.toHexString(v); + return "#" + str.substring(str.length() - 8); + } + + private void writeGuideline(int orientation, + int guideBegin, + int guideEnd, + float guidePercent) throws IOException { + writeVariable("orientation", orientation); + writeVariable("guideBegin", guideBegin); + writeVariable("guideEnd", guideEnd); + writeVariable("guidePercent", guidePercent); + } + + private void writeDimension(String dimString, + int dim, + int dimDefault, + float dimPercent, + int dimMin, + int dimMax, + boolean unusedConstrainedDim) throws IOException { + if (dim == 0) { + if (dimMax != UNSET || dimMin != UNSET) { + String s = "-----"; + switch (dimDefault) { + case 0: // spread + s = INDENT + dimString + ": {value:'spread'"; + break; + case 1: // wrap + s = INDENT + dimString + ": {value:'wrap'"; + break; + case 2: // percent + s = INDENT + dimString + ": {value: '" + dimPercent + "%'"; + break; + } + if (dimMax != UNSET) { + s += ", max: " + dimMax; + } + if (dimMax != UNSET) { + s += ", min: " + dimMin; + } + s += "},\n"; + mWriter.write(s); + return; + } + + switch (dimDefault) { + case 0: // spread is the default + break; + case 1: // wrap + mWriter.write(INDENT + dimString + ": '???????????',\n"); + return; + case 2: // percent + mWriter.write(INDENT + dimString + ": '" + dimPercent + "%',\n"); + } + + } else if (dim == -2) { + mWriter.write(INDENT + dimString + ": 'wrap',\n"); + } else if (dim == -1) { + mWriter.write(INDENT + dimString + ": 'parent',\n"); + } else { + mWriter.write(INDENT + dimString + ": " + dim + ",\n"); + } + } + + private String getSimpleName(int id) { + if (mIdMap.containsKey(id)) { + return "" + mIdMap.get(id); + } + if (id == 0) { + return "parent"; + } + String name = lookup(id); + mIdMap.put(id, name); + return "" + name + ""; + } + + private String getName(int id) { + return "'" + getSimpleName(id) + "'"; + } + + private String lookup(int id) { + try { + if (mNames.containsKey(id)) { + return mNames.get(id); + } + if (id != -1) { + return mContext.getResources().getResourceEntryName(id); + } else { + return "unknown" + ++mUnknownCount; + } + } catch (Exception ex) { + return "unknown" + ++mUnknownCount; + } + } + + private void writeConstraint(String my, + int constraint, + String other, + int margin, + int goneMargin) throws IOException { + if (constraint == UNSET) { + return; + } + mWriter.write(INDENT + my); + mWriter.write(":["); + mWriter.write(getName(constraint)); + mWriter.write(", "); + mWriter.write("'" + other + "'"); + if (margin != 0 || goneMargin != UNSET_GONE_MARGIN) { + mWriter.write(", " + margin); + if (goneMargin != UNSET_GONE_MARGIN) { + mWriter.write(", " + goneMargin); + } + } + mWriter.write("],\n"); + } + + private void writeCircle(int circleConstraint, + float circleAngle, + int circleRadius) throws IOException { + if (circleConstraint == UNSET) { + return; + } + mWriter.write(INDENT + "circle"); + mWriter.write(":["); + mWriter.write(getName(circleConstraint)); + mWriter.write(", " + circleAngle); + mWriter.write(circleRadius + "],\n"); + } + + private void writeVariable(String name, int value) throws IOException { + if (value == 0 || value == -1) { + return; + } + mWriter.write(INDENT + name); + mWriter.write(": " + value); + mWriter.write(",\n"); + } + + private void writeVariable(String name, float value) throws IOException { + if (value == UNSET) { + return; + } + mWriter.write(INDENT + name); + mWriter.write(": " + value); + mWriter.write(",\n"); + } + + private void writeVariable(String name, float value, float def) throws IOException { + if (value == def) { + return; + } + mWriter.write(INDENT + name); + mWriter.write(": " + value); + mWriter.write(",\n"); + } + + private void writeVariable(String name, boolean value, boolean def) throws IOException { + if (value == def) { + return; + } + mWriter.write(INDENT + name); + mWriter.write(": " + value); + mWriter.write(",\n"); + } + + private void writeVariable(String name, int[] value) throws IOException { + if (value == null) { + return; + } + mWriter.write(INDENT + name); + mWriter.write(": "); + for (int i = 0; i < value.length; i++) { + mWriter.write(((i == 0) ? "[" : ", ") + getName(value[i])); + } + mWriter.write("],\n"); + } + + private void writeVariable(String name, String value) throws IOException { + if (value == null) { + return; + } + mWriter.write(INDENT + name); + mWriter.write(": '" + value); + mWriter.write("',\n"); + } + } +} diff --git a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml index e0e76eb6f..5f35e22f7 100644 --- a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml +++ b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml @@ -816,6 +816,21 @@ + + + + + + + + + + + + + + + diff --git a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/Transition.java b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/Transition.java index 877cafd9d..ef40272dc 100644 --- a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/Transition.java +++ b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/Transition.java @@ -74,6 +74,13 @@ public Transition(@NonNull CorePixelDp dpToPixel) { mToPixel = dpToPixel; } + /** + * Create transition with a 1 to 1 DP to pixel (usually used in testing + */ + public Transition() { + this((dp) -> dp); + } + // @TODO: add description @SuppressWarnings("HiddenTypeParameter") OnSwipe createOnSwipe() { diff --git a/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/CheckDumpJson.java b/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/CheckDumpJson.java index b0e3e22a1..3c6d9e4f1 100644 --- a/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/CheckDumpJson.java +++ b/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/CheckDumpJson.java @@ -23,7 +23,6 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import androidx.constraintlayout.motion.widget.Debug; import androidx.constraintlayout.motion.widget.MotionLayout; import androidx.constraintlayout.widget.ConstraintLayout; @@ -75,7 +74,7 @@ private void dumpJson() { if (mLayout instanceof MotionLayout) { fileName = MotionLayoutToJason.writeJSonToFile((MotionLayout) mLayout, layout_name); } else { - fileName = ConstraintLayoutToJason.writeJSonToFile(mLayout, layout_name); + fileName = ConstraintLayoutToJason.toFile(mLayout, layout_name); } allFiles.add(fileName); current++; diff --git a/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/ConstraintLayoutToJason.java b/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/ConstraintLayoutToJason.java index 023c59dad..db6e94b36 100644 --- a/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/ConstraintLayoutToJason.java +++ b/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/ConstraintLayoutToJason.java @@ -7,6 +7,7 @@ import android.os.Environment; import android.util.Log; import android.view.View; +import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; @@ -24,142 +25,161 @@ import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; +import java.util.concurrent.atomic.AtomicInteger; -public class MotionLayoutToJason { - MotionLayout mMotionLayout; - Context mContext; - private String TAG = "ML_DEBUG"; +public class ConstraintLayoutToJason { + private static String TAG = "ML_DEBUG"; + HashMap names = new HashMap<>(); + private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1); + private ConstraintLayoutToJason() { + } + private static String escape(String str) { + return str.replaceAll("\'", "\\'"); + } - public void writeJSonToFile(MotionLayout motionLayout, String name) { + private static int generateViewId() { + for (; ; ) { + final int result = sNextGeneratedId.get(); + int newValue = result + 1; + if (newValue > 0x00FFFFFF) { + newValue = 1; + } + if (sNextGeneratedId.compareAndSet(result, newValue)) { + return result; + } + } + } + /** + * This writes the JSON5 description of the constraintLayout to a file named fileName.json5 + * in the download directory which can be pulled with: + * "adb pull "/storage/emulated/0/Download/" ." + * + * @param constraintLayout + * @param fileName + * @return + */ + public static String toFile(ConstraintLayout constraintLayout, String fileName) { FileOutputStream outputStream; + ConstraintLayoutToJason c = new ConstraintLayoutToJason(); try { File down = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - File file = new File(down, name + ".json5"); + File file = new File(down, fileName + ".json5"); outputStream = new FileOutputStream(file); - // Write data to file - outputStream.write(motionLayoutToJson(motionLayout).getBytes()); - // Close the file + outputStream.write(c.constraintLayoutToJson(constraintLayout).getBytes()); outputStream.close(); - Log.v(TAG, "\"" + file.getCanonicalPath() + "\""); + return file.getCanonicalPath(); } catch (IOException e) { e.printStackTrace(); } + return null; + } + + /** + * This Logs the json to the LOG + * + * @param tag The tag use in Log + * @param constraintLayout + */ + public static void log(String tag, ConstraintLayout constraintLayout) { + ConstraintLayoutToJason c = new ConstraintLayoutToJason(); + Log.v(tag, c.constraintLayoutToJson(constraintLayout)); } - public void setMotionLayout(MotionLayout motionLayout) { - Log.v(TAG, motionLayoutToJson(motionLayout)); + /** + * Get a JSON5 String that represents the Constraints in a running ConstraintLayout + * + * @param constraintLayout + * @return + */ + public static String asString(ConstraintLayout constraintLayout) { + ConstraintLayoutToJason c = new ConstraintLayoutToJason(); + return c.constraintLayoutToJson(constraintLayout); } - public String motionLayoutToJson(MotionLayout motionLayout) { - mMotionLayout = motionLayout; - mContext = motionLayout.getContext(); - int[] mid = motionLayout.getConstraintSetIds(); + private String constraintLayoutToJson(ConstraintLayout constraintLayout) { StringWriter writer = new StringWriter(); - writer.append("{\n"); - writeTransitions(writer); - writeWidgets(writer, motionLayout); - writer.append(" ConstraintSets:{\n"); - for (int i = 0; i < mid.length; i++) { - String name = Debug.getName(motionLayout.getContext(), mid[i]); - if (name.equals("motion_base")) { - continue; - } - ConstraintSet set = motionLayout.getConstraintSet(mid[i]); - try { - writer.append(name + ":"); - new WriteJsonEngine(writer, set, motionLayout, 0).writeLayout(); - writer.append("\n"); - } catch (IOException e) { - throw new RuntimeException(e); + int count = constraintLayout.getChildCount(); + for (int i = 0; i < count; i++) { + View v = constraintLayout.getChildAt(i); + if (v.getId() == -1) { + int id; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + id = View.generateViewId(); + } else { + id = generateViewId();; + } + v.setId(id); + names.put(id, "noid_" + v.getClass().getSimpleName()); } } + writer.append("{\n"); + + writeWidgets(writer, constraintLayout); + writer.append(" ConstraintSet:{\n"); + ConstraintSet set = new ConstraintSet(); + set.clone(constraintLayout); + String name = (constraintLayout.getId() == -1) ? "cset" : Debug.getName(constraintLayout); + try { + writer.append(name + ":"); + new WriteJsonEngine(writer, set, constraintLayout, names, 0).writeLayout(); + writer.append("\n"); + } catch (IOException e) { + throw new RuntimeException(e); + } + writer.append(" }\n"); writer.append("}\n"); return writer.toString(); - - } - - String[] autoName = { - "'none'", - "'jumpToStart'", - "'jumpToEnd'", - "'jumpToStart'", - "'animateToEnd'", - "'animateToStart'" - }; - - String[] arcMode = { - "'none'", "'startVertical'", "'startHorizontal'", "'flip'", "'arcDown'", "'arcUp'" - }; - - private void writeTransitions(StringWriter writer) { - ArrayList t = mMotionLayout.getDefinedTransitions(); - writer.append("Transitions:{\n"); - int titleCount = 0; - for (MotionScene.Transition transition : t) { - int id = transition.getId(); - if (id == -1) { - writer.append(((titleCount == 0) ? " default" : (" default" + (titleCount + 1))) + ":{\n"); - titleCount++; - } else { - writer.append(Debug.getName(mContext, id) + ":{\n"); - } - int from = transition.getStartConstraintSetId(); - int to = transition.getEndConstraintSetId(); - writer.append(" from: '" + Debug.getName(mContext, from) + "',\n"); - writer.append(" to: '" + Debug.getName(mContext, to) + "',\n"); - int dur = transition.getDuration(); - writer.append(" duration: " + dur + ",\n"); - - int auto = transition.getAutoTransition(); - if (auto != MotionScene.Transition.AUTO_NONE) { - writer.append(" auto: " + autoName[auto] + ",\n"); - } - int arc = transition.getPathMotionArc(); - if (arc != UNSET) { - writer.append(" pathMotionArc: " + arcMode[arc] + ",\n"); - } - float stagger = transition.getStagger(); - if (stagger != 0.0f) { - writer.append(" stagger: " + stagger + ",\n"); - - } - writer.append(" }\n"); - - } - writer.append("},\n"); } - private void writeWidgets(StringWriter writer, MotionLayout motionLayout) { + private void writeWidgets(StringWriter writer, ConstraintLayout constraintLayout) { writer.append("Widgets:{\n"); - int count = motionLayout.getChildCount(); + int count = constraintLayout.getChildCount(); - for (int i = 0; i < count; i++) { - View v = motionLayout.getChildAt(i); + for (int i = -1; i < count; i++) { + View v = (i == -1) ? constraintLayout : constraintLayout.getChildAt(i); int id = v.getId(); - String name = Debug.getName(v); + String name = (names.containsKey(id)) ? names.get(id) + : ((i == -1) ? "parent" : Debug.getName(v)); String cname = v.getClass().getSimpleName(); - String bounds = ", bounds: [" + v.getLeft() + ", " + v.getTop() + ", " + v.getRight() + ", " + v.getBottom() + "]},\n"; - if (cname.contains("Text")) { + String bounds = ", bounds: [" + v.getLeft() + ", " + v.getTop() + + ", " + v.getRight() + ", " + v.getBottom() + "]},\n"; + writer.append(" " + name + ": { "); + if (i == -1) { + writer.append("type: '" + v.getClass().getSimpleName() + "' , "); + + try { + ViewGroup.LayoutParams p = (ViewGroup.LayoutParams) v.getLayoutParams(); + + String w = p.width == -1 ? "'MATCH_PARENT'" : + (p.width == -2) ? "'WRAP_CONTENT'" : p.width + ""; + writer.append("width: " + w + ", "); + String h = p.height == -1 ? "'MATCH_PARENT'" : + (p.height == -2) ? "'WRAP_CONTENT'" : p.height + ""; + writer.append("height: ").append(h); + } catch (Exception e) { + + } + } else if (cname.contains("Text")) { if (v instanceof TextView) { - writer.append(name + ": { type: 'Text', label: '" + ((TextView) v).getText()+"'" ); + writer.append("type: 'Text', label: '" + escape(((TextView) v).getText().toString()) + "'"); } else { - writer.append(name + ": { type: 'Text' },\n"); + writer.append("type: 'Text' },\n"); } } else if (cname.contains("Button")) { if (v instanceof Button) { - writer.append(name + ": { type: 'Button', label: '" + ((Button) v).getText()); + writer.append("type: 'Button', label: '" + ((Button) v).getText()+"'"); } else - writer.append(name + ": { type: 'Button'"); + writer.append("type: 'Button'"); } else if (cname.contains("Image")) { - writer.append(name + ": { type: 'Image'"); + writer.append("type: 'Image'"); } else if (cname.contains("View")) { - writer.append(name + ": { type: 'Box'"); + writer.append("type: 'Box'"); } else { - writer.append(name + ": { type: '"+v.getClass().getSimpleName() +"'"); + writer.append("type: '" + v.getClass().getSimpleName() + "'"); } writer.append(bounds); } @@ -196,32 +216,38 @@ static class WriteJsonEngine { final String mTOP = "top"; final String mSTART = "start"; final String mEND = "end"; - private static final String SPACE = " "; - private static final String SP = " "; - - WriteJsonEngine(Writer writer, ConstraintSet set, ConstraintLayout layout, int flags) throws IOException { + private static final String INDENT = " "; + private static final String SMALL_INDENT = " "; + HashMap names; + + WriteJsonEngine(Writer writer, + ConstraintSet set, + ConstraintLayout layout, + HashMap names, + int flags) throws IOException { this.mWriter = writer; this.mLayout = layout; + this.names = names; this.mContext = layout.getContext(); this.mFlags = flags; this.set = set; set.getConstraint(2); } - int[] getIDs() { + private int[] getIDs() { return set.getKnownIds(); } - ConstraintSet.Constraint getConstraint(int id) { + private ConstraintSet.Constraint getConstraint(int id) { return set.getConstraint(id); } - void writeLayout() throws IOException { + private void writeLayout() throws IOException { mWriter.write("{\n"); for (Integer id : getIDs()) { ConstraintSet.Constraint c = getConstraint(id); String idName = getSimpleName(id); - mWriter.write(SP + idName + ":{\n"); + mWriter.write(SMALL_INDENT + idName + ":{\n"); ConstraintSet.Layout l = c.layout; if (l.mReferenceIds != null) { String ref = "type: '_" + idName + "_' , contains: ["; @@ -232,7 +258,7 @@ void writeLayout() throws IOException { mWriter.write(ref + "]\n"); } if (l.mReferenceIdString != null) { - String ref = SP + "type: '???' , contains: ["; + String ref = SMALL_INDENT + "type: '???' , contains: ["; String[] rids = l.mReferenceIdString.split(","); for (int r = 0; r < rids.length; r++) { String rid = rids[r]; @@ -289,27 +315,35 @@ void writeLayout() throws IOException { } writeTransform(c.transform); writeCustom(c.mCustomConstraints); - writeMotion(c.motion); + mWriter.write(" },\n"); } mWriter.write("},\n"); } - private void writeMotion(ConstraintSet.Motion motion) { - } - - private void writeTransform(ConstraintSet.Transform transform) { + private void writeTransform(ConstraintSet.Transform transform) throws IOException { + if (transform.applyElevation) { + writeVariable("elevation", transform.elevation); + } + writeVariable("rotationX", transform.rotationX, 0); + writeVariable("rotationY", transform.rotationY, 0); + writeVariable("rotationZ", transform.rotation, 0); + writeVariable("scaleX", transform.scaleX, 1); + writeVariable("scaleY", transform.scaleY, 1); + writeVariable("translationX", transform.translationX, 0); + writeVariable("translationY", transform.translationY, 0); + writeVariable("translationZ", transform.translationZ, 0); } - void writeCustom(HashMap cset) throws IOException { + private void writeCustom(HashMap cset) throws IOException { if (cset != null && cset.size() > 0) { - mWriter.write(SPACE + "custom: {\n"); + mWriter.write(INDENT + "custom: {\n"); for (String s : cset.keySet()) { ConstraintAttribute attr = cset.get(s); - String custom = SPACE + SP + attr.getName() + ": "; + String custom = INDENT + SMALL_INDENT + attr.getName() + ": "; switch (attr.getType()) { case INT_TYPE: custom += attr.getIntegerValue(); @@ -335,11 +369,11 @@ void writeCustom(HashMap cset) throws IOException { mWriter.write(custom + ",\n"); } } - mWriter.write(SP + " } \n"); + mWriter.write(SMALL_INDENT + " } \n"); } } - static String colorString(int v) { + private static String colorString(int v) { String str = "00000000" + Integer.toHexString(v); return "#" + str.substring(str.length() - 8); } @@ -368,13 +402,13 @@ private void writeDimension(String dimString, String s = "-----"; switch (dimDefault) { case 0: // spread - s = SPACE + dimString + ": {value:'spread'"; + s = INDENT + dimString + ": {value:'spread'"; break; case 1: // wrap - s = SPACE + dimString + ": {value:'wrap'"; + s = INDENT + dimString + ": {value:'wrap'"; break; case 2: // percent - s = SPACE + dimString + ": {value: '" + dimPercent + "%'"; + s = INDENT + dimString + ": {value: '" + dimPercent + "%'"; break; } if (dimMax != UNSET) { @@ -392,25 +426,24 @@ private void writeDimension(String dimString, case 0: // spread is the default break; case 1: // wrap - mWriter.write(SPACE + dimString + ": '???????????',\n"); + mWriter.write(INDENT + dimString + ": '???????????',\n"); return; case 2: // percent - mWriter.write(SPACE + dimString + ": '" + dimPercent + "%',\n"); - return; + mWriter.write(INDENT + dimString + ": '" + dimPercent + "%',\n"); } } else if (dim == -2) { - mWriter.write(SPACE + dimString + ": 'wrap',\n"); + mWriter.write(INDENT + dimString + ": 'wrap',\n"); } else if (dim == -1) { - mWriter.write(SPACE + dimString + ": 'parent',\n"); + mWriter.write(INDENT + dimString + ": 'parent',\n"); } else { - mWriter.write(SPACE + dimString + ": " + dim + ",\n"); + mWriter.write(INDENT + dimString + ": " + dim + ",\n"); } } HashMap mIdMap = new HashMap<>(); - String getSimpleName(int id) { + private String getSimpleName(int id) { if (mIdMap.containsKey(id)) { return "" + mIdMap.get(id); } @@ -422,12 +455,15 @@ String getSimpleName(int id) { return "" + name + ""; } - String getName(int id) { - return "\'" + getSimpleName(id) + "\'"; + private String getName(int id) { + return "'" + getSimpleName(id) + "'"; } - String lookup(int id) { + private String lookup(int id) { try { + if (names.containsKey(id)) { + return names.get(id); + } if (id != -1) { return mContext.getResources().getResourceEntryName(id); } else { @@ -438,103 +474,91 @@ String lookup(int id) { } } - void writeConstraint(String my, - int leftToLeft, - String other, - int margin, - int goneMargin) throws IOException { - if (leftToLeft == UNSET) { + private void writeConstraint(String my, + int constraint, + String other, + int margin, + int goneMargin) throws IOException { + if (constraint == UNSET) { return; } - mWriter.write(SPACE + my); + mWriter.write(INDENT + my); mWriter.write(":["); - mWriter.write(getName(leftToLeft)); + mWriter.write(getName(constraint)); mWriter.write(", "); mWriter.write("'" + other + "'"); if (margin != 0 || goneMargin != UNSET_GONE_MARGIN) { mWriter.write(", " + margin); - if (goneMargin != 0) { + if (goneMargin != UNSET_GONE_MARGIN) { mWriter.write(", " + goneMargin); } } mWriter.write("],\n"); - } - void writeCircle(int circleConstraint, - float circleAngle, - int circleRadius) throws IOException { + private void writeCircle(int circleConstraint, + float circleAngle, + int circleRadius) throws IOException { if (circleConstraint == UNSET) { return; } - mWriter.write(SPACE + "circle"); + mWriter.write(INDENT + "circle"); mWriter.write(":["); mWriter.write(getName(circleConstraint)); mWriter.write(", " + circleAngle); - mWriter.write(circleRadius + "]"); + mWriter.write(circleRadius + "],\n"); } - void writeVariable(String name, int value) throws IOException { + private void writeVariable(String name, int value) throws IOException { if (value == 0 || value == -1) { return; } - mWriter.write(SPACE + name); - mWriter.write(":"); - - mWriter.write(", " + value); - mWriter.write("\n"); - + mWriter.write(INDENT + name); + mWriter.write(": " + value); + mWriter.write(",\n"); } - void writeVariable(String name, float value) throws IOException { + private void writeVariable(String name, float value) throws IOException { if (value == UNSET) { return; } - mWriter.write(SPACE + name); - + mWriter.write(INDENT + name); mWriter.write(": " + value); mWriter.write(",\n"); - } - void writeVariable(String name, float value, float def) throws IOException { + private void writeVariable(String name, float value, float def) throws IOException { if (value == def) { return; } - mWriter.write(SPACE + name); - + mWriter.write(INDENT + name); mWriter.write(": " + value); mWriter.write(",\n"); - } - void writeVariable(String name, boolean value) throws IOException { + private void writeVariable(String name, boolean value) throws IOException { if (!value) { return; } - mWriter.write(SPACE + name); - + mWriter.write(INDENT + name); mWriter.write(": " + value); mWriter.write(",\n"); - } - void writeVariable(String name, boolean value, boolean def) throws IOException { + private void writeVariable(String name, boolean value, boolean def) throws IOException { if (value == def) { return; } - mWriter.write(SPACE + name); - + mWriter.write(INDENT + name); mWriter.write(": " + value); mWriter.write(",\n"); - } - void writeVariable(String name, int[] value) throws IOException { + private void writeVariable(String name, int[] value) throws IOException { if (value == null) { return; } - mWriter.write(SPACE + name); + mWriter.write(INDENT + name); mWriter.write(": "); for (int i = 0; i < value.length; i++) { mWriter.write(((i == 0) ? "[" : ", ") + getName(value[i])); @@ -542,15 +566,13 @@ void writeVariable(String name, int[] value) throws IOException { mWriter.write("],\n"); } - void writeVariable(String name, String value) throws IOException { + private void writeVariable(String name, String value) throws IOException { if (value == null) { return; } - mWriter.write(SPACE + name); - mWriter.write(":"); - mWriter.write(", " + value); - mWriter.write("\n"); - + mWriter.write(INDENT + name); + mWriter.write(": '" + value); + mWriter.write("',\n"); } } diff --git a/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/MotionLayoutToJason.java b/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/MotionLayoutToJason.java index 603849209..706679750 100644 --- a/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/MotionLayoutToJason.java +++ b/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/MotionLayoutToJason.java @@ -78,7 +78,7 @@ public static String writeJSonToFile(MotionLayout motionLayout, String name) { return null; } - public static void setMotionLayout(MotionLayout motionLayout) { + public static void logMotionLayout(MotionLayout motionLayout) { MotionLayoutToJason m = new MotionLayoutToJason(); Log.v(TAG, m.motionLayoutToJson(motionLayout)); diff --git a/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/VerificationActivity.java b/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/VerificationActivity.java index 3ebf9890d..66a58a9f6 100644 --- a/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/VerificationActivity.java +++ b/projects/MotionLayoutVerification/app/src/main/java/android/support/constraint/app/VerificationActivity.java @@ -143,9 +143,8 @@ MotionLayout findMotionLayout(ViewGroup group) { } public void dumpMotionLayout() { - MotionLayoutToJason mlJson = new MotionLayoutToJason(); Log.v(TAG,Debug.getLoc()+" MotionLayoutToJason "); - mlJson.setMotionLayout(mMotionLayout); + MotionLayoutToJason.logMotionLayout(mMotionLayout); } @Override diff --git a/projects/MotionLayoutVerification/app/src/main/res/layout/constraint_set_01.xml b/projects/MotionLayoutVerification/app/src/main/res/layout/constraint_set_01.xml index d30d62366..2bd66364e 100644 --- a/projects/MotionLayoutVerification/app/src/main/res/layout/constraint_set_01.xml +++ b/projects/MotionLayoutVerification/app/src/main/res/layout/constraint_set_01.xml @@ -4,8 +4,16 @@ android:id="@+id/rootView" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white" - > + android:background="@color/white"> + + - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/toolsAndroid/CLAnalyst/app/src/main/java/android/support/clanalyst/DumpCL.java b/toolsAndroid/CLAnalyst/app/src/main/java/android/support/clanalyst/DumpCL.java index 980c7df01..63d90a7b5 100644 --- a/toolsAndroid/CLAnalyst/app/src/main/java/android/support/clanalyst/DumpCL.java +++ b/toolsAndroid/CLAnalyst/app/src/main/java/android/support/clanalyst/DumpCL.java @@ -174,7 +174,7 @@ private void writeWidgets(StringWriter writer, ConstraintLayout constraintLayout } } else if (cname.contains("Button")) { if (v instanceof Button) { - writer.append("type: 'Button', label: '" + ((Button) v).getText()); + writer.append("type: 'Button', label: '" + ((Button) v).getText()+"'"); } else writer.append("type: 'Button'"); } else if (cname.contains("Image")) { diff --git a/toolsAndroid/CLAnalyst/app/src/main/res/layout/foo.xml b/toolsAndroid/CLAnalyst/app/src/main/res/layout/foo.xml new file mode 100644 index 000000000..6bf5d4472 --- /dev/null +++ b/toolsAndroid/CLAnalyst/app/src/main/res/layout/foo.xml @@ -0,0 +1,85 @@ + + + + + +