解决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类型)。

看来上面代码就是问题的原因了,但我们更希望能在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" />
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容

  • 记得友人曾问过我,还再相信爱情吗?初听这句话心里犯着嘀咕。想着难道友人情路坎坷?说罢,便成了友人的情感被倾诉对象。...
    随风而逝Wind阅读 186评论 0 0
  • 在还是以女子温文尔雅为美德的年代,在外沉默寡言的女人总是更能得到社会和家庭的认可。而现在的时代,沉默常常被打上“内...
    郝小夕阅读 160评论 0 0
  • 花开不败 文/复旦 职烨 编辑/韩佑释 整理/袁辰 01 我不知道应该怎样写,准确地...
    韩佑释阅读 3,950评论 0 7
  • 继续雾霾天,继续的每天学习、然后吃饭、然后睡觉。 每天码字,对于我来说,是必做的事情,不管我今天多累多忙。 搬过来...
    星星姑娘来自小镇阅读 275评论 0 0