简体   繁体   English

如何使ImageSpan按基线对齐

[英]How to make ImageSpan align by baseline

Question : Why is ImageSpan ALIGN_BASELINE not aligning accurately to baseline and how to fix this alignment issue? 问题 :为什么ImageSpan ALIGN_BASELINE无法准确对齐基线以及如何解决此对齐问题?

In my activity, i create a SpannableString and replace part of the string with ImageSpan . 在我的活动,我创建了一个SpannableString和替换字符串的一部分ImageSpan ImageSpan uses a 24x24 pixel pure black png image and is set to ALIGN_BASELINE . 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);
    }
}

I have a two views in my layout: a TextView and a View to show the baseline 我的布局中有两个视图: TextViewView以显示基线

<?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> 

As you can see, the line is correctly align to to TextView 's baseline but the ImageSpan is slightly lower than the baseline. 如您所见,该行正确对齐TextView的基线,但ImageSpan略低于基线。

在此输入图像描述

That is just, well, wrong. 那是对的,错误的。 The drawable's vertical placement is off since ALIGN_BASELINE is defined as follows: 由于ALIGN_BASELINE的定义如下,因此drawable的垂直位置是关闭的:

A constant indicating that the bottom of this span should be aligned with the baseline of the surrounding text. 一个常量,指示此跨度的底部应与周围文本的基线对齐。

That is clearly not happening. 这显然没有发生。 See the end of this post for what is going wrong. 出了什么问题,请看这篇文章的结尾。 I suggest the following for a fix: 我建议以下内容进行修复:

// 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();
    }
};

This code produces the following image which, I think, is correct. 此代码生成以下图像,我认为是正确的。

在此输入图像描述


Why is the box not drawn on the baseline? 为什么盒子没有画在基线上?

Here is the source that draws the box from DynamicDrawableSpan#draw() : 以下是从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();
}

And the documentation for DynamicDrawableSpan#draw() . 以及DynamicDrawableSpan#draw()的文档。 The only argument to draw() that interests us is bottom : 我们感兴趣的draw()的唯一参数是bottom

bottom int: Bottom of the line. bottom int:行的底部。

For the solution code and the emulator used, transY is calculated by DynamicDrawableSpan as 94, ie, the box will be drawn 94 pixels from the top. 对于解决方案代码和使用的模拟器, transYDynamicDrawableSpan计算为94,即,框将从顶部绘制94个像素。 top is 178 while the baseline is defined as zero, so 178 - 94 = 84 pixels which is the height of the box or b.getBounds().bottom . top是178,而基线定义为零,所以178 - 94 = 84像素,这是盒子的高度或b.getBounds().bottom This checks out. 这检查出来。

In DynamicDrawableSpan#draw() in the same emulator, bottom = 224 while the drawable's height is still 84 as shown above. 在同一模拟器中的DynamicDrawableSpan#draw()中,bottom = 224,而drawable的高度仍然是84,如上所示。 Why is bottom 224? 为什么底部224? This is the line height of the font. 这是字体的行高。

transY is now calculated as 224 - 84 = 140 pixels. transY现在计算为224 - 84 = 140像素。 This will place the bottom of the box at the bottom of the line but below the baseline. 这会将框的底部放在线的底部但在基线下方。

One more adjustment is made: 再做一次调整:

transY -= paint.getFontMetricsInt().descent;

On the test, paint.getFontMetricsInt().descent is 41, so transY now becomes 99. Since 94 is the right answer, 99 places the box five pixels too low and this is what you see. 在测试中, paint.getFontMetricsInt().descent是41,所以transY现在变为99.由于94是正确的答案,99将盒子放置为5个像素太低,这就是你所看到的。

See Android 101: Typography for description of Android font metrics. 有关Android字体指标的说明,请参阅Android 101:排版

Without seeing drawable's xml, I'd say this line is the fault 没有看到drawable的xml,我会说这条线是错误的

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

And here's why: 这就是为什么:

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

Which means you're calling setBounds() with following arguments: 这意味着您使用以下参数调用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
}

edit: 编辑:

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

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM