为什么 Shape 不起作用

作者: 南腔北调集合 | 来源:发表于2016-05-07 12:35 被阅读2222次
    基础知识

    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的效果。

    相关文章

      网友评论

      本文标题:为什么 Shape 不起作用

      本文链接:https://www.haomeiwen.com/subject/vkajrttx.html