简体   繁体   中英

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?

In my activity, i create a SpannableString and replace part of the string with ImageSpan . ImageSpan uses a 24x24 pixel pure black png image and is set to 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

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

在此输入图像描述

That is just, well, wrong. The drawable's vertical placement is off since ALIGN_BASELINE is defined as follows:

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() :

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() . The only argument to draw() that interests us is bottom :

bottom int: Bottom of the line.

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. 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 . This checks out.

In DynamicDrawableSpan#draw() in the same emulator, bottom = 224 while the drawable's height is still 84 as shown above. Why is bottom 224? This is the line height of the font.

transY is now calculated as 224 - 84 = 140 pixels. 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.

See Android 101: Typography for description of Android font metrics.

Without seeing drawable's xml, I'd say this line is the fault

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

And here's why:

Drawable.getIntrinsicHeight() returns -1 , same goes for getIntrinsicWidth().

Which means you're calling setBounds() with following arguments:

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

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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