Android Text View 去掉默认的padding的实现方法

先看下最终实现效果,满意您在往下看:

TextView 绘制的时候自带一定的Padding值,要想实现去掉默认的padding值,xml文件可以设置一个属性值 :

android:includeFontPadding="false"

然后运行起来就会发现,并没有什么卵用,也不能说完全没有,但效果差点意思。我就不运行了,在编译器上也能大概看到效果,如下图,我们可以看到依然有着无法删除的padding值。

我们先来看一下绘制TextView时的几条基准线:

  • top:在给定文本大小下,字体中最高字形高于基线的最大距离,即能绘制的最高点
  • ascent:单倍行距文本的基线以上建议距离,即推荐的文字绘制上边缘线
  • base:文字绘制基准线,也就是坐标轴,X轴就是Baseline
  • decent:单间距文本低于基线的建议距离,即推荐的文字绘制下边缘线
  • bottom:能绘制的最低点

    Textview绘制文字会在  ascent 和 decent 之间,外面的距离我们可以理解为 类似 padding一样的间隔,但又并不是我们设置的paddingTop,paddingBottom。

    要解决这个这个问题,首先我们要知道textview的内容文字绘制的真实区域:

    红色的区域就是内容的真实高度,蓝色的部分就是textview绘制的多余的部分,现在我们要去掉这一部分,首先可以通过 getTextBounds 来获取绘制的真实区域

    textView.getPaint().getTextBounds(text, 0, text.length(), textRect);

    获取到真实区域后,那么再来看textview绘制的几条基准线,你想到了什么,是的,我们只需要稍微移动一下这几条线把高度压缩到文字的展示绘制区域即可,实现用 SpannableString 来实现,SpannableString 是 android  里面专门用来实现设置 textview 文字样式的类,这个不清楚的自行查询一下,这里不赘述了,具体我们用的是  LineHeightSpan ,可以通过修改 textview 的行高来实现我们的目的。具体看下代码:

    spannableString.setSpan(new LineHeightSpan() {
        @Override
        public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt fm) {
            Rect textRect = new Rect();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                textView.getPaint().getTextBounds(text, 0, text.length(), textRect);
            } else {
                textView.getPaint().getTextBounds(text.toString(), 0, text.length(), textRect);
            }
            // 直接把 textview 绘制区域 缩小到 文字真实大小的区域
            // 这个是有一点问题的,看下图
            fm.top = textRect.top;
            fm.bottom = textRect.bottom;
            fm.ascent = fm.top;
            fm.descent = fm.bottom;
        }
    }, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

     Paint.FontMetricsInt 里的 top bottom ascent descent 就是用来调整 绘制的时候的具体位置的,我们把textview的绘制区域直接设置到 FontMetricsInt 里面,实现的效果如下图:

    看起来确实去掉了padding,而且去的干干净净,需要这种效果的可以停下来,施展CV大法。

    这种实现方式原理也很简单,是使文字真实的绘制区域高度为所绘制内容中字符的最大高度,这样可能会造成排版问题,文字对不齐,那我们就需要统一下绘制内容的高度,我们知道TextView 有个属性TextSize ,它的值最终决定了Textview 的高度,然后我们略微修改一下代码:

    spannableString.setSpan(new LineHeightSpan() {
        @Override
        public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt fm) {
            Rect textRect = new Rect();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                textView.getPaint().getTextBounds(text, 0, text.length(), textRect);
            } else {
                textView.getPaint().getTextBounds(text.toString(), 0, text.length(), textRect);
            }
            Log.e("NoPaddingText", "修改之前 " + fm.toString());
            Log.e("NoPaddingText", textRect.toString());
            Log.e("NoPaddingText", "textSize: " + textView.getTextSize());
            if (textRect.bottom - textRect.top < textView.getTextSize()) {
                // 一般我们认为字体的textview的textsize为textview的高度,当然有一定的误差
                // 当字体的高度没有字体的textsize大时,我们把大小设置成textsize,这样就可以解决文字的排版问题了
                float tempPadding = (textView.getTextSize() - (textRect.bottom - textRect.top)) / 2f;
                fm.top = (int) (textRect.top - tempPadding);
                fm.bottom = (int) (textRect.bottom + tempPadding);
            } else {
                fm.top = textRect.top;
                fm.bottom = textRect.bottom;
            }
            fm.ascent = fm.top;
            fm.descent = fm.bottom;
            Log.e("NoPaddingText", "修改之后 " + fm.toString());
        }
    }, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

    我们用TextView的TextSize来约束统一绘制高度,不足TextSize 的,补充添加一个padding值补齐正常的高度,实现效果如下:

    这样看着顺眼多了,继续CV大法。

    完整代码如下:(两种实现形式:1、工具类; 2、自定义View形式实现)

    点这里跳转项目源码地址

    import android.graphics.Paint;
    import android.graphics.Rect;
    import android.os.Build;
    import android.text.SpannableString;
    import android.text.Spanned;
    import android.text.style.LineHeightSpan;
    import android.util.Log;
    import android.widget.TextView;
    // 工具类 实现
    public class TextUtil {
        // 设置上下取消绘制的padding值 考虑textsize
        public static void setNoVerticalPaddingText(TextView textView, CharSequence text) {
            if (textView == null || text == null)
                return;
            // 如果原先上下有padding,重置为 0
            textView.setPadding(textView.getPaddingLeft(), 0, textView.getPaddingRight(), 0);
            // 利用 LineHeightSpan 快速实现去除 padding 的效果
            SpannableString spannableString = new SpannableString(text);
            spannableString.setSpan(new LineHeightSpan() {
                @Override
                public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt fm) {
                    Rect textRect = new Rect();
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        textView.getPaint().getTextBounds(text, 0, text.length(), textRect);
                    } else {
                        textView.getPaint().getTextBounds(text.toString(), 0, text.length(), textRect);
                    }
                    Log.e("NoPaddingText", "修改之前 " + fm.toString());
                    Log.e("NoPaddingText", textRect.toString());
                    Log.e("NoPaddingText", "textSize: " + textView.getTextSize());
                    if (textRect.bottom - textRect.top < textView.getTextSize()) {
                        // 一般我们认为字体的textview的textsize为textview的高度,当然有一定的误差 当然也可以自定义View 形式
                        // 当字体的高度没有字体的textsize大时,我们把大小设置成textsize,这样就可以解决文字的排版问题了
                        float tempPadding = (textView.getTextSize() - (textRect.bottom - textRect.top)) / 2f;
                        fm.top = (int) (textRect.top - tempPadding);
                        fm.bottom = (int) (textRect.bottom + tempPadding);
                    } else {
                        // 这么设置可以完全消除padding,但是会有问题,
                        // 同样textsize的Textview 会因为设置的内容不一样而高度不一样
                        // 如果有什么特殊需求可以考虑用一下
                        fm.top = textRect.top;
                        fm.bottom = textRect.bottom;
                    }
                    fm.ascent = fm.top;
                    fm.descent = fm.bottom;
                    Log.e("NoPaddingText", "修改之后 " + fm.toString());
                }
            }, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            textView.setText(spannableString);
        }
        // 设置上下取消绘制的padding值 不考虑textsize
        public static void setNoVerticalPaddingText2(TextView textView, CharSequence text) {
            if (textView == null || text == null)
                return;
            // 如果原先上下有padding,重置为 0
            textView.setPadding(textView.getPaddingLeft(), 0, textView.getPaddingRight(), 0);
            // 利用 LineHeightSpan 快速实现去除 padding 的效果
            SpannableString spannableString = new SpannableString(text);
            spannableString.setSpan(new LineHeightSpan() {
                @Override
                public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt fm) {
                    Rect textRect = new Rect();
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        textView.getPaint().getTextBounds(text, 0, text.length(), textRect);
                    } else {
                        textView.getPaint().getTextBounds(text.toString(), 0, text.length(), textRect);
                    }
                    Log.e("NoPaddingText", "修改之前 " + fm.toString());
                    Log.e("NoPaddingText", textRect.toString());
                    Log.e("NoPaddingText", "textSize: " + textView.getTextSize());
                    // 这么设置可以完全消除padding,但是会有问题,
                    // 同样textsize的Textview 会因为设置的内容不一样而高度不一样
                    // 如果有什么特殊需求可以考虑用一下
                    fm.top = textRect.top;
                    fm.bottom = textRect.bottom;
                    fm.ascent = fm.top;
                    fm.descent = fm.bottom;
                    Log.e("NoPaddingText", "修改之后 " + fm.toString());
                }
            }, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            textView.setText(spannableString);
        }
    }
    import android.content.Context;
    import android.graphics.Paint;
    import android.graphics.Rect;
    import android.os.Build;
    import android.text.SpannableString;
    import android.text.Spanned;
    import android.text.style.LineHeightSpan;
    import android.util.AttributeSet;
    import android.widget.TextView;
    import androidx.annotation.NonNull;
    import androidx.annotation.Nullable;
    import androidx.appcompat.widget.AppCompatTextView;
    /**
     * 自定义view 形式 实现 NoPaddingTextView 布局可以预览
     */
    public class NoPaddingTextView extends AppCompatTextView {
        public NoPaddingTextView(@NonNull Context context) {
            this(context, null);
        }
        public NoPaddingTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
        public NoPaddingTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
        @Override
        public void setText(CharSequence text, BufferType type) {
            super.setText(getNoVerticalPaddingText(this,text), type);
        }
        // 设置上下取消绘制的padding值 考虑textsize
        public final SpannableString getNoVerticalPaddingText(TextView textView, CharSequence text) {
            if (textView == null || text == null)
                return new SpannableString("");
            // 如果原先上下有padding,重置为 0
            textView.setPadding(textView.getPaddingLeft(), 0, textView.getPaddingRight(), 0);
            // 利用 LineHeightSpan 快速实现去除 padding 的效果
            SpannableString spannableString = new SpannableString(text);
            spannableString.setSpan(new LineHeightSpan() {
                @Override
                public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt fm) {
                    Rect textRect = new Rect();
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        textView.getPaint().getTextBounds(text, 0, text.length(), textRect);
                    } else {
                        textView.getPaint().getTextBounds(text.toString(), 0, text.length(), textRect);
                    }
                    //Log.e("NoPaddingText", "修改之前 " + fm.toString());
                    //Log.e("NoPaddingText", textRect.toString());
                    //Log.e("NoPaddingText", "textSize: " + textView.getTextSize());
                    if (textRect.bottom - textRect.top < textView.getTextSize()) {
                        // 一般我们认为字体的textview的textsize为textview的高度,当然有一定的误差 当然也可以自定义View 形式
                        // 当字体的高度没有字体的textsize大时,我们把大小设置成textsize,这样就可以解决文字的排版问题了
                        float tempPadding = (textView.getTextSize() - (textRect.bottom - textRect.top)) / 2f;
                        fm.top = (int) (textRect.top - tempPadding);
                        fm.bottom = (int) (textRect.bottom + tempPadding);
                    } else {
                        // 这么设置可以完全消除padding,但是会有问题,
                        // 同样textsize的Textview 会因为设置的内容不一样而高度不一样
                        // 如果有什么特殊需求可以考虑用一下
                        fm.top = textRect.top;
                        fm.bottom = textRect.bottom;
                    }
                    fm.ascent = fm.top;
                    fm.descent = fm.bottom;
                    //Log.e("NoPaddingText", "修改之后 " + fm.toString());
                }
            }, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            return spannableString;
        }
    }