Drawable源码分析与自定义Drawable Demo

上一篇文章写了 Android Drawable 基础常识 , 今天写写对于 android.graphics.drawable 的源码分析以及在开发中可以用上的例子。

一、自定义Drawable VS 自定义View

在实际项目中,我们在会有一些自定义控件,常常是写自定义View来实现,其实使用自定义的Drawable也可以实现大部分的效果,那么现在谈谈自定义Drawable VS 自定义View

自定义Drawable VS 自定义View的好处:轻量级,更容易实现,提高UI性能优化

Paste_Image.png

先简单看代码

public class CustomView extends Drawable{

    @Override
    public void draw(Canvas canvas) {

    }

    @Override
    public void setAlpha(int i) {

    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {

    }

    @Override
    public int getOpacity() {
        return 0;
    }
}

发现继承Drawable 绘图只有 draw 方法,如果是自定义View 则有onDraw onMeasure onLayout,这样子我们去看 Drawable 的源码,这里抽取几个常用的方法来讲:

(一)创建Drawable实例的几种不同方法(流、XML、文件地址),源码如下:
     /**
     * Create a drawable from an inputstream
     */
    public static Drawable createFromStream(InputStream is, String srcName) {
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, srcName != null ? srcName : "Unknown drawable");
        try {
            return createFromResourceStream(null, null, is, srcName);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }

     /**
     * Create a drawable from an inputstream, using the given resources and
     * value to determine density information.
     */
    public static Drawable createFromResourceStream(Resources res, TypedValue value,
            InputStream is, String srcName) {
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, srcName != null ? srcName : "Unknown drawable");
        try {
            return createFromResourceStream(res, value, is, srcName, null);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }

    /**
     * Create a drawable from an inputstream, using the given resources and
     * value to determine density information.
     */
    public static Drawable createFromResourceStream(Resources res, TypedValue value,
            InputStream is, String srcName, BitmapFactory.Options opts) {
        if (is == null) {
            return null;
        }

        /*  ugh. The decodeStream contract is that we have already allocated
            the pad rect, but if the bitmap does not had a ninepatch chunk,
            then the pad will be ignored. If we could change this to lazily
            alloc/assign the rect, we could avoid the GC churn of making new
            Rects only to drop them on the floor.
        */
        Rect pad = new Rect();

        // Special stuff for compatibility mode: if the target density is not
        // the same as the display density, but the resource -is- the same as
        // the display density, then don't scale it down to the target density.
        // This allows us to load the system's density-correct resources into
        // an application in compatibility mode, without scaling those down
        // to the compatibility density only to have them scaled back up when
        // drawn to the screen.
        if (opts == null) opts = new BitmapFactory.Options();
        opts.inScreenDensity = res != null
                ? res.getDisplayMetrics().noncompatDensityDpi : DisplayMetrics.DENSITY_DEVICE;
        Bitmap  bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);
        if (bm != null) {
            byte[] np = bm.getNinePatchChunk();
            if (np == null || !NinePatch.isNinePatchChunk(np)) {
                np = null;
                pad = null;
            }

            final Rect opticalInsets = new Rect();
            bm.getOpticalInsets(opticalInsets);
            return drawableFromBitmap(res, bm, np, pad, opticalInsets, srcName);
        }
        return null;
    }

    /**
     * Create a drawable from an XML document. For more information on how to
     * create resources in XML, see
     * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
     */
    public static Drawable createFromXml(Resources r, XmlPullParser parser)
            throws XmlPullParserException, IOException {
        return createFromXml(r, parser, null);
    }

    /**
     * Create a drawable from an XML document using an optional {@link Theme}.
     * For more information on how to create resources in XML, see
     * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
     */
    public static Drawable createFromXml(Resources r, XmlPullParser parser, Theme theme)
            throws XmlPullParserException, IOException {
        AttributeSet attrs = Xml.asAttributeSet(parser);

        int type;
        while ((type=parser.next()) != XmlPullParser.START_TAG &&
                type != XmlPullParser.END_DOCUMENT) {
            // Empty loop
        }

        if (type != XmlPullParser.START_TAG) {
            throw new XmlPullParserException("No start tag found");
        }

        Drawable drawable = createFromXmlInner(r, parser, attrs, theme);

        if (drawable == null) {
            throw new RuntimeException("Unknown initial tag: " + parser.getName());
        }

        return drawable;
    }

    /**
     * Create from inside an XML document.  Called on a parser positioned at
     * a tag in an XML document, tries to create a Drawable from that tag.
     * Returns null if the tag is not a valid drawable.
     */
    public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs)
            throws XmlPullParserException, IOException {
        return createFromXmlInner(r, parser, attrs, null);
    }

    /**
     * Create a drawable from inside an XML document using an optional
     * {@link Theme}. Called on a parser positioned at a tag in an XML
     * document, tries to create a Drawable from that tag. Returns {@code null}
     * if the tag is not a valid drawable.
     */
    public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs,
            Theme theme) throws XmlPullParserException, IOException {
        final Drawable drawable;

        final String name = parser.getName();
        switch (name) {
            case "selector":
                drawable = new StateListDrawable();
                break;
            case "animated-selector":
                drawable = new AnimatedStateListDrawable();
                break;
            case "level-list":
                drawable = new LevelListDrawable();
                break;
            case "layer-list":
                drawable = new LayerDrawable();
                break;
            case "transition":
                drawable = new TransitionDrawable();
                break;
            case "ripple":
                drawable = new RippleDrawable();
                break;
            case "color":
                drawable = new ColorDrawable();
                break;
            case "shape":
                drawable = new GradientDrawable();
                break;
            case "vector":
                drawable = new VectorDrawable();
                break;
            case "animated-vector":
                drawable = new AnimatedVectorDrawable();
                break;
            case "scale":
                drawable = new ScaleDrawable();
                break;
            case "clip":
                drawable = new ClipDrawable();
                break;
            case "rotate":
                drawable = new RotateDrawable();
                break;
            case "animated-rotate":
                drawable = new AnimatedRotateDrawable();
                break;
            case "animation-list":
                drawable = new AnimationDrawable();
                break;
            case "inset":
                drawable = new InsetDrawable();
                break;
            case "bitmap":
                drawable = new BitmapDrawable(r);
                if (r != null) {
                    ((BitmapDrawable) drawable).setTargetDensity(r.getDisplayMetrics());
                }
                break;
            case "nine-patch":
                drawable = new NinePatchDrawable();
                if (r != null) {
                    ((NinePatchDrawable) drawable).setTargetDensity(r.getDisplayMetrics());
                }
                break;
            default:
                throw new XmlPullParserException(parser.getPositionDescription() +
                        ": invalid drawable tag " + name);

        }
        drawable.inflate(r, parser, attrs, theme);
        return drawable;
    }


    /**
     * Create a drawable from file path name.
     */
    public static Drawable createFromPath(String pathName) {
        if (pathName == null) {
            return null;
        }

        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, pathName);
        try {
            Bitmap bm = BitmapFactory.decodeFile(pathName);
            if (bm != null) {
                return drawableFromBitmap(null, bm, null, null, null, pathName);
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }

        return null;
    }

(二)从XML文件中加载Drawable实例 的方法
     /**
     * Inflate this Drawable from an XML resource optionally styled by a theme.
     * Drawable实例接受主题设置的风格
     * @param r Resources used to resolve attribute values
     * @param parser XML parser from which to inflate this Drawable
     * @param attrs Base set of attribute values
     * @param theme Theme to apply, may be null
     * @throws XmlPullParserException
     * @throws IOException
     */
    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
            throws XmlPullParserException, IOException {
        final TypedArray a;
        if (theme != null) {
            a = theme.obtainStyledAttributes(
                    attrs, com.android.internal.R.styleable.Drawable, 0, 0);
        } else {
            a = r.obtainAttributes(attrs, com.android.internal.R.styleable.Drawable);
        }

        inflateWithAttributes(r, parser, a, com.android.internal.R.styleable.Drawable_visible);
        a.recycle();
    }

    /**
     * Inflate a Drawable from an XML resource.
     *
     * @throws XmlPullParserException
     * @throws IOException
     */
    void inflateWithAttributes(Resources r, XmlPullParser parser, TypedArray attrs, int visibleAttr)
            throws XmlPullParserException, IOException {
        mVisible = attrs.getBoolean(visibleAttr, mVisible);
    }
(三)ConstantState
     /**
     *ConstantState这个抽象类被用于存储 多个Drawable实例间 共享的 常量状态值及数据。
    如从同一个图片资源创建的多个BitmapDrawable实例,它们将共享
    同一个存储在它们的ConstantState中的Bitmap。
     * This abstract class is used by {@link Drawable}s to store shared constant state and data
     * between Drawables. {@link BitmapDrawable}s created from the same resource will for instance
     * share a unique bitmap stored in their ConstantState.
     *
     * <p>
     * {@link #newDrawable(Resources)} can be used as a factory to create new Drawable instances
     * from this ConstantState.
     * </p>
     *Drawable#getConstantState可以获取一个Drawable关联的ConstantState。
    调用Drawable#mutate(),则将为新创建的Drawable实例单独关联一个ConstantState。
     * Use {@link Drawable#getConstantState()} to retrieve the ConstantState of a Drawable. Calling
     * {@link Drawable#mutate()} on a Drawable should typically create a new ConstantState for that
     * Drawable.
     */
    public static abstract class ConstantState {
         /**
        运用ConstantState创建一个新的Drawable实例
         */
        public abstract Drawable newDrawable();

         /**
         运用ConstantState创建一个新的Drawable实例
         */
        public Drawable newDrawable(Resources res) {
            return newDrawable();
        }

        /**
         * Create a new Drawable instance from its constant state. This must be
         * implemented for drawables that can have a theme applied.
         */
        public Drawable newDrawable(Resources res, Theme theme) {
            return newDrawable(null);
        }

        /**
        返回会影响Drawable实例的一个bit掩码变化设置
         */
        public abstract int getChangingConfigurations();

        /**
         *返回所有的像素数
         */
        public int addAtlasableBitmaps(Collection<Bitmap> atlasList) {
            return 0;
        }

        /** @hide */
        protected final boolean isAtlasable(Bitmap bitmap) {
            return bitmap != null && bitmap.getConfig() == Bitmap.Config.ARGB_8888;
        }

        /**
         * 返回当前共享状态是否可以设置主题
         */
        public boolean canApplyTheme() {
            return false;
        }
    }
(四)onBoundsChange

图片不断的绘制呈现过程中变化的回调方法。

  /**
     * Override this in your subclass to change appearance if you vary based on
     * the bounds.
     */
    protected void onBoundsChange(Rect bounds) {
    }
(五)updateTintFilter

确保色彩过滤器和当前色彩与色彩模式一致

  
    @Nullable PorterDuffColorFilter updateTintFilter(@Nullable PorterDuffColorFilter tintFilter,
            @Nullable ColorStateList tint, @Nullable PorterDuff.Mode tintMode) {
        if (tint == null || tintMode == null) {
            return null;
        }
        final int color = tint.getColorForState(getState(), Color.TRANSPARENT);
        if (tintFilter == null) {
            return new PorterDuffColorFilter(color, tintMode);
        }
        tintFilter.setColor(color);
        tintFilter.setMode(tintMode);
        return tintFilter;
    }
(六)setLevel

设置自定义Drawable绘制的渐变的程度 , 图片渐变的范围 0-10000

 /**
     * Specify the level for the drawable.  This allows a drawable to vary its
     * imagery based on a continuous controller, for example to show progress
     * or volume level.
     *
     * <p>If the new level you are supplying causes the appearance of the
     * Drawable to change, then it is responsible for calling
     * {@link #invalidateSelf} in order to have itself redrawn, <em>and</em>
     * true will be returned from this function.
     *
     * @param level The new level, from 0 (minimum) to 10000 (maximum).
     *
     * @return Returns true if this change in level has caused the appearance
     * of the Drawable to change (hence requiring an invalidate), otherwise
     * returns false.
     */
    public final boolean setLevel(int level) {
        if (mLevel != level) {
            mLevel = level;
            return onLevelChange(level);
        }
        return false;
    }
二、 Drawable绘制流程

Drawable实例到底是如何被绘制到屏幕上面?

1 通过Resource获取Drawable实例

(.9图返回1个NinePatchDrawable实例,普通图片返回1个BitmapDrawable实例。)

2 将获取的Drawable实例当做背景设置给View

最常用写法:targetView.setBackgroundDrawable(Drawable bg)

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ****
    public void setBackgroundDrawable(Drawable background) {
        ****
        if (background == mBackground) {
            //如果当前背景和background相同,直接return
            return;
        }
        boolean requestLayout = false;
        mBackgroundResource = 0;
        if (mBackground != null) {
            if (isAttachedToWindow()) {
                //如果当前View实例已经被绘制到屏幕上,则首先取消
                //该View实例原始背景Drawable的动画
                mBackground.setVisible(false, false);
            }
            //移除该View实例原始背景Drawable的动画监听接口
            mBackground.setCallback(null);
            //取消该View实例原始背景Drawable的所有事件
            unscheduleDrawable(mBackground);
        }
        if (background != null) {
            ****
            //设置background的布局方向和View实例一致,
            //Drawable.setLayoutDirection见上一篇文章
            background.setLayoutDirection(getLayoutDirection());
            if (background.getPadding(padding)) {
                //如果Drawable实例background有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会将四个参数值和View实例的padding进行比对,若不同则会重新布局+重建View的外部轮廓
                        internalSetPadding(padding.left, padding.top, padding.right, padding.bottom);
                }
                mLeftPaddingDefined = false;
                mRightPaddingDefined = false;
            }
            if (mBackground == null
                    || mBackground.getMinimumHeight() != background.getMinimumHeight()
                    || mBackground.getMinimumWidth() != background.getMinimumWidth()) {
                requestLayout = true;
            }
            //设置当前View实例的背景为传入的Drawable实例 background
            mBackground = background;
            if (background.isStateful()) {
                //如果background会根据状态值变更外观,则设置其状态为
                //当前View实例的state
                background.setState(getDrawableState());
            }
            if (isAttachedToWindow()) {
                //如果当前View实例已经被绘制到屏幕上
                //且实例和实例的父控件及递归获得的根布局都处于可见状态,
                //则设置background开启动画效果
                background.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
            }
            applyBackgroundTint();
            //设置background动画接口监听为View实例本身(View实现了 Drawable.Callback):
            //public class View implements Drawable.Callback
            background.setCallback(this);
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
                mPrivateFlags &= ~PFLAG_SKIP_DRAW;
                //需要重新布局
                requestLayout = true;
            }
        } else {
            mBackground = null;
            if ((mViewFlags & WILL_NOT_DRAW) != 0
                    && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
                mPrivateFlags |= PFLAG_SKIP_DRAW;
            }
            requestLayout = true;
        }
        computeOpaqueFlags();
        if (requestLayout) {
            //重新布局
            requestLayout();
        }
        mBackgroundSizeChanged = true;
        //重绘View实例
        invalidate(true);
        //重建View实例的外部轮廓
        invalidateOutline();
    }
}

由此可见,
1:setBackgroundDrawable方法,调用了Drawable的一系列方法,设置了Drawable实例一系列属性值,最终引发了View实例的重新布局(requestLayout()),重绘(invalidate(true))及重建View实例的外部轮廓(invalidateOutline())。
2:在View实例重绘过程的第一步,将得到的Drawable实例(View实例的背景)绘制到屏幕上,实质是调用了Drawable.draw(@NonNull Canvas canvas)。
3:Drawable.draw本身是个抽象方法,绘制具体逻辑由其子类实现。
我们以之前获得的BitmapDrawable为例进行分析:。
最终调用了Canvas.drawBitmap方法,将Drawable实例中的bitmap绘制到View实例关联的画布上。

三、Demo
原理
通过level变量因子实现颜色变化

0---左边全灰
10000---全灰
5000---蓝色

5000---0混合颜色状态
5000-10000混合颜色状态

重点抠出灰色区域(宽度?--》level)

看代码

**
 * @创建 HaiJia
 * @时间 2017/3/11 11:42
 * @描述 自定义渐变View
 */

public class ReveaView extends Drawable{

    private Drawable unSelectedDrawble;
    private Drawable seletedDrawable;
    private Rect outRect = new Rect();


    public ReveaView(Drawable unSelectedDrawble,Drawable seletedDrawable){
        super();
        this.unSelectedDrawble = unSelectedDrawble;
        this.seletedDrawable = seletedDrawable;
    }

    //onDraw onMeasure onLayout


    @Override
    public void draw(Canvas canvas) {
        int level = getLevel();
        if(level ==0 || level ==10000){//绘制全灰的图片
            unSelectedDrawble.draw(canvas);
        }else if(level == 5000){//绘制全蓝图片
            seletedDrawable.draw(canvas);
        }else{//混合颜色图片
            //渐变 先绘制左边的区域(从灰色图片抠出左边+从彩色区域抠出右边
            Rect bounds = getBounds();
            {//绘制左边
                float ratio = level/5000f - 1f;
                int width = bounds.width();
                int height = bounds.height();
                width = (int) (width*Math.abs(ratio));
                int gravity = ratio<0?Gravity.LEFT:Gravity.RIGHT;
                //进行抠的动作
                Gravity.apply(gravity,width,height,
                        bounds,//在没有扣之前矩形区域
                        outRect);//目标矩形区域
                canvas.save();//保存canvas
                canvas.clipRect(outRect);
                unSelectedDrawble.draw(canvas);
                canvas.restore();//进行还原

            }


            {
                //右边
                float ratio = level/5000f - 1f;
                int width = bounds.width();
                int height = bounds.height();
                width = (int) (width-width*Math.abs(ratio));
                int gravity = ratio<0?Gravity.LEFT:Gravity.RIGHT;
                //进行抠的动作
                Gravity.apply(gravity,width,height,
                        bounds,//在没有扣之前矩形区域
                        outRect);//目标矩形区域
                canvas.save();//保存canvas
                canvas.clipRect(outRect);
                seletedDrawable.draw(canvas);
                canvas.restore();//进行还原

            }

        }
    }

    @Override
    public void setAlpha(int i) {
        //设置p
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {

    }

    @Override
    public int getOpacity() {
        return 0;
    }

    //呈现图片 回调
    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        //图片动的时候,选择区域也在进行变化
        //确定好drawable边距,也就是bounds
        //
        unSelectedDrawble.setBounds(bounds);
        seletedDrawable.setBounds(bounds);
    }

    @Override
    protected boolean onLevelChange(int level) {
        //Level 改变的时候不断刷新它本身,所以设为返回true
        return true;
    }

    //图片的宽高
    @Override
    public int getIntrinsicWidth() {
        return unSelectedDrawble.getIntrinsicWidth();
    }

    @Override
    public int getIntrinsicHeight() {
        return unSelectedDrawble.getIntrinsicHeight();
    }
}

推荐阅读更多精彩内容