如何使ImageSpan与基线对齐

3
问题:为什么ImageSpan的ALIGN_BASELINE无法精确对齐基线,如何解决对齐问题?
在我的活动中,我创建了一个SpannableString,并用ImageSpan代替字符串的一部分。 ImageSpan使用一个24x24像素的纯黑png图像,并设置为ALIGN_BASELINE。
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView myTextView = findViewById(R.id.myTextView);
        SpannableString ss = new SpannableString("Hello world!");

        Drawable d = ContextCompat.getDrawable(this, R.drawable.box);
        d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
        ImageSpan imageSpan = new ImageSpan(d, ImageSpan.ALIGN_BASELINE);
        ss.setSpan(imageSpan, 2, 5, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);

        myTextView.setText(ss);
    }
}

我在我的布局中有两个视图:一个 TextView 和一个用于显示基线的 View
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/myTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ff9800"
        android:textSize="48sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1px"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBaseline_toBaselineOf="@id/myTextView"
        android:background="#de4caf50"/>

</android.support.constraint.ConstraintLayout> 

正如您所看到的,该行与 TextView 的基线正确对齐,但是 ImageSpan 稍微低于基线。

enter image description here


你尝试过使用ImageSpan.ALIGN_BOTTOM吗? - Chetan Joshi
2个回答

7
那只是错误的,因为drawable的垂直放置偏离了,原因是ALIGN_BASELINE被定义为以下内容:
常量指示此跨度的底部应与周围文本的基线对齐。
显然这并没有发生。请参见本帖子末尾发生了什么错误。我建议采取以下修复方法:
// Override draw() to place the drawable correctly when ALIGN_BASELINE.
ImageSpan imageSpan = new ImageSpan(d, ImageSpan.ALIGN_BASELINE) {
    public void draw(Canvas canvas, CharSequence text, int start,
                     int end, float x, int top, int y, int bottom,
                     @NonNull Paint paint) {
        if (mVerticalAlignment != ALIGN_BASELINE) {
            super.draw(canvas, text, start, end, x, top, y, bottom, paint);
            return;
        }
        Drawable b = getDrawable();
        canvas.save();
        // If we set transY = 0, then the drawable will be drawn at the top of the text.
        // y is the the distance from the baseline to the top of the text, so
        // transY = y will draw the top of the drawable on the baseline. We want the             // bottom of the drawable on the baseline, so we subtract the height
        // of the drawable.
        int transY = y - b.getBounds().bottom;
        canvas.translate(x, transY);
        b.draw(canvas);

        canvas.restore();
    }
};

这段代码生成了以下图片,我认为是正确的。 enter image description here
为什么方框没有画在基线上? 这里是从DynamicDrawableSpan#draw()中绘制方框的源代码。
public void draw(@NonNull Canvas canvas, CharSequence text,
        @IntRange(from = 0) int start, @IntRange(from = 0) int end, float x,
        int top, int y, int bottom, @NonNull Paint paint) {
    Drawable b = getCachedDrawable();
    canvas.save();
    int transY = bottom - b.getBounds().bottom;
    if (mVerticalAlignment == ALIGN_BASELINE) {
        transY -= paint.getFontMetricsInt().descent;
    }
    canvas.translate(x, transY);
    b.draw(canvas);
    canvas.restore();
}

以下是DynamicDrawableSpan#draw()的文档。我们感兴趣的仅有的一个参数是bottom:

bottom int:行的底部。

对于所使用的解决方案代码和仿真器,transY的计算结果为94,即框距顶部94像素。 top为178,而基线被定义为零,因此178-94 = 84像素,这是框的高度或者b.getBounds().bottom。这是正确的。

在同一台仿真器中的DynamicDrawableSpan#draw()中,bottom = 224,而上面显示的drawable的高度仍然是84。为什么bottom为224?这是字体的行高。

现在计算出transY为224 - 84 = 140像素。这将把框的底部放置在行的底部,但在基线下面。

还需要进行一些调整:

transY -= paint.getFontMetricsInt().descent;

在测试中,paint.getFontMetricsInt().descent的值为41,因此transY现在变成了99。由于94是正确的答案,因此99会使框向下偏移五个像素,这就是您看到的效果。
请参见Android 101: Typography,了解Android字体度量的描述。

这是一个很好的回答,先生! - azizbekian
谢谢你,@azizbekian! - Cheticamp
这看起来很有前途。在接受你的答案之前,让我在接下来的一两天内验证一下。 - Weizhi
@Cheticamp,如果我正确理解了您的解释,那么这是DynamicDrawableSpan的一个错误吗?似乎DynamicDrawableSpan#draw对于ALIGN_BOTTOM会正常工作,但对于ALIGN_BASELINE则会出现问题,因为编写此方法的人认为从bottomdescent的距离与从bottombaseline的距离相同? - Weizhi
1
@Weizhi Bug,我觉得是这个问题。我快速浏览了一下问题跟踪器,并找到了这个错误报告,它似乎解决了相同的问题。如果您的文本超过一行,我不知道“仅在最后一行上工作不正确”,但您可能需要研究一下这个问题。 - Cheticamp
1
@Weizhi 刚刚进行了快速测试:DynamicDrawableSpan 代码在多行 TextView 中的所有行都能正常工作,除了最后一行。我发布的代码似乎无论涉及哪一行都能正常工作。 - Cheticamp

0

没有看到drawable的xml,我会说这一行是错误的

d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());

以下是原因:

Drawable.getIntrinsicHeight() 返回 -1,getIntrinsicWidth() 也是如此。

这意味着您正在使用以下参数调用 setBounds():

d.setBounds(0, 0, -1, -1);

/**
 * Specify a bounding rectangle for the Drawable. This is where the drawable
 * will draw when its draw() method is called.
 */
public void setBounds(int left, int top, int right, int bottom) {
... // implementation
}

编辑:

package android.graphics.drawable;

public abstract class Drawable {

...


/**
     * Returns the drawable's intrinsic height.
     * <p>
     * Intrinsic height is the height at which the drawable would like to be
     * laid out, including any inherent padding. If the drawable has no
     * intrinsic height, such as a solid color, this method returns -1.
     *
     * @return the intrinsic height, or -1 if no intrinsic height
     */
    public int getIntrinsicHeight() {
        return -1;
    }

}

没有可用的可绘制XML。如问题所述,黑色可绘制对象是24x24像素的PNG图像。 - Weizhi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接