Merging constraint layout 1.1

Bug: N/A
Test: Done in another repository
Change-Id: I53b124f88dae4b0e1c1b3f6eef93d7c0d95db4d0
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 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 @@
     }
 
     /**
-     * @hide
      * @param attrs
+     * @hide
      */
     @Override
     protected void init(AttributeSet attrs) {
@@ -196,4 +196,12 @@
         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 @@
     /**
      * @hide
      */
-    protected int mCount = 0;
+    protected int mCount;
 
     /**
      * @hide
@@ -50,7 +50,7 @@
     /**
      * @hide
      */
-    protected android.support.constraint.solver.widgets.Helper mHelperWidget = null;
+    protected android.support.constraint.solver.widgets.Helper mHelperWidget;
     /**
      * @hide
      */
@@ -235,7 +235,7 @@
         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.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 @@
     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 @@
             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 @@
         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 @@
             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.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 @@
     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 @@
         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 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 @@
             clone.circleConstraint = circleConstraint;
             clone.circleRadius = circleRadius;
             clone.circleAngle = circleAngle;
+            clone.mBarrierAllowsGoneWidgets = mBarrierAllowsGoneWidgets;
             return clone;
         }
 
@@ -640,6 +663,8 @@
             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 @@
                     }
                 }
             }
+            if (view instanceof Barrier) {
+                Barrier barrier = ((Barrier) view);
+                constraint.mBarrierAllowsGoneWidgets = barrier.allowsGoneWidget();
+                constraint.mReferenceIds = barrier.getReferencedIds();
+                constraint.mBarrierDirection = barrier.getType();
+            }
         }
     }
 
@@ -775,18 +806,24 @@
             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 @@
                     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 @@
                 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 @@
         }
     }
 
+    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.SolverVariable;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 
 /**
  * A Barrier takes multiple widgets
@@ -47,6 +48,8 @@
 
     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 @@
         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 @@
             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 @@
                                 strength);
                     }
                 }
-                previousVisibleWidget = widget;
+                if (widget.getVisibility() != GONE) {
+                    previousVisibleWidget = widget;
+                }
                 widget = next;
             }
         } else if (isChainSpreadInside && firstVisibleWidget != null) {
@@ -340,7 +345,10 @@
             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 @@
                                 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 @@
 
     private void defineChainProperties(){
         int offset = mOrientation * 2;
+        ConstraintWidget lastVisited = mFirst;
 
         // TraverseChain
         ConstraintWidget widget = mFirst;
@@ -84,23 +85,20 @@
         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 @@
                         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 @@
         }
         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 @@
     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 @@
         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 @@
     // 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 @@
     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 @@
     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 @@
         if (mResolutionHeight != null) {
             mResolutionHeight.reset();
         }
+        mBelongingGroup = null;
+        mOptimizerMeasurable = false;
+        mOptimizerMeasured = false;
+        mGroupsToSolver = false;
     }
 
     /*-----------------------------------------------------------------------*/
@@ -829,6 +843,22 @@
     }
 
     /**
+     * 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 @@
     }
 
     /**
+     * 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 @@
     }
 
     /**
+     * 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 @@
         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 @@
     }
 
     /**
+     * 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 @@
     }
 
     /**
+     * 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 @@
         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 @@
             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 @@
         }
 
         // 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 @@
             }
 
             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 @@
 
             } 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) {
+                startStrength = SolverVariable.STRENGTH_FIXED;
+                endStrength = SolverVariable.STRENGTH_FIXED;
             }
 
-            if (applyBoundsCheck) {
-                // Al >= Tl & Ar <= Tr
-                system.addGreaterThan(begin, beginTarget, beginAnchor.getMargin(), SolverVariable.STRENGTH_FIXED);
-                system.addLowerThan(end, endTarget, -endAnchor.getMargin(), 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 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 @@
     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 @@
         mPaddingRight = 0;
         mPaddingTop = 0;
         mPaddingBottom = 0;
+        mWidgetGroups.clear();
+        mSkipSolver = false;
         super.reset();
     }
 
@@ -313,7 +324,9 @@
             if (!optimizeFor(Optimizer.OPTIMIZATION_DIMENSIONS)) {
                 optimizeReset();
             }
-            optimize();
+            if (!optimizeFor(Optimizer.OPTIMIZATION_GROUPS)) {
+                optimize();
+            }
             mSystem.graphOptimizer = true;
         } else {
             mSystem.graphOptimizer = false;
@@ -331,154 +344,197 @@
         // 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 {
-                    createObjectVariables(mSystem);
+                    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 {
+                    updateFromSolver(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 {
-                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;
-                    }
-                    if (widget.mListDimensionBehaviors[DIMENSION_VERTICAL] == DimensionBehaviour.MATCH_CONSTRAINT
-                        && widget.getHeight() < widget.getWrapHeight()) {
-                        Optimizer.flags[Optimizer.FLAG_RECOMPUTE_BOUNDS] = true;
-                        break;
-                    }
-                }
-            }
-            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 (DEBUG_LAYOUT) {
-                            System.out.println("layout override width from " + getWidth() + " vs " + maxX);
+                        if (widget.mListDimensionBehaviors[DIMENSION_HORIZONTAL]
+                            == DimensionBehaviour.MATCH_CONSTRAINT
+                            && widget.getWidth() < widget.getWrapWidth()) {
+                            Optimizer.flags[Optimizer.FLAG_RECOMPUTE_BOUNDS] = true;
+                            break;
                         }
-                        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);
+                        if (widget.mListDimensionBehaviors[DIMENSION_VERTICAL]
+                            == DimensionBehaviour.MATCH_CONSTRAINT
+                            && widget.getHeight() < widget.getWrapHeight()) {
+                            Optimizer.flags[Optimizer.FLAG_RECOMPUTE_BOUNDS] = true;
+                            break;
                         }
-                        setHeight(maxY);
-                        mListDimensionBehaviors[DIMENSION_VERTICAL] = WRAP_CONTENT; // force using the solver
-                        wrap_override = true;
-                        needsSolving = true;
                     }
                 }
-            }
+                needsSolving = false;
 
-            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);
+                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());
                     }
-                    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) {
+                    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 3, width from " + getWidth() + " vs " + prew);
+                                System.out.println(
+                                    "layout override width from " + getWidth() + " vs " + maxX);
                             }
-                            mWidthMeasuredTooSmall = true;
+                            setWidth(maxX);
+                            mListDimensionBehaviors[DIMENSION_HORIZONTAL] = WRAP_CONTENT; // force using the solver
                             wrap_override = true;
-                            mListDimensionBehaviors[DIMENSION_HORIZONTAL] = DimensionBehaviour.FIXED;
-                            setWidth(prew);
                             needsSolving = true;
                         }
                     }
-                    if (mListDimensionBehaviors[DIMENSION_VERTICAL] == WRAP_CONTENT && preh > 0) {
-                        if (getHeight() > preh) {
+                    if (originalVerticalDimensionBehaviour == WRAP_CONTENT) {
+                        if (getHeight() < maxY) {
                             if (DEBUG_LAYOUT) {
-                                System.out.println("layout override 3, height from " + getHeight() + " vs " + preh);
+                                System.out.println(
+                                    "layout override height from " + getHeight() + " vs " + maxY);
                             }
-                            mHeightMeasuredTooSmall = true;
+                            setHeight(maxY);
+                            mListDimensionBehaviors[DIMENSION_VERTICAL] = WRAP_CONTENT; // force using the solver
                             wrap_override = true;
-                            mListDimensionBehaviors[DIMENSION_VERTICAL] = DimensionBehaviour.FIXED;
-                            setHeight(preh);
                             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);
+                                }
+                                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);
+                                }
+                                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 @@
         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 @@
      */
     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 @@
      */
     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 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 @@
                 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 @@
                         return false;
                     }
                 }
+                if (widget.mDimensionRatio != 0.0f) {
+                    return false;
+                }
             }
 
             // go to the next widget
@@ -481,7 +488,7 @@
 
         // 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 @@
             }
             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 @@
             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 @@
             } else if (isChainSpreadInside) {
                 distance -= extraMargin;
             }
-            widget = firstVisibleWidget;
+            widget = first;
             float gap = distance / (float) (numVisibleWidgets + 1);
             if (isChainSpreadInside) {
                 if (numVisibleWidgets > 1) {
@@ -605,7 +612,10 @@
                     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 @@
                     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 @@
 
         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 @@
         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 @@
     }
 
     /**
+     * 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 @@
         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 @@
         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 @@
         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 @@
 
         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();