ConstraintLayout can apply constraints only to direct children. Since the TextInputEditText view you want to constrain the button to is a child of the TextInputLayout and not a direct child of the ConstraintLayout, any attempt to apply constraints between the button and the TextInputEditText fails.
We could set a layout listener on the ConstraintLayout, capture the size and location of the TextInputEditText and modify the button view to set its size and position. This would require special coding to make the changes and is a valid solution.
However, what if we had a reusable type of view that could reference a view embedded in a child ViewGroup and replicate the embedded view's size and placement as a direct child of the ConstraintLayout? We could then constrain the button to this new type of view and get the placement that we want.
Such a custom view could be based upon the View class, but it could also be built upon the ConstraintHelper class which extends View. This class is the basis for other "helper" classes in ConstraintLayout such as Group, GuideLine and others. From my perspective, ConstraintHelper is a good fit for the custom view.
Here is such a custom view:
ViewSurrogate.kt
class ViewSurrogate @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {
private lateinit var mContainer: ConstraintLayout
private var mTargetId: Int = NO_ID
private lateinit var mTargetView: View
private var mDrawOutline = false
private var mPaint: Paint? = null
init {
context.theme.obtainStyledAttributes(
attrs, R.styleable.ViewSurrogate, 0, 0
).apply {
try {
mDrawOutline = getBoolean(R.styleable.ViewSurrogate_drawOutline, false)
mTargetId = getResourceId(R.styleable.ViewSurrogate_targetView, NO_ID)
} finally {
recycle()
}
}
if (mTargetId == NO_ID) throw InvalidTarget("app:targetId was not specified.")
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (this::mContainer.isInitialized) return
mContainer = (parent as ConstraintLayout).also { container ->
mTargetView = container.findViewById(mTargetId)
?: throw InvalidTarget("Can't find target view in layout.")
}
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
if (!mDrawOutline) return
val paint = mPaint
?: Paint().apply {
style = Paint.Style.STROKE
color = Color.RED
strokeWidth = 10f
pathEffect = DashPathEffect(floatArrayOf(15f, 15f, 15f, 15f), 0f)
}
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setDimensionsFromTarget(mTargetView)
}
override fun updatePostLayout(container: ConstraintLayout?) {
super.updatePostLayout(container)
mTargetView.let { targetView ->
val pos = getPositionInCommonAncestor(targetView)
val lp = layoutParams as ViewGroup.MarginLayoutParams
if (lp.marginStart != pos.x || lp.topMargin != pos.y) {
lp.setMargins(pos.x, pos.y, 0, 0)
requestLayout()
} else if (width != targetView.width || height != targetView.height) {
requestLayout()
}
}
}
override fun getBaseline() = mTargetView.baseline
private fun getPositionInCommonAncestor(targetView: View): TargetViewOffset {
var viewY = targetView.y
var viewX = targetView.x
var targetAncestor: ViewGroup? = targetView.parent as ViewGroup
while (targetAncestor != null && targetAncestor != parent) {
viewY += targetAncestor.y
viewX += targetAncestor.x
targetAncestor = targetAncestor.parent as ViewGroup
}
if (targetAncestor == null) {
throw WrongParentException("ViewSurrogate must be a direct child of an ancestor of the target view.")
}
return TargetViewOffset(viewX.toInt(), viewY.toInt())
}
private fun setDimensionsFromTarget(targetView: View) {
if (width == targetView.width && height == targetView.height) return
setMeasuredDimension(targetView.width, targetView.height)
}
private data class TargetViewOffset(val x: Int, val y: Int)
private class WrongParentException(msg: String) : Exception(msg)
private class InvalidTarget(msg: String) : Exception(msg)
}
Add the following to the layout and constrain the button to the top and bottom of this view:
<[your package name].ViewSurrogate
android:id="@+id/surrogateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:drawOutline="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:targetView="@id/chat_text"
tools:layout_marginTop="359dp" />
drawOutline
is a boolean that determines whether the outline of the view is drawn to aid in debugging.
targetView
is the id of the view to replicate. Here it is the TextInputEditText.
Positioning of the surrogate view will be accomplished by setting the top and left margins of the surrogate view, so we constrain the view to the top and left of the parent.
The layout editor doesn't completely replicate the Android system's layout processing, so the tools:layout_marginTop
is needed to approximate the location of the surrogate view in the Android Studio designer. We would also need to do this if the position of the button were hard-coded in our app.
Here are the styleable definitions:
<resources>
<declare-styleable name="ViewSurrogate">
<attr name="drawOutline" format="boolean" />
<attr name="targetView" format="reference" />
</declare-styleable>
</resources>
Here is the layout as it appears in an emulator. The red outline shows the extent of the surrogate view. Notice how the button is centered on the TextInputEditText. (The red outline shows the extent of the surrogate view.)

If we want the button to appear to extend to the top and bottom of the TextInputEditText we would need to set the top and bottom insets to 0dp
.
android:insetTop="0dp"
android:insetBottom="0dp"
So, it will now look like this:

I think that this could be a general solution to reach inside any ViewGroup embedded within a ConstraintLayout to pull out a view for the purpose of applying constraints.
(Lightly tested with ConstraintLayout 2.0.4)