为什么在多行TextView填充父级中包装内容?

时间:2011-09-16 03:11:06

标签: android android-layout textview

我有一个多行文本视图设置为android:layout_width="wrap_content",在渲染时,获取父级的所有可用宽度。当文本可以放在一行上时,wrap_content工作正常,但在两行或多行时,文本视图似乎与父项宽度匹配,在两侧留下看似填充的内容。

因为文本不能放在一行上,文本视图是否需要所有可用的宽度?我希望视图受最小可能尺寸的限制。

有什么想法吗?

供参考,这是布局定义:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:singleLine="false"
    android:textSize="24sp"
    android:textStyle="bold"
    android:textColor="@color/white"
    android:gravity="center_horizontal"
/>

5 个答案:

答案 0 :(得分:46)

我也有同样的问题...... 您可以使用自定义TextView和重写方法onMeasure()来计算宽度:

public class WrapWidthTextView extends TextView {

...

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    Layout layout = getLayout();
    if (layout != null) {
        int width = (int) Math.ceil(getMaxLineWidth(layout))
                + getCompoundPaddingLeft() + getCompoundPaddingRight();
        int height = getMeasuredHeight();            
        setMeasuredDimension(width, height);
    }
}

private float getMaxLineWidth(Layout layout) {
    float max_width = 0.0f;
    int lines = layout.getLineCount();
    for (int i = 0; i < lines; i++) {
        if (layout.getLineWidth(i) > max_width) {
            max_width = layout.getLineWidth(i);
        }
    }
    return max_width;
}
}

答案 1 :(得分:4)

我参加聚会很晚,但是我希望我的解决方案对某人可能很有用,因为它支持任何文本对齐/重力和RTL。为了支持任何对齐方式,我还必须重写onDraw方法。

我写了article on medium并附有实施说明。

import android.graphics.Canvas
import android.text.Layout
import android.text.Layout.Alignment.ALIGN_CENTER
import android.text.Layout.Alignment.ALIGN_NORMAL
import android.text.Layout.Alignment.ALIGN_OPPOSITE
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.ceil

/**
 * Created by Max Diland
 */

/**
 * Improved solution
 * https://stackoverflow.com/questions/7439748/why-is-wrap-content-in-multiple-line-textview-filling-parent
 * It is a hacky implementation and because of the hack please use it to display texts only!
 * Now it supports any textAlignment, RTL.
 * What is not supported (supported but unusable):
 * - compound drawables,
 * - background drawables
 */
class AccurateWidthTextView @JvmOverloads constructor(
    context: android.content.Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {

    private var extraPaddingRight: Int? = null

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (layout == null || layout.lineCount < 2) return

        val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
        val uselessPaddingWidth = layout.width - maxLineWidth

        val width = measuredWidth - uselessPaddingWidth
        val height = measuredHeight
        setMeasuredDimension(width, height)
    }

    private fun getMaxLineWidth(layout: Layout): Float {
        return (0 until layout.lineCount)
            .map { layout.getLineWidth(it) }
            .max()
            ?: 0.0f
    }

    override fun onDraw(canvas: Canvas) {
        if (layout == null || layout.lineCount < 2) return super.onDraw(canvas)

        val explicitLayoutAlignment = layout.getExplicitAlignment()
        if (explicitLayoutAlignment == ExplicitLayoutAlignment.MIXED) return super.onDraw(canvas)

        val layoutWidth = layout.width
        val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()

        if (layoutWidth == maxLineWidth) return super.onDraw(canvas)

        when (explicitLayoutAlignment) {
            ExplicitLayoutAlignment.RIGHT -> {
                drawTranslatedHorizontally(
                    canvas,
                    -1 * (layoutWidth - maxLineWidth)
                ) { super.onDraw(it) }
                return
            }

            ExplicitLayoutAlignment.CENTER -> {
                drawTranslatedHorizontally(
                    canvas,
                    -1 * (layoutWidth - maxLineWidth) / 2
                ) { super.onDraw(it) }
                return
            }

            else -> return super.onDraw(canvas)
        }
    }

    private fun drawTranslatedHorizontally(
        canvas: Canvas,
        xTranslation: Int,
        drawingAction: (Canvas) -> Unit
    ) {
        extraPaddingRight = xTranslation
        canvas.save()
        canvas.translate(xTranslation.toFloat(), 0f)
        drawingAction.invoke(canvas)
        extraPaddingRight = null
        canvas.restore()
    }

    /*
    This textView does not support compound drawables correctly so the function is used not on purpose.
    It affects clipRect's width which gets formed inside the onDraw() method.
    Negative - increases.
    Positive - shrinks
    So before onDraw you should set some value to the field extraPaddingRight
    to change clip rect bounds and set null right after onDraw
     */
    override fun getCompoundPaddingRight(): Int {
        return extraPaddingRight ?: super.getCompoundPaddingRight()
    }
}

/*
It does not matter whether the text is LTR or RLT at the end of the day it is either aligned left
or right or centered. Mixed means the layout has more than 1 paragraph and the paragraphs have
different alignments
 */
private enum class ExplicitLayoutAlignment {
    LEFT, CENTER, RIGHT, MIXED
}

private fun Layout.getExplicitAlignment(): ExplicitLayoutAlignment {
    if (lineCount == 0) return ExplicitLayoutAlignment.LEFT

    val explicitAlignments = (0 until this.lineCount)
        .mapNotNull { this.getLineExplicitAlignment(it) }
        .distinct()

    return if (explicitAlignments.size > 1) {
        ExplicitLayoutAlignment.MIXED
    } else {
        explicitAlignments.firstOrNull() ?: ExplicitLayoutAlignment.LEFT
    }
}

private fun Layout.getLineExplicitAlignment(line: Int): ExplicitLayoutAlignment? {
    if (line !in 0 until this.lineCount) return null

    val isDirectionLtr = getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT
    val alignment = getParagraphAlignment(line)

    return when {
        alignment.name == "ALIGN_RIGHT" -> ExplicitLayoutAlignment.RIGHT
        alignment.name == "ALIGN_LEFT" -> ExplicitLayoutAlignment.LEFT
        // LTR and RTL
        alignment == ALIGN_CENTER -> ExplicitLayoutAlignment.CENTER
        // LTR
        isDirectionLtr && alignment == ALIGN_NORMAL -> ExplicitLayoutAlignment.LEFT
        isDirectionLtr && alignment == ALIGN_OPPOSITE -> ExplicitLayoutAlignment.RIGHT
        // RTL
        alignment == ALIGN_NORMAL -> ExplicitLayoutAlignment.RIGHT
        else -> ExplicitLayoutAlignment.LEFT
    }
}

答案 2 :(得分:3)

针对上面接受的答案的一点点优化解决方案:

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    int widthMode = MeasureSpec.getMode(widthSpec);

    // if wrap_content 
    if (widthMode == MeasureSpec.AT_MOST) {
        Layout layout = getLayout();
        if (layout != null) {
            int maxWidth = (int) Math.ceil(getMaxLineWidth(layout)) + 
                           getCompoundPaddingLeft() + getCompoundPaddingRight();
            widthSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST);
        }
    }
    super.onMeasure(widthSpec, heightSpec);
}

答案 3 :(得分:0)

基于@Vitaliy答案,如果有人愿意,可以转换为Kotlin:

class WrapWidthTextView @JvmOverloads constructor(
        context: android.content.Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0
) : TextView(context, attrs, defStyleAttr) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val layout = this.layout ?: return
        val width = ceil(getMaxLineWidth(layout)).toInt() + compoundPaddingLeft + compoundPaddingRight
        val height = measuredHeight
        setMeasuredDimension(width, height)
    }

    private fun getMaxLineWidth(layout: Layout): Float {
        var maxWidth = 0.0f
        val lines = layout.lineCount
        for (i in 0 until lines) {
            if (layout.getLineWidth(i) > maxWidth) {
                maxWidth = layout.getLineWidth(i)
            }
        }
        return maxWidth
    }
}

并且通常以与其他TextView相同的方式在XML中使用:

<com.yourpackage.WrapWidthTextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            ...
                            />

答案 4 :(得分:0)

如果有人对材质按钮有同样的问题,这里是我用来避免按钮填充父级的类。设置图标后它仍然有效。最大线宽计算基于本文中的其他答案。

import android.content.Context
import android.graphics.Canvas
import android.text.Layout
import android.util.AttributeSet
import com.google.android.material.button.MaterialButton
import kotlin.math.ceil

/**
 * A material button that does not automatically fill the parent when it is multiline
 * but orientates on the widest line while still being compatible with icons
 */
class WrapWidthMaterialButton : MaterialButton {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int)
            : super(context, attrs, defStyleAttr)

    private var actualWidthToParentWidthDifference = 0
    private var isDrawing = false

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (layout == null || layout.lineCount < 2) return

        val actualWidth = getActualWidth(layout)
        if (actualWidth < measuredWidth) {
            actualWidthToParentWidthDifference = measuredWidth - actualWidth
            setMeasuredDimension(actualWidth, measuredHeight)
        }

    }

    /**
     * computes the actual width the view should be measured to by using the width
     * of the widest line plus the paddings for the compound drawables
     */
    private fun getActualWidth(layout: Layout): Int {
        val maxLineWidth = (0 until layout.lineCount)
            .map { layout.getLineWidth(it) }
            .maxOrNull() ?: 0.0f
        return ceil(maxLineWidth).toInt() + compoundPaddingLeft + compoundPaddingRight
    }

    override fun onDraw(canvas: Canvas) {
        isDrawing = true
        super.onDraw(canvas)
        isDrawing = false
    }

    /**
     * a workaround to make the TextView.onDraw method draw the text in the new centre of the view
     * by substracting half of the actual width difference from the returned value
     * This should only be done during drawing however, since otherwise the initial width calculation will fail
     */
    override fun getCompoundPaddingLeft(): Int {
        return super.getCompoundPaddingLeft().let { value ->
            if (isDrawing) value - actualWidthToParentWidthDifference / 2 else value
        }
    }


}