解决Android开发中经常与设计稿不吻合的问题

一个正常的开发流程中会由设计同学给到设计稿,再有开发同学根据标注完成应用页面的开发。不过开发一段时间就会发现在做一些长页面,有时候元素已经超出屏幕范围了,然而在设计稿上却可以刚好放满一个页面。其实除了这些还有一些控件,也会感觉出来的效果要比设计稿大打折扣,明明都是按照设计稿的尺寸做的,为什么会有人眼可以明显分辨的差距呢。

不看下面的废话,直接看结论点这里(简书跳转不了,直接翻到最下面就好)

尝试解决问题

第一次发现这个问题还是去年年初的时候,发现问题之后就是通过搜索引擎去查询有没有类似的问题,然后找到一个线索就是Android TextView有默认的顶部和底部边距,所以如果通过上下的Margin去做就会导致一定的误差。里面也给出了一个解决方案,就是这个边距的值大概为字体的0.1倍大小,虽然这个经验方案很有效。但是如果手机更换了比较特殊的字体的话,那么这个经验值也会有较大偏差。

寻求问题原因

昨天发现又有同事因为这个问题再花费大量精力调整界面,看来这个问题其实大部分都没注意到。所以有了写一篇博客简单分享的想法,查找更正规的设置方法

为了找到问题出现的原因,做出了两种假设:

  1. 在Java层TextView绘制文字时造成的
  2. native层文字绘制的实现中就有这个问题

分析Android java层绘制流程

简单分析TextView代码,可以发现实际控制文字绘制的是StaticLayout。由于问题是TextView上下的间距,所以首先分析StaticLayout中对行的处理,搜索下对行有写处理的方法:

private int out(CharSequence text, int start, int end,
                      int above, int below, int top, int bottom, int v,
                      float spacingmult, float spacingadd,
                      LineHeightSpan[] chooseHt, int[] chooseHtv,
                      Paint.FontMetricsInt fm, int flags,
                      boolean needMultiply, byte[] chdirs, int dir,
                      boolean easy, int bufEnd, boolean includePad,
                      boolean trackPad, char[] chs,
                      float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
                      float ellipsisWidth, float textWidth,
                      TextPaint paint, boolean moreChars) {
        /*省略无关代码*/
        if (firstLine) {
            if (trackPad) {
                mTopPadding = top - above; // 看起来很可疑
            }

            if (includePad) {
                above = top;
            }
        }

        int extra;

        if (lastLine) {
            if (trackPad) {
                mBottomPadding = bottom - below; // 看起来很可疑
            }

            if (includePad) {
                below = bottom;
            }
        }


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

       /*省略无关代码*/

        mLineCount++;
        return v;
    }

上面方法中的mTopPaddingmBottomPadding一看就是很可疑的变量。把这两个等式有关的变量找出来如下(我们不关心真实的绘制逻辑, 只找出对这个问题有影响的变量就好了)

above = fm.ascent;
below = fm.descent;
top = fm.top;
bottom = fm.bottom;
...
mTopPadding = top - above; 
mBottomPadding = bottom - below; 

很明显这个值的大小跟字体的不同也会有关系,这和我之前遇到经验法不能解决的问题是一致的。关于字体参数的意义可以查看FontMetrics(fm就是FontMetrics类型)。

《解决Android开发中经常与设计稿不吻合的问题》

看来上面代码就是问题的原因了,但我们更希望能在TextView中找到解决问题的方法,查询调用了out方法的地方:

void generate(Builder b, boolean includepad, boolean trackpad) {
    ...
    if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
                mLineCount < mMaximumVisibleLineCount) {
            // Log.e("text", "output last " + bufEnd);

            measured.setPara(source, bufEnd, bufEnd, textDir, b);

            paint.getFontMetricsInt(fm);

            v = out(source,
                    bufEnd, bufEnd, fm.ascent, fm.descent,
                    fm.top, fm.bottom,
                    v,
                    spacingmult, spacingadd, null,
                    null, fm, 0,
                    needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
                    includepad, trackpad, null,
                    null, bufStart, ellipsize,
                    ellipsizedWidth, 0, paint, false);
        }

trackpad的值是外部参数传递过来的(trackpad是判断是否设置mTopPadding/mBottomPadding的条件,这也是我们的线索),搜索generate方法,发现是在构造函数中调用,所以下一步查询TextView中构建StaticLayout的代码:

            StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
                    0, mTransformed.length(), mTextPaint, wantWidth)
                    .setAlignment(alignment)
                    .setTextDirection(mTextDir)
                    .setLineSpacing(mSpacingAdd, mSpacingMult)
                    .setIncludePad(mIncludePad)
                    .setBreakStrategy(mBreakStrategy)
                    .setHyphenationFrequency(mHyphenationFrequency);
            if (shouldEllipsize) {
                builder.setEllipsize(effectiveEllipsize)
                        .setEllipsizedWidth(ellipsisWidth)
                        .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
            }
            // TODO: explore always setting maxLines
            result = builder.build();

再结合Builder的代码,我们会发现mIncludePad的值即trackpad的值。查询mIncludePad的值我们会发现两个方法与之有关:

    /**
     * Set whether the TextView includes extra top and bottom padding to make
     * room for accents that go above the normal ascent and descent.
     * The default is true.
     *
     * @see #getIncludeFontPadding()
     *
     * @attr ref android.R.styleable#TextView_includeFontPadding
     */
    public void setIncludeFontPadding(boolean includepad) {
        if (mIncludePad != includepad) {
            mIncludePad = includepad;

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

    /**
     * Gets whether the TextView includes extra top and bottom padding to make
     * room for accents that go above the normal ascent and descent.
     *
     * @see #setIncludeFontPadding(boolean)
     *
     * @attr ref android.R.styleable#TextView_includeFontPadding
     */
    public boolean getIncludeFontPadding() {
        return mIncludePad;
    }

根据注释也知道了,这就是所有问题的答案了,遗憾的是没有通过xml中设置属性去掉这个默认头部和底部的距离,xml中可以通过android:includeFontPadding="false"设置该属性。

总结

造成实际输出和设计稿不同的原因是TextView的默认上下边距,可以通过调用下面的方法来移除这个默认的上下边距:

TextView#setIncludeFontPadding(false)

或者xml中设置includeFontPadding为false

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:includeFontPadding="false" />
    原文作者:水手辛巴
    原文地址: https://www.jianshu.com/p/008177feaae7
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞