diff --git a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstrainScope.kt b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstrainScope.kt index 30c52cfd..e35626ec 100644 --- a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstrainScope.kt +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstrainScope.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.LayoutScopeMarker import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.layout.FirstBaseline +import androidx.compose.ui.layout.LastBaseline import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.constraintlayout.core.parser.CLArray @@ -83,6 +84,11 @@ class ConstrainScope internal constructor( */ val baseline: BaselineAnchorable = ConstraintBaselineAnchorable(containerObject) + /** + * The [LastBaseline] of the layout - can be constrained using [LastBaselineAnchorable.linkTo]. + */ + val lastBaseline: LastBaselineAnchorable = ConstraintLastBaselineAnchorable(containerObject) + /** * The width of the [ConstraintLayout] child. */ @@ -399,6 +405,7 @@ class ConstrainScope internal constructor( containerObject.remove("top") containerObject.remove("bottom") containerObject.remove("baseline") + containerObject.remove("lastBaseline") } /** @@ -523,6 +530,23 @@ private class ConstraintBaselineAnchorable constructor( containerObject.put("baseline", constraintArray) } + /** + * Adds a link towards a [ConstraintLayoutBaseScope.LastBaselineAnchor]. + */ + override fun linkTo( + anchor: ConstraintLayoutBaseScope.LastBaselineAnchor, + margin: Dp, + goneMargin: Dp + ) { + val constraintArray = CLArray(charArrayOf()).apply { + add(CLString.from(anchor.id.toString())) + add(CLString.from("lastBaseline")) + add(CLNumber(margin.value)) + add(CLNumber(goneMargin.value)) + } + containerObject.put("baseline", constraintArray) + } + /** * Adds a link towards a [ConstraintLayoutBaseScope.HorizontalAnchor]. */ @@ -540,4 +564,64 @@ private class ConstraintBaselineAnchorable constructor( } containerObject.put("baseline", constraintArray) } +} + +/** + * Represents the [LastBaseline] of a layout that can be anchored + * using [linkTo] in their `Modifier.constrainAs` blocks. + */ +private class ConstraintLastBaselineAnchorable constructor( + private val containerObject: CLObject +) : LastBaselineAnchorable { + /** + * Adds a link towards a [ConstraintLayoutBaseScope.BaselineAnchor]. + */ + override fun linkTo( + anchor: ConstraintLayoutBaseScope.BaselineAnchor, + margin: Dp, + goneMargin: Dp + ) { + val constraintArray = CLArray(charArrayOf()).apply { + add(CLString.from(anchor.id.toString())) + add(CLString.from("baseline")) + add(CLNumber(margin.value)) + add(CLNumber(goneMargin.value)) + } + containerObject.put("lastBaseline", constraintArray) + } + + /** + * Adds a link towards a [ConstraintLayoutBaseScope.LastBaselineAnchor]. + */ + override fun linkTo( + anchor: ConstraintLayoutBaseScope.LastBaselineAnchor, + margin: Dp, + goneMargin: Dp + ) { + val constraintArray = CLArray(charArrayOf()).apply { + add(CLString.from(anchor.id.toString())) + add(CLString.from("lastBaseline")) + add(CLNumber(margin.value)) + add(CLNumber(goneMargin.value)) + } + containerObject.put("lastBaseline", constraintArray) + } + + /** + * Adds a link towards a [ConstraintLayoutBaseScope.HorizontalAnchor]. + */ + override fun linkTo( + anchor: ConstraintLayoutBaseScope.HorizontalAnchor, + margin: Dp, + goneMargin: Dp + ) { + val targetAnchorName = AnchorFunctions.horizontalAnchorIndexToAnchorName(anchor.index) + val constraintArray = CLArray(charArrayOf()).apply { + add(CLString.from(anchor.id.toString())) + add(CLString.from(targetAnchorName)) + add(CLNumber(margin.value)) + add(CLNumber(goneMargin.value)) + } + containerObject.put("lastBaseline", constraintArray) + } } \ No newline at end of file diff --git a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt index 72139b8d..8136cd0a 100644 --- a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayout.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.layout.AlignmentLine import androidx.compose.ui.layout.FirstBaseline +import androidx.compose.ui.layout.LastBaseline import androidx.compose.ui.layout.LayoutIdParentData import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasurePolicy @@ -1210,6 +1211,8 @@ internal open class Measurer( val baseline = if (currentPlaceable != null && state.isBaselineNeeded(constraintWidget)) { currentPlaceable[FirstBaseline] + } else if (currentPlaceable != null && state.isLastBaselineNeeded(constraintWidget)) { + currentPlaceable[LastBaseline] } else { AlignmentLine.Unspecified } diff --git a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt index dc5a9787..0b32eedc 100644 --- a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt @@ -19,6 +19,7 @@ package androidx.constraintlayout.compose import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.layout.FirstBaseline +import androidx.compose.ui.layout.LastBaseline import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.constraintlayout.core.parser.CLArray @@ -105,6 +106,19 @@ abstract class ConstraintLayoutBaseScope internal constructor(extendFrom: CLObje val reference: LayoutReference ) + /** + * Represents a horizontal anchor corresponding to the [LastBaseline] of a layout that other + * layouts can link to in their `Modifier.constrainAs` or `constrain` blocks. + * + * @param reference The [LayoutReference] that this anchor belongs to. + */ + // TODO(popam): investigate if this can be just a HorizontalAnchor + @Stable + data class LastBaselineAnchor internal constructor( + internal val id: Any, + val reference: LayoutReference + ) + /** * Specifies additional constraints associated to the horizontal chain identified with [ref]. */ @@ -1740,6 +1754,12 @@ class ConstrainedLayoutReference(override val id: Any) : LayoutReference(id) { */ @Stable val baseline = ConstraintLayoutBaseScope.BaselineAnchor(id, this) + + /** + * The lastBaseline anchor of this layout. + */ + @Stable + val lastBaseline = ConstraintLayoutBaseScope.LastBaselineAnchor(id, this) } /** @@ -1918,6 +1938,7 @@ class VerticalAlign internal constructor( val Bottom = VerticalAlign("bottom") val Center = VerticalAlign("center") val Baseline = VerticalAlign("baseline") + val LastBaseline = VerticalAlign("lastBaseline") } } diff --git a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintScopeCommon.kt b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintScopeCommon.kt index 11c8a8ed..8817cab4 100644 --- a/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintScopeCommon.kt +++ b/constraintlayout/compose/src/main/java/androidx/constraintlayout/compose/ConstraintScopeCommon.kt @@ -18,6 +18,7 @@ package androidx.constraintlayout.compose import android.util.Log import androidx.compose.ui.layout.FirstBaseline +import androidx.compose.ui.layout.LastBaseline import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.constraintlayout.core.parser.CLArray @@ -64,6 +65,15 @@ interface HorizontalAnchorable { margin: Dp = 0.dp, goneMargin: Dp = 0.dp ) + + /** + * Adds a link towards a [ConstraintLayoutBaseScope.LastBaselineAnchor]. + */ + fun linkTo( + anchor: ConstraintLayoutBaseScope.LastBaselineAnchor, + margin: Dp = 0.dp, + goneMargin: Dp = 0.dp + ) } @JvmDefaultWithCompatibility @@ -81,6 +91,49 @@ interface BaselineAnchorable { goneMargin: Dp = 0.dp ) + /** + * Adds a link towards a [ConstraintLayoutBaseScope.LastBaselineAnchor]. + */ + fun linkTo( + anchor: ConstraintLayoutBaseScope.LastBaselineAnchor, + margin: Dp = 0.dp, + goneMargin: Dp = 0.dp + ) + + /** + * Adds a link towards a [ConstraintLayoutBaseScope.HorizontalAnchor]. + */ + fun linkTo( + anchor: ConstraintLayoutBaseScope.HorizontalAnchor, + margin: Dp = 0.dp, + goneMargin: Dp = 0.dp + ) +} + +@JvmDefaultWithCompatibility +/** + * Represents the [LastBaseline] of a layout that can be anchored + * using [linkTo] in their `Modifier.constrainAs` blocks. + */ +interface LastBaselineAnchorable { + /** + * Adds a link towards a [ConstraintLayoutBaseScope.BaselineAnchor]. + */ + fun linkTo( + anchor: ConstraintLayoutBaseScope.BaselineAnchor, + margin: Dp = 0.dp, + goneMargin: Dp = 0.dp + ) + + /** + * Adds a link towards a [ConstraintLayoutBaseScope.LastBaselineAnchor]. + */ + fun linkTo( + anchor: ConstraintLayoutBaseScope.LastBaselineAnchor, + margin: Dp = 0.dp, + goneMargin: Dp = 0.dp + ) + /** * Adds a link towards a [ConstraintLayoutBaseScope.HorizontalAnchor]. */ @@ -147,6 +200,20 @@ internal abstract class BaseHorizontalAnchorable( } containerObject.put(anchorName, constraintArray) } + + final override fun linkTo( + anchor: ConstraintLayoutBaseScope.LastBaselineAnchor, + margin: Dp, + goneMargin: Dp + ) { + val constraintArray = CLArray(charArrayOf()).apply { + add(CLString.from(anchor.id.toString())) + add(CLString.from("lastBaseline")) + add(CLNumber(margin.value)) + add(CLNumber(goneMargin.value)) + } + containerObject.put(anchorName, constraintArray) + } } internal object AnchorFunctions { diff --git a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintReference.java b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintReference.java index 53e09a9e..8dc1d481 100644 --- a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintReference.java +++ b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintReference.java @@ -87,7 +87,9 @@ public interface ConstraintReferenceFactory { protected int mMarginBottomGone = 0; int mMarginBaseline = 0; + int mMarginLastBaseline = 0; int mMarginBaselineGone = 0; + int mMarginLastBaselineGone = 0; float mPivotX = Float.NaN; float mPivotY = Float.NaN; @@ -118,12 +120,19 @@ public interface ConstraintReferenceFactory { protected Object mTopToTop = null; protected Object mTopToBottom = null; @Nullable Object mTopToBaseline = null; + @Nullable Object mTopToLastBaseline = null; protected Object mBottomToTop = null; protected Object mBottomToBottom = null; @Nullable Object mBottomToBaseline = null; + @Nullable Object mBottomToLastBaseline = null; Object mBaselineToBaseline = null; + Object mBaselineToLastBaseline = null; Object mBaselineToTop = null; Object mBaselineToBottom = null; + Object mLastBaselineToTop = null; + Object mLastBaselineToBottom = null; + Object mLastBaselineToBaseline = null; + Object mLastBaselineToLastBaseline = null; Object mCircularConstraint = null; private float mCircularAngle; private float mCircularDistance; @@ -292,6 +301,7 @@ public void setVerticalChainWeight(float weight) { public ConstraintReference clearVertical() { top().clear(); baseline().clear(); + lastBaseline().clear(); bottom().clear(); return this; } @@ -487,6 +497,12 @@ public ConstraintReference baseline() { return this; } + // @TODO: add description + public ConstraintReference lastBaseline() { + mLast = State.Constraint.LASTBASELINE_TO_LASTBASELINE; + return this; + } + // @TODO: add description public void addCustomColor(String name, int color) { mCustomColors.put(name, color); @@ -514,8 +530,13 @@ private void dereference() { mBottomToTop = get(mBottomToTop); mBottomToBottom = get(mBottomToBottom); mBaselineToBaseline = get(mBaselineToBaseline); + mBaselineToLastBaseline = get(mBaselineToLastBaseline); mBaselineToTop = get(mBaselineToTop); mBaselineToBottom = get(mBaselineToBottom); + mLastBaselineToBaseline = get(mLastBaselineToBaseline); + mLastBaselineToLastBaseline = get(mLastBaselineToLastBaseline); + mLastBaselineToTop = get(mLastBaselineToTop); + mLastBaselineToBottom = get(mLastBaselineToBottom); } // @TODO: add description @@ -594,6 +615,12 @@ ConstraintReference topToBaseline(Object reference) { return this; } + ConstraintReference topToLastBaseline(Object reference) { + mLast = State.Constraint.TOP_TO_LASTBASELINE; + mTopToLastBaseline = reference; + return this; + } + // @TODO: add description public ConstraintReference bottomToTop(Object reference) { mLast = State.Constraint.BOTTOM_TO_TOP; @@ -614,6 +641,12 @@ ConstraintReference bottomToBaseline(Object reference) { return this; } + ConstraintReference bottomToLastBaseline(Object reference) { + mLast = State.Constraint.BOTTOM_TO_LASTBASELINE; + mBottomToLastBaseline = reference; + return this; + } + // @TODO: add description public ConstraintReference baselineToBaseline(Object reference) { mLast = State.Constraint.BASELINE_TO_BASELINE; @@ -621,6 +654,13 @@ public ConstraintReference baselineToBaseline(Object reference) { return this; } + // @TODO: add description + public ConstraintReference baselineToLastBaseline(Object reference) { + mLast = State.Constraint.BASELINE_TO_LASTBASELINE; + mBaselineToLastBaseline = reference; + return this; + } + // @TODO: add description public ConstraintReference baselineToTop(Object reference) { mLast = State.Constraint.BASELINE_TO_TOP; @@ -635,6 +675,34 @@ public ConstraintReference baselineToBottom(Object reference) { return this; } + // @TODO: add description + public ConstraintReference lastBaselineToTop(Object reference) { + mLast = State.Constraint.LASTBASELINE_TO_TOP; + mLastBaselineToTop = reference; + return this; + } + + // @TODO: add description + public ConstraintReference lastBaselineToBottom(Object reference) { + mLast = State.Constraint.LASTBASELINE_TO_BOTTOM; + mLastBaselineToBottom = reference; + return this; + } + + // @TODO: add description + public ConstraintReference lastBaselineToBaseline(Object reference) { + mLast = State.Constraint.LASTBASELINE_TO_BASELINE; + mLastBaselineToBaseline = reference; + return this; + } + + // @TODO: add description + public ConstraintReference lastBaselineToLastBaseline(Object reference) { + mLast = State.Constraint.LASTBASELINE_TO_LASTBASELINE; + mLastBaselineToLastBaseline = reference; + return this; + } + // @TODO: add description public ConstraintReference centerHorizontally(Object reference) { Object ref = get(reference); @@ -731,22 +799,32 @@ public ConstraintReference margin(int value) { break; case TOP_TO_TOP: case TOP_TO_BOTTOM: - case TOP_TO_BASELINE: { + case TOP_TO_BASELINE: + case TOP_TO_LASTBASELINE: { mMarginTop = value; } break; case BOTTOM_TO_TOP: case BOTTOM_TO_BOTTOM: - case BOTTOM_TO_BASELINE: { + case BOTTOM_TO_BASELINE: + case BOTTOM_TO_LASTBASELINE: { mMarginBottom = value; } break; case BASELINE_TO_BOTTOM: case BASELINE_TO_TOP: - case BASELINE_TO_BASELINE: { + case BASELINE_TO_BASELINE: + case BASELINE_TO_LASTBASELINE: { mMarginBaseline = value; } break; + case LASTBASELINE_TO_BOTTOM: + case LASTBASELINE_TO_TOP: + case LASTBASELINE_TO_BASELINE: + case LASTBASELINE_TO_LASTBASELINE: { + mMarginLastBaseline = value; + } + break; case CIRCULAR_CONSTRAINT: { mCircularDistance = value; } @@ -791,22 +869,32 @@ public ConstraintReference marginGone(int value) { break; case TOP_TO_TOP: case TOP_TO_BOTTOM: - case TOP_TO_BASELINE: { + case TOP_TO_BASELINE: + case TOP_TO_LASTBASELINE: { mMarginTopGone = value; } break; case BOTTOM_TO_TOP: case BOTTOM_TO_BOTTOM: - case BOTTOM_TO_BASELINE: { + case BOTTOM_TO_BASELINE: + case BOTTOM_TO_LASTBASELINE: { mMarginBottomGone = value; } break; case BASELINE_TO_TOP: case BASELINE_TO_BOTTOM: - case BASELINE_TO_BASELINE: { + case BASELINE_TO_BASELINE: + case BASELINE_TO_LASTBASELINE: { mMarginBaselineGone = value; } break; + case LASTBASELINE_TO_TOP: + case LASTBASELINE_TO_BOTTOM: + case LASTBASELINE_TO_BASELINE: + case LASTBASELINE_TO_LASTBASELINE: { + mMarginLastBaselineGone = value; + } + break; default: break; } @@ -855,9 +943,11 @@ public ConstraintReference bias(float value) { case TOP_TO_TOP: case TOP_TO_BOTTOM: case TOP_TO_BASELINE: + case TOP_TO_LASTBASELINE: case BOTTOM_TO_TOP: case BOTTOM_TO_BOTTOM: - case BOTTOM_TO_BASELINE: { + case BOTTOM_TO_BASELINE: + case BOTTOM_TO_LASTBASELINE: { mVerticalBias = value; } break; @@ -890,6 +980,11 @@ public ConstraintReference clearAll() { mBottomToBottom = null; mMarginBottom = 0; mBaselineToBaseline = null; + mLastBaselineToTop = null; + mLastBaselineToBottom = null; + mLastBaselineToLastBaseline = null; + mLastBaselineToBaseline = null; + mMarginLastBaseline = 0; mCircularConstraint = null; mHorizontalBias = 0.5f; mVerticalBias = 0.5f; @@ -940,20 +1035,24 @@ public ConstraintReference clear() { break; case TOP_TO_TOP: case TOP_TO_BOTTOM: - case TOP_TO_BASELINE: { + case TOP_TO_BASELINE: + case TOP_TO_LASTBASELINE: { mTopToTop = null; mTopToBottom = null; mTopToBaseline = null; + mTopToLastBaseline = null; mMarginTop = 0; mMarginTopGone = 0; } break; case BOTTOM_TO_TOP: case BOTTOM_TO_BOTTOM: - case BOTTOM_TO_BASELINE: { + case BOTTOM_TO_BASELINE: + case BOTTOM_TO_LASTBASELINE: { mBottomToTop = null; mBottomToBottom = null; mBottomToBaseline = null; + mBottomToLastBaseline = null; mMarginBottom = 0; mMarginBottomGone = 0; } @@ -962,6 +1061,22 @@ public ConstraintReference clear() { mBaselineToBaseline = null; } break; + case LASTBASELINE_TO_LASTBASELINE: { + mLastBaselineToLastBaseline = null; + } + break; + case LASTBASELINE_TO_BASELINE: { + mLastBaselineToBaseline = null; + } + break; + case LASTBASELINE_TO_TOP: { + mLastBaselineToTop = null; + } + break; + case LASTBASELINE_TO_BOTTOM: { + mLastBaselineToBottom = null; + } + break; case CIRCULAR_CONSTRAINT: { mCircularConstraint = null; } @@ -1046,7 +1161,8 @@ private void applyConnection(ConstraintWidget widget, ConstraintAnchor.Type.BOTTOM), mMarginTop, mMarginTopGone, false); } break; - case TOP_TO_BASELINE: { + case TOP_TO_BASELINE: + case TOP_TO_LASTBASELINE: { widget.immediateConnect(ConstraintAnchor.Type.TOP, target, ConstraintAnchor.Type.BASELINE, mMarginTop, mMarginTopGone); } @@ -1061,22 +1177,28 @@ private void applyConnection(ConstraintWidget widget, ConstraintAnchor.Type.BOTTOM), mMarginBottom, mMarginBottomGone, false); } break; - case BOTTOM_TO_BASELINE: { + case BOTTOM_TO_BASELINE: + case BOTTOM_TO_LASTBASELINE: { widget.immediateConnect(ConstraintAnchor.Type.BOTTOM, target, ConstraintAnchor.Type.BASELINE, mMarginBottom, mMarginBottomGone); } break; - case BASELINE_TO_BASELINE: { + case BASELINE_TO_BASELINE: + case BASELINE_TO_LASTBASELINE: + case LASTBASELINE_TO_BASELINE: + case LASTBASELINE_TO_LASTBASELINE: { widget.immediateConnect(ConstraintAnchor.Type.BASELINE, target, ConstraintAnchor.Type.BASELINE, mMarginBaseline, mMarginBaselineGone); } break; - case BASELINE_TO_TOP: { + case BASELINE_TO_TOP: + case LASTBASELINE_TO_TOP: { widget.immediateConnect(ConstraintAnchor.Type.BASELINE, target, ConstraintAnchor.Type.TOP, mMarginBaseline, mMarginBaselineGone); } break; - case BASELINE_TO_BOTTOM: { + case BASELINE_TO_BOTTOM: + case LASTBASELINE_TO_BOTTOM: { widget.immediateConnect(ConstraintAnchor.Type.BASELINE, target, ConstraintAnchor.Type.BOTTOM, mMarginBaseline, mMarginBaselineGone); } @@ -1105,13 +1227,27 @@ public void applyWidgetConstraints() { applyConnection(mConstraintWidget, mTopToTop, State.Constraint.TOP_TO_TOP); applyConnection(mConstraintWidget, mTopToBottom, State.Constraint.TOP_TO_BOTTOM); applyConnection(mConstraintWidget, mTopToBaseline, State.Constraint.TOP_TO_BASELINE); + applyConnection(mConstraintWidget, mTopToLastBaseline, + State.Constraint.TOP_TO_LASTBASELINE); applyConnection(mConstraintWidget, mBottomToTop, State.Constraint.BOTTOM_TO_TOP); applyConnection(mConstraintWidget, mBottomToBottom, State.Constraint.BOTTOM_TO_BOTTOM); applyConnection(mConstraintWidget, mBottomToBaseline, State.Constraint.BOTTOM_TO_BASELINE); + applyConnection(mConstraintWidget, mBottomToLastBaseline, + State.Constraint.BOTTOM_TO_LASTBASELINE); applyConnection(mConstraintWidget, mBaselineToBaseline, State.Constraint.BASELINE_TO_BASELINE); + applyConnection(mConstraintWidget, mBaselineToLastBaseline, + State.Constraint.BASELINE_TO_LASTBASELINE); applyConnection(mConstraintWidget, mBaselineToTop, State.Constraint.BASELINE_TO_TOP); applyConnection(mConstraintWidget, mBaselineToBottom, State.Constraint.BASELINE_TO_BOTTOM); + applyConnection(mConstraintWidget, mLastBaselineToBaseline, + State.Constraint.LASTBASELINE_TO_BASELINE); + applyConnection(mConstraintWidget, mLastBaselineToLastBaseline, + State.Constraint.LASTBASELINE_TO_LASTBASELINE); + applyConnection(mConstraintWidget, mLastBaselineToTop, + State.Constraint.LASTBASELINE_TO_TOP); + applyConnection(mConstraintWidget, mLastBaselineToBottom, + State.Constraint.LASTBASELINE_TO_BOTTOM); applyConnection(mConstraintWidget, mCircularConstraint, State.Constraint.CIRCULAR_CONSTRAINT); } diff --git a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java index d94a9e9f..98de8483 100644 --- a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java +++ b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java @@ -411,6 +411,7 @@ static void override(CLObject baseJson, base.remove("top"); base.remove("bottom"); base.remove("baseline"); + base.remove("lastBaseline"); base.remove("center"); base.remove("centerHorizontally"); base.remove("centerVertically"); @@ -1867,6 +1868,10 @@ static void parseConstraint( state.baselineNeededFor(targetReference.getKey()); reference.topToBaseline(targetReference); break; + case "lastBaseline": + state.lastBaselineNeededFor(targetReference.getKey()); + reference.topToLastBaseline(targetReference); + break; } break; case "bottom": @@ -1880,6 +1885,9 @@ static void parseConstraint( case "baseline": state.baselineNeededFor(targetReference.getKey()); reference.bottomToBaseline(targetReference); + case "lastBaseline": + state.lastBaselineNeededFor(targetReference.getKey()); + reference.bottomToLastBaseline(targetReference); } break; case "baseline": @@ -1889,6 +1897,11 @@ static void parseConstraint( state.baselineNeededFor(targetReference.getKey()); reference.baselineToBaseline(targetReference); break; + case "lastBaseline": + state.lastBaselineNeededFor(reference.getKey()); + state.lastBaselineNeededFor(targetReference.getKey()); + reference.baselineToLastBaseline(targetReference); + break; case "top": state.baselineNeededFor(reference.getKey()); reference.baselineToTop(targetReference); @@ -1899,6 +1912,28 @@ static void parseConstraint( break; } break; + case "lastBaseline": + switch (anchor) { + case "baseline": + state.lastBaselineNeededFor(reference.getKey()); + state.lastBaselineNeededFor(targetReference.getKey()); + reference.lastBaselineToBaseline(targetReference); + break; + case "lastBaseline": + state.lastBaselineNeededFor(reference.getKey()); + state.lastBaselineNeededFor(targetReference.getKey()); + reference.lastBaselineToLastBaseline(targetReference); + break; + case "top": + state.lastBaselineNeededFor(reference.getKey()); + reference.lastBaselineToTop(targetReference); + break; + case "bottom": + state.lastBaselineNeededFor(reference.getKey()); + reference.lastBaselineToBottom(targetReference); + break; + } + break; case "left": isHorizontalConstraint = true; isHorOriginLeft = true; @@ -1984,6 +2019,11 @@ static void parseConstraint( state.baselineNeededFor(targetReference.getKey()); reference.baselineToBaseline(targetReference); break; + case "lastBaseline": + state.lastBaselineNeededFor(reference.getKey()); + state.lastBaselineNeededFor(targetReference.getKey()); + reference.lastBaselineToLastBaseline(targetReference); + break; } } } diff --git a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/State.java b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/State.java index 30ad1b82..601b2c88 100644 --- a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/State.java +++ b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/state/State.java @@ -68,12 +68,19 @@ public enum Constraint { TOP_TO_TOP, TOP_TO_BOTTOM, TOP_TO_BASELINE, + TOP_TO_LASTBASELINE, BOTTOM_TO_TOP, BOTTOM_TO_BOTTOM, BOTTOM_TO_BASELINE, + BOTTOM_TO_LASTBASELINE, BASELINE_TO_BASELINE, + BASELINE_TO_LASTBASELINE, BASELINE_TO_TOP, + LASTBASELINE_TO_TOP, BASELINE_TO_BOTTOM, + LASTBASELINE_TO_BOTTOM, + LASTBASELINE_TO_BASELINE, + LASTBASELINE_TO_LASTBASELINE, CENTER_HORIZONTALLY, CENTER_VERTICALLY, CIRCULAR_CONSTRAINT @@ -688,4 +695,35 @@ public boolean isBaselineNeeded(ConstraintWidget constraintWidget) { } return mBaselineNeededWidgets.contains(constraintWidget); } + + // ================= add lastBaseline code================================ + ArrayList mLastBaselineNeeded = new ArrayList<>(); + ArrayList mLastBaselineNeededWidgets = new ArrayList<>(); + boolean mDirtyLastBaselineNeededWidgets = true; + + /** + * Baseline is needed for this object + */ + public void lastBaselineNeededFor(Object id) { + mLastBaselineNeeded.add(id); + mDirtyLastBaselineNeededWidgets = true; + } + + /** + * Does this constraintWidget need a lastBaseline + * + * @return true if the constraintWidget needs a lastBaseline + */ + public boolean isLastBaselineNeeded(ConstraintWidget constraintWidget) { + if (mDirtyLastBaselineNeededWidgets) { + mLastBaselineNeededWidgets.clear(); + for (Object id : mLastBaselineNeeded) { + ConstraintWidget widget = mReferences.get(id).getConstraintWidget(); + if (widget != null) mLastBaselineNeededWidgets.add(widget); + } + + mDirtyLastBaselineNeededWidgets = false; + } + return mLastBaselineNeededWidgets.contains(constraintWidget); + } } diff --git a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintAnchor.java b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintAnchor.java index be335b27..15a8b143 100644 --- a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintAnchor.java +++ b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintAnchor.java @@ -100,7 +100,8 @@ public boolean hasFinalValue() { /** * Define the type of anchor */ - public enum Type {NONE, LEFT, TOP, RIGHT, BOTTOM, BASELINE, CENTER, CENTER_X, CENTER_Y} + public enum Type {NONE, LEFT, TOP, RIGHT, BOTTOM, BASELINE, + LAST_BASELINE, CENTER, CENTER_X, CENTER_Y} private static final int UNSET_GONE_MARGIN = Integer.MIN_VALUE; @@ -286,14 +287,17 @@ public boolean isValidConnection(ConstraintAnchor anchor) { if (mType == Type.BASELINE && (!anchor.getOwner().hasBaseline() || !getOwner().hasBaseline())) { return false; + } else if (mType == Type.LAST_BASELINE + && (!anchor.getOwner().hasLastBaseline() || !getOwner().hasLastBaseline())) { + return false; } return true; } switch (mType) { case CENTER: { // allow everything but baseline and center_x/center_y - return target != Type.BASELINE && target != Type.CENTER_X - && target != Type.CENTER_Y; + return target != Type.BASELINE && target != Type.LAST_BASELINE + && target != Type.CENTER_X && target != Type.CENTER_Y; } case LEFT: case RIGHT: { @@ -311,7 +315,8 @@ public boolean isValidConnection(ConstraintAnchor anchor) { } return isCompatible; } - case BASELINE: { + case BASELINE: + case LAST_BASELINE: { if (target == Type.LEFT || target == Type.RIGHT) { return false; } @@ -338,6 +343,7 @@ public boolean isSideAnchor() { case BOTTOM: return true; case BASELINE: + case LAST_BASELINE: case CENTER: case CENTER_X: case CENTER_Y: @@ -361,7 +367,7 @@ public boolean isSimilarDimensionConnection(ConstraintAnchor anchor) { } switch (mType) { case CENTER: { - return target != Type.BASELINE; + return target != Type.BASELINE && target != Type.LAST_BASELINE; } case LEFT: case RIGHT: @@ -371,9 +377,10 @@ public boolean isSimilarDimensionConnection(ConstraintAnchor anchor) { case TOP: case BOTTOM: case CENTER_Y: - case BASELINE: { - return target == Type.TOP || target == Type.BOTTOM - || target == Type.CENTER_Y || target == Type.BASELINE; + case BASELINE: + case LAST_BASELINE: { + return target == Type.TOP || target == Type.BOTTOM || target == Type.CENTER_Y + || target == Type.BASELINE || target == Type.LAST_BASELINE; } case NONE: return false; @@ -419,6 +426,7 @@ public boolean isVerticalAnchor() { case TOP: case BOTTOM: case BASELINE: + case LAST_BASELINE: case NONE: return true; } @@ -525,6 +533,7 @@ public final ConstraintAnchor getOpposite() { return mOwner.mTop; } case BASELINE: + case LAST_BASELINE: case CENTER: case CENTER_X: case CENTER_Y: diff --git a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidget.java b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidget.java index 0cabeb50..428b819e 100644 --- a/constraintlayout/core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidget.java +++ b/constraintlayout/core/src/main/java/androidx/constraintlayout/core/widgets/ConstraintWidget.java @@ -190,6 +190,9 @@ public void setFinalVertical(int y1, int y2) { if (mHasBaseline) { mBaseline.setFinalValue(y1 + mBaselineDistance); } + if (mHasLastBaseline) { + mLastBaseline.setFinalValue(y1 + mLastBaselineDistance); + } mResolvedVertical = true; if (LinearSystem.FULL_DEBUG) { System.out.println("*** SET FINAL VERTICAL FOR " + getDebugName() @@ -211,6 +214,20 @@ public void setFinalBaseline(int baselineValue) { mResolvedVertical = true; } + // @TODO: add description + public void setFinalLastBaseline(int lastBaselineValue) { + if (!mHasLastBaseline) { + return; + } + int y1 = lastBaselineValue - mLastBaselineDistance; + int y2 = y1 + mHeight; + mY = y1; + mTop.setFinalValue(y1); + mBottom.setFinalValue(y2); + mLastBaseline.setFinalValue(lastBaselineValue); + mResolvedVertical = true; + } + public boolean isResolvedHorizontally() { return mResolvedHorizontal || (mLeft.hasFinalValue() && mRight.hasFinalValue()); } @@ -255,7 +272,8 @@ public boolean hasDanglingDimension(int orientation) { return horizontalTargets < 2; } else { int verticalTargets = (mTop.mTarget != null ? 1 : 0) - + (mBottom.mTarget != null ? 1 : 0) + (mBaseline.mTarget != null ? 1 : 0); + + (mBottom.mTarget != null ? 1 : 0) + + (mBaseline.mTarget != null || mLastBaseline.mTarget != null ? 1 : 0); return verticalTargets < 2; } } @@ -333,6 +351,7 @@ public boolean hasResolvedTargets(int orientation, int size) { private int[] mMaxDimension = {Integer.MAX_VALUE, Integer.MAX_VALUE}; public float mCircleConstraintAngle = Float.NaN; private boolean mHasBaseline = false; + private boolean mHasLastBaseline = false; private boolean mInPlaceholder; private boolean mInVirtualLayout = false; @@ -385,6 +404,14 @@ public boolean getHasBaseline() { return mHasBaseline; } + public boolean getHasLastBaseline() { + return mHasLastBaseline; + } + + public void setHasLastBaseline(boolean hasLastBaseline) { + this.mHasLastBaseline = hasLastBaseline; + } + public boolean isInPlaceholder() { return mInPlaceholder; } @@ -457,6 +484,8 @@ public enum DimensionBehaviour { public ConstraintAnchor mRight = new ConstraintAnchor(this, ConstraintAnchor.Type.RIGHT); public ConstraintAnchor mBottom = new ConstraintAnchor(this, ConstraintAnchor.Type.BOTTOM); public ConstraintAnchor mBaseline = new ConstraintAnchor(this, ConstraintAnchor.Type.BASELINE); + public ConstraintAnchor mLastBaseline = new ConstraintAnchor(this, + ConstraintAnchor.Type.LAST_BASELINE); ConstraintAnchor mCenterX = new ConstraintAnchor(this, ConstraintAnchor.Type.CENTER_X); ConstraintAnchor mCenterY = new ConstraintAnchor(this, ConstraintAnchor.Type.CENTER_Y); public ConstraintAnchor mCenter = new ConstraintAnchor(this, ConstraintAnchor.Type.CENTER); @@ -500,6 +529,9 @@ public enum DimensionBehaviour { // Baseline distance relative to the top of the widget int mBaselineDistance = 0; + // LastBaseline distance relative to the top of the widget + int mLastBaselineDistance = 0; + // Minimum sizes for the widget protected int mMinWidth; protected int mMinHeight; @@ -730,6 +762,7 @@ public StringBuilder serialize(StringBuilder ret) { serializeAnchor(ret, "right", mRight); serializeAnchor(ret, "bottom", mBottom); serializeAnchor(ret, "baseline", mBaseline); + serializeAnchor(ret, "lastBaseline", mLastBaseline); serializeAnchor(ret, "centerX", mCenterX); serializeAnchor(ret, "centerY", mCenterY); serializeCircle(ret, mCenter, mCircleConstraintAngle); @@ -1332,6 +1365,15 @@ public boolean hasBaseline() { return mHasBaseline; } + /** + * Return true if this widget has a lastBaseline + * + * @return true if the widget has a lastBaseline, false otherwise + */ + public boolean hasLastBaseline() { + return mHasLastBaseline; + } + /** * Return the baseline distance relative to the top of the widget * @@ -2233,7 +2275,8 @@ public void resetAnchors() { /** * Given a type of anchor, returns the corresponding anchor. * - * @param anchorType type of the anchor (LEFT, TOP, RIGHT, BOTTOM, BASELINE, CENTER_X, CENTER_Y) + * @param anchorType type of the anchor (LEFT, TOP, RIGHT, BOTTOM, BASELINE, LAST_BASELINE, + * CENTER_X, CENTER_Y) * @return the matching anchor */ public ConstraintAnchor getAnchor(ConstraintAnchor.Type anchorType) { @@ -2253,6 +2296,9 @@ public ConstraintAnchor getAnchor(ConstraintAnchor.Type anchorType) { case BASELINE: { return mBaseline; } + case LAST_BASELINE: { + return mLastBaseline; + } case CENTER_X: { return mCenterX; } @@ -3763,6 +3809,7 @@ public void getSceneString(StringBuilder ret) { getSceneString(ret, "right", mRight); getSceneString(ret, "bottom", mBottom); getSceneString(ret, "baseline", mBaseline); + serializeAnchor(ret, "lastBaseline", mLastBaseline); getSceneString(ret, "centerX", mCenterX); getSceneString(ret, "centerY", mCenterY); getSceneString(ret, " width", diff --git a/projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/LastBaselineDemo.kt b/projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/LastBaselineDemo.kt new file mode 100644 index 00000000..83eb84c1 --- /dev/null +++ b/projects/ComposeConstraintLayout/app/src/main/java/com/example/constraintlayout/LastBaselineDemo.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 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 com.example.constraintlayout + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.* + +@Preview(group = "baseline") +@Composable +fun LastBaselineDSL() { + ConstraintLayout( + ConstraintSet { + val a = createRefFor("num1") + val b = createRefFor("num2") + constrain(a) { + start.linkTo(parent.start) + end.linkTo(b.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + constrain(b) { + top.linkTo(a.lastBaseline) + start.linkTo(a.end) + end.linkTo(parent.end) + } + }, + modifier = Modifier.fillMaxSize() + ) { + Button( + modifier = Modifier.layoutId("num1").width(100.dp), + onClick = {}, + ) { + Text(text = String.format("Text-%s", 1), fontSize = 30.sp) + } + + Button( + modifier = Modifier.layoutId("num2").size(200.dp), + onClick = {}, + ) { + Text(text = String.format("Text-%s", 2), fontSize = 30.sp) + } + } +} + +@Preview(group = "baseline1") +@Composable +fun LastBaselineJSON() { + ConstraintLayout( + androidx.constraintlayout.compose.ConstraintSet( + """ + { + num1 : { + left: ['parent', 'left'], + right: ['num2', 'left'], + lastBaseline: ['num2', 'top'] + }, + num2 : { + top: ['parent', 'top'], + bottom: ['parent', 'bottom'], + left: ['num1', 'right'], + right: ['parent', 'right'], + } + + } + """.trimIndent() + ), + modifier = Modifier.fillMaxSize()) { + val numArray = arrayOf("1", "2") + for (num in numArray) { + Button( + modifier = Modifier.layoutId("num$num").width(100.dp), + onClick = {}, + ) { + Text(text = String.format("Text-%s", num), fontSize = 30.sp) + } + } + } +} \ No newline at end of file