Andorid Drawable类

246 阅读7分钟

1 Drawable可以方便做出一些特殊的UI效果,它比自定义View的成本要低,非图片类的Drawable占用空间较小,这对减小apk的大小很有帮助。

2 Drawable通过获取Drawable的宽高,比如一张图片形成的Drawable内部宽高就是图片的宽高,而一个颜色形成的Drawable就没有内部宽高的概念,Drawable的内部宽高不等于它的大小。

public int getIntrinsicWidth() {
    return -1;
}

/**
 * Return the intrinsic height of the underlying drawable object. Returns
 * -1 if it has no intrinsic height, such as with a solid color.
 */
public int getIntrinsicHeight() {
    return -1;
}

3 在实际开发中,Drawable常被作为View的背景使用,Drawable会被拉伸至View同等大小。 4 Drawable通常被用作View的背景或者在ImageView等控件显示。 5 在ImageView中显示,xml文件中设置'android:src'属性,或者在代码中调用setImageDrawable()方法,getDrawable()方法获取到Drawable。 6 作为View背景,在xnl文件中设置android:background属性,或者在代码中调用setBackground方法,通过getBackground方法获取背景Drawable。

它是一个抽象类,提供了一些API方法去处理各种资源的绘制,但是又不具备View的事件与交互处理能力。可以简单认为是一个辅助绘制工具类,把各种东西都封装好直接给Canvas去画。

它有4个抽象方法子类必须实现:

public abstract void draw(Canvas canvas);
public abstract void setAlpha(@IntRange(from=0,to=255) int alpha);
public abstract void setColorFilter(ColorFilter colorFilter);
public abstract @PixelFormat.Opacity int getOpacity();

draw:在setBounds方法设置的区域内的Canvas中进行Drawable的绘制。 setAlpha:给Drawable指定一个alpha值,在0—255之间。 setColorFilter:设置滤镜效果 getOpactiy:返回Drawable的透明度,取值为PixelFormat.UNKNOWN,PixelFormat.TRANSLUCENT,PixelFormat.TRANSPARENT,PixelFormat.OPAQUE 中的一个。

public void setBounds(int left, int top, int right, int bottom);
public void setBounds(@NonNull Rect bounds);
protected void onBoundsChange(Rect bounds)

setBounds设置绘制绘制区域边界,draw方法调用时会用到其设置的值,不设置默认边界均为0,所以自定义Drawable时要重写该方法。 onBoundsChange 方法中新旧bounds方法变化时回调,默认为空方法。

public int getIntrinsicWidth();
public int getIntrinsicHeight();

获取Drawable的内部宽高,它是包含padding,一张图片形成的Drawable内部宽高就是图片的宽高,不同的Drawable子类是有不同的实现的,而一个颜色所形成的Drawable就没有内部宽高的概念,在用作View的background时自动拉伸成View大小。

下面我们来看下Drawable调用流程 Drawable一般用作View的背景,在基类View中声明了一个成员变量Drawable mBackgrounf作为View的背景。

image.png

1 在xml文件中通过android:background属性给View设置背景

    //View构造方法中间省略代码
    public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        ......
        //获取View的属性
        final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
        //定义一个局部背景变量Drawable
        Drawable background = null;
        ......
        //获取到设置的background属性,也就是我们平时给各个控件在xml中设置的android:background属性值
        case com.android.internal.R.styleable.View_background:
        background = a.getDrawable(attr);
        break;
        ......
        //当设置有背景Drawable属性时调用setBackground方法
        if (background != null) {
            setBackground(background);
        }
        ......
    }

这里获取xml中设置的背景后最终调用setBackground方法。 2 我们可以在代码中直接调用一系列的setBackgroundXX方法:

public void setBackgroundColor(@ColorInt int color) {
    if (mBackground instanceof ColorDrawable) {
        ((ColorDrawable) mBackground.mutate()).setColor(color);
        computeOpaqueFlags();
        mBackgroundResource = 0;
    } else {
        setBackground(new ColorDrawable(color));
    }
}
public void setBackgroundResource(@DrawableRes int resid) {
    if (resid != 0 && resid == mBackgroundResource) {
        return;
    }

    Drawable d = null;
    if (resid != 0) {
        d = mContext.getDrawable(resid);
    }
    setBackground(d);

    mBackgroundResource = resid;
}

都是最终调用setBackground方法,而它又是调用setBackgroundDrawable方法

public void setBackground(Drawable background) {
    //noinspection deprecation
    setBackgroundDrawable(background);
}
@Deprecated
public void setBackgroundDrawable(Drawable background) {
    computeOpaqueFlags();

    if (background == mBackground) {
        return;
    }

    boolean requestLayout = false;

    mBackgroundResource = 0;

    /*
     * Regardless of whether we're setting a new background or not, we want
     * to clear the previous drawable.
     */
    //每次设置新的background时先进行复位操作
    if (mBackground != null) {
    //Drawable回调断开
        mBackground.setCallback(null);
        //移除Drawable绘制队列,实质触发回调的该方法
        unscheduleDrawable(mBackground);
    }

    if (background != null) {//当有设置背景Drawable
        Rect padding = sThreadLocal.get();
        if (padding == null) {
            padding = new Rect();
            sThreadLocal.set(padding);
        }
        resetResolvedDrawablesInternal();
        //给Drawable设置View当前的布局方向
        background.setLayoutDirection(getLayoutDirection());
        //判断当前Drawable是否设置有padding
        if (background.getPadding(padding)) {
        //这里面代码就是依据Drawable的这个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;
        }

        // Compare the minimum sizes of the old Drawable and the new.  If there isn't an old or
        // if it has a different minimum size, we should layout again
        //比较上次(可能没有)和现在设置的Drawable的最小高度,发现不一样就需要给requestLayout标记赋值为true。
        if (mBackground == null
                || mBackground.getMinimumHeight() != background.getMinimumHeight()
                || mBackground.getMinimumWidth() != background.getMinimumWidth()) {
            requestLayout = true;
        }
        //设置Drawable的callback,在View继承关系中有实现Drawable的callback

        background.setCallback(this);
      
        if (background.isStateful()) {
            background.setState(getDrawableState());
        }
        background.setVisible(getVisibility() == VISIBLE, false);
        mBackground = background;

        applyBackgroundTint();

        if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
            mPrivateFlags &= ~PFLAG_SKIP_DRAW;
            requestLayout = true;
        }
    } else {
        /* Remove the background */
        mBackground = null;
        if ((mViewFlags & WILL_NOT_DRAW) != 0
                && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
            mPrivateFlags |= PFLAG_SKIP_DRAW;
        }

        /*
         * When the background is set, we try to apply its padding to this
         * View. When the background is removed, we don't touch this View's
         * padding. This is noted in the Javadocs. Hence, we don't need to
         * requestLayout(), the invalidate() below is sufficient.
         */

        // The old background's minimum size could have affected this
        // View's layout, so let's requestLayout
        requestLayout = true;
    }

    computeOpaqueFlags();
    //如果需要重新布局,触发重新布局
    if (requestLayout) {
        requestLayout();
    }

    mBackgroundSizeChanged = true;
    //通知重新绘制刷新操作
    invalidate(true);
}

每次当我们对控件通过xml或者java设置background后触发的其实就是上面这一堆操作,实质最后触发的就是layout或者draw。在View的draw方法中调用drawBackground方法。

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * 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);
    }
private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    //实质调用了Drawable的setBounds方法,把当前View测量好的矩形区域顶点赋值给Drawable,说明接下来Drawable绘制区域与View大小相同。

    setBackgroundBounds();

    // Attempt to use a display list if requested.
    if (canvas.isHardwareAccelerated() && mAttachInfo != null
            && mAttachInfo.mHardwareRenderer != null) {
        mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

        final RenderNode renderNode = mBackgroundRenderNode;
        if (renderNode != null && renderNode.isValid()) {
            setBackgroundRenderNodeProperties(renderNode);
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            return;
        }
    }
    //View要是能滑动,当View滑到哪里,Drawable背景画到哪(其实就是每次滑动都按照可见区域绘制Drawabl,Canvas在平移了)

    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        canvas.translate(scrollX, scrollY);
        //这就是Drawable的draw方法,最后看来View会调用Drawable的draw方法
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}
void setBackgroundBounds() {
    if (mBackgroundSizeChanged && mBackground != null) {
        mBackground.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
}

学习Drawable好处,可以在自定义View时如果能用Drawable实现会不会更简单,更优化呢?

image.png

这个效果是某个APP一张图标上左边有个角标,右边有个图标加文字,同时顶部有一段渐变阴影。如果我们当初直接用一个相对布局,顶部采用ImageView放一段阴影,左上角再放一个ImageView,右上角放一个TextView,使用drawableLeft设一个图标。

但是我们如果对Drawable理解够深刻,它是可以绘制任何东西的,毕竟重写的draw方法都给你canvas了。在平时的开发过程中要将以前固定的思维方式转向到Drawable上面来。

这样我们可以将这几个View都转换成一个自定义Drawable,这样一来View的数量少了,View的层级也减少了,性能也优化了,代码也更优雅了。(但一般而言图片上面叠加图片的推荐使用一个Drawable来自己画,比如阴影,角标等。如果遇到文字很多,那还是建议用TextView来实现)。

Drawable系统中有很多子类可以简化开发。

LayerDrawable 包含多个Drawable的Drawable,是一个可以管理一组drawable对象的drawable。在LayerDrawable的drawable资源按照列表的顺序绘制,所以列表的最后绘制在最上层。

image.png

badgeForeground下面垫了一个bdgeColor颜色背景。

InsetDrawale有时候我们希望点击区域不只是图片本身的大小,那么这个Drawable就可以设置外面一圈空白区域来调整Drawable实际大小,让点击更轻松。

VectorDrawable定义一个静态的drawable,类似svg格式,每个矢量图被定义成由path和group对象构成的树状结构。 每个path包含了对象的几何轮廓,group包含了变化的具体规则,所有的path会按照xml中定义的顺序依次绘制。

image.png

还有很多系统Drawable等等。主要还是思维的转换,脑子里要想到Drawable这个概念。