为什么 Shape 不起作用

基础知识

Android里,我们经常会用shape去定义View的形状。如下是在xml里定义一个简单shape的代码:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
    <solid android:color="#00FF00" />
    <corners
        android:bottomLeftRadius="10dp"
        android:bottomRightRadius="10dp"
        android:topLeftRadius="10dp"
        android:topRightRadius="10dp" />
</shape>

使用时,将它设置在view 的背景上,有的同学这样问,如下使用shape,为什么不起作用?
第一例, 不起作用:

 <ImageView
    android:id="@+id/img_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/round_corner_rectangle"
    android:scaleType="fitXY"
    android:src="@drawable/img"/>

第二例,不起作用,看不到圆角效果

<FrameLayout    
   android:layout_width="wrap_content"   
   android:layout_height="wrap_content"    
   android:background="@drawable/round_corner_rectangle">    
   <TextView        
        android:layout_width="wrap_content"   
        android:layout_height="wrap_content" />
</FrameLayout>

第三例,TextView 有圆角,正常

<TextView
    android:background="@drawable/round_corner_rectangle"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content" />

首先,shape是什么?

以圆角矩形<shape>为例,其中 shape 标签在解析后对应于 GradientDrawable类(注意不是ShapeDrawable),即在xml里定义<shape>,运行期间会生成对应的GradientDrawable对象,同时传入xml里定义的圆角属性值。

查看GradientDrawable 源码,将看到在xml里<shape>设定的各个角圆角弧度,被传入并保存在数组mRadiusArray:

private void updateDrawableCorners(TypedArray a) {
......
            setCornerRadii(new float[] {
                    topLeftRadius, topLeftRadius,
                    topRightRadius, topRightRadius,
                    bottomRightRadius, bottomRightRadius,
                    bottomLeftRadius, bottomLeftRadius
            });
}

所以,设定shape标签即设生成drawable 对象。

  • Drawable 可以理解为:二维平面上,能画出来的图形图像,如:BitmapDrawable, ShapeDrawable, PictureDrawable, LayerDrawable, 等等派生类。Drawable 都有自己的draw() 方法,来操纵 canvas
  • canvas 画布是透明的,可以在上面涂抹任意形状,并填充上颜色、渐变等,即 Drawable

继续查看GradientDrawable源码,其绘制过程是基本图形绘制,涉及:Canvas、Path、 Paint。其中path 定义封闭形状,并设定好圆角,paint 画笔设置颜色等,最终在canvas 画布上画出图形,步骤如下:

  • path定义封闭形状代码如下:
    private void buildPathIfDirty() {
        final GradientState st = mGradientState;
        if (mPathIsDirty) {
            ensureValidRect();
            mPath.reset();
            mPath.addRoundRect(mRect, st.mRadiusArray, Path.Direction.CW);
            mPathIsDirty = false;
        }
    }
  • 画线及填充,558行:
switch (st.mShape) {
            case RECTANGLE:
                if (st.mRadiusArray != null) {
                    buildPathIfDirty();
                    // 画线及填充
                    canvas.drawPath(mPath, mFillPaint);
                    if (haveStroke) {
                        // 描边
                        canvas.drawPath(mPath, mStrokePaint);
                    }
                }

以上分析了定义一个 圆角矩形时,GradientDrawable 将在 canvas上自我绘制的过程。

View设置各种drawable为背景,怎么起作用的?

以第三例为例,设置TextView的background,先了解以下基础:

  1. TextView 继承自View基类
  2. 设置各种背景都将转化为drawable对象
  3. View 里有一个公用画布 canvas

查看View源码,View 里背景和内容的绘制步骤:

  1. 首先绘制底部 background
  2. 绘制具体的内容,通过onDraw 通知继承View类子类绘制具体内容。
   
 boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) 的源码及注释 16153 行:

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // 接着,绘制内容,dispatchDraw 通知该 View 上的子结点进行自我绘制。

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // we're done...
            return;
        }

绘制Background 的过程,简化一下即为drawable 直接调用自身 draw 方法,在同一画布上进行绘制。

  private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
…

            background.draw(canvas);

    }

以上分析解释了View绘制背景和内容的区别,同时,也顺便可以解释Imageview 的 background 和 src 的不同之处:

  1. 本质上无区别,都是各种不同类型的drawable,本质上都通过自身的draw方法在canvas上绘制。
  2. background 是背景,首先会在View基类的draw里被绘制。src 是内容,随后在子类ImageView的 ondraw 里被绘制。

这里验证一下,如果将 android:background=“@null” 会发生什么

  <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@null"
        android:text="此处背景透明"
        android:layout_alignParentBottom="true"/>

会发现,将会取和设置 transparent 也是一样透明的效果。

将会看到,是和设置背景为 transparent 一样透明的效果。

回头来看文中开头处提到的shape不起作用的例子:
第二例
其中TextView为外部 FrameLayout 的子结点,外部FrameLayout设置的的标签与TextView无关,TextView的绘制范围仅宽高受FrameLayout的影响,标签只代表了一个图像,不影响子节点。
解决方法:
FrameLayout 设置padding, 或者TextView设置 margin,padding要大于等于sqrt(r),其中r为所设圆角半径值,并且两者背景颜色一致。为何为sqrt(r),请自行画图计算。

第一例
ImageView 设置圆角为何不起作用。参见 ImageView 里源码,src 对应 mDrawable,绘制时,将覆盖底层 background,即设置了圆角的drawable。

    private void updateDrawable(Drawable d) {
……
       mDrawable = d;
}

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
…
            mDrawable.draw(canvas);

    }

解决办法
那该怎么给ImageView 画圆角呢?办法是通过paint 的SRC_IN模式:
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

SRC_IN 模式设置后,将两个绘制的效果叠加后取交集后展现,比如:第一个绘制的是个圆形,第二个绘制的是个Bitmap,于是交集为圆形,就实现了圆形图片效果。

而且,android Tint 也是靠 SRC_IN 来自动变成我们想要的背景颜色,来达到Material Design的效果。

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

推荐阅读更多精彩内容