diff options
author | Isaac Chai <ichai@google.com> | 2018-10-02 13:43:09 -0700 |
---|---|---|
committer | Isaac Chai <ichai@google.com> | 2018-10-02 13:43:09 -0700 |
commit | c0de6383b1db33a235451a7b0f806a0b2d1fae6a (patch) | |
tree | 54b11b1b87d1082cdd32945caf85fb30cc3f8c63 | |
parent | c3ea82fc0bdbaa65f61066816c35147c9b26a9bf (diff) | |
download | sherpa-studio-beta.tar.gz |
Merging constraint layout 1.1studio-4.3.0studio-4.2.0studio-4.1.1studio-4.1.0studio-4.0.0studio-2024.1.2-canary1studio-2024.1.1-canary8studio-2024.1.1-canary6studio-2024.1.1-beta2studio-2023.3.1-rc2studio-2023.3.1-canary8studio-2023.3.1-canary03studio-2023.3.1studio-2023.2.1-rc1studio-2023.2.1-canary05studio-2023.2.1-beta02studio-2023.2.1studio-2023.1.1-patch2studio-2023.1.1-patch1studio-2023.1.1-canary3studio-2023.1.1-canary11studio-2023.1.1-beta05studio-2023.1.1studio-2022.3.1-rc1studio-2022.3.1-canary8studio-2022.3.1-canary1studio-2022.3.1-beta2studio-2022.3.1studio-2022.2.1-canary7studio-2022.2.1-canary2studio-2022.2.1-canary10studio-2022.2.1-beta5studio-2022.2.1-beta1studio-2022.2.1studio-2022.1.1-rc3studio-2022.1.1-canary2studio-2022.1.1-canarystudio-2022.1.1-beta4studio-2022.1.1-beta2studio-2022.1.1studio-2021.3.1-betastudio-2021.3.1studio-2021.2.1-patch2studio-2021.2.1-patch1studio-2021.2.1studio-2021.1.1studio-2020.3.1studio-master-devstudio-mainstudio-canarystudio-betastudio-2022.1.1-canarymirror-goog-studio-master-devmirror-goog-studio-main
Bug: N/A
Test: Done in another repository
Change-Id: I53b124f88dae4b0e1c1b3f6eef93d7c0d95db4d0
19 files changed, 2111 insertions, 225 deletions
diff --git a/constraintlayout/src/main/java/android/support/constraint/Barrier.java b/constraintlayout/src/main/java/android/support/constraint/Barrier.java index e3e28b1..65eac8d 100644 --- a/constraintlayout/src/main/java/android/support/constraint/Barrier.java +++ b/constraintlayout/src/main/java/android/support/constraint/Barrier.java @@ -107,8 +107,8 @@ public class Barrier extends ConstraintHelper { */ public static final int END = START + 1; - private int mIndicatedType = LEFT; - private int mResolvedType = LEFT; + private int mIndicatedType; + private int mResolvedType; private android.support.constraint.solver.widgets.Barrier mBarrier; public Barrier(Context context) { @@ -173,8 +173,8 @@ public class Barrier extends ConstraintHelper { } /** - * @hide * @param attrs + * @hide */ @Override protected void init(AttributeSet attrs) { @@ -196,4 +196,12 @@ public class Barrier extends ConstraintHelper { validateParams(); } + public void setAllowsGoneWidget(boolean supportGone) { + mBarrier.setAllowsGoneWidget(supportGone); + } + + public boolean allowsGoneWidget() { + return mBarrier.allowsGoneWidget(); + } + } diff --git a/constraintlayout/src/main/java/android/support/constraint/ConstraintHelper.java b/constraintlayout/src/main/java/android/support/constraint/ConstraintHelper.java index d496899..790b658 100644 --- a/constraintlayout/src/main/java/android/support/constraint/ConstraintHelper.java +++ b/constraintlayout/src/main/java/android/support/constraint/ConstraintHelper.java @@ -41,7 +41,7 @@ public abstract class ConstraintHelper extends View { /** * @hide */ - protected int mCount = 0; + protected int mCount; /** * @hide @@ -50,7 +50,7 @@ public abstract class ConstraintHelper extends View { /** * @hide */ - protected android.support.constraint.solver.widgets.Helper mHelperWidget = null; + protected android.support.constraint.solver.widgets.Helper mHelperWidget; /** * @hide */ @@ -235,7 +235,7 @@ public abstract class ConstraintHelper extends View { mHelperWidget.removeAllIds(); for (int i = 0; i < mCount; i++) { int id = mIds[i]; - View view = container.findViewById(id); + View view = container.getViewById(id); if (view != null) { mHelperWidget.add(container.getViewWidget(view)); } diff --git a/constraintlayout/src/main/java/android/support/constraint/ConstraintLayout.java b/constraintlayout/src/main/java/android/support/constraint/ConstraintLayout.java index 0d01e33..0b080dc 100644 --- a/constraintlayout/src/main/java/android/support/constraint/ConstraintLayout.java +++ b/constraintlayout/src/main/java/android/support/constraint/ConstraintLayout.java @@ -27,6 +27,12 @@ import android.os.Build; import android.support.constraint.solver.LinearSystem; import android.support.constraint.solver.Metrics; import android.support.constraint.solver.widgets.*; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.constraint.solver.widgets.ConstraintAnchor; +import android.support.constraint.solver.widgets.ConstraintWidget; +import android.support.constraint.solver.widgets.ConstraintWidgetContainer; import android.support.constraint.solver.widgets.Guideline; import android.util.AttributeSet; import android.util.Log; @@ -484,7 +490,7 @@ public class ConstraintLayout extends ViewGroup { private static final boolean CACHE_MEASURED_DIMENSION = false; /** @hide */ - public static final String VERSION = "ConstraintLayout-1.1.2"; + public static final String VERSION = "ConstraintLayout-1.1.3"; private static final String TAG = "ConstraintLayout"; private static final boolean USE_CONSTRAINTS_HELPER = true; @@ -1126,6 +1132,12 @@ public class ConstraintLayout extends ViewGroup { return mLayoutWidget; } else { View view = mChildrenByIds.get(id); + if (view == null) { + view = findViewById(id); + if (view != null && view != this && view.getParent() == this) { + onViewAdded(view); + } + } if (view == this) { return mLayoutWidget; } @@ -1542,9 +1554,12 @@ public class ConstraintLayout extends ViewGroup { setSelfDimensionBehaviour(widthMeasureSpec, heightMeasureSpec); int startingWidth = mLayoutWidget.getWidth(); int startingHeight = mLayoutWidget.getHeight(); + + boolean runAnalyzer = false; if (mDirtyHierarchy) { mDirtyHierarchy = false; updateHierarchy(); + runAnalyzer = true; } final boolean optimiseDimensions = (mOptimizationLevel & Optimizer.OPTIMIZATION_DIMENSIONS) @@ -1564,6 +1579,43 @@ public class ConstraintLayout extends ViewGroup { return; } + if (getChildCount() > 0 && runAnalyzer) { + Analyzer.determineGroups(mLayoutWidget); + } + if (mLayoutWidget.mGroupsWrapOptimized) { + if (mLayoutWidget.mHorizontalWrapOptimized && widthMode == MeasureSpec.AT_MOST) { + if (mLayoutWidget.mWrapFixedWidth < widthSize) { + mLayoutWidget.setWidth(mLayoutWidget.mWrapFixedWidth); + } + mLayoutWidget + .setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED); + } + if (mLayoutWidget.mVerticalWrapOptimized && heightMode == MeasureSpec.AT_MOST) { + if (mLayoutWidget.mWrapFixedHeight < heightSize) { + mLayoutWidget.setHeight(mLayoutWidget.mWrapFixedHeight); + } + mLayoutWidget + .setVerticalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.FIXED); + } + } + // Reposition widgets dependent of layout dimension when necessary. + if ((mOptimizationLevel & Optimizer.OPTIMIZATION_GROUPS) == Optimizer.OPTIMIZATION_GROUPS) { + int width = mLayoutWidget.getWidth(); + int height = mLayoutWidget.getHeight(); + if (mLastMeasureWidth != width && widthMode == MeasureSpec.EXACTLY) { + Analyzer.setPosition(mLayoutWidget.mWidgetGroups, HORIZONTAL, width); + } + if (mLastMeasureHeight != height && heightMode == MeasureSpec.EXACTLY) { + Analyzer.setPosition(mLayoutWidget.mWidgetGroups, VERTICAL, height); + } + if (mLayoutWidget.mHorizontalWrapOptimized && mLayoutWidget.mWrapFixedWidth > widthSize) { + Analyzer.setPosition(mLayoutWidget.mWidgetGroups, HORIZONTAL, widthSize); + } + if (mLayoutWidget.mVerticalWrapOptimized && mLayoutWidget.mWrapFixedHeight > heightSize) { + Analyzer.setPosition(mLayoutWidget.mWidgetGroups, VERTICAL, heightSize); + } + } + // let's solve the linear system. if (getChildCount() > 0) { solveLinearSystem("First pass"); diff --git a/constraintlayout/src/main/java/android/support/constraint/ConstraintSet.java b/constraintlayout/src/main/java/android/support/constraint/ConstraintSet.java index 2e942d3..b54cd80 100644 --- a/constraintlayout/src/main/java/android/support/constraint/ConstraintSet.java +++ b/constraintlayout/src/main/java/android/support/constraint/ConstraintSet.java @@ -33,6 +33,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.lang.reflect.Array; import java.lang.reflect.Field; import java.util.HashMap; import java.util.HashSet; @@ -260,7 +261,14 @@ public class ConstraintSet { private static final int CIRCLE = 61; private static final int CIRCLE_RADIUS = 62; private static final int CIRCLE_ANGLE = 63; - private static final int UNUSED = 64; + + private static final int WIDTH_PERCENT = 69; + private static final int HEIGHT_PERCENT = 70; + private static final int CHAIN_USE_RTL = 71; + private static final int BARRIER_DIRECTION = 72; + private static final int CONSTRAINT_REFERENCED_IDS = 73; + private static final int BARRIER_ALLOWS_GONE_WIDGETS = 74; + private static final int UNUSED = 75; static { mapToConstant.append(R.styleable.ConstraintSet_layout_constraintLeft_toLeftOf, LEFT_TO_LEFT); @@ -333,6 +341,18 @@ public class ConstraintSet { mapToConstant.append(R.styleable.ConstraintSet_layout_constraintCircleRadius, CIRCLE_RADIUS); mapToConstant.append(R.styleable.ConstraintSet_layout_constraintCircleAngle, CIRCLE_ANGLE); mapToConstant.append(R.styleable.ConstraintSet_android_id, VIEW_ID); + + mapToConstant.append(R.styleable.ConstraintSet_layout_constraintWidth_percent, WIDTH_PERCENT); + mapToConstant.append(R.styleable.ConstraintSet_layout_constraintHeight_percent, HEIGHT_PERCENT); + + mapToConstant.append(R.styleable.ConstraintSet_chainUseRtl, CHAIN_USE_RTL); + mapToConstant.append(R.styleable.ConstraintSet_barrierDirection, BARRIER_DIRECTION); + mapToConstant.append(R.styleable.ConstraintSet_constraint_referenced_ids, CONSTRAINT_REFERENCED_IDS); + mapToConstant.append(R.styleable.ConstraintSet_barrierAllowsGoneWidgets, BARRIER_ALLOWS_GONE_WIDGETS); + } + + public Constraint getParameters(int mId) { + return get(mId); } private static class Constraint { @@ -412,9 +432,11 @@ public class ConstraintSet { public int heightMin = UNSET; public float widthPercent = 1; public float heightPercent = 1; + public boolean mBarrierAllowsGoneWidgets = false; public int mBarrierDirection = UNSET; public int mHelperType = UNSET; public int [] mReferenceIds; + public String mReferenceIdString; public Constraint clone() { Constraint clone = new Constraint(); @@ -496,6 +518,7 @@ public class ConstraintSet { clone.circleConstraint = circleConstraint; clone.circleRadius = circleRadius; clone.circleAngle = circleAngle; + clone.mBarrierAllowsGoneWidgets = mBarrierAllowsGoneWidgets; return clone; } @@ -640,6 +663,8 @@ public class ConstraintSet { param.guideEnd = guideEnd; param.width = mWidth; param.height = mHeight; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { param.setMarginStart(startMargin); param.setMarginEnd(endMargin); @@ -718,6 +743,12 @@ public class ConstraintSet { } } } + if (view instanceof Barrier) { + Barrier barrier = ((Barrier) view); + constraint.mBarrierAllowsGoneWidgets = barrier.allowsGoneWidget(); + constraint.mReferenceIds = barrier.getReferencedIds(); + constraint.mBarrierDirection = barrier.getType(); + } } } @@ -775,18 +806,24 @@ public class ConstraintSet { if (mConstraints.containsKey(id)) { used.remove(id); Constraint constraint = mConstraints.get(id); + if (view instanceof Barrier) { + constraint.mHelperType = BARRIER_TYPE; + } if (constraint.mHelperType != UNSET) { switch (constraint.mHelperType) { case BARRIER_TYPE: Barrier barrier = (Barrier) view; barrier.setId(id); - barrier.setReferencedIds(constraint.mReferenceIds); barrier.setType(constraint.mBarrierDirection); - ConstraintLayout.LayoutParams param = constraintLayout - .generateDefaultLayoutParams(); - constraint.applyTo(param); + barrier.setAllowsGoneWidget(constraint.mBarrierAllowsGoneWidgets); + if (constraint.mReferenceIds != null) { + barrier.setReferencedIds(constraint.mReferenceIds); + } else if (constraint.mReferenceIdString != null) { + constraint.mReferenceIds = convertReferenceString(barrier, + constraint.mReferenceIdString); + barrier.setReferencedIds(constraint.mReferenceIds); + } break; - } } ConstraintLayout.LayoutParams param = (ConstraintLayout.LayoutParams) view @@ -825,14 +862,20 @@ public class ConstraintSet { case BARRIER_TYPE: Barrier barrier = new Barrier(constraintLayout.getContext()); barrier.setId(id); - barrier.setReferencedIds(constraint.mReferenceIds); + if (constraint.mReferenceIds != null) { + barrier.setReferencedIds(constraint.mReferenceIds); + } else if (constraint.mReferenceIdString != null) { + constraint.mReferenceIds = convertReferenceString(barrier, + constraint.mReferenceIdString); + barrier.setReferencedIds(constraint.mReferenceIds); + } barrier.setType(constraint.mBarrierDirection); ConstraintLayout.LayoutParams param = constraintLayout .generateDefaultLayoutParams(); + barrier.validateParams(); constraint.applyTo(param); constraintLayout.addView(barrier, param); break; - } } if (constraint.mIsGuideline) { @@ -2366,6 +2409,25 @@ public class ConstraintSet { case DIMENSION_RATIO: c.dimensionRatio = a.getString(attr); break; + case WIDTH_PERCENT: + c.widthPercent = a.getFloat(attr, 1); + break; + case HEIGHT_PERCENT: + c.heightPercent = a.getFloat(attr, 1); + break; + case CHAIN_USE_RTL: + Log.e(TAG, "CURRENTLY UNSUPPORTED"); // TODO add support or remove + // TODO add support or remove c.mChainUseRtl = a.getBoolean(attr,c.mChainUseRtl); + break; + case BARRIER_DIRECTION: + c.mBarrierDirection = a.getInt(attr,c.mBarrierDirection); + break; + case CONSTRAINT_REFERENCED_IDS: + c.mReferenceIdString = a.getString(attr); + break; + case BARRIER_ALLOWS_GONE_WIDGETS: + c.mBarrierAllowsGoneWidgets = a.getBoolean(attr,c.mBarrierAllowsGoneWidgets); + break; case UNUSED: Log.w(TAG, "unused attribute 0x" + Integer.toHexString(attr) + " " + mapToConstant.get(attr)); @@ -2377,4 +2439,41 @@ public class ConstraintSet { } } + private int[] convertReferenceString(View view, String referenceIdString) { + String[] split = referenceIdString.split(","); + Context context = view.getContext(); + int[]tags = new int[split.length]; + int count = 0; + for (int i = 0; i < split.length; i++) { + String idString = split[i]; + idString = idString.trim(); + int tag = 0; + try { + Class res = R.id.class; + Field field = res.getField(idString); + tag = field.getInt(null); + } + catch (Exception e) { + // Do nothing + } + if (tag == 0) { + tag = context.getResources().getIdentifier(idString, "id", + context.getPackageName()); + } + + if (tag == 0 && view.isInEditMode() && view.getParent() instanceof ConstraintLayout) { + ConstraintLayout constraintLayout = (ConstraintLayout) view.getParent(); + Object value = constraintLayout.getDesignInformation(0, idString); + if (value != null && value instanceof Integer) { + tag = (Integer) value; + } + } + tags[count++] = tag; + } + if (count!=split.length) { + tags = Arrays.copyOf(tags,count); + } + return tags; + } + } diff --git a/constraintlayout/src/main/res/values/attrs.xml b/constraintlayout/src/main/res/values/attrs.xml index d9d25dc..6940600 100644 --- a/constraintlayout/src/main/res/values/attrs.xml +++ b/constraintlayout/src/main/res/values/attrs.xml @@ -141,11 +141,12 @@ <!-- Used to set the type of optimization we apply. This is a mask. --> <attr name="layout_optimizationLevel"> <flag name="none" value="0"/> - <flag name="standard" value="3" /> <!-- for now only direct & barriers --> + <flag name="standard" value="7" /> <!-- direct, barriers, chains --> <flag name="direct" value="1"/> <flag name="barrier" value="2"/> <flag name="chains" value="4"/> <flag name="dimensions" value="8"/> + <flag name="groups" value="32"/> </attr> <!-- Specify the style of match constraint --> @@ -340,7 +341,14 @@ <attr name="layout_constraintVertical_chainStyle" /> <attr name="layout_editor_absoluteX" /> <attr name="layout_editor_absoluteY"/> - + <attr name="barrierDirection" /> + <attr name="constraint_referenced_ids" /> + <attr name="android:maxHeight" /> + <attr name="android:maxWidth" /> + <attr name="android:minHeight" /> + <attr name="android:minWidth" /> + <attr name="barrierAllowsGoneWidgets" /> + <attr name="chainUseRtl" /> </declare-styleable> diff --git a/solver/src/main/java/android/support/constraint/solver/widgets/Analyzer.java b/solver/src/main/java/android/support/constraint/solver/widgets/Analyzer.java new file mode 100644 index 0000000..1f48990 --- /dev/null +++ b/solver/src/main/java/android/support/constraint/solver/widgets/Analyzer.java @@ -0,0 +1,550 @@ +/* + * Copyright (C) 2018 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 android.support.constraint.solver.widgets; + +import android.support.constraint.solver.widgets.ConstraintWidget.DimensionBehaviour; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class to do widget constraints analysis. + * <p> + * Identify groups of widgets independent from each other. + * TODO: Identify Chains here instead. + */ +public class Analyzer { + + private Analyzer() { + } + + /** + * Find groups of constrained widgets. + * <p> + * Used to simplify the resolution process to layout the widgets when using optimizations. + * Wrap_content layouts require measuring the final size, groups are identified when + * the layout can be measured. + * + * @param layoutWidget Layout to analyze. + */ + public static void determineGroups(ConstraintWidgetContainer layoutWidget) { + if ((layoutWidget.getOptimizationLevel() & Optimizer.OPTIMIZATION_GROUPS) != Optimizer.OPTIMIZATION_GROUPS) { + singleGroup(layoutWidget); + return; + } + layoutWidget.mSkipSolver = true; + layoutWidget.mGroupsWrapOptimized = false; + layoutWidget.mHorizontalWrapOptimized = false; + layoutWidget.mVerticalWrapOptimized = false; + final List<ConstraintWidget> widgets = layoutWidget.mChildren; + final List<ConstraintWidgetGroup> widgetGroups = layoutWidget.mWidgetGroups; + boolean horizontalWrapContent = layoutWidget.getHorizontalDimensionBehaviour() == DimensionBehaviour.WRAP_CONTENT; + boolean verticalWrapContent = layoutWidget.getVerticalDimensionBehaviour() == DimensionBehaviour.WRAP_CONTENT; + boolean hasWrapContent = horizontalWrapContent || verticalWrapContent; + widgetGroups.clear(); + + for (ConstraintWidget widget : widgets) { + widget.mBelongingGroup = null; + widget.mGroupsToSolver = false; + widget.resetResolutionNodes(); + } + for (ConstraintWidget widget : widgets) { + if (widget.mBelongingGroup == null) { + if (!determineGroups(widget, widgetGroups, hasWrapContent)) { + singleGroup(layoutWidget); + layoutWidget.mSkipSolver = false; + return; + } + } + } + int measuredWidth = 0; + int measuredHeight = 0; + // Resolve solvable widgets. + for (ConstraintWidgetGroup group : widgetGroups) { + measuredWidth = Math.max(measuredWidth, + getMaxDimension(group, ConstraintWidget.HORIZONTAL)); + measuredHeight = Math.max(measuredHeight, + getMaxDimension(group, ConstraintWidget.VERTICAL)); + } + // Change container to fixed and set resolved dimensions. + if (horizontalWrapContent) { + layoutWidget.setHorizontalDimensionBehaviour(DimensionBehaviour.FIXED); + layoutWidget.setWidth(measuredWidth); + layoutWidget.mGroupsWrapOptimized = true; + layoutWidget.mHorizontalWrapOptimized = true; + layoutWidget.mWrapFixedWidth = measuredWidth; + } + if (verticalWrapContent) { + layoutWidget.setVerticalDimensionBehaviour(DimensionBehaviour.FIXED); + layoutWidget.setHeight(measuredHeight); + layoutWidget.mGroupsWrapOptimized = true; + layoutWidget.mVerticalWrapOptimized = true; + layoutWidget.mWrapFixedHeight = measuredHeight; + } + setPosition(widgetGroups, ConstraintWidget.HORIZONTAL, layoutWidget.getWidth()); + setPosition(widgetGroups, ConstraintWidget.VERTICAL, layoutWidget.getHeight()); + } + + /** + * @param widget Widget being traversed. + * @param widgetGroups Starting list to contain the widgets in this group. + * @param hasWrapContent Indicating if any dimension of the parent is in wrap_content. + * @return False if the group can't be optimized in any way. + */ + private static boolean determineGroups(ConstraintWidget widget, + List<ConstraintWidgetGroup> widgetGroups, boolean hasWrapContent) { + ConstraintWidgetGroup traverseList = new ConstraintWidgetGroup(new ArrayList<ConstraintWidget>(), true); + widgetGroups.add(traverseList); + return traverse(widget, traverseList, widgetGroups, hasWrapContent); + } + + /** + * Recursive function to traverse constrained widgets. + * The objective is to maintain in a single list all the widgets that can be reached through + * their constraints except for their parent. + * + * @param widget Widget being traversed. + * @param upperGroup List being passed down, originally by {@link #determineGroups(ConstraintWidget, List, boolean)}. + * @param widgetGroups List of widget groups identified. + * @param hasWrapContent Indicates if the layout has any dimension as wrap_content. + * @return If the group analysis failed or can't be done. + */ + private static boolean traverse(ConstraintWidget widget, ConstraintWidgetGroup upperGroup, + List<ConstraintWidgetGroup> widgetGroups, boolean hasWrapContent) { + if (widget == null) { + return true; + } + widget.mOptimizerMeasured = false; + ConstraintWidgetContainer layoutWidget = (ConstraintWidgetContainer) widget.getParent(); + if (widget.mBelongingGroup == null) { + // If it hasn't been assigned to a group. + widget.mOptimizerMeasurable = true; + upperGroup.mConstrainedGroup.add(widget); + widget.mBelongingGroup = upperGroup; + // Determine if group is measurable. + if (widget.mLeft.mTarget == null + && widget.mRight.mTarget == null + && widget.mTop.mTarget == null + && widget.mBottom.mTarget == null + && widget.mBaseline.mTarget == null + && widget.mCenter.mTarget == null) { + invalidate(layoutWidget, widget, upperGroup); + if (hasWrapContent) { + return false; + } + } + // Check if it has vertical bias. + if (widget.mTop.mTarget != null && widget.mBottom.mTarget != null) { + // Allow if it has no wrap content in that dimension an constrained to the parent. + boolean wrap = layoutWidget.getVerticalDimensionBehaviour() == DimensionBehaviour.WRAP_CONTENT; + if (hasWrapContent) { + invalidate(layoutWidget, widget, upperGroup); + return false; + } else if (!(widget.mTop.mTarget.mOwner == widget.getParent() + && widget.mBottom.mTarget.mOwner == widget.getParent())) { + invalidate(layoutWidget, widget, upperGroup); + } + } + // Check if it has horizontal bias. + if (widget.mLeft.mTarget != null && widget.mRight.mTarget != null) { + // Allow if it has no wrap content in that dimension an constrained to the parent. + boolean wrap = layoutWidget.getHorizontalDimensionBehaviour() == DimensionBehaviour.WRAP_CONTENT; + if (hasWrapContent) { + invalidate(layoutWidget, widget, upperGroup); + return false; + } else if (!(widget.mLeft.mTarget.mOwner == widget.getParent() + && widget.mRight.mTarget.mOwner == widget.getParent())) { + invalidate(layoutWidget, widget, upperGroup); + } + } + if ((widget.getHorizontalDimensionBehaviour() == DimensionBehaviour.MATCH_CONSTRAINT + ^ widget.getVerticalDimensionBehaviour() == DimensionBehaviour.MATCH_CONSTRAINT) + && widget.mDimensionRatio != 0.0f) { + // Calculate dimension. + resolveDimensionRatio(widget); + } else if (!(widget.getHorizontalDimensionBehaviour() != DimensionBehaviour.MATCH_CONSTRAINT + && widget.getVerticalDimensionBehaviour() != DimensionBehaviour.MATCH_CONSTRAINT)) { + invalidate(layoutWidget, widget, upperGroup); + if (hasWrapContent) { + return false; + } + } + // Is Horizontal start + if (((widget.mLeft.mTarget == null && widget.mRight.mTarget == null) + || (widget.mLeft.mTarget != null && widget.mLeft.mTarget.mOwner == widget.mParent && widget.mRight.mTarget == null) + || (widget.mRight.mTarget != null && widget.mRight.mTarget.mOwner == widget.mParent && widget.mLeft.mTarget == null) + || (widget.mLeft.mTarget != null && widget.mLeft.mTarget.mOwner == widget.mParent + && widget.mRight.mTarget != null && widget.mRight.mTarget.mOwner == widget.mParent)) + && (widget.mCenter.mTarget == null)) { + if (!(widget instanceof Guideline) && !(widget instanceof Helper)) { + upperGroup.mStartHorizontalWidgets.add(widget); + } + + } + // Is Vertical start + if (((widget.mTop.mTarget == null && widget.mBottom.mTarget == null) + || (widget.mTop.mTarget != null && widget.mTop.mTarget.mOwner == widget.mParent && widget.mBottom.mTarget == null) + || (widget.mBottom.mTarget != null && widget.mBottom.mTarget.mOwner == widget.mParent && widget.mTop.mTarget == null) + || (widget.mTop.mTarget != null && widget.mTop.mTarget.mOwner == widget.mParent + && widget.mBottom.mTarget != null && widget.mBottom.mTarget.mOwner == widget.mParent)) + && (widget.mCenter.mTarget == null && widget.mBaseline.mTarget == null)) { + if (!(widget instanceof Guideline) && !(widget instanceof Helper)) { + upperGroup.mStartVerticalWidgets.add(widget); + } + } + } else { + // If it has, join the list and re-assign. Remove joint list from mWidgetGroups (if its a different list) + if (widget.mBelongingGroup != upperGroup) { + upperGroup.mConstrainedGroup.addAll(widget.mBelongingGroup.mConstrainedGroup); + upperGroup.mStartHorizontalWidgets.addAll(widget.mBelongingGroup.mStartHorizontalWidgets); + upperGroup.mStartVerticalWidgets.addAll(widget.mBelongingGroup.mStartVerticalWidgets); + if (widget.mBelongingGroup.mSkipSolver == false) { + upperGroup.mSkipSolver = false; + } + widgetGroups.remove(widget.mBelongingGroup); + for (ConstraintWidget auxWidget : widget.mBelongingGroup.mConstrainedGroup) { + auxWidget.mBelongingGroup = upperGroup; + } + } + return true; + } + // Proceed to traverse widgets, start with HelperWidgets since they contain multiple widgets. + if (widget instanceof Helper) { + invalidate(layoutWidget, widget, upperGroup); + if (hasWrapContent) { + return false; + } + final Helper hWidget = (Helper) widget; + for (int widgetsCount = 0; widgetsCount < hWidget.mWidgetsCount; widgetsCount++) { + if (!traverse(hWidget.mWidgets[widgetsCount], upperGroup, widgetGroups, hasWrapContent)) { + return false; + } + } + } + // We traverse every anchor, for wrap_content we ignore center (circular constraints). + final int anchorsSize = widget.mListAnchors.length; + for (int i = 0; i < anchorsSize; i++) { + final ConstraintAnchor anchor = widget.mListAnchors[i]; + if (anchor.mTarget != null && anchor.mTarget.mOwner != widget.getParent()) { + if (anchor.mType == ConstraintAnchor.Type.CENTER) { + invalidate(layoutWidget, widget, upperGroup); + if (hasWrapContent) { + return false; + } + } else { + setConnection(anchor); + } + if (!traverse(anchor.mTarget.mOwner, upperGroup, widgetGroups, hasWrapContent)) { + return false; + } + } + } + return true; + } + + private static void invalidate(ConstraintWidgetContainer layoutWidget, ConstraintWidget widget, ConstraintWidgetGroup group) { + group.mSkipSolver = false; + layoutWidget.mSkipSolver = false; + widget.mOptimizerMeasurable = false; + } + + /** + * Obtain the max length of a {@link ConstraintWidgetGroup} on a specific orientation. + * Length is saved on the group for future use as well. + * + * @param group Group of widgets being measured. + * @param orientation Orientation being measured. + * @return Max dimension in the group. + */ + private static int getMaxDimension(ConstraintWidgetGroup group, int orientation) { + int dimension = 0; + int offset = orientation * 2; + List<ConstraintWidget> startWidgets = group.getStartWidgets(orientation); + final int size = startWidgets.size(); + for (int i = 0; i < size; i++) { + ConstraintWidget widget = startWidgets.get(i); + boolean topLeftFlow = widget.mListAnchors[offset + 1].mTarget == null + || (widget.mListAnchors[offset].mTarget != null + && widget.mListAnchors[offset + 1].mTarget != null); + dimension = Math.max(dimension, getMaxDimensionTraversal(widget, orientation, topLeftFlow, 0)); + } + + group.mGroupDimensions[orientation] = dimension; + return dimension; + } + + /** + * Traverse from a widget at the start of a tree (a widget constrained to any side of their parent), + * find the maximum length of the tree. + * Avoids cases when a widget's dimension shouldn't be considered. + * + * @param widget Widget being traversed. + * @param orientation Dimension being measured (HORIZONTAL/VERTICAL). + * @param topLeftFlow Indicates if the tree starts at the top or left of the container. + * @param depth How far the widget is from the start of the tree. + * @return Max dimension from the widget being traversed. + */ + private static int getMaxDimensionTraversal(ConstraintWidget widget, int orientation, boolean topLeftFlow, int depth) { + // Start and end offset used to point to the correct anchors according to the flow + // of the widget at the start of the tree. + if (!widget.mOptimizerMeasurable) { + return 0; + } + int startOffset; + int endOffset; + int dimension = 0; + int dimensionPre = 0; + int dimensionPost = 0; + final int flow; + final int baselinePreDistance; + final int baselinePostDistance; + // If it has baseline, the dimensions change, despite maintaining the flow. + final boolean hasBaseline = widget.mBaseline.mTarget != null && orientation == ConstraintWidget.VERTICAL; + + if (topLeftFlow) { + baselinePreDistance = widget.getBaselineDistance(); + baselinePostDistance = widget.getHeight() - widget.getBaselineDistance(); + startOffset = orientation * 2; + endOffset = startOffset + 1; + } else { + baselinePreDistance = widget.getHeight() - widget.getBaselineDistance(); + baselinePostDistance = widget.getBaselineDistance(); + endOffset = orientation * 2; + startOffset = endOffset + 1; + } + + // Define the correct flow of direction. left -> right or left <- right. + // If the flow is going opposite from the startWidget, lengths and margin subtract. + if (widget.mListAnchors[endOffset].mTarget != null && widget.mListAnchors[startOffset].mTarget == null) { + flow = -1; + int aux = startOffset; + startOffset = endOffset; + endOffset = aux; + } else { + flow = 1; + } + + if (hasBaseline) { + depth -= baselinePreDistance; + } + // Get position from horizontal/vertical bias. + dimension = widget.mListAnchors[startOffset].getMargin() * flow + getParentBiasOffset(widget, orientation); + int downDepth = dimension + depth; + int postTemp = ((orientation == ConstraintWidget.HORIZONTAL) ? widget.getWidth() : widget.getHeight()) * flow; + for (ResolutionNode targetNode : widget.mListAnchors[startOffset].getResolutionNode().dependents) { + final ResolutionAnchor anchor = (ResolutionAnchor) targetNode; + dimensionPre = Math.max(dimensionPre, getMaxDimensionTraversal(anchor.myAnchor.mOwner, orientation, topLeftFlow, downDepth)); + } + for (ResolutionNode targetNode : widget.mListAnchors[endOffset].getResolutionNode().dependents) { + final ResolutionAnchor anchor = (ResolutionAnchor) targetNode; + dimensionPost = Math.max(dimensionPost, getMaxDimensionTraversal(anchor.myAnchor.mOwner, orientation, topLeftFlow, postTemp + downDepth)); + } + if (hasBaseline) { + dimensionPre -= baselinePreDistance; + dimensionPost += baselinePostDistance; + } else { + dimensionPost += ((orientation == ConstraintWidget.HORIZONTAL) ? widget.getWidth() : widget.getHeight()) * flow; + } + + // Baseline, only add distance from baseline to bottom instead of entire height. + int dimensionBaseline = 0; + if (orientation == ConstraintWidget.VERTICAL) { + for (ResolutionNode targetNode : widget.mBaseline.getResolutionNode().dependents) { + final ResolutionAnchor anchor = (ResolutionAnchor) targetNode; + if (flow == 1) { + dimensionBaseline = Math.max(dimensionBaseline, getMaxDimensionTraversal(anchor.myAnchor.mOwner, orientation, topLeftFlow, baselinePreDistance + downDepth)); + } else { + dimensionBaseline = Math.max(dimensionBaseline, getMaxDimensionTraversal(anchor.myAnchor.mOwner, orientation, topLeftFlow, (baselinePostDistance * flow) + downDepth)); + } + } + if (widget.mBaseline.getResolutionNode().dependents.size() > 0 && !hasBaseline) { + if (flow == 1) { + dimensionBaseline += baselinePreDistance; + } else { + dimensionBaseline -= baselinePostDistance; + } + } + } + + int distanceBeforeWidget = dimension; + dimension += Math.max(dimensionPre, Math.max(dimensionPost, dimensionBaseline)); + int leftTop = depth + distanceBeforeWidget; + int end = leftTop + postTemp; + if (flow == -1) { + int aux = end; + end = leftTop; + leftTop = aux; + } + if (topLeftFlow) { + Optimizer.setOptimizedWidget(widget, orientation, leftTop); + widget.setFrame(leftTop, end, orientation); + } else { + widget.mBelongingGroup.addWidgetsToSet(widget, orientation); + widget.setRelativePositioning(leftTop, orientation); + } + // Assuming widgets with only one dimension on Match_constraint would be measurable. + if (widget.getDimensionBehaviour(orientation) == DimensionBehaviour.MATCH_CONSTRAINT + && widget.mDimensionRatio != 0.0f) { + widget.mBelongingGroup.addWidgetsToSet(widget, orientation); + } + // Assuming is not measurable when the parent is on wrap_content. + if (widget.mListAnchors[startOffset].mTarget != null + && widget.mListAnchors[endOffset].mTarget != null) { + final ConstraintWidget parent = widget.getParent(); + if (widget.mListAnchors[startOffset].mTarget.mOwner == parent + && widget.mListAnchors[endOffset].mTarget.mOwner == parent) { + widget.mBelongingGroup.addWidgetsToSet(widget, orientation); + } + } + return dimension; + } + + private static void setConnection(ConstraintAnchor originAnchor) { + ResolutionNode originNode = originAnchor.getResolutionNode(); + if (originAnchor.mTarget != null && originAnchor.mTarget.mTarget != originAnchor) { + // Go to Owner and add the dependent. + originAnchor.mTarget.getResolutionNode().addDependent(originNode); + } + } + + /** + * Used when the Analyzer cannot simplify in independent groups. + * This will make it so all widgets are included in the same group. + * + * @param layoutWidget ConstrainedWidgetContainer being analyzed. + */ + private static void singleGroup(ConstraintWidgetContainer layoutWidget) { + layoutWidget.mWidgetGroups.clear(); + layoutWidget.mWidgetGroups.add(0, new ConstraintWidgetGroup(layoutWidget.mChildren)); + } + + /** + * Update widgets positions. + * Necessary for widgets dependent on the right/bottom side of the Container. + * + * @param groups Groups of widgets being updated. + * @param orientation Dimension to update on the widgets. + * @param containerLength Length of the widget container. + */ + public static void setPosition(List<ConstraintWidgetGroup> groups, int orientation, int containerLength) { + final int groupsSize = groups.size(); + for (int i = 0; i < groupsSize; i++) { + ConstraintWidgetGroup group = groups.get(i); + for (ConstraintWidget widget : group.getWidgetsToSet(orientation)) { + // We can only update those that we can measure. + if (widget.mOptimizerMeasurable) { + updateSizeDependentWidgets(widget, orientation, containerLength); + } + } + } + } + + /** + * Update the final layout position of widgets that depend on the size of the container. + * Exception for dimension-ratio as a work-around. + * + * @param widget Widget being updated. + * @param orientation Orientation being updated. + * @param containerLength The final container dimension in the orientation. + */ + private static void updateSizeDependentWidgets(ConstraintWidget widget, int orientation, int containerLength) { + final int end; + final int start; + final int offset = orientation * 2; + ConstraintAnchor startAnchor = widget.mListAnchors[offset]; + ConstraintAnchor endAnchor = widget.mListAnchors[offset + 1]; + boolean hasBias = startAnchor.mTarget != null && endAnchor.mTarget != null; + if (hasBias) { + start = getParentBiasOffset(widget, orientation) + startAnchor.getMargin(); + Optimizer.setOptimizedWidget(widget, orientation, start); + return; + } + /* + * ConstraintLayout::internalMeasureChildren() workaround (it would reset the widget's + * dimension even if it was set beforehand). + * It is assumed that the left/top anchor has been resolved. Since only the dimension is being reset. + */ + if (widget.mDimensionRatio != 0.0f && widget.getDimensionBehaviour(orientation) == DimensionBehaviour.MATCH_CONSTRAINT) { + int length = resolveDimensionRatio(widget); + start = (int) widget.mListAnchors[offset].getResolutionNode().resolvedOffset; + end = start + length; + endAnchor.getResolutionNode().resolvedTarget = startAnchor.getResolutionNode(); + endAnchor.getResolutionNode().resolvedOffset = length; + endAnchor.getResolutionNode().state = ResolutionNode.RESOLVED; + widget.setFrame(start, end, orientation); + return; + } + end = containerLength - widget.getRelativePositioning(orientation); + start = end - widget.getLength(orientation); + widget.setFrame(start, end, orientation); + Optimizer.setOptimizedWidget(widget, orientation, start); + } + + /** + * Get the offset of a widget with bias exclusively with the parent. + * Offset is the distance from the left/top side of the parent to the start of the widget. + * + * @param orientation Orientation for the offset. + * @return The distance from the root based on the bias (does not include margin distance). 0 if it can't be calculated. + */ + private static int getParentBiasOffset(ConstraintWidget widget, int orientation) { + int offset = orientation * 2; + ConstraintAnchor startAnchor = widget.mListAnchors[offset]; + ConstraintAnchor endAnchor = widget.mListAnchors[offset + 1]; + if (startAnchor.mTarget != null && startAnchor.mTarget.mOwner == widget.mParent + && endAnchor.mTarget != null && endAnchor.mTarget.mOwner == widget.mParent) { + int length = 0; + int widgetDimension = 0; + float bias = 0.0f; + length = widget.mParent.getLength(orientation); + bias = (orientation == ConstraintWidget.HORIZONTAL) ? widget.mHorizontalBiasPercent : + widget.mVerticalBiasPercent; + widgetDimension = widget.getLength(orientation); + length = length - startAnchor.getMargin() - endAnchor.getMargin(); + length = length - widgetDimension; + length = ((int) ((float) length * bias)); + return length; + } else { + return 0; + } + } + + /** + * Calculate the widget's dimension based on dimension ratio. + * + * @return The dimension calculated. + */ + private static int resolveDimensionRatio(ConstraintWidget widget) { + int length = ConstraintWidget.UNKNOWN; + if (widget.getHorizontalDimensionBehaviour() == DimensionBehaviour.MATCH_CONSTRAINT) { + if (widget.mDimensionRatioSide == ConstraintWidget.HORIZONTAL) { + length = (int) ((float) widget.getHeight() * widget.mDimensionRatio); + } else { + length = (int) ((float) widget.getHeight() / widget.mDimensionRatio); + } + widget.setWidth(length); + } else if (widget.getVerticalDimensionBehaviour() == DimensionBehaviour.MATCH_CONSTRAINT) { + if (widget.mDimensionRatioSide == ConstraintWidget.VERTICAL) { + length = (int) ((float) widget.getWidth() * widget.mDimensionRatio); + } else { + length = (int) ((float) widget.getWidth() / widget.mDimensionRatio); + } + widget.setHeight(length); + } + return length; + } +} diff --git a/solver/src/main/java/android/support/constraint/solver/widgets/Barrier.java b/solver/src/main/java/android/support/constraint/solver/widgets/Barrier.java index f6a09c2..16c8910 100644 --- a/solver/src/main/java/android/support/constraint/solver/widgets/Barrier.java +++ b/solver/src/main/java/android/support/constraint/solver/widgets/Barrier.java @@ -20,6 +20,7 @@ import android.support.constraint.solver.LinearSystem; import android.support.constraint.solver.SolverVariable; import java.util.ArrayList; +import java.util.Arrays; /** * A Barrier takes multiple widgets @@ -47,6 +48,8 @@ public class Barrier extends Helper { public void setAllowsGoneWidget(boolean allowsGoneWidget) { mAllowsGoneWidget = allowsGoneWidget; } + public boolean allowsGoneWidget() { return mAllowsGoneWidget; } + @Override public void resetResolutionNodes() { super.resetResolutionNodes(); diff --git a/solver/src/main/java/android/support/constraint/solver/widgets/Chain.java b/solver/src/main/java/android/support/constraint/solver/widgets/Chain.java index 94a1324..e73dea0 100644 --- a/solver/src/main/java/android/support/constraint/solver/widgets/Chain.java +++ b/solver/src/main/java/android/support/constraint/solver/widgets/Chain.java @@ -242,7 +242,7 @@ class Chain { if (DEBUG) { widget = firstVisibleWidget; while (widget != null) { - next = widget.mListNextVisibleWidget[orientation]; + next = widget.mNextChainWidget[orientation]; widget.mListAnchors[offset].mSolverVariable.setName("" + widget.getDebugName() + ".left"); widget.mListAnchors[offset + 1].mSolverVariable.setName("" + widget.getDebugName() + ".right"); widget = next; @@ -278,7 +278,10 @@ class Chain { ConstraintWidget previousVisibleWidget = firstVisibleWidget; boolean applyFixedEquality = chainHead.mWidgetsMatchCount > 0 && (chainHead.mWidgetsCount == chainHead.mWidgetsMatchCount); while (widget != null) { - next = widget.mListNextVisibleWidget[orientation]; + next = widget.mNextChainWidget[orientation]; + while (next != null && next.getVisibility() == GONE) { + next = next.mNextChainWidget[orientation]; + } if (next != null || widget == lastVisibleWidget) { ConstraintAnchor beginAnchor = widget.mListAnchors[offset]; SolverVariable begin = beginAnchor.mSolverVariable; @@ -331,7 +334,9 @@ class Chain { strength); } } - previousVisibleWidget = widget; + if (widget.getVisibility() != GONE) { + previousVisibleWidget = widget; + } widget = next; } } else if (isChainSpreadInside && firstVisibleWidget != null) { @@ -340,7 +345,10 @@ class Chain { ConstraintWidget previousVisibleWidget = firstVisibleWidget; boolean applyFixedEquality = chainHead.mWidgetsMatchCount > 0 && (chainHead.mWidgetsCount == chainHead.mWidgetsMatchCount); while (widget != null) { - next = widget.mListNextVisibleWidget[orientation]; + next = widget.mNextChainWidget[orientation]; + while (next != null && next.getVisibility() == GONE) { + next = next.mNextChainWidget[orientation]; + } if (widget != firstVisibleWidget && widget != lastVisibleWidget && next != null) { if (next == lastVisibleWidget) { next = null; @@ -383,7 +391,9 @@ class Chain { strength); } } - previousVisibleWidget = widget; + if (widget.getVisibility() != GONE) { + previousVisibleWidget = widget; + } widget = next; } ConstraintAnchor begin = firstVisibleWidget.mListAnchors[offset]; diff --git a/solver/src/main/java/android/support/constraint/solver/widgets/ChainHead.java b/solver/src/main/java/android/support/constraint/solver/widgets/ChainHead.java index df18d4a..8c5bd21 100644 --- a/solver/src/main/java/android/support/constraint/solver/widgets/ChainHead.java +++ b/solver/src/main/java/android/support/constraint/solver/widgets/ChainHead.java @@ -77,6 +77,7 @@ public class ChainHead { private void defineChainProperties(){ int offset = mOrientation * 2; + ConstraintWidget lastVisited = mFirst; // TraverseChain ConstraintWidget widget = mFirst; @@ -84,23 +85,20 @@ public class ChainHead { boolean done = false; while (!done) { mWidgetsCount++; - widget.mListNextVisibleWidget[mOrientation] = null; + widget.mNextChainWidget[mOrientation] = null; widget.mListNextMatchConstraintsWidget[mOrientation] = null; - if(widget.getVisibility() != ConstraintWidget.GONE) { + if (widget.getVisibility() != ConstraintWidget.GONE) { // Visible widgets linked list. if (mFirstVisibleWidget == null) { mFirstVisibleWidget = widget; } - if(mLastVisibleWidget != null){ - mLastVisibleWidget.mListNextVisibleWidget[mOrientation] = widget; - } mLastVisibleWidget = widget; // Match constraint linked list. - if(widget.mListDimensionBehaviors[mOrientation] == DimensionBehaviour.MATCH_CONSTRAINT - && (widget.mResolvedMatchConstraintDefault[mOrientation] == MATCH_CONSTRAINT_SPREAD - || widget.mResolvedMatchConstraintDefault[mOrientation] == MATCH_CONSTRAINT_RATIO - || widget.mResolvedMatchConstraintDefault[mOrientation] == MATCH_CONSTRAINT_PERCENT)) { + if (widget.mListDimensionBehaviors[mOrientation] == DimensionBehaviour.MATCH_CONSTRAINT + && (widget.mResolvedMatchConstraintDefault[mOrientation] == MATCH_CONSTRAINT_SPREAD + || widget.mResolvedMatchConstraintDefault[mOrientation] == MATCH_CONSTRAINT_RATIO + || widget.mResolvedMatchConstraintDefault[mOrientation] == MATCH_CONSTRAINT_PERCENT)) { mWidgetsMatchCount++; float weight = widget.mWeight[mOrientation]; if (weight > 0) { @@ -119,15 +117,19 @@ public class ChainHead { mWeightedMatchConstraintsWidgets.add(widget); } - if(mFirstMatchConstraintWidget == null){ + if (mFirstMatchConstraintWidget == null) { mFirstMatchConstraintWidget = widget; } - if(mLastMatchConstraintWidget != null){ + if (mLastMatchConstraintWidget != null) { mLastMatchConstraintWidget.mListNextMatchConstraintsWidget[mOrientation] = widget; } mLastMatchConstraintWidget = widget; } } + if (lastVisited != widget) { + lastVisited.mNextChainWidget[mOrientation] = widget; + } + lastVisited = widget; // go to the next widget ConstraintAnchor nextAnchor = widget.mListAnchors[offset + 1].mTarget; @@ -148,9 +150,9 @@ public class ChainHead { } mLast = widget; - if(mOrientation == ConstraintWidget.HORIZONTAL && mIsRtl) { + if (mOrientation == ConstraintWidget.HORIZONTAL && mIsRtl) { mHead = mLast; - }else{ + } else { mHead = mFirst; } diff --git a/solver/src/main/java/android/support/constraint/solver/widgets/ConstraintWidget.java b/solver/src/main/java/android/support/constraint/solver/widgets/ConstraintWidget.java index 271be33..d6c0cec 100644 --- a/solver/src/main/java/android/support/constraint/solver/widgets/ConstraintWidget.java +++ b/solver/src/main/java/android/support/constraint/solver/widgets/ConstraintWidget.java @@ -89,6 +89,11 @@ public class ConstraintWidget { int mResolvedDimensionRatioSide = UNKNOWN; float mResolvedDimensionRatio = 1.0f; + /** + * Contains itself and any other widget its connected to. + */ + ConstraintWidgetGroup mBelongingGroup = null; + private int mMaxDimension[] = {Integer.MAX_VALUE, Integer.MAX_VALUE}; private float mCircleConstraintAngle = 0; @@ -104,8 +109,8 @@ public class ConstraintWidget { mMaxDimension[HORIZONTAL] = maxWidth; } - public void setMaxHeight(int maxWidth) { - mMaxDimension[VERTICAL] = maxWidth; + public void setMaxHeight(int maxHeight) { + mMaxDimension[VERTICAL] = maxHeight; } public boolean isSpreadWidth() { @@ -175,6 +180,8 @@ public class ConstraintWidget { // Origin of the widget protected int mX = 0; protected int mY = 0; + int mRelX = 0; + int mRelY = 0; // Current draw position in container's coordinate private int mDrawX = 0; @@ -227,6 +234,9 @@ public class ConstraintWidget { boolean mBottomHasCentered; boolean mHorizontalWrapVisited; boolean mVerticalWrapVisited; + boolean mOptimizerMeasurable = false; + boolean mOptimizerMeasured = false; + boolean mGroupsToSolver = false; // Chain support int mHorizontalChainStyle = CHAIN_SPREAD; @@ -237,7 +247,7 @@ public class ConstraintWidget { float[] mWeight = { UNKNOWN, UNKNOWN}; protected ConstraintWidget[] mListNextMatchConstraintsWidget = {null, null}; - protected ConstraintWidget[] mListNextVisibleWidget = {null, null}; + protected ConstraintWidget[] mNextChainWidget = {null, null}; ConstraintWidget mHorizontalNextWidget = null; ConstraintWidget mVerticalNextWidget = null; @@ -307,6 +317,10 @@ public class ConstraintWidget { if (mResolutionHeight != null) { mResolutionHeight.reset(); } + mBelongingGroup = null; + mOptimizerMeasurable = false; + mOptimizerMeasured = false; + mGroupsToSolver = false; } /*-----------------------------------------------------------------------*/ @@ -829,6 +843,22 @@ public class ConstraintWidget { } /** + * Get a dimension of the widget in a particular orientation. + * + * @param orientation + * @return The dimension of the specified orientation. + */ + public int getLength(int orientation) { + if (orientation == HORIZONTAL) { + return getWidth(); + } else if (orientation == VERTICAL) { + return getHeight(); + } else { + return 0; + } + } + + /** * Return the x position of the widget, relative to the root * * @return x position @@ -967,6 +997,23 @@ public class ConstraintWidget { } /** + * Return the percentage bias that is used when two opposite connections exist of the same + * strength in a particular orientation. + * + * @param orientation Orientation {@link #HORIZONTAL}/{@link #VERTICAL}. + * @return Respective percentage bias. + */ + public float getBiasPercent(int orientation) { + if (orientation == HORIZONTAL) { + return mHorizontalBiasPercent; + } else if (orientation == VERTICAL) { + return mVerticalBiasPercent; + } else { + return UNKNOWN; + } + } + + /** * Return true if this widget has a baseline * * @return true if the widget has a baseline, false otherwise @@ -1182,6 +1229,20 @@ public class ConstraintWidget { } /** + * Set the dimension of a widget in a particular orientation. + * + * @param length Size of the dimension. + * @param orientation + */ + public void setLength(int length, int orientation) { + if (orientation == HORIZONTAL) { + setWidth(length); + } else if (orientation == VERTICAL) { + setHeight(length); + } + } + + /** * Set the horizontal style when MATCH_CONSTRAINT is set * * @param horizontalMatchStyle MATCH_CONSTRAINT_SPREAD or MATCH_CONSTRAINT_WRAP @@ -1430,6 +1491,23 @@ public class ConstraintWidget { if (LinearSystem.FULL_DEBUG) { System.out.println("update from solver " + mDebugName + " " + mX + ":" + mY + " - " + mWidth + " x " + mHeight); } + mOptimizerMeasured = true; + } + + /** + * Set the position+dimension of the widget based on starting/ending positions on one dimension. + * + * @param start Left/Top side position of the widget. + * @param end Right/Bottom side position of the widget. + * @param orientation Orientation being set (HORIZONTAL/VERTICAL). + */ + public void setFrame(int start, int end, int orientation) { + if (orientation == HORIZONTAL) { + setHorizontalDimension(start, end); + } else if (orientation == VERTICAL) { + setVerticalDimension(start, end); + } + mOptimizerMeasured = true; } /** @@ -1461,6 +1539,36 @@ public class ConstraintWidget { } /** + * Get the left/top position of the widget relative to the outer side of the container (right/bottom). + * + * @param orientation + * @return The relative position of the widget. + */ + int getRelativePositioning(int orientation) { + if (orientation == HORIZONTAL) { + return mRelX; + } else if (orientation == VERTICAL) { + return mRelY; + } else { + return 0; + } + } + + /** + * Set the left/top position of the widget relative to the outer side of the container (right/bottom). + * + * @param offset Offset of the relative position. + * @param orientation Orientation of the offset being set. + */ + void setRelativePositioning(int offset, int orientation) { + if (orientation == HORIZONTAL) { + mRelX = offset; + } else if (orientation == VERTICAL) { + mRelY = offset; + } + } + + /** * Set the baseline distance relative to the top of the widget * * @param baseline the distance of the baseline relative to the widget's top @@ -2071,6 +2179,22 @@ public class ConstraintWidget { } /** + * Get the widget's {@link DimensionBehaviour} in an specific orientation. + * + * @param orientation + * @return The {@link DimensionBehaviour} of the widget. + */ + public DimensionBehaviour getDimensionBehaviour(int orientation) { + if (orientation == HORIZONTAL) { + return getHorizontalDimensionBehaviour(); + } else if (orientation == VERTICAL) { + return getVerticalDimensionBehaviour(); + } else { + return null; + } + } + + /** * Set the widget's behaviour for the horizontal dimension * * @param behaviour the horizontal dimension's behaviour @@ -2179,6 +2303,21 @@ public class ConstraintWidget { return found; } + /** + * Determine if the widget is the first element of a chain in a given orientation. + * + * @param orientation Either {@link #HORIZONTAL} or {@link #VERTICAL} + * @return if the widget is the head of a chain + */ + private boolean isChainHead(int orientation) { + int offset = orientation * 2; + return (mListAnchors[offset].mTarget != null + && mListAnchors[offset].mTarget.mTarget != mListAnchors[offset]) + && (mListAnchors[offset + 1].mTarget != null + && mListAnchors[offset + 1].mTarget.mTarget == mListAnchors[offset + 1]); + } + + /*-----------------------------------------------------------------------*/ // Constraints /*-----------------------------------------------------------------------*/ @@ -2210,33 +2349,30 @@ public class ConstraintWidget { horizontalParentWrapContent = mParent != null ? mParent.mListDimensionBehaviors[DIMENSION_HORIZONTAL] == WRAP_CONTENT : false; verticalParentWrapContent = mParent != null ? mParent.mListDimensionBehaviors[DIMENSION_VERTICAL] == WRAP_CONTENT : false; - // Add this widget to an horizontal chain if dual connections are found - if((mLeft.mTarget != null && mLeft.mTarget.mTarget != mLeft) && - mRight.mTarget != null && mRight.mTarget.mTarget == mRight){ + // Add this widget to a horizontal chain if it is the Head of it. + if (isChainHead(HORIZONTAL)) { ((ConstraintWidgetContainer) mParent).addChain(this, HORIZONTAL); - } - if ((mLeft.mTarget != null && mLeft.mTarget.mTarget == mLeft) - || (mRight.mTarget != null && mRight.mTarget.mTarget == mRight)) { inHorizontalChain = true; + } else { + inHorizontalChain = isInHorizontalChain(); } - // Add this widget to an vertical chain if dual connections are found - if((mTop.mTarget != null && mTop.mTarget.mTarget != mTop) && - mBottom.mTarget != null && mBottom.mTarget.mTarget == mBottom){ + + // Add this widget to a vertical chain if it is the Head of it. + if (isChainHead(VERTICAL)) { ((ConstraintWidgetContainer) mParent).addChain(this, VERTICAL); - } - if ((mTop.mTarget != null && mTop.mTarget.mTarget == mTop) - || (mBottom.mTarget != null && mBottom.mTarget.mTarget == mBottom)) { inVerticalChain = true; + } else { + inVerticalChain = isInVerticalChain(); } if (horizontalParentWrapContent && mVisibility != GONE - && mLeft.mTarget == null && mRight.mTarget == null) { + && mLeft.mTarget == null && mRight.mTarget == null) { SolverVariable parentRight = system.createObjectVariable(mParent.mRight); system.addGreaterThan(parentRight, right, 0, SolverVariable.STRENGTH_LOW); } if (verticalParentWrapContent && mVisibility != GONE - && mTop.mTarget == null && mBottom.mTarget == null && mBaseline == null) { + && mTop.mTarget == null && mBottom.mTarget == null && mBaseline == null) { SolverVariable parentBottom = system.createObjectVariable(mParent.mBottom); system.addGreaterThan(parentBottom, bottom, 0, SolverVariable.STRENGTH_LOW); } @@ -2252,8 +2388,10 @@ public class ConstraintWidget { } // Dimensions can be either fixed (a given value) or dependent on the solver if set to MATCH_CONSTRAINT - boolean horizontalDimensionFixed = mListDimensionBehaviors[DIMENSION_HORIZONTAL] != DimensionBehaviour.MATCH_CONSTRAINT; - boolean verticalDimensionFixed = mListDimensionBehaviors[DIMENSION_VERTICAL] != DimensionBehaviour.MATCH_CONSTRAINT; + boolean horizontalDimensionFixed = + mListDimensionBehaviors[DIMENSION_HORIZONTAL] != DimensionBehaviour.MATCH_CONSTRAINT; + boolean verticalDimensionFixed = + mListDimensionBehaviors[DIMENSION_VERTICAL] != DimensionBehaviour.MATCH_CONSTRAINT; // We evaluate the dimension ratio here as the connections can change. // TODO: have a validation pass after connection instead @@ -2562,19 +2700,11 @@ public class ConstraintWidget { } if (matchMinDimension > 0) { - if (parentWrapContent) { - system.addGreaterThan(end, begin, matchMinDimension, SolverVariable.STRENGTH_FIXED); - } else { - system.addGreaterThan(end, begin, matchMinDimension, SolverVariable.STRENGTH_FIXED); - } + system.addGreaterThan(end, begin, matchMinDimension, SolverVariable.STRENGTH_FIXED); dimension = Math.max(dimension, matchMinDimension); } if (matchMaxDimension > 0) { - if (parentWrapContent) { - system.addLowerThan(end, begin, matchMaxDimension, SolverVariable.STRENGTH_LOW); - } else { - system.addLowerThan(end, begin, matchMaxDimension, SolverVariable.STRENGTH_FIXED); - } + system.addLowerThan(end, begin, matchMaxDimension, SolverVariable.STRENGTH_FIXED); dimension = Math.min(dimension, matchMaxDimension); } if (matchConstraintDefault == MATCH_CONSTRAINT_WRAP) { @@ -2684,21 +2814,36 @@ public class ConstraintWidget { } else { applyCentering = true; - if (parentWrapContent) { - system.addGreaterThan(begin, beginTarget, beginAnchor.getMargin(), SolverVariable.STRENGTH_EQUALITY); - system.addLowerThan(end, endTarget, -endAnchor.getMargin(), SolverVariable.STRENGTH_EQUALITY); - } } + int startStrength = SolverVariable.STRENGTH_EQUALITY; + int endStrength = SolverVariable.STRENGTH_EQUALITY; + boolean applyStartConstraint = parentWrapContent; + boolean applyEndConstraint = parentWrapContent; if (applyCentering) { system.addCentering(begin, beginTarget, beginAnchor.getMargin(), bias, endTarget, end, endAnchor.getMargin(), centeringStrength); //SolverVariable.STRENGTH_EQUALITY); + boolean isBeginAnchorBarrier = beginAnchor.mTarget.mOwner instanceof Barrier; + boolean isEndAnchorBarrier = endAnchor.mTarget.mOwner instanceof Barrier; + + if (isBeginAnchorBarrier && !isEndAnchorBarrier) { + endStrength = SolverVariable.STRENGTH_FIXED; + applyEndConstraint = true; + } else if (!isBeginAnchorBarrier && isEndAnchorBarrier) { + startStrength = SolverVariable.STRENGTH_FIXED; + applyStartConstraint = true; + } } - if (applyBoundsCheck) { - // Al >= Tl & Ar <= Tr - system.addGreaterThan(begin, beginTarget, beginAnchor.getMargin(), SolverVariable.STRENGTH_FIXED); - system.addLowerThan(end, endTarget, -endAnchor.getMargin(), SolverVariable.STRENGTH_FIXED); + startStrength = SolverVariable.STRENGTH_FIXED; + endStrength = SolverVariable.STRENGTH_FIXED; + } + + if ((!variableSize && applyStartConstraint) || applyBoundsCheck) { + system.addGreaterThan(begin, beginTarget, beginAnchor.getMargin(), startStrength); + } + if ((!variableSize && applyEndConstraint) || applyBoundsCheck) { + system.addLowerThan(end, endTarget, -endAnchor.getMargin(), endStrength); } if (parentWrapContent) { diff --git a/solver/src/main/java/android/support/constraint/solver/widgets/ConstraintWidgetContainer.java b/solver/src/main/java/android/support/constraint/solver/widgets/ConstraintWidgetContainer.java index 42ab6f1..aadad1b 100644 --- a/solver/src/main/java/android/support/constraint/solver/widgets/ConstraintWidgetContainer.java +++ b/solver/src/main/java/android/support/constraint/solver/widgets/ConstraintWidgetContainer.java @@ -21,6 +21,7 @@ import android.support.constraint.solver.Metrics; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import static android.support.constraint.solver.LinearSystem.FULL_DEBUG; import static android.support.constraint.solver.widgets.ConstraintWidget.DimensionBehaviour.WRAP_CONTENT; @@ -58,7 +59,15 @@ public class ConstraintWidgetContainer extends WidgetContainer { ChainHead[] mVerticalChainsArray = new ChainHead[4]; ChainHead[] mHorizontalChainsArray = new ChainHead[4]; + public List<ConstraintWidgetGroup> mWidgetGroups = new ArrayList<>(); + public boolean mGroupsWrapOptimized = false; + public boolean mHorizontalWrapOptimized = false; + public boolean mVerticalWrapOptimized = false; + public int mWrapFixedWidth = 0; + public int mWrapFixedHeight = 0; + private int mOptimizationLevel = Optimizer.OPTIMIZATION_STANDARD; + public boolean mSkipSolver = false; private boolean mWidthMeasuredTooSmall = false; private boolean mHeightMeasuredTooSmall = false; @@ -139,6 +148,8 @@ public class ConstraintWidgetContainer extends WidgetContainer { mPaddingRight = 0; mPaddingTop = 0; mPaddingBottom = 0; + mWidgetGroups.clear(); + mSkipSolver = false; super.reset(); } @@ -313,7 +324,9 @@ public class ConstraintWidgetContainer extends WidgetContainer { if (!optimizeFor(Optimizer.OPTIMIZATION_DIMENSIONS)) { optimizeReset(); } - optimize(); + if (!optimizeFor(Optimizer.OPTIMIZATION_GROUPS)) { + optimize(); + } mSystem.graphOptimizer = true; } else { mSystem.graphOptimizer = false; @@ -331,154 +344,197 @@ public class ConstraintWidgetContainer extends WidgetContainer { // Reset the chains before iterating on our children resetChains(); - // Before we solve our system, we should call layout() on any - // of our children that is a container. - final int count = mChildren.size(); - for (int i = 0; i < count; i++) { - ConstraintWidget widget = mChildren.get(i); - if (widget instanceof WidgetContainer) { - ((WidgetContainer) widget).layout(); - } + if (mWidgetGroups.size() == 0){ + mWidgetGroups.clear(); + mWidgetGroups.add(0, new ConstraintWidgetGroup(mChildren)); } - // Now let's solve our system as usual - boolean needsSolving = true; int countSolve = 0; - while (needsSolving) { - countSolve++; - try { - mSystem.reset(); - if (DEBUG) { - setDebugSolverName(mSystem, getDebugName()); - for (int i = 0; i < count; i++) { - ConstraintWidget widget = mChildren.get(i); - if (widget.getDebugName() != null) { - widget.setDebugSolverName(mSystem, widget.getDebugName()); + final int groupSize = mWidgetGroups.size(); + final List<ConstraintWidget> allChildren = mChildren; + boolean hasWrapContent = getHorizontalDimensionBehaviour() == WRAP_CONTENT || getVerticalDimensionBehaviour() == WRAP_CONTENT; + + for (int groupIndex = 0; groupIndex < groupSize && !mSkipSolver; groupIndex++) { + if (mWidgetGroups.get(groupIndex).mSkipSolver) { + continue; + } + if (optimizeFor(Optimizer.OPTIMIZATION_GROUPS)) { + if (getHorizontalDimensionBehaviour() == DimensionBehaviour.FIXED && getVerticalDimensionBehaviour() == DimensionBehaviour.FIXED) { + mChildren = (ArrayList<ConstraintWidget>) mWidgetGroups.get(groupIndex).getWidgetsToSolve(); + } else { + mChildren = (ArrayList<ConstraintWidget>) mWidgetGroups.get(groupIndex).mConstrainedGroup; + } + } + resetChains(); + final int count = mChildren.size(); + countSolve = 0; + + // Before we solve our system, we should call layout() on any + // of our children that is a container. + for (int i = 0; i < count; i++) { + ConstraintWidget widget = mChildren.get(i); + if (widget instanceof WidgetContainer) { + ((WidgetContainer) widget).layout(); + } + } + + // Now let's solve our system as usual + boolean needsSolving = true; + while (needsSolving) { + countSolve++; + try { + mSystem.reset(); + resetChains(); + if (DEBUG) { + setDebugSolverName(mSystem, getDebugName()); + for (int i = 0; i < count; i++) { + ConstraintWidget widget = mChildren.get(i); + if (widget.getDebugName() != null) { + widget.setDebugSolverName(mSystem, widget.getDebugName()); + } + } + } else { + createObjectVariables(mSystem); + for (int i = 0; i < count; i++) { + ConstraintWidget widget = mChildren.get(i); + widget.createObjectVariables(mSystem); } } + needsSolving = addChildrenToSolver(mSystem); + if (needsSolving) { + mSystem.minimize(); + } + } catch (Exception e) { + e.printStackTrace(); + System.out.println("EXCEPTION : " + e); + } + if (needsSolving) { + updateChildrenFromSolver(mSystem, Optimizer.flags); } else { - createObjectVariables(mSystem); + updateFromSolver(mSystem); for (int i = 0; i < count; i++) { ConstraintWidget widget = mChildren.get(i); - widget.createObjectVariables(mSystem); + if (widget.mListDimensionBehaviors[DIMENSION_HORIZONTAL] + == DimensionBehaviour.MATCH_CONSTRAINT + && widget.getWidth() < widget.getWrapWidth()) { + Optimizer.flags[Optimizer.FLAG_RECOMPUTE_BOUNDS] = true; + break; + } + if (widget.mListDimensionBehaviors[DIMENSION_VERTICAL] + == DimensionBehaviour.MATCH_CONSTRAINT + && widget.getHeight() < widget.getWrapHeight()) { + Optimizer.flags[Optimizer.FLAG_RECOMPUTE_BOUNDS] = true; + break; + } } } - needsSolving = addChildrenToSolver(mSystem); - if (needsSolving) { - mSystem.minimize(); - } - } catch (Exception e) { - e.printStackTrace(); - System.out.println("EXCEPTION : " + e); - } - if (needsSolving) { - updateChildrenFromSolver(mSystem, Optimizer.flags); - } else { - updateFromSolver(mSystem); - for (int i = 0; i < count; i++) { - ConstraintWidget widget = mChildren.get(i); - if (widget.mListDimensionBehaviors[DIMENSION_HORIZONTAL] == DimensionBehaviour.MATCH_CONSTRAINT - && widget.getWidth() < widget.getWrapWidth()) { - Optimizer.flags[Optimizer.FLAG_RECOMPUTE_BOUNDS] = true; - break; + needsSolving = false; + + if (hasWrapContent && countSolve < MAX_ITERATIONS + && Optimizer.flags[Optimizer.FLAG_RECOMPUTE_BOUNDS]) { + // let's get the new bounds + int maxX = 0; + int maxY = 0; + for (int i = 0; i < count; i++) { + ConstraintWidget widget = mChildren.get(i); + maxX = Math.max(maxX, widget.mX + widget.getWidth()); + maxY = Math.max(maxY, widget.mY + widget.getHeight()); } - if (widget.mListDimensionBehaviors[DIMENSION_VERTICAL] == DimensionBehaviour.MATCH_CONSTRAINT - && widget.getHeight() < widget.getWrapHeight()) { - Optimizer.flags[Optimizer.FLAG_RECOMPUTE_BOUNDS] = true; - break; + maxX = Math.max(mMinWidth, maxX); + maxY = Math.max(mMinHeight, maxY); + if (originalHorizontalDimensionBehaviour == WRAP_CONTENT) { + if (getWidth() < maxX) { + if (DEBUG_LAYOUT) { + System.out.println( + "layout override width from " + getWidth() + " vs " + maxX); + } + setWidth(maxX); + mListDimensionBehaviors[DIMENSION_HORIZONTAL] = WRAP_CONTENT; // force using the solver + wrap_override = true; + needsSolving = true; + } + } + if (originalVerticalDimensionBehaviour == WRAP_CONTENT) { + if (getHeight() < maxY) { + if (DEBUG_LAYOUT) { + System.out.println( + "layout override height from " + getHeight() + " vs " + maxY); + } + setHeight(maxY); + mListDimensionBehaviors[DIMENSION_VERTICAL] = WRAP_CONTENT; // force using the solver + wrap_override = true; + needsSolving = true; + } } } - } - needsSolving = false; - - if (countSolve < MAX_ITERATIONS && Optimizer.flags[Optimizer.FLAG_RECOMPUTE_BOUNDS]) { - // let's get the new bounds - int maxX = 0; - int maxY = 0; - for (int i = 0; i < count; i++) { - ConstraintWidget widget = mChildren.get(i); - maxX = Math.max(maxX, widget.mX + widget.getWidth()); - maxY = Math.max(maxY, widget.mY + widget.getHeight()); - } - maxX = Math.max(mMinWidth, maxX); - maxY = Math.max(mMinHeight, maxY); - if (originalHorizontalDimensionBehaviour == WRAP_CONTENT) { - if (getWidth() < maxX) { + if (true) { + int width = Math.max(mMinWidth, getWidth()); + if (width > getWidth()) { if (DEBUG_LAYOUT) { - System.out.println("layout override width from " + getWidth() + " vs " + maxX); + System.out.println( + "layout override 2, width from " + getWidth() + " vs " + width); } - setWidth(maxX); - mListDimensionBehaviors[DIMENSION_HORIZONTAL] = WRAP_CONTENT; // force using the solver + setWidth(width); + mListDimensionBehaviors[DIMENSION_HORIZONTAL] = DimensionBehaviour.FIXED; wrap_override = true; needsSolving = true; } - } - if (originalVerticalDimensionBehaviour == WRAP_CONTENT) { - if (getHeight() < maxY) { + int height = Math.max(mMinHeight, getHeight()); + if (height > getHeight()) { if (DEBUG_LAYOUT) { - System.out.println("layout override height from " + getHeight() + " vs " + maxY); + System.out.println( + "layout override 2, height from " + getHeight() + " vs " + height); } - setHeight(maxY); - mListDimensionBehaviors[DIMENSION_VERTICAL] = WRAP_CONTENT; // force using the solver + setHeight(height); + mListDimensionBehaviors[DIMENSION_VERTICAL] = DimensionBehaviour.FIXED; wrap_override = true; needsSolving = true; } - } - } - if (true) { - int width = Math.max(mMinWidth, getWidth()); - if (width > getWidth()) { - if (DEBUG_LAYOUT) { - System.out.println("layout override 2, width from " + getWidth() + " vs " + width); - } - setWidth(width); - mListDimensionBehaviors[DIMENSION_HORIZONTAL] = DimensionBehaviour.FIXED; - wrap_override = true; - needsSolving = true; - } - int height = Math.max(mMinHeight, getHeight()); - if (height > getHeight()) { - if (DEBUG_LAYOUT) { - System.out.println("layout override 2, height from " + getHeight() + " vs " + height); - } - setHeight(height); - mListDimensionBehaviors[DIMENSION_VERTICAL] = DimensionBehaviour.FIXED; - wrap_override = true; - needsSolving = true; - } - - if (!wrap_override) { - if (mListDimensionBehaviors[DIMENSION_HORIZONTAL] == WRAP_CONTENT && prew > 0) { - if (getWidth() > prew) { - if (DEBUG_LAYOUT) { - System.out.println("layout override 3, width from " + getWidth() + " vs " + prew); + if (!wrap_override) { + if (mListDimensionBehaviors[DIMENSION_HORIZONTAL] == WRAP_CONTENT + && prew > 0) { + if (getWidth() > prew) { + if (DEBUG_LAYOUT) { + System.out.println( + "layout override 3, width from " + getWidth() + " vs " + + prew); + } + mWidthMeasuredTooSmall = true; + wrap_override = true; + mListDimensionBehaviors[DIMENSION_HORIZONTAL] = DimensionBehaviour.FIXED; + setWidth(prew); + needsSolving = true; } - mWidthMeasuredTooSmall = true; - wrap_override = true; - mListDimensionBehaviors[DIMENSION_HORIZONTAL] = DimensionBehaviour.FIXED; - setWidth(prew); - needsSolving = true; } - } - if (mListDimensionBehaviors[DIMENSION_VERTICAL] == WRAP_CONTENT && preh > 0) { - if (getHeight() > preh) { - if (DEBUG_LAYOUT) { - System.out.println("layout override 3, height from " + getHeight() + " vs " + preh); + if (mListDimensionBehaviors[DIMENSION_VERTICAL] == WRAP_CONTENT + && preh > 0) { + if (getHeight() > preh) { + if (DEBUG_LAYOUT) { + System.out.println( + "layout override 3, height from " + getHeight() + " vs " + + preh); + } + mHeightMeasuredTooSmall = true; + wrap_override = true; + mListDimensionBehaviors[DIMENSION_VERTICAL] = DimensionBehaviour.FIXED; + setHeight(preh); + needsSolving = true; } - mHeightMeasuredTooSmall = true; - wrap_override = true; - mListDimensionBehaviors[DIMENSION_VERTICAL] = DimensionBehaviour.FIXED; - setHeight(preh); - needsSolving = true; } } } } + if (DEBUG_LAYOUT) { + System.out.println( + "Solved system in " + countSolve + " iterations (" + getWidth() + " x " + + getHeight() + ")"); + } + // Update UnresolvedWidgets that did not need solver. + mWidgetGroups.get(groupIndex).updateUnresolvedWidgets(); } - if (DEBUG_LAYOUT) { - System.out.println("Solved system in " + countSolve + " iterations (" + getWidth() + " x " + getHeight() + ")"); - } + mChildren = (ArrayList<ConstraintWidget>)allChildren; + if (mParent != null && USE_SNAPSHOT) { int width = Math.max(mMinWidth, getWidth()); int height = Math.max(mMinHeight, getHeight()); @@ -498,7 +554,9 @@ public class ConstraintWidgetContainer extends WidgetContainer { if (DEBUG_GRAPH) { for (int i = 0; i < mChildren.size(); i++) { ConstraintWidget widget = mChildren.get(i); - System.out.println("final child [" + i + "/" + mChildren.size() + "] - " + widget.mLeft.getResolutionNode() + System.out.println( + "final child [" + i + "/" + mChildren.size() + "] - " + widget.mLeft + .getResolutionNode() + ", " + widget.mTop.getResolutionNode() + ", " + widget.mRight.getResolutionNode() + ", " + widget.mBottom.getResolutionNode()); @@ -684,7 +742,8 @@ public class ConstraintWidgetContainer extends WidgetContainer { */ private void addHorizontalChain(ConstraintWidget widget) { if (mHorizontalChainsSize + 1 >= mHorizontalChainsArray.length) { - mHorizontalChainsArray = Arrays.copyOf(mHorizontalChainsArray, mHorizontalChainsArray.length * 2); + mHorizontalChainsArray = Arrays + .copyOf(mHorizontalChainsArray, mHorizontalChainsArray.length * 2); } mHorizontalChainsArray[mHorizontalChainsSize] = new ChainHead(widget, HORIZONTAL, isRtl()); mHorizontalChainsSize++; @@ -698,10 +757,23 @@ public class ConstraintWidgetContainer extends WidgetContainer { */ private void addVerticalChain(ConstraintWidget widget) { if (mVerticalChainsSize + 1 >= mVerticalChainsArray.length) { - mVerticalChainsArray = Arrays.copyOf(mVerticalChainsArray, mVerticalChainsArray.length * 2); + mVerticalChainsArray = Arrays + .copyOf(mVerticalChainsArray, mVerticalChainsArray.length * 2); } mVerticalChainsArray[mVerticalChainsSize] = new ChainHead(widget, VERTICAL, isRtl()); mVerticalChainsSize++; } + /*-----------------------------------------------------------------------*/ + // Widgets + /*-----------------------------------------------------------------------*/ + + /** + * {@link #mWidgetGroups} getter. + * + * @return The list of independently constrained widget groups. + */ + public List<ConstraintWidgetGroup> getWidgetGroups() { + return mWidgetGroups; + } } diff --git a/solver/src/main/java/android/support/constraint/solver/widgets/ConstraintWidgetGroup.java b/solver/src/main/java/android/support/constraint/solver/widgets/ConstraintWidgetGroup.java new file mode 100644 index 0000000..6781dcc --- /dev/null +++ b/solver/src/main/java/android/support/constraint/solver/widgets/ConstraintWidgetGroup.java @@ -0,0 +1,245 @@ + /* + * Copyright (C) 2018 The Android Open Source Project * Copyright (C) 201 + * + * 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 android.support.constraint.solver.widgets; + + import static android.support.constraint.solver.widgets.ConstraintWidget.HORIZONTAL; + import static android.support.constraint.solver.widgets.ConstraintWidget.UNKNOWN; + import static android.support.constraint.solver.widgets.ConstraintWidget.VERTICAL; + + import java.util.ArrayList; + import java.util.HashSet; + import java.util.List; + import java.util.Set; + + /** + * Class for groups of widgets constrained between each other in a ConstraintWidgetContainer. + * <p> + * Will possess the list of ConstraintWidget in the group. + * The mChains that exist in the group. + * Each group can be solved for individually. + */ + public class ConstraintWidgetGroup { + + public List<ConstraintWidget> mConstrainedGroup; + int mGroupWidth = UNKNOWN; + int mGroupHeight = UNKNOWN; + public boolean mSkipSolver = false; + public final int[] mGroupDimensions = {mGroupWidth, mGroupHeight}; + /** + * Arrays to contain the widgets that determine the start of a group relative to their layout. + * A widget is at the start, if their left/top anchor is constrained to their parent. + * If the left/top constraint is null, is considered at the start if there are no widgets + * constrained to it from their right/bottom anchor. + */ + List<ConstraintWidget> mStartHorizontalWidgets = new ArrayList<>(); + List<ConstraintWidget> mStartVerticalWidgets = new ArrayList<>(); + HashSet<ConstraintWidget> mWidgetsToSetHorizontal = new HashSet<>(); + HashSet<ConstraintWidget> mWidgetsToSetVertical = new HashSet<>(); + List<ConstraintWidget> mWidgetsToSolve = new ArrayList<>(); + List<ConstraintWidget> mUnresolvedWidgets = new ArrayList<>(); + + ConstraintWidgetGroup(List<ConstraintWidget> widgets) { + this.mConstrainedGroup = widgets; + } + + ConstraintWidgetGroup(List<ConstraintWidget> widgets, boolean skipSolver) { + this.mConstrainedGroup = widgets; + this.mSkipSolver = skipSolver; + } + + public List<ConstraintWidget> getStartWidgets(int orientation) { + if (orientation == HORIZONTAL) { + return mStartHorizontalWidgets; + } else if (orientation == VERTICAL) { + return mStartVerticalWidgets; + } + return null; + } + + Set<ConstraintWidget> getWidgetsToSet(int orientation) { + if (orientation == HORIZONTAL) { + return mWidgetsToSetHorizontal; + } else if (orientation == VERTICAL) { + return mWidgetsToSetVertical; + } + return null; + } + + void addWidgetsToSet(ConstraintWidget widget, int orientation) { + if (orientation == HORIZONTAL) { + mWidgetsToSetHorizontal.add(widget); + } else if (orientation == VERTICAL) { + mWidgetsToSetVertical.add(widget); + } + } + + /** + * Get a list of widgets that haven't been fully resolved and require the Linear Solver + * to resolve. + * Sets {@link #mUnresolvedWidgets} with the widgets that haven't been resolved, but don't + * require the Linear Solver. + * + * @return List of widgets to be solved. + */ + List<ConstraintWidget> getWidgetsToSolve() { + if (!mWidgetsToSolve.isEmpty()) { + return mWidgetsToSolve; + } + final int size = mConstrainedGroup.size(); + for (int i = 0; i < size; i++) { + ConstraintWidget widget = mConstrainedGroup.get(i); + if (!widget.mOptimizerMeasurable) { + getWidgetsToSolveTraversal((ArrayList<ConstraintWidget>)mWidgetsToSolve, widget); + } + } + mUnresolvedWidgets.clear(); + mUnresolvedWidgets.addAll(mConstrainedGroup); + mUnresolvedWidgets.removeAll(mWidgetsToSolve); + return mWidgetsToSolve; + } + + /** + * Helper method to find widgets to be solved. + * + * @param widgetsToSolve Current list of widgets to be solved. + * @param widget Widget being traversed. + */ + private void getWidgetsToSolveTraversal(ArrayList<ConstraintWidget> widgetsToSolve, ConstraintWidget widget) { + if (widget.mGroupsToSolver) { + return; + } + widgetsToSolve.add(widget); + widget.mGroupsToSolver = true; + if (widget.isFullyResolved()) { + return; + } + if (widget instanceof Helper) { + Helper helper = (Helper) widget; + final int widgetCount = helper.mWidgetsCount; + for (int i = 0; i < widgetCount; i++) { + getWidgetsToSolveTraversal(widgetsToSolve, helper.mWidgets[i]); + } + } + // Propagate from every unmeasurable widget to the parent. + final int count = widget.mListAnchors.length; + for (int i = 0; i < count; i++) { + ConstraintAnchor targetAnchor = widget.mListAnchors[i].mTarget; + ConstraintWidget targetWidget = null; + if (targetAnchor != null) { + targetWidget = targetAnchor.mOwner; + } else { + continue; + } + // Traverse until we hit a resolved widget or the parent. + if (targetAnchor != null && (targetWidget != widget.getParent())) { + getWidgetsToSolveTraversal(widgetsToSolve, targetWidget); + } + } + } + + /** + * After solving, update any widgets that depended on unmeasurable widgets. + */ + void updateUnresolvedWidgets() { + final int size = mUnresolvedWidgets.size(); + for (int i = 0; i < size; i++) { + ConstraintWidget widget = mUnresolvedWidgets.get(i); + // Needs start, end, orientation. + // Or left,right/top, bottom. + updateResolvedDimension(widget); + } + } + + /** + * Update widget's dimension according to the widget it depends on. + * + * @param widget Widget to resolve dimension. + */ + private void updateResolvedDimension(ConstraintWidget widget) { + int start = 0, end = 0; + if (widget.mOptimizerMeasurable) { + // No need to update dimension if it has been resolved. + if (widget.isFullyResolved()) { + return; + } + // Horizontal. + boolean rightSide = widget.mRight.mTarget != null; + ConstraintAnchor targetAnchor; + // Get measure if target is resolved, otherwise, resolve target. + if (rightSide) { + targetAnchor = widget.mRight.mTarget; + } else { + targetAnchor = widget.mLeft.mTarget; + } + if (targetAnchor != null) { + if (!targetAnchor.mOwner.mOptimizerMeasured) { + updateResolvedDimension(targetAnchor.mOwner); + } + if (targetAnchor.mType == ConstraintAnchor.Type.RIGHT) { + end = targetAnchor.mOwner.mX + targetAnchor.mOwner.getWidth(); + } else if (targetAnchor.mType == ConstraintAnchor.Type.LEFT) { + end = targetAnchor.mOwner.mX; + } + } + if (rightSide) { + end -= widget.mRight.getMargin(); + } else { + end += widget.mLeft.getMargin() + widget.getWidth(); + } + start = end - widget.getWidth(); + widget.setHorizontalDimension(start, end); + // Vertical. + if (widget.mBaseline.mTarget != null) { + targetAnchor = widget.mBaseline.mTarget; + if (!targetAnchor.mOwner.mOptimizerMeasured) { + updateResolvedDimension(targetAnchor.mOwner); + } + start = targetAnchor.mOwner.mY + targetAnchor.mOwner.mBaselineDistance + - widget.mBaselineDistance; + end = start + widget.mHeight; + widget.setVerticalDimension(start, end); + widget.mOptimizerMeasured = true; + return; + } + boolean bottomSide = widget.mBottom.mTarget != null; + // Get measure if target is resolved, otherwise, resolve target. + if (bottomSide) { + targetAnchor = widget.mBottom.mTarget; + } else { + targetAnchor = widget.mTop.mTarget; + } + if (targetAnchor != null) { + if (!targetAnchor.mOwner.mOptimizerMeasured) { + updateResolvedDimension(targetAnchor.mOwner); + } + if (targetAnchor.mType == ConstraintAnchor.Type.BOTTOM) { + end = targetAnchor.mOwner.mY + targetAnchor.mOwner.getHeight(); + } else if (targetAnchor.mType == ConstraintAnchor.Type.TOP) { + end = targetAnchor.mOwner.mY; + } + } + if (bottomSide) { + end -= widget.mBottom.getMargin(); + } else { + end += widget.mTop.getMargin() + widget.getHeight(); + } + start = end - widget.getHeight(); + widget.setVerticalDimension(start, end); + widget.mOptimizerMeasured = true; + } + } + } diff --git a/solver/src/main/java/android/support/constraint/solver/widgets/Optimizer.java b/solver/src/main/java/android/support/constraint/solver/widgets/Optimizer.java index a0a3fe6..48d0ff7 100644 --- a/solver/src/main/java/android/support/constraint/solver/widgets/Optimizer.java +++ b/solver/src/main/java/android/support/constraint/solver/widgets/Optimizer.java @@ -33,9 +33,10 @@ public class Optimizer { public static final int OPTIMIZATION_CHAIN = 1 << 2; public static final int OPTIMIZATION_DIMENSIONS = 1 << 3; public static final int OPTIMIZATION_RATIO = 1 << 4; + public static final int OPTIMIZATION_GROUPS = 1 << 5; public static final int OPTIMIZATION_STANDARD = OPTIMIZATION_DIRECT | OPTIMIZATION_BARRIER - /* | OPTIMIZATION_CHAIN */ + | OPTIMIZATION_CHAIN /* | OPTIMIZATION_DIMENSIONS */ ; @@ -429,6 +430,9 @@ public class Optimizer { if (widget != firstVisibleWidget) { totalSize += widget.mListAnchors[offset].getMargin(); } + if (widget != lastVisibleWidget) { + totalSize += widget.mListAnchors[offset + 1].getMargin(); + } totalMargins += widget.mListAnchors[offset].getMargin(); totalMargins += widget.mListAnchors[offset + 1].getMargin(); } @@ -452,6 +456,9 @@ public class Optimizer { return false; } } + if (widget.mDimensionRatio != 0.0f) { + return false; + } } // go to the next widget @@ -481,7 +488,7 @@ public class Optimizer { // let's look at the endpoints if (firstNode.target.state != ResolutionAnchor.RESOLVED - && lastNode.target.state != ResolutionAnchor.RESOLVED) { + || lastNode.target.state != ResolutionAnchor.RESOLVED) { // No resolved endpoints, let's exit return false; } @@ -516,29 +523,26 @@ public class Optimizer { } distance += totalSize; distance -= totalMargins; - widget = firstVisibleWidget; + widget = first; float position = firstOffset; - if (isChainSpread) { - distance -= (totalMargins - extraMargin); - } - if (isChainSpread) { - position += widget.mListAnchors[offset + 1].getMargin(); - next = widget.mListNextVisibleWidget[orientation]; - if (next != null) { - position += next.mListAnchors[offset].getMargin(); - } - } while (widget != null) { if (system.sMetrics != null) { system.sMetrics.nonresolvedWidgets--; system.sMetrics.resolvedWidgets++; system.sMetrics.chainConnectionResolved++; } - next = widget.mListNextVisibleWidget[orientation]; - if (next != null || widget == lastVisibleWidget) { + next = widget.mNextChainWidget[orientation]; + if (next != null || widget == last) { float dimension = distance / numMatchConstraints; if (totalWeights > 0) { - dimension = widget.mWeight[orientation] * distance / totalWeights; + if (widget.mWeight[orientation] == UNKNOWN) { + dimension = 0; + } else { + dimension = widget.mWeight[orientation] * distance / totalWeights; + } + } + if (widget.getVisibility() == GONE) { + dimension = 0; } position += widget.mListAnchors[offset].getMargin(); widget.mListAnchors[offset].getResolutionNode().resolve(firstNode.resolvedTarget, @@ -555,23 +559,26 @@ public class Optimizer { return true; } - if (distance < totalSize) { - return false; + // If there is not enough space, the chain has to behave as a packed chain. + if (distance < 0) { + isChainSpread = false; + isChainSpreadInside = false; + isChainPacked = true; } if (isChainPacked) { distance -= extraMargin; // Now let's iterate on those widgets - widget = firstVisibleWidget; - distance = firstOffset + (distance * first.getHorizontalBiasPercent()); // start after the gap + widget = first; + distance = firstOffset + (distance * first.getBiasPercent(orientation)); // start after the gap while (widget != null) { if (system.sMetrics != null) { system.sMetrics.nonresolvedWidgets--; system.sMetrics.resolvedWidgets++; system.sMetrics.chainConnectionResolved++; } - next = widget.mListNextVisibleWidget[orientation]; - if (next != null || widget == lastVisibleWidget) { + next = widget.mNextChainWidget[orientation]; + if (next != null || widget == last) { float dimension = 0; if (orientation == HORIZONTAL) { dimension = widget.getWidth(); @@ -596,7 +603,7 @@ public class Optimizer { } else if (isChainSpreadInside) { distance -= extraMargin; } - widget = firstVisibleWidget; + widget = first; float gap = distance / (float) (numVisibleWidgets + 1); if (isChainSpreadInside) { if (numVisibleWidgets > 1) { @@ -605,7 +612,10 @@ public class Optimizer { gap = distance / 2f; // center } } - distance = firstOffset + gap; // start after the gap + distance = firstOffset; + if (first.getVisibility() != GONE) { + distance += gap; // start after the gap + } if (isChainSpreadInside && numVisibleWidgets > 1) { distance = firstOffset + firstVisibleWidget.mListAnchors[offset].getMargin(); } @@ -620,21 +630,27 @@ public class Optimizer { system.sMetrics.resolvedWidgets++; system.sMetrics.chainConnectionResolved++; } - next = widget.mListNextVisibleWidget[orientation]; - if (next != null || widget == lastVisibleWidget) { + next = widget.mNextChainWidget[orientation]; + if (next != null || widget == last) { float dimension = 0; if (orientation == HORIZONTAL) { dimension = widget.getWidth(); } else { dimension = widget.getHeight(); } + if (widget != firstVisibleWidget) { + distance += widget.mListAnchors[offset].getMargin(); + } widget.mListAnchors[offset].getResolutionNode().resolve(firstNode.resolvedTarget, distance); widget.mListAnchors[offset + 1].getResolutionNode().resolve(firstNode.resolvedTarget, distance + dimension); widget.mListAnchors[offset].getResolutionNode().addResolvedValue(system); widget.mListAnchors[offset + 1].getResolutionNode().addResolvedValue(system); - distance += dimension + gap; + distance += dimension + widget.mListAnchors[offset + 1].getMargin(); + if (next != null && next.getVisibility() != GONE) { + distance += gap; + } } widget = next; } @@ -642,4 +658,29 @@ public class Optimizer { return true; // optimized! } + + //TODO: Might want to use ResolutionAnchor::resolve(target, offset). + /** + * Set a {@link ConstraintWidget} optimized position and dimension in an specific orientation. + * + * @param widget Widget to be optimized. + * @param orientation Orientation to set optimization (HORIZONTAL{0}/VERTICAL{1}). + * @param resolvedOffset The resolved offset of the widget with respect to the root. + */ + static void setOptimizedWidget(ConstraintWidget widget, int orientation, int resolvedOffset) { + final int startOffset = orientation * 2; + final int endOffset = startOffset + 1; + // Left/top of widget. + widget.mListAnchors[startOffset].getResolutionNode().resolvedTarget = + widget.getParent().mLeft.getResolutionNode(); + widget.mListAnchors[startOffset].getResolutionNode().resolvedOffset = + resolvedOffset; + widget.mListAnchors[startOffset].getResolutionNode().state = ResolutionNode.RESOLVED; + // Right/bottom of widget. + widget.mListAnchors[endOffset].getResolutionNode().resolvedTarget = + widget.mListAnchors[startOffset].getResolutionNode(); + widget.mListAnchors[endOffset].getResolutionNode().resolvedOffset = + widget.getLength(orientation); + widget.mListAnchors[endOffset].getResolutionNode().state = ResolutionNode.RESOLVED; + } } diff --git a/solver/src/main/java/android/support/constraint/solver/widgets/ResolutionAnchor.java b/solver/src/main/java/android/support/constraint/solver/widgets/ResolutionAnchor.java index 67865aa..9e3379f 100644 --- a/solver/src/main/java/android/support/constraint/solver/widgets/ResolutionAnchor.java +++ b/solver/src/main/java/android/support/constraint/solver/widgets/ResolutionAnchor.java @@ -304,10 +304,10 @@ public class ResolutionAnchor extends ResolutionNode { SolverVariable sv = myAnchor.getSolverVariable(); if (resolvedTarget == null) { - system.addEquality(sv, (int) resolvedOffset); + system.addEquality(sv, (int) (resolvedOffset + 0.5f)); } else { SolverVariable v = system.createObjectVariable(resolvedTarget.myAnchor); - system.addEquality(sv, v, (int) resolvedOffset, SolverVariable.STRENGTH_FIXED); + system.addEquality(sv, v, (int) (resolvedOffset + 0.5f), SolverVariable.STRENGTH_FIXED); } } diff --git a/solver/src/main/java/android/support/constraint/solver/widgets/WidgetContainer.java b/solver/src/main/java/android/support/constraint/solver/widgets/WidgetContainer.java index c6b9796..3eb32ce 100644 --- a/solver/src/main/java/android/support/constraint/solver/widgets/WidgetContainer.java +++ b/solver/src/main/java/android/support/constraint/solver/widgets/WidgetContainer.java @@ -78,6 +78,18 @@ public class WidgetContainer extends ConstraintWidget { } /** + * Add multiple child widgets. + * + * @param widgets to add + */ + public void add(ConstraintWidget... widgets) { + final int count = widgets.length; + for (int i = 0; i < count; i++) { + add(widgets[i]); + } + } + + /** * Remove a child widget * * @param widget to remove diff --git a/solver/src/test/java/android/support/constraint/solver/AnalyzerTest.java b/solver/src/test/java/android/support/constraint/solver/AnalyzerTest.java new file mode 100644 index 0000000..db82c61 --- /dev/null +++ b/solver/src/test/java/android/support/constraint/solver/AnalyzerTest.java @@ -0,0 +1,518 @@ +/* + * Copyright (C) 2018 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 android.support.constraint.solver; + +import android.support.constraint.solver.widgets.Analyzer; +import android.support.constraint.solver.widgets.ConstraintAnchor.Type; +import android.support.constraint.solver.widgets.ConstraintWidget; +import android.support.constraint.solver.widgets.ConstraintWidget.DimensionBehaviour; +import android.support.constraint.solver.widgets.ConstraintWidgetContainer; +import android.support.constraint.solver.widgets.Optimizer; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class AnalyzerTest { + + @Test + public void basicAnalyzerTest() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + ConstraintWidget C = new ConstraintWidget(20, 20); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + B.connect(Type.LEFT, root, Type.LEFT); + B.connect(Type.BOTTOM, root, Type.BOTTOM); + C.connect(Type.RIGHT, root, Type.RIGHT); + C.connect(Type.TOP, root, Type.TOP); + + root.add(A, B, C); + root.layout(); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + + Analyzer.determineGroups(root); + + assertEquals(root.getWidgetGroups().size(), 3); + } + + @Test + public void basicAnalyzerTest2() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + ConstraintWidget C = new ConstraintWidget(20, 20); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + A.connect(Type.RIGHT, B, Type.RIGHT); + B.connect(Type.TOP, root, Type.TOP); + B.connect(Type.RIGHT, C, Type.LEFT); + C.connect(Type.RIGHT, root, Type.RIGHT); + C.connect(Type.TOP, root, Type.TOP); + + root.add(A, B, C); + root.layout(); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + + Analyzer.determineGroups(root); + + assertEquals(root.getWidgetGroups().size(), 1); + } + + @Test + public void extendedAnalyzerTest() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + ConstraintWidget C = new ConstraintWidget(20, 20); + ConstraintWidget D = new ConstraintWidget(20, 20); + ConstraintWidget E = new ConstraintWidget(20, 20); + ConstraintWidget F = new ConstraintWidget(20, 20); + ConstraintWidget G = new ConstraintWidget(20, 20); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + D.setDebugSolverName(root.getSystem(), "D"); + E.setDebugSolverName(root.getSystem(), "E"); + F.setDebugSolverName(root.getSystem(), "F"); + G.setDebugSolverName(root.getSystem(), "G"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.BOTTOM, root, Type.BOTTOM); + A.connect(Type.RIGHT, B, Type.LEFT); + B.connect(Type.LEFT, A, Type.RIGHT); + B.connect(Type.BOTTOM, root, Type.BOTTOM); + B.connect(Type.RIGHT, C, Type.LEFT); + C.connect(Type.LEFT, B, Type.RIGHT); + C.connect(Type.BOTTOM, root, Type.BOTTOM); + C.connect(Type.RIGHT, root, Type.RIGHT); + + D.connect(Type.LEFT, root, Type.LEFT); + D.connect(Type.BOTTOM, A, Type.TOP); + + E.connect(Type.RIGHT, root, Type.RIGHT); + E.connect(Type.BOTTOM, C, Type.TOP); + E.connect(Type.TOP, F, Type.BOTTOM); + + F.connect(Type.LEFT, root, Type.LEFT); + F.connect(Type.BOTTOM, D, Type.TOP); + + G.connect(Type.RIGHT, root, Type.RIGHT); + G.connect(Type.BOTTOM, root, Type.BOTTOM); + + root.add(A, B, C, D, E, F, G); + root.layout(); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + + Analyzer.determineGroups(root); + + assertEquals(root.getWidgetGroups().size(), 2); + assertEquals(root.getWidgetGroups().get(0).mConstrainedGroup.size(), 6); + assertEquals(root.getWidgetGroups().get(1).mConstrainedGroup.size(), 1); + + assertFalse(root.getWidgetGroups().get(0).mConstrainedGroup + .contains(root.getWidgetGroups().get(1).mConstrainedGroup.get(0))); + } + + @Test + public void basicWrapContentGroup() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + ConstraintWidget C = new ConstraintWidget(20, 20); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + B.connect(Type.LEFT, root, Type.LEFT); + B.connect(Type.TOP, A, Type.BOTTOM); + C.connect(Type.LEFT, root, Type.LEFT); + C.connect(Type.TOP, A, Type.TOP); + + root.add(A, B, C); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + root.setHorizontalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + root.setVerticalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + + Analyzer.determineGroups(root); + + // Make sure the right number of groups is identified and have the right properties. + assertEquals(root.getWidgetGroups().size(), 1); + assertEquals(root.getWidgetGroups().get(0).mGroupDimensions[0], 20); + assertEquals(root.getWidgetGroups().get(0).mGroupDimensions[1], 40); + assertEquals(root.getWidgetGroups().get(0).getStartWidgets(0).size(), 3); + assertEquals(root.getWidgetGroups().get(0).getStartWidgets(1).size(), 1); + // The layout widget should now have the maximum width and height and be set as fixed. + assertEquals(root.getWidth(), 20); + assertEquals(root.getHeight(), 40); + assertEquals(root.getHorizontalDimensionBehaviour(), DimensionBehaviour.FIXED); + assertEquals(root.getVerticalDimensionBehaviour(), DimensionBehaviour.FIXED); + } + + @Test + public void twoDirectionWrapContentGroup() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(10, 10); + + ConstraintWidget C = new ConstraintWidget(20, 20); + ConstraintWidget D = new ConstraintWidget(20, 20); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + D.setDebugSolverName(root.getSystem(), "D"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + B.connect(Type.LEFT, root, Type.LEFT); + B.connect(Type.TOP, A, Type.BOTTOM); + + C.connect(Type.LEFT, root, Type.LEFT); + C.connect(Type.BOTTOM, root, Type.BOTTOM); + D.connect(Type.LEFT, root, Type.LEFT); + D.connect(Type.BOTTOM, C, Type.BOTTOM); + + root.add(A, B, C, D); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + root.setHorizontalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + root.setVerticalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + + Analyzer.determineGroups(root); + + assertEquals(root.getWidgetGroups().size(), 2); + assertEquals(root.getWidgetGroups().get(0).mGroupDimensions[0], 20); + assertEquals(root.getWidgetGroups().get(0).mGroupDimensions[1], 30); + assertEquals(root.getWidgetGroups().get(1).mGroupDimensions[0], 20); + assertEquals(root.getWidgetGroups().get(1).mGroupDimensions[1], 20); + assertEquals(root.getWidth(), 20); + assertEquals(root.getHeight(), 30); + } + + @Test + public void horizontalVerticalWrapContentGroup() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(25, 22); + ConstraintWidget B = new ConstraintWidget(25, 22); + + ConstraintWidget C = new ConstraintWidget(20, 10); + ConstraintWidget D = new ConstraintWidget(20, 20); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + D.setDebugSolverName(root.getSystem(), "D"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + B.connect(Type.LEFT, root, Type.LEFT); + B.connect(Type.TOP, A, Type.BOTTOM); + + C.connect(Type.LEFT, root, Type.LEFT); + C.connect(Type.BOTTOM, root, Type.BOTTOM); + D.connect(Type.LEFT, C, Type.RIGHT); + D.connect(Type.BOTTOM, root, Type.BOTTOM); + + root.add(A, B, C, D); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + root.setHorizontalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + root.setVerticalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + + Analyzer.determineGroups(root); + + assertEquals(root.getWidgetGroups().size(), 2); + assertEquals(root.getWidgetGroups().get(0).mGroupDimensions[0], 25); + assertEquals(root.getWidgetGroups().get(0).mGroupDimensions[1], 44); + assertEquals(root.getWidgetGroups().get(1).mGroupDimensions[0], 40); + assertEquals(root.getWidgetGroups().get(1).mGroupDimensions[1], 20); + assertEquals(root.getWidth(), 40); + assertEquals(root.getHeight(), 44); + } + + @Test + public void ignoreWidgetOnReverseFlowWrapContentGroup() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + ConstraintWidget C = new ConstraintWidget(30, 100); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + B.connect(Type.LEFT, root, Type.LEFT); + B.connect(Type.TOP, A, Type.BOTTOM); + // Widget C is going in a different direction: bottom to top, instead of top to bottom. + C.connect(Type.LEFT, root, Type.LEFT); + C.connect(Type.BOTTOM, B, Type.BOTTOM); + + root.add(A, B, C); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + root.setHorizontalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + root.setVerticalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + + Analyzer.determineGroups(root); + + assertEquals(root.getWidgetGroups().size(), 1); + assertEquals(root.getWidth(), 30); + assertEquals(root.getHeight(), 40); + } + + @Test + public void maxSizeOnReverseDirectionWrapTest() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + ConstraintWidget C = new ConstraintWidget(30, 100); + ConstraintWidget D = new ConstraintWidget(20, 200); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + D.setDebugSolverName(root.getSystem(), "D"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + B.connect(Type.LEFT, root, Type.LEFT); + B.connect(Type.TOP, A, Type.BOTTOM); + // Widget C is going in a different direction: bottom to top, instead of top to bottom. + C.connect(Type.LEFT, root, Type.LEFT); + C.connect(Type.BOTTOM, B, Type.BOTTOM); + D.connect(Type.LEFT, root, Type.LEFT); + D.connect(Type.TOP, C, Type.TOP); + + root.add(A, B, C, D); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + root.setHorizontalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + root.setVerticalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + + Analyzer.determineGroups(root); + + assertEquals(root.getWidgetGroups().size(), 1); + assertEquals(root.getWidth(), 30); + assertEquals(root.getHeight(), 140); + } + + @Test + public void basicBaselineWrapTest() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 400, 400); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + A.setBaselineDistance(10); + B.setBaselineDistance(5); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + B.connect(Type.LEFT, root, Type.LEFT); + B.connect(Type.BASELINE, A, Type.BASELINE); + + root.add(A, B); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + root.setHorizontalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + root.setVerticalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + + Analyzer.determineGroups(root); + + assertEquals(root.getWidgetGroups().size(), 1); + assertEquals(root.getWidth(), 20); + assertEquals(root.getHeight(), 25); + } + + @Test + public void baselineToBaselineWrapTest() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 400, 400); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + ConstraintWidget C = new ConstraintWidget(20, 80); + A.setBaselineDistance(5); + B.setBaselineDistance(10); + C.setBaselineDistance(15); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.BOTTOM, root, Type.BOTTOM); + B.connect(Type.LEFT, A, Type.RIGHT); + B.connect(Type.BASELINE, A, Type.BASELINE); + C.connect(Type.LEFT, B, Type.RIGHT); + C.connect(Type.BASELINE, A, Type.BASELINE); + + root.add(A, B, C); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + root.setHorizontalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + root.setVerticalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + + Analyzer.determineGroups(root); + + assertEquals(root.getWidgetGroups().size(), 1); + assertEquals(root.getWidth(), 60); + assertEquals(root.getHeight(), 30); + } + + @Test + public void skipSolverBasic() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + ConstraintWidget C = new ConstraintWidget(20, 20); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + B.connect(Type.LEFT, A, Type.LEFT); + B.connect(Type.TOP, A, Type.BOTTOM); + C.connect(Type.LEFT, B, Type.LEFT); + C.connect(Type.TOP, B, Type.BOTTOM); + + root.add(A, B, C); + root.setHorizontalDimensionBehaviour(DimensionBehaviour.FIXED); + root.setVerticalDimensionBehaviour(DimensionBehaviour.FIXED); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + Analyzer.determineGroups(root); + + assertTrue(root.mSkipSolver); + assertEquals(root.mWidgetGroups.size(), 1); + assertTrue(root.mWidgetGroups.get(0).mSkipSolver); + } + + @Test + public void skipSolverWrap() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + ConstraintWidget C = new ConstraintWidget(20, 20); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + B.connect(Type.LEFT, A, Type.LEFT); + B.connect(Type.TOP, A, Type.BOTTOM); + C.connect(Type.LEFT, B, Type.LEFT); + C.connect(Type.TOP, B, Type.BOTTOM); + + root.add(A, B, C); + root.setHorizontalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + root.setVerticalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + Analyzer.determineGroups(root); + + assertTrue(root.mSkipSolver); + assertEquals(root.mWidgetGroups.size(), 1); + assertTrue(root.mWidgetGroups.get(0).mSkipSolver); + } + + @Test + public void skipSolverMixedDimBehaviour() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + ConstraintWidget C = new ConstraintWidget(20, 20); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + B.connect(Type.LEFT, A, Type.LEFT); + B.connect(Type.TOP, A, Type.BOTTOM); + C.connect(Type.LEFT, B, Type.LEFT); + C.connect(Type.TOP, B, Type.BOTTOM); + + root.add(A, B, C); + root.setHorizontalDimensionBehaviour(DimensionBehaviour.WRAP_CONTENT); + root.setVerticalDimensionBehaviour(DimensionBehaviour.FIXED); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + Analyzer.determineGroups(root); + + assertTrue(root.mSkipSolver); + assertEquals(root.mWidgetGroups.size(), 1); + assertTrue(root.mWidgetGroups.get(0).mSkipSolver); + } + + @Test + public void skipSolverOneGroup() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 800, 800); + ConstraintWidget A = new ConstraintWidget(20, 20); + ConstraintWidget B = new ConstraintWidget(20, 20); + ConstraintWidget C = new ConstraintWidget(20, 20); + + root.setDebugSolverName(root.getSystem(), "root"); + A.setDebugSolverName(root.getSystem(), "A"); + B.setDebugSolverName(root.getSystem(), "B"); + C.setDebugSolverName(root.getSystem(), "C"); + + A.connect(Type.LEFT, root, Type.LEFT); + A.connect(Type.TOP, root, Type.TOP); + B.connect(Type.RIGHT, root, Type.RIGHT); + B.connect(Type.TOP, root, Type.TOP); + B.connect(Type.BOTTOM, C, Type.TOP); + C.connect(Type.RIGHT, root, Type.RIGHT); + C.connect(Type.BOTTOM, root, Type.BOTTOM); + + root.add(A, B, C); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_STANDARD | Optimizer.OPTIMIZATION_GROUPS); + Analyzer.determineGroups(root); + + assertFalse(root.mSkipSolver); + assertEquals(root.mWidgetGroups.size(), 2); + assertTrue(root.mWidgetGroups.get(0).mSkipSolver); + assertFalse(root.mWidgetGroups.get(1).mSkipSolver); + } +} diff --git a/solver/src/test/java/android/support/constraint/solver/ChainTest.java b/solver/src/test/java/android/support/constraint/solver/ChainTest.java index cf1dca6..520a83b 100644 --- a/solver/src/test/java/android/support/constraint/solver/ChainTest.java +++ b/solver/src/test/java/android/support/constraint/solver/ChainTest.java @@ -180,6 +180,7 @@ public class ChainTest { B.setDebugName("B"); root.add(A); root.add(B); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_NONE); A.connect(ConstraintAnchor.Type.LEFT, root, ConstraintAnchor.Type.LEFT); A.connect(ConstraintAnchor.Type.RIGHT, B, ConstraintAnchor.Type.LEFT); B.connect(ConstraintAnchor.Type.LEFT, A, ConstraintAnchor.Type.RIGHT); @@ -279,6 +280,120 @@ public class ChainTest { assertEquals(A.getWidth() + B.getWidth(), root.getWidth()); } + /** + * testPackChain with current Chain Optimizations. + */ + @Test + public void testPackChainOpt() { + ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 600, 600); + ConstraintWidget A = new ConstraintWidget(100, 20); + ConstraintWidget B = new ConstraintWidget(100, 20); + root.setDebugName("root"); + A.setDebugName("A"); + B.setDebugName("B"); + root.add(A); + root.add(B); + root.setOptimizationLevel(Optimizer.OPTIMIZATION_DIRECT | Optimizer.OPTIMIZATION_BARRIER + | Optimizer.OPTIMIZATION_CHAIN); + A.connect(ConstraintAnchor.Type.LEFT, root, ConstraintAnchor.Type.LEFT); + A.connect(ConstraintAnchor.Type.RIGHT, B, ConstraintAnchor.Type.LEFT); + B.connect(ConstraintAnchor.Type.LEFT, A, ConstraintAnchor.Type.RIGHT); + B.connect(ConstraintAnchor.Type.RIGHT, root, ConstraintAnchor.Type.RIGHT); + A.setHorizontalChainStyle(ConstraintWidget.CHAIN_PACKED); + root.layout(); + System.out.println("a) A: " + A + " B: " + B); + assertEquals(A.getWidth(), 100); + assertEquals(B.getWidth(), 100); + assertEquals(A.getLeft(), root.getWidth() - B.getRight()); + assertEquals(B.getLeft(), A.getLeft() + A.getWidth()); + A.setVisibility(ConstraintWidget.GONE); + root.layout(); + System.out.println("b) A: " + A + " B: " + B); + assertEquals(A.getWidth(), 0); + assertEquals(B.getWidth(), 100); + assertEquals(A.getLeft(), root.getWidth() - B.getRight()); + assertEquals(B.getLeft(), A.getLeft() + A.getWidth()); + B.setVisibility(ConstraintWidget.GONE); + root.layout(); + System.out.println("c) A: " + A + " B: " + B); + assertEquals(A.getWidth(), 0); + assertEquals(B.getWidth(), 0); + assertEquals(A.getLeft(), 300); + assertEquals(B.getLeft(), A.getLeft() + A.getWidth()); + A.setVisibility(ConstraintWidget.VISIBLE); + A.setWidth(100); + root.layout(); + System.out.println("d) A: " + A + " B: " + B); + assertEquals(A.getWidth(), 100); + assertEquals(B.getWidth(), 0); + assertEquals(A.getLeft(), root.getWidth() - B.getRight()); + assertEquals(B.getLeft(), A.getLeft() + A.getWidth()); + A.setVisibility(ConstraintWidget.VISIBLE); + A.setWidth(100); + A.setHeight(20); + B.setVisibility(ConstraintWidget.VISIBLE); + B.setWidth(100); + B.setHeight(20); + B.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT); + B.setHorizontalMatchStyle(ConstraintWidget.MATCH_CONSTRAINT_WRAP, 0, 0, 1); + root.layout(); + System.out.println("e) A: " + A + " B: " + B); + assertEquals(A.getWidth(), 100); + assertEquals(B.getWidth(), 100); + assertEquals(A.getLeft(), root.getWidth() - B.getRight()); + assertEquals(B.getLeft(), A.getLeft() + A.getWidth()); + B.setHorizontalMatchStyle(ConstraintWidget.MATCH_CONSTRAINT_SPREAD, 0, 0, 1); + root.layout(); + System.out.println("f) A: " + A + " B: " + B); + assertEquals(A.getWidth(), 100); + assertEquals(B.getWidth(), 500); + assertEquals(A.getLeft(), 0); + assertEquals(B.getLeft(), 100); + B.setHorizontalMatchStyle(ConstraintWidget.MATCH_CONSTRAINT_SPREAD, 0, 50, 1); + root.layout(); + System.out.println("g) A: " + A + " B: " + B); + assertEquals(A.getWidth(), 100); + assertEquals(B.getWidth(), 50); + assertEquals(A.getLeft(), root.getWidth() - B.getRight()); + assertEquals(B.getLeft(), A.getLeft() + A.getWidth()); + B.setHorizontalMatchStyle(ConstraintWidget.MATCH_CONSTRAINT_PERCENT, 0, 0, 0.3f); + root.layout(); + System.out.println("h) A: " + A + " B: " + B); + assertEquals(A.getWidth(), 100); + assertEquals(B.getWidth(), (int) (0.3f * 600)); + assertEquals(A.getLeft(), root.getWidth() - B.getRight()); + assertEquals(B.getLeft(), A.getLeft() + A.getWidth()); + B.setDimensionRatio("16:9"); + B.setHorizontalMatchStyle(ConstraintWidget.MATCH_CONSTRAINT_RATIO, 0, 0, 1); + root.layout(); + System.out.println("i) A: " + A + " B: " + B); + assertEquals(A.getWidth(), 100); + assertEquals(B.getWidth(), (int)(16f/9f*20), 1); + assertEquals(A.getLeft(), root.getWidth() - B.getRight(), 1); + assertEquals(B.getLeft(), A.getLeft() + A.getWidth()); + A.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT); + A.setHorizontalMatchStyle(ConstraintWidget.MATCH_CONSTRAINT_SPREAD, 0, 0, 1); + B.setHorizontalDimensionBehaviour(ConstraintWidget.DimensionBehaviour.MATCH_CONSTRAINT); + B.setHorizontalMatchStyle(ConstraintWidget.MATCH_CONSTRAINT_SPREAD, 0, 0, 1); + B.setDimensionRatio(0, 0); + A.setVisibility(ConstraintWidget.VISIBLE); + A.setWidth(100); + A.setHeight(20); + B.setVisibility(ConstraintWidget.VISIBLE); + B.setWidth(100); + B.setHeight(20); + root.layout(); + System.out.println("j) A: " + A + " B: " + B); + assertEquals(A.getWidth(), B.getWidth()); + assertEquals(A.getWidth() + B.getWidth(), root.getWidth()); + A.setHorizontalWeight(1); + B.setHorizontalWeight(3); + root.layout(); + System.out.println("k) A: " + A + " B: " + B); + assertEquals(A.getWidth() * 3, B.getWidth()); + assertEquals(A.getWidth() + B.getWidth(), root.getWidth()); + } + @Test public void testSpreadChain() { ConstraintWidgetContainer root = new ConstraintWidgetContainer(0, 0, 600, 600); diff --git a/solver/src/test/java/android/support/constraint/solver/MatchConstraintTest.java b/solver/src/test/java/android/support/constraint/solver/MatchConstraintTest.java index 8780fee..f379825 100644 --- a/solver/src/test/java/android/support/constraint/solver/MatchConstraintTest.java +++ b/solver/src/test/java/android/support/constraint/solver/MatchConstraintTest.java @@ -46,11 +46,17 @@ public class MatchConstraintTest { assertEquals(A.getWidth(), 150); assertEquals(B.getWidth(), 100); assertEquals(root.getWidth(), 150); - root.setWidth(150); B.setWidth(200); root.setWidth(0); root.layout(); - System.out.println("root: " + root + " A: " + A + " B: " + B); + System.out.println("a) root: " + root + " A: " + A + " B: " + B); + assertEquals(A.getWidth(), 150); + assertEquals(B.getWidth(), 200); + assertEquals(root.getWidth(), 200); + A.setHorizontalMatchStyle(ConstraintWidget.MATCH_CONSTRAINT_SPREAD, 150, 200, 1); + root.setWidth(0); + root.layout(); + System.out.println("b) root: " + root + " A: " + A + " B: " + B); assertEquals(A.getWidth(), 200); assertEquals(B.getWidth(), 200); assertEquals(root.getWidth(), 200); diff --git a/solver/src/test/java/android/support/constraint/solver/OptimizationsTest.java b/solver/src/test/java/android/support/constraint/solver/OptimizationsTest.java index 7b7abb7..2d07969 100644 --- a/solver/src/test/java/android/support/constraint/solver/OptimizationsTest.java +++ b/solver/src/test/java/android/support/constraint/solver/OptimizationsTest.java @@ -48,9 +48,9 @@ public class OptimizationsTest { System.out.println("1) A: " + A); assertEquals(A.getLeft(), 8); - assertEquals(A.getTop(), 162); + assertEquals(A.getTop(), 163); assertEquals(A.getRight(), 592); - assertEquals(A.getBottom(), 172); + assertEquals(A.getBottom(), 173); A.setVisibility(ConstraintWidget.GONE); root.layout(); |