设置Background导致Padding无效问题追溯

1, 问题描述

很多同学可能都遇到过这个问题:
明明在布局文件中设置了View的padding, 然后程序中动态设置了背景, 运行后发现padding不对.

如下代码:

<TextView
        android:id="@+id/text_view2"
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:layout_marginTop="20dp"
        android:layout_centerHorizontal="true"
        android:layout_below="@id/text_view1"
        android:background="@android:color/holo_green_dark"
        android:padding="20dp"
        android:text="Using the Hello World guide, you’ll create a repository, start a branch,
write comments, and open a pull request"
        />
mTextView2.setBackgroundResource(R.drawable.patch9_bg);

明明加了20dp的padding值, 实际效果却是这样的:


problem

2, 问题原因

这是为什么呢, 我们通过一个简单的Demo程序来验证了下.
如标题所言, 问题就出在, 我们在代码中动态设置该View的Background.

mTextView2.setBackgroundResource(R.drawable.patch9_bg);

删除该代码, 我们再看效果:


no_problem

3, 解决方案

知道了问题原因, 其实已经就解决了一半了, 解决办法无外乎就是在调用setBackgroundResource之后再设置一遍padding值.

可参考http://stackoverflow.com/questions/10095196/whered-padding-go-when-setting-background-drawable

4, 探个究竟

正所谓知其然, 知其所以然. 本文的目的也并不是说就提出这个问题及其解决方案, 更多的是想更深层次的取了解这个问题的Root Cause. 从中了解下Google开发人员为什么会埋下这么一个"坑", 是否是有其他考量.

我们来看下setBackgroundResource做了什么:

先来看下View.java节选代码(请注意中文注释的关键代码):

public void setBackgroundResource(@DrawableRes int resid) {
    if (resid != 0 && resid == mBackgroundResource) {
        return;
    }

    Drawable d = null;
    if (resid != 0) {
    
        // 1, 通过Resource Id获取Drawable对象.
        d = mContext.getDrawable(resid);
    }
    setBackground(d);

    mBackgroundResource = resid;
}

public void setBackground(Drawable background) {
    setBackgroundDrawable(background);
}

public void setBackgroundDrawable(Drawable background) {
    ...
    
    if (background != null) {
        // 2, 取一个存放padding值的Rect对象
        // Rect是一个持有四个值(left, top, right bottom)的矩形对象.
        Rect padding = sThreadLocal.get();
        ...
        
        // 3, 判断用来设置background的Drawable对象是否有padding值. 
        // 注意, 这里将存放padding值Rect传入了.
        if (background.getPadding(padding)) {
        
            // 4, 如果background有padding值, 则改变该View的padding属性.
            resetResolvedPaddingInternal();
                switch (background.getLayoutDirection()) {
                    case LAYOUT_DIRECTION_RTL:
                        mUserPaddingLeftInitial = padding.right;
                        mUserPaddingRightInitial = padding.left;
                        internalSetPadding(padding.right, padding.top, padding.left, padding.bottom);
                        break;
                    case LAYOUT_DIRECTION_LTR:
                    default:
                        mUserPaddingLeftInitial = padding.left;
                        mUserPaddingRightInitial = padding.right;
                        internalSetPadding(padding.left, padding.top, padding.right, padding.bottom);
                }
                mLeftPaddingDefined = false;
                mRightPaddingDefined = false;
        }
        ...
    } else {
        /* Remove the background */
        mBackground = null;
        ...
    }

    ...
    invalidate(true);
}

我们发现, 第3步background.getPadding(padding)是一个关键点, 来看下Drawable的此方法实现:

Drawable.java节选代码:

public abstract class Drawable {
    ...
    /**
     * Return in padding the insets suggested by this Drawable for placing
     * content inside the drawable's bounds. Positive values move toward the
     * center of the Drawable (set Rect.inset).
     *
     * @return true if this drawable actually has a padding, else false. When false is returned,
     * the padding is always set to 0.
     */
    public boolean getPadding(@NonNull Rect padding) {
        padding.set(0, 0, 0, 0);
        return false;
    }
    ...
}

根据注释和代码, 我们发现:

  • 默认返回false.
  • 传入的Rect会被设值.
  • Drawable是一个抽象类, 那么我们这里setBackgroundResource时通过resId生成的Drawable到底是那个Drawable呢? 它的getPadding方法会做什么处理呢?

让我们看下Drawable的子类:


Drawable classes

真的是太多, 简直无从分析.
既然我们的background是通过resource id的方式设置的, 我们再来看下Drawable Resource有哪些吧:
官方文档

以下图片节选比较常用的几种:


drawable resources

一般来说, 我们用到的最多的就是png, .9, state selector图片作为resource了.

我们来分别看下BitmapDrawable, NinePatchDrawable, StateListDrawable的getPadding实现:
可以发现, BitmapDrawable和StateListDrawable都没有重写该方法.
NinePatchDrawable的getPadding重写实现:

    @Override
    public boolean getPadding(Rect padding) {
        final Rect scaledPadding = mPadding;
        if (scaledPadding != null) {
            if (needsMirroring()) {
                padding.set(scaledPadding.right, scaledPadding.top,
                        scaledPadding.left, scaledPadding.bottom);
            } else {
                padding.set(scaledPadding);
            }
            
            // 当此NinePatchDrawable的mPadding的任意一方有值, 则返回true. 
            // 意味着会改变View的当前padding情况.
            return (padding.left | padding.top | padding.right | padding.bottom) != 0;
        }
        return false;
    }

5, 分析验证

如第四章节的分析, 我们大体可以得出结论:

  1. 只有设置background是.9图片时才有可能会改变原来的padding, 导致padding无效.
  2. 为什么说是有可能呢? 因为根据代码, 当我们的.9图片自身四边都不带padding的时候, 返回的也是false, 不会改变原来的padding值.

以上结论针对我们调研的几种常用的Drawable实现, 如有特殊的Drawable请查看其源码的getPadding实现.

那么我们来验证下我们的分析, 做一个Demo, 有三个View, 第一个使用png的普通背景, 第二个使用有padding的.9图片, 第三个使用无padding的.9图片.

mTextView1.setBackgroundResource(R.drawable.bg);
mTextView2.setBackgroundResource(R.drawable.patch9_bg);
mTextView3.setBackgroundResource(R.drawable.patch9_bg_no_padding);

完整代码

效果:


compare

可以看到, 这个例子完全验证了我们的分析.
当设置Background图片资源是有padding的.9图片时才会出现导致原来的设置的padding无效的问题.

顺带提下, 何为有padding的.9图片, 如下图:


nine patch padding

我们在做.9图时, 左上是patch域(即交叉点放大适配), 下方, 右方是内容域, 如果没有全部画上, 留下的部分是不能放内容的, 就形成了Padding.

6, 结语

这个问题, 可能会让很多人苦恼, 比较难以定位, 可能只是代码调用的顺序问题.
也可以说并不困难, 因为知道其根本原因后, 规避解决起来也的确很简单.

在此, 我更多的是想借这个问题来传达一种做技术的"知其然, 知其所以然"的探索思想. 共勉之.

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,397评论 25 707
  • 概述 Android把任何可绘制在屏幕上的图形图像都称为drawable 资源,你可以通过类似getDrawabl...
    小芸论阅读 2,707评论 2 5
  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,725评论 1 92
  • 概述 今天我们来探究一下android的样式。其实,几乎所有的控件都可以使用 background属性去引用自定义...
    CokeNello阅读 4,806评论 1 19
  • 说起刘惜君,就像是说起了某个年代。并不是在讲她在某个时代之中举足轻重,而是像是一个线索般的那种感觉。就好比摸到了一...
    枕边音乐哦阅读 545评论 0 4