1. ホーム
  2. java

Android TextViewの行間解析

2022-02-18 13:18:09
<パス

TextViewの行間設定

レイアウトXMLには、TextViewの行間を設定するためのパラメータが2つあります。
それらは、android:lineSpacingExtra と android:lineSpacingMultiplier です。
コード内でTextViewのsetLineSpacing()メソッドで設定することができます。

android:lineSpacingExtra

android:lineSpacingExtraは追加の行間値を示し、通常はdp単位で指定します。
android:lineSpacingExtraの値には、負、10進数、0があります。値が正の場合は行間を広げる、負の場合は行間を狭める、0の場合は変化がないことを意味します。

android:lineSpacingMultiplier

android:lineSpacingMultiplierは、行間の倍率を単位なしで表します。例えば、android:lineSpacingMultiplier="1.2"のようになります。
android:lineSpacingMultiplierの値は、任意の浮動小数点数です。1.0より大きい値は行間を広げることを意味し、1.0より小さい値は行間を狭めることを意味します。

android:lineSpacingExtra と android:lineSpacingMultiplier を一緒に使用する。

android:lineSpacingExtra と android:lineSpacingMultiplier は、同じ TextView に対して一緒に設定することができ、一緒に使用すると、まず android:lineSpacingMultiplier で設定した乗数を増加し、その後 android :lineSpacingExtra で設定した追加の間隔を追加することになります。

setLineSpacing()

setLineSpacing()のプロトタイプは public void setLineSpacing(float add, float mult) です。
パラメータaddは追加するスペーシングの値を示し、android:lineSpacingExtraパラメータに対応する。
multパラメータは、追加するスペーシングの倍率を示し、android:lineSpacingMultiplierパラメータに対応する。

行間設定の違いによる表示効果

android:lineSpacingExtraの設定を変更した場合の表示効果は以下の通りです。

通常の行間

android:lineSpacingExtra="2dp" を設定した場合の表示効果。

android:lineSpacingExtra="-6dp" を設定した場合の表示効果。

android:lineSpacingExtra="-10dp" を設定した場合の表示効果。

別のandroid:lineSpacingMultiplierを設定すると、以下のような効果が表示されます。
通常の行間

android:lineSpacingMultiplier="1.2" を設定した場合の表示効果。

android:lineSpacingMultiplier="2.0" を設定した場合の表示効果。

android:lineSpacingMultiplier="0.5" を設定した場合の表示効果。

android:lineSpacingMultiplier="0" を設定した場合の表示効果(0にするとテキストが表示されない)。

行間設定の影響分析

通常の行間、android:lineSpacingExtra="2dp"、android:lineSpacingMultiplier="2"の設定で実際に行間をとってみると以下の値が得られます。
通常の行間では、実際の行間は12pxになります

実際の行間は+2dpの行間で18pxです。

2倍の行間での実際の間隔は64pxです。

android:lineSpacingExtra="2dp"の設定では、実際の行間も6px(テスト携帯では1dp=3px)増えていますが、android:lineSpacingMultiplier="2"の設定では、通常の行間の2倍にならないことがわかりますね。
さらに、文字の高さを含めて測定すると、次のような値が得られる。
通常の行間では、含まれるテキストの高さの間隔は52pxです

+2dpの行間は、58pxの高さの間隔を持つテキストを含みます。

は、行間2倍で高さ104pxのテキストを含む(ここでの測定は、後述の理由により、2行目の間隔を使用する)。

通常の感覚では、行間は1行のテキストの上から下までの距離、行高は1行のテキストの上から上までの距離です。
比較すると、android:lineSpacingExtraとandroid:lineSpacingMultiplierの設定は行間に直接影響せず、行の高さを介して行間に影響することがわかります。android:lineSpacingExtra="2dp" と設定すると、行の高さが2dp増え、テキストの高さは変わらないので行間は2dp増えます。android:lineSpacingMultiplier="2" と設定すると、行の高さが2倍になり、テキストの高さは変わらず、行間が広がる("テキストの高さ" + "text normal line spacing" )ようになります。

TextViewの行の高さを取得する

TextViewは、TextViewの行の高さを取得するためにgetLineHeight()メソッドを提供しています。
通常の行間の場合、android:lineSpacingExtra="2dp"とandroid:lineSpacingMultiplier="2"の設定でgetLineHeight() メソッドで得られる線の高さは次のようになります。
通常の行間です。52px
android:lineSpacingExtra="2dp": 58px
android:lineSpacingMultiplier="2":104px
実測値と同じです。

TextViewの行の高さとスペーシングの例外

TextViewが表示する各行の実際の高さは、getLineHeight()メソッドで得られる行の高さと必ずしも一致しません(このことは、各行の間隔が、getLineHeight()メソッドで得られる行高さからテキストの高さを引いたものと必ずしも一致しないことも意味しています)。
TextViewのgetLineHeight()メソッドのアノテーションは以下の通りです。

    /**
     * @return the height of one standard line in pixels. **Note that markup
     * within the text can cause individual lines to be taller or shorter
     * than this height, and the layout may contain additional first- * or last-line padding.
     ** or last-line padding.**
     */

つまり、テキスト行の内部にマークアップ(それが何であるかわからない)がある場合、そのテキスト行の高さは getLineHeight() メソッドが返す高さよりも大きくなったり小さくなったりする可能性があります。さらに、TextViewの最初の行と最後の行に追加のパディングスペーシングがあり、これにより実際の行の高さがgetLineHeight()メソッドで得られる行の高さより大きくなり、実際のスペーシングがgetLineHeight()で計算されるスペーシングより大きくなる。
1行目の高さを実測すると、以下のような値が得られます。
android:lineSpacingMultiplier="1" の場合、"1行目の高さ=通常の行の高さ"となります。
android:lineSpacingMultiplier="2", "1行目の高さ=通常行の高さ+6px"。
android:lineSpacingMultiplier="3", "1行目の高さ=通常行の高さ+12px"。
android:lineSpacingMultiplier="4", "一行目の高さ=通常行の高さ+18px"。
つまり、"1行目の高さ = 通常行の高さ + 6 × (android:lineSpacingMultiplier - 1)" となります。

TextViewのソースコード解析

android:lineSpacingExtraとandroid:lineSpacingMultiplierの設定がなぜ行間ではなく行の高さに影響するのか、なぜ最初と最後の行が通常の行の高さと同じではないのかを理解するには、TextViewのソースコードを見ていく必要があります。
TextViewのコンストラクタのソースコードとsetLineSpacing()のソースコードは次の通りです。

    case com.android.internal.R.styleable.TextView_lineSpacingExtra:
        mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
        break;
    case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
        mSpacingMult = a.getFloat(attr, mSpacingMult);
        break;
public void setLineSpacing(float add, float mult) {
        if (mSpacingAdd ! = add || mSpacingMult ! = mult) {
            mSpacingAdd = add;
            mSpacingMult = mult;

            if (mLayout ! = null) {
                nullLayouts();
                requestLayout();
                invalidate();
            }
        }
    }

    public int getLineHeight() {
        return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd);
    }

つまり、android:lineSpacingExtraの設定はmSpacingAddメンバ変数に、android:lineSpacingMultiplierの設定はmSpacingMultメンバ変数に格納されます。

TextViewのgetLineHeight()のソースコードは以下の通りです。

if (needMultiply && !lastLine) {
    double ex = (below - above) * (spacingmult - 1) + spacingadd;
    if (ex >= 0) {
        extra = (int)(ex + EXTRA_ROUNDING);
    } else {
        extra = -(int)(-ex + EXTRA_ROUNDING);
    }
} else {
    extra = 0;
}

TextViewは通常の高さにmSpacingMultを掛け、それにmSpacingAddを加えて行の高さを計算していることがわかります。
mSpacingAdd と mSpacingMult のメンバ変数を追跡すると、これらは new StaticLayout のコンストラクタを呼び出す際に makeSingleLayout() メソッドで使用されていることが分かります。StaticLayoutクラスのコンストラクタ・メソッドでは、generate()メソッドが呼ばれ、generate()メソッドでは、out()メソッドが呼ばれ、out()メソッドでは、以下のコードを見ることができます。

boolean needMultiply = (spacingmult ! = 1 || spacingadd ! = 0);

ここで、needMultiplyの値は、android:lineSpacingExtraとandroid:lineSpacingMultiplierのどちらかが設定されていれば、needMultiplyの値は真である、と定義される。

lines[off + TOP] = v;
...
v += (below - above) + extra;
...
lines[off + mColumns + TOP] = v;

そして、android:lineSpacingExtra と android:lineSpacingMultiplier のいずれかが設定されるとすぐに、(下 - 上) * (spacingmult - 1) + 切り上げ後の spacingadd の値に相当する追加値が算出されます。
この後、以下のコードがあります。

if (firstLine) {
    if (trackPad) {
        mTopPadding = top - above;
    }

    if (includePad) {
        above = top;
    }
}

lines[]に関連するコードをたどると、line[]は各テキスト行の縦位置情報を保持するmLinesへの参照であり、共通のテキスト行はmLinesの3要素に対応する3つの位置を持つ(mColumns = COLUMNS_NORMAL = 3, START = 0, TOP = 1 , DESCENT = 2)ことがわかります。
つまり、lines[off + mColumns + TOP]は次の行のTOP位置情報を表し、lines[off + mColumns + TOP] - lines[off + TOP]はこの行のTOP位置から次の行のTOP位置、つまり行高さを取得することになります。明らかに lines[off + mColumns + TOP] - lines[off + TOP] = (below - above) + extra です。余分な計算の丸め処理を無視すれば、 lines[off + mColumns + TOP] - lines[off + TOP] = (below - above) + extra = (below - above) + (below - above) * (spacingmult - 1) + spacingadd = (below - above) * spacingmult + spacingadd となります。これは、android: lineSpacingExtra と android:lineSpacingMultiplier の設定が、行間ではなく行の高さに影響する理由を説明しています。
out()メソッドのさらに前方には、次のようなコードが表示されます。

if (firstLine) {
    if (trackPad) {
        mTopPadding = top - above;
    }

    if (includePad) {
        above = top;
    }
}

includePad変数をたどると、TextViewのmIncludePad変数の値であり、TextViewではmIncludePadはtrueに固定されているので、ここでabove = top; が実行され、テキストの最初の行について、その上の位置が上の位置と同じであり、行 [off + mColumns + TOP] - lines[off + TOP] = (below - above) * spacingmult + spacingadd = (below - top) * spacingmult + spacingaddとなることがわかります。つまり、テキストの最初の行の高さは、通常の行よりも (top - above) * spacingmult + spacingadd. above) * spacingmult = (below - top) * spacingmult + spacingadd.
above = topの設定では、テキストの行の高さを測定するときに、テキストのtop - aboveピクセルの上の空白から開始する必要があることに注意してください。上記の最初の行の高さの測定と組み合わせると、次のような値になります。
android:lineSpacingMultiplier="1", "1行目の高さ=通常行の高さ+6px"。
android:lineSpacingMultiplier="2", "1行目の高さ=通常行の高さ+12px"。
android:lineSpacingMultiplier="3", "一行目の高さ=通常行の高さ+18px"。
android:lineSpacingMultiplier="4", "一行目の高さ=通常行の高さ+24px"。
つまり、1行目の高さ-通常の行の高さ=6×(android:lineSpacingMultiplier)"ここで、6は明らかにtop - aboveの値です。これは、コードと同じ結論です。