首页 > 解决方案 > 如何使用嵌套在 TextInputLayout 内的 TextInputEditText 约束视图

问题描述

我试图将 MaterialButton 的高度限制为与 TextInputEditText 的高度相同,方法是将它们从上到下和从下到下对齐。

但是,我无法做到这一点,因为 TextInputEditText 嵌套在 TextInputLayout 中。有了这个,我只能用 TextInputLayout 约束 MaterialButton,它给出了以下内容:

在此处输入图像描述

我不想删除计​​数器(或 TextInputEditText 下方的任何帮助文本)。

有没有办法用嵌套的TextInputEditText (从上到下和从下到下)约束 MaterialButton ,使它们以相同的高度出现?

这是我的代码:

<androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/chat_panel"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toBottomOf="@id/finish_panel">

    <com.google.android.material.textfield.TextInputLayout
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
        android:id="@+id/chat_text_container"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/chat_send_button"
        app:endIconDrawable="@drawable/ic_clear"
        app:endIconMode="clear_text"
        app:helperTextEnabled="true"
        app:counterMaxLength="200"
        app:counterEnabled="true"
        app:errorEnabled="true"
        android:hint="Chat">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/chat_text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:inputType="textNoSuggestions"
            android:maxHeight="200dp"/>

    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/chat_send_button"
        style="@style/Widget.MaterialComponents.Button"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="@id/chat_text_container"
        app:layout_constraintBottom_toBottomOf="@id/chat_text_container"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toRightOf="@id/chat_text_container"
        android:minWidth="0dp"
        android:insetBottom="0dp"
        app:icon="@drawable/ic_send"
        app:iconPadding="0dp"
        app:iconGravity="textStart"/>
</androidx.constraintlayout.widget.ConstraintLayout>

标签: androidandroid-constraintlayoutandroid-textinputlayoutandroid-textinputedittext

解决方案


ConstraintLayout只能将约束应用于直接子级。由于要将按钮约束到的TextInputEditText视图是TextInputLayout的子视图,而不是 ConstraintLayout 的直接子视图,因此在按钮和TextInputEditText之间应用约束的任何尝试都会失败。

我们可以在ConstraintLayout上设置一个布局监听器,捕获TextInputEditText的大小和位置,并修改按钮视图以设置其大小和位置。这将需要特殊编码来进行更改,并且是一个有效的解决方案。

但是,如果我们有一个可重用类型的视图,它可以引用嵌入在子ViewGroup中的视图并将嵌入视图的大小和位置复制为ConstraintLayout的直接子级,该怎么办?然后我们可以将按钮约束到这种新类型的视图并获得我们想要的位置。

这样的自定义视图可以基于View类,但也可以基于扩展View的ConstraintHelper类。此类是ConstraintLayout中其他“帮助”类的基础,例如GroupGuideLine等。从我的角度来看,ConstraintHelper非常适合自定义视图。

这是这样一个自定义视图:

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)
}

将以下内容添加到布局并将按钮约束到此视图的顶部和底部:

<[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是一个布尔值,用于确定是否绘制视图的轮廓以帮助调试。

targetView是要复制的视图的 ID。这是TextInputEditText

代理视图的定位将通过设置代理视图的顶部和左侧边距来完成,因此我们将视图限制在父视图的顶部和左侧。

布局编辑器并没有完全复制 Android 系统的布局处理,因此tools:layout_marginTop需要在 Android Studio 设计器中近似代理视图的位置。如果按钮的位置在我们的应用程序中是硬编码的,我们也需要这样做。

以下是可样式化的定义:

<resources>
    <declare-styleable name="ViewSurrogate">
        <attr name="drawOutline" format="boolean" />
        <attr name="targetView" format="reference" />
    </declare-styleable>
</resources>    

这是在模拟器中显示的布局。红色轮廓显示代理视图的范围。注意按钮如何以TextInputEditText为中心。(红色轮廓显示代理视图的范围。)

在此处输入图像描述

如果我们希望按钮看起来延伸到TextInputEditText的顶部和底部,我们需要将顶部和底部插入设置为0dp.

android:insetTop="0dp"  
android:insetBottom="0dp"

所以,它现在看起来像这样:

在此处输入图像描述

我认为这可能是一个通用的解决方案,可以到达嵌入在ConstraintLayout中的任何ViewGroup以拉出视图以应用约束。

(使用 ConstraintLayout 2.0.4 进行了轻微测试)


推荐阅读