Android 应用层开发 Drawable 的一些叨叨絮

2,704 阅读17分钟

1 背景

博客也该开张了,懒惰都是惯的。既然这样,那就拿一个简单问题来叨叨絮吧;故事的起因是这样的,群里有一哥们仿写别人自定义控件,没整明白 Drawable 咋回事,然后群里炸开了锅,为了维护群里的世界和平,随决定叨叨絮一下 Android Drawable。关于 Android Drawable 可绘制对象资源的介绍大家可以去看看官方文档即可;而关于 Drawable 的使用细节其实你在 Drawable.Java 的开篇大段注释中可以找寻到秘笈,所以说,如果你想玩转 Android 自定义控件 Drawable 系列,请务必先去拜读搞明白上面提到的两个秘笈,否则最直接的后果就是天下大乱。
(我不喜欢蹭刷访问量和博文篇数,所以没必要把这个知识点分几篇博客来写,所以看时别闲长,这已经是我的习惯了。)

【工匠若水 blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

2 Drawable 源码浅析

关于 Android Drawable 抽象类有多少个实现子类或者这些实现子类怎么使用及原理就不多 BB 了,为了世界和平,我需要重点强调的是给你一个抽象 Drawable,还我一个自定义 Drawable 的技能;不过这里有一点兼容性问题要注意:所有 Android 版本都是兼容通过 Java 实例化继承 Drawable 自定义的,但是只有 API 24 开始才允许在 XML 中使用自定义的 Customer Drawable

理解 Drawable 的精髓其实就是理解下面这一段话而已:

A Drawable is a general abstraction for "something that can be drawn." 
Most often you will deal with Drawable as the type of resource retrieved for drawing things to the screen; the Drawable class provides a generic API for dealing with an underlying visual resource that may take a variety of forms. 
Unlike a {@link android.view.View}, a Drawable does not have any facility to receive events or otherwise interact with the user.

上面这段话简单粗暴理解就是说,Drawable 是一个抽象类,提供了一些 API 方法去处理各种资源的绘制,但是又不具备 View 的事件与交互处理能力。额,再简单粗暴一点认为就是一个辅助绘制工具类,把各种东西都封装搞好以后直接给Canvas去画。既然是工具类,说白了就是个模板,你就把它类比 View 或者 Paint 来看吧,不过这玩意要切记兼容性问题,下面列到的源码解释中很多方法存在兼容性问题,请自己解决或者使用 DrawableCompat 即可。
(下面源码是基于 android-24 解释的,略多,不过我们一般自定义 Drawable 时只会用其中几个方法,看不下去的可以直接跳到下一个小节即可,这一段源码解释完全可以当作日后自定义的参考手册即可。)

public abstract class Drawable {
    ......
    /**
     * 画在setBounds设置的区域内,子类必须实现,canvas就是要被绘制的地方,譬如View的onDraw的canvas。
     * 要绘制状态效果的话可由setAlpha和setColorFilter等方法控制。
     * 具体被调用样例可以去ImageView draw中瞅瞅,或者去看看之前文章分析的View在draw中绘制Background Drawable时调用。
     */
    public abstract void draw(@NonNull Canvas canvas);

    /**
     * 指定绘制矩形边界区域,在draw方法调用时用到其设置的值。
     * 当该方法调用后新旧bounds发生变化时会触发onBoundsChange方法,不设置默认边界均为0,故很多人自定义Drawable发现不显示的可能因素之一在此。
     */
    public void setBounds(int left, int top, int right, int bottom) {...}
    public void setBounds(@NonNull Rect bounds) {...}
    protected void onBoundsChange(Rect bounds) {...}

    /**
     * 给调用者通过bounds参数或者返回值复制返回一个setBounds设置的边界,外界修改不影响已经设置的Bounds,值拷贝。
     */
    public final void copyBounds(@NonNull Rect bounds) {...}
    public final Rect copyBounds() {...}

    /**
     * 给调用者通过返回一个setBounds设置的边界,切记外界修改会影响已经设置的Bounds,尽量不要改。
     */
    public final Rect getBounds() {...}
    public Rect getDirtyBounds() {...}

    /**
     * 在系统Configurate change时调用该方法来处理Drawable状态,这一组方法不常用。
     * 系统config改变的参数类型参见:android.content.pm.ActivityInfo
     */
    public void setChangingConfigurations(@Config int configs) {...}
    public @Config int getChangingConfigurations() {...}

    /**
     * 当使用bitmap位图时可进行滤波处理,类似我们自定义时使用Paint的setAntiAlias()和setBitmapFilter(true)一样,
     * 一个用来防边缘锯齿,一个用来对位图滤波处理;当自定义的Drawable不是bitmap位图时该设置失效。
     */
    public void setFilterBitmap(boolean filter) {}
    public boolean isFilterBitmap() {...}

    /**
     * 回调接口类,若要实现自定义动画drawable,可以通过setCallBack(Callback)实现对动画的调度和执行。
     */
    public interface Callback {
        /**
         * 当drawable重画时触发,who为要重画的drawable。
         */
        void invalidateDrawable(@NonNull Drawable who);

        /**
         * 预先安排动画的下一帧,也可通过Handler.postAtTime(Runnable, Object, long)实现。
         */
        void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);

        /**
         * 删除预先安排的动画某帧,也可通过Handler.removeCallbacks(Runnable, Object)实现。
         */
        void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);
    }

    /**
     * 给需要动画的Drawable设置回调。(已经修改为弱引用了,防止以前低版本Drawable的CallBack内存泄漏,锅锅锅~)
     */
    public final void setCallback(@Nullable Callback cb) {...}
    public Callback getCallback() {...}

    /**
     * 当通过setCallback设置回调后调用该方法会触发回调Callback.invalidateDrawable(@NonNull Drawable who)实现。
     * 当没有设置CallBack时该方法无任何效果。
     */
    public void invalidateSelf() {...}

    /**
     * 当通过setCallback设置回调后调用该方法会触发回调Callback.scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when)实现。
     * 当没有设置CallBack时该方法无任何效果。
     */
    public void scheduleSelf(@NonNull Runnable what, long when) {...}

    /**
     * 当通过setCallback设置回调后调用该方法会触发回调Callback.unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what)实现。
     * 当没有设置CallBack时该方法无任何效果。
     */
    public void unscheduleSelf(@NonNull Runnable what) {...}

    /**
     * 获取与设置RTL或者LTR布局方向,譬如让Drawable支持阿拉伯语布局,可以参见View的LAYOUT_DIRECTION_LTR和LAYOUT_DIRECTION_RTL。
     * 当RTL/LTR布局动态变化时触发onLayoutDirectionChanged。
     * 好处就是如果我们的自定义Drawable要支持阿拉伯等布局方向切换,我们不用定义两个Drawable了,通过这一组方法实现即可。
     */
    public @View.ResolvedLayoutDir int getLayoutDirection() {...}
    public final boolean setLayoutDirection(@View.ResolvedLayoutDir int layoutDirection) {...}
    public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {...}

    /**
     * 给当前Drawable设置和获取alpha透明度,setAlpha为抽象方法,子类必须实现,默认一般单态Drawable时重写直接返回255即可。
     * 有时候我们自定义Drawable时会在内部定义一个Paint,所以setAlpha实现也可以直接为mPaint.setAlpha(alpha);
     */
    public abstract void setAlpha(@IntRange(from=0,to=255) int alpha);
    public int getAlpha() {return 0xFF;}

    /**
     * @hide 被匿了,给将来准备的货,这就尴尬了,类比Paint的Xfermode吧。
     */
    public void setXfermode(@Nullable Xfermode mode) {...}

    /**
     * 设置滤镜效果,类似Paint的setColorFilter()方法,譬如我们常干的一件事:
     * textview.getBackground().setColorFilter(new LightingColorFilter(0xAAFFCCEE, 0xFF00BBAA));
     * 不懂的去前面翻自定义博客文章吧,取消滤镜效果直接传null即可。
     * 有时候我们自定义Drawable时会在内部定义一个Paint,所以setAlpha实现也可以直接为mPaint.setColorFilter(colorFilter);
     */
    public abstract void setColorFilter(@Nullable ColorFilter colorFilter);

    /**
     * 设置滤镜效果,同上setColorFilter(@Nullable ColorFilter colorFilter);
     * 其实就是setColorFilter(new PorterDuffColorFilter(color, mode));的封装。
     */
    public void setColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode) {...}
    public @Nullable ColorFilter getColorFilter() {...}
    public void clearColorFilter() {...}

    /**
     * Drawable的Tint变色处理,setTint为setTintList的封装而已,注意API兼容问题,一般可用DrawableCompat的。
     * 实际用处譬如我们自定义的selector drawable,getResources().getColorStateList(R.color.selector_XXX));
     * 注意:如果设置了setColorFilter,则setTint和setTintList将无效。
     */
    public void setTint(@ColorInt int tintColor) {...}
    public void setTintList(@Nullable ColorStateList tint) {...}

    /**
     * 设置上面Tint变色时的PorterDuff.Mode模式,默认为PorterDuff.Mode.SRC_IN。
     * 注意:如果设置了setColorFilter,则setTintMode将无效。
     */
    public void setTintMode(@NonNull PorterDuff.Mode tintMode) {}

    /**
     * 设置热点坐标,5.0加入,坑爹。
     * RippleDrawable就是一个以波纹效果来显示状态变化的Drawable,其中波纹的位置就是这玩意设置的。
     */
    public void setHotspot(float x, float y) {}
    public void setHotspotBounds(int left, int top, int right, int bottom) {}
    public void getHotspotBounds(@NonNull Rect outRect) {...}

    /**
     * 设置和获取状态,有变化时会触发onStateChange。譬如:
     * {@link android.R.attr#state_focused}
     * {@link android.R.attr#state_pressed}
     * 在View状态改变的时候,会调用Drawable的setState函数。
     */
    public boolean setState(@NonNull final int[] stateSet) {...}
    protected boolean onStateChange(int[] state) {...}
    public @NonNull int[] getState() {...}

    /**
     * 上面setState只能定义有限的几种状态,如果需要更多的状态,就可以使用图像级别资源。
     * 图像级别资源文件中可以定义任意多个图像级别,可以通过该方法来切换不同状态的图像。
     */
    public final boolean setLevel(@IntRange(from=0,to=10000) int level) {...}
    public final @IntRange(from=0,to=10000) int getLevel() {...}
    protected boolean onLevelChange(int level) {...}

    /**
     * 设置或者获取Drawable是否可见。
     */
    public boolean setVisible(boolean visible, boolean restart) {...}
    public final boolean isVisible() {}

    /**
     * 返回当前Drawable透明或者半透明或者不透明等,默认不清楚时直接返回TRANSLUCENT是最好的选择。
     * {@link android.graphics.PixelFormat}:
     * {@link android.graphics.PixelFormat#UNKNOWN},
     * {@link android.graphics.PixelFormat#TRANSLUCENT},
     * {@link android.graphics.PixelFormat#TRANSPARENT}, or
     * {@link android.graphics.PixelFormat#OPAQUE}.
     */
    public abstract @PixelFormat.Opacity int getOpacity();

    /**
     * 返回Drawable的真实宽高,包含padding等,如果没有宽高(譬如纯Color)则返回-1即可。
     */
    public int getIntrinsicWidth() {...}
    public int getIntrinsicHeight() {...}

    /**
     * 返回建议的最小宽高,一般这个宽高决定了自定义View在有些情况下的宽高。
     */
    public int getMinimumWidth() {...}
    public int getMinimumHeight() {...}

    /**
     * 返回Drawable的padding,没有padding时return false,反之。
     */
    public boolean getPadding(@NonNull Rect padding) {...}

    /**
     * 如果有多个控件同时使用某一资源且要改变该资源的状态,我们就需要用mutate方法。
     * 使用mutate方法是为了更改一个资源状态时其它引用该资源的控件不被改变,因为默认情况下,所有从同一资源(R.drawable.XXX)
     * 加载的Drawable实例都共享一个共用的状态ConstantState,如果我们更改了一个实例的状态,其余的实例都会接收到相同的更改变化。
     * @see ConstantState
     * @see #getConstantState()
     */
    public @NonNull Drawable mutate() {return this;}

    /**
     * 通过inputstream、xml、FilePath创建一个Drawable。
     */
    public static Drawable createFromStream(InputStream is, String srcName) {...}
    public static Drawable createFromResourceStream(Resources res, TypedValue value, InputStream is, String srcName) {...}
    public static Drawable createFromResourceStream(Resources res, TypedValue value, InputStream is, String srcName, BitmapFactory.Options opts) {...}
    public static Drawable createFromXml(Resources r, XmlPullParser parser) throws XmlPullParserException, IOException {...}
    public static Drawable createFromXml(Resources r, XmlPullParser parser, Theme theme) throws XmlPullParserException, IOException {...}
    public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {...}
    public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException {...}
    public static Drawable createFromPath(String pathName) {...}

    public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs) throws XmlPullParserException, IOException {...}
    public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException {...}
    void inflateWithAttributes(@NonNull @SuppressWarnings("unused") Resources r, @NonNull @SuppressWarnings("unused") XmlPullParser parser, @NonNull TypedArray attrs, @AttrRes int visibleAttr) throws XmlPullParserException, IOException {...}

    /**
     * 每一个Drawable对象都关联一个ConstantState对象,目的是为了保存Drawable对象的一些恒定不变数据,
     * 为了节约内存,Android系统对于从同一个res创建的Drawable对象会共享同一个ConstantState对象。
     */
    public static abstract class ConstantState {
        public abstract @NonNull Drawable newDrawable();
        public @NonNull Drawable newDrawable(@Nullable Resources res) {return newDrawable();}
        public @NonNull Drawable newDrawable(@Nullable Resources res, @Nullable @SuppressWarnings("unused") Theme theme) {return newDrawable(res);}
        public abstract @Config int getChangingConfigurations();
        public int addAtlasableBitmaps(@NonNull Collection<Bitmap> atlasList) {return 0;}
        protected final boolean isAtlasable(@Nullable Bitmap bitmap) {...}
        public boolean canApplyTheme() {return false;}
    }

    /**
     * 返回一个当前Drawable的ConstantState,参见mutate方法。
     */
    public @Nullable ConstantState getConstantState() {return null;}
    ......
}

确实闷逼!上面涉及 Paint 和 Canvas 的注释不懂的可以参考我这篇博文《Android应用自定义View绘制方法手册》。不过这和自定义 View 一样,为了强大,所以提供的方法都很基础专一,只有这样才能自由组合为所欲为;不过对于我们大多数自定义需求来说使用 Drawable 的方法是十分有限的,所以不要被那么多方法蒙蔽了双眼。

【工匠若水 blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

3 Drawable 调用流程浅析

能耐心看完上面的是真爱,有了上面的解释我想你此刻一定在想,我自定义 View 好歹也有个调用流程、好歹也有个规律可循的结构啊,这自定义 Drawable 怎么是完全闷逼的,这就对了,这一小节就是打算通过源码给你揭开 Drawable 的调用流程,以此让你在自定义 Drawable 时做到胸有成竹。

为了让大家浅显易懂的知道 Drawable 的作用和用法,有了上面 Drawable 的分析外我们还需要给一个使用样例分析;其实关于 Drawable 最好的使用样例是 ImageView,鉴于 ImageView 源码的庞大复杂性,我们这里选择之前有铺垫分析的 View 为样例,看过《Android应用层View绘制流程与源码分析》的同学都知道,在 Android 中每个 View 都至少有一个 Drawable 成员变量,尤其在基类 View 中有一个背景 Drawable。我们来看看这段暧昧的代码:

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
    ......
    private Drawable mBackground;
    ......

    //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);
        }
        ......
    }
}

首先我们会发现在 Android 任何 View 控件中都有一个 Background 属性,框架会在 View 构造方法中获取这个属性 Drawable,然后传入 setBackground(background) 方法,这方法大家一定都很熟悉吧,我们通常总是通过各种 setBackgroundXXX 方法来给控件设置背景,用的就是这个系列方法,所以我们就直接去看看这个系列方法最终归处 setBackgroundDrawable(Drawable background) 方法,如下:

    //几种设置背景的方法终归都是这个
    public void setBackgroundDrawable(Drawable background) {
        ......
        //每次设置新的background后就进行复位操作
        if (mBackground != null) {
            if (isAttachedToWindow()) {
                //Drawable设置为不可见(这部就用上上面Drawable的分析了么)
                mBackground.setVisible(false, false);
            }
            //Drawable回调断开(这部就用上上面Drawable的分析了么)
            mBackground.setCallback(null);
            //移除Drawable绘制队列,实质触发回调的该方法(这部就用上上面Drawable的分析了么)
            unscheduleDrawable(mBackground);
        }

        if (background != null) {
            //当有设置背景Drawable
            ......
            //给Drawable设置View当前的布局方向(这部就用上上面Drawable的分析了么)
            background.setLayoutDirection(getLayoutDirection());
            //判断当前Drawable是否设置有padding(这部就用上上面Drawable的分析了么,有padding则返回true)
            if (background.getPadding(padding)) {
                //依据Drawable的这个padding去给当前View相关padding属性建议修改
                ......
            }
            //比较上次旧的(可能没有)和现在设置的Drawable的最小宽高,发现不一样就预示着我们接下来需要对View再次layout,做标记
            if (mBackground == null
                    || mBackground.getMinimumHeight() != background.getMinimumHeight()
                    || mBackground.getMinimumWidth() != background.getMinimumWidth()) {
                requestLayout = true;
            }
            //把要新设置的Drawable正式赋值给View的mBackground成员
            mBackground = background;
            //判断当状态改变时当前Drawable是否需要切换图片,一般在StateListDrawable实现中为true(这部就用上上面Drawable的分析了么)
            if (background.isStateful()) {
                //设置Drawable状态(这部就用上上面Drawable的分析了么)
                background.setState(getDrawableState());
            }
            //如果View已经attached to window了就把Drawable设置为可见(这部就用上上面Drawable的分析了么)
            if (isAttachedToWindow()) {
                background.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
            }
            //设置Drawable的callback,在View继承关系中有实现Drawable的callback
            background.setCallback(this);
            ......
        } else {
            //当没有设置背景Drawable时清空背景Drawable,然后设置View的重新layout标志
            mBackground = null;
            ......
            requestLayout = true;
        }
        ......
        //需要重新布局,触发重新布局
        if (requestLayout) {
            requestLayout();
        }
        //通知重新绘制刷新操作
        mBackgroundSizeChanged = true;
        invalidate(true);
        invalidateOutline();
    }

可以看见,每逢我们对控件通过 xml 或者 java 设置 background 后触发的其实就是上面这一堆操作,实质最后触发的就是 layout 或者 draw。我们知道 View 默认的 onLayout 和 onDraw 是空实现,所以我们顺着流程去看看 View 的 draw 方法,如下:

    public void draw(Canvas canvas) {
        /*
         *      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)
         */
        ......
        drawBackground(canvas);
        ......
    }

有点意思,再来看看 drawBackground(canvas) 方法,如下:

    private void drawBackground(Canvas canvas) {
        ......
        //实质调用了Drawable的setBounds方法,把当前View测量好的矩形区域顶点赋值给Drawable,说明接下来Drawable绘制区域与View大小相同。
        //该方法实质:mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
        setBackgroundBounds();
        //有意思!
        //简单粗暴理解就是View要是能滑动,化到哪Drawable背景画到哪(其实就是每次滑动都按可见区域绘制Drawable,因为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方法,传入的是当前View的canvas而已。
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

握草,View 与 Drawable 的暧昧几乎真相大白了,由此更加验证了背景知识简单粗暴的介绍—— Drawable 就是一个框架工具,配合给 View 绘制来用的而已,所以通过上面两大节源码分析,我想你至少脑袋中已经产生了下面这个抽象的不能再抽象的对比图,然后也能恍然大悟吧。
这里写图片描述

这时候机智的人肯定又会问,那我设置完 background 的 Drawable 后 selector 那种状态切换又是咋回事呢?这儿只能回答你,自己动手去 View 中翻翻 state 相关的代码吧,这儿懒得说了,很简单的,核心就是 Android 中 Drawable 的一个特殊实现子类而已。

【工匠若水 blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我

4 Drawable 自定义实战

都说了,作为程序员和老大 PK 的真谛是 “Talk is easy, show me the code!”,上面几个小节 BB 了那么多源码,估计很多人都看不到这里就关闭博文了,没事,下面不 BB 了,我们直接来实战,这样你再通过实战的例子回过头去看看上面的源码分析,你会发现你已经完全领悟了。

下面先给出自定义 Drawable 的一般套路模板(通过自定义 Drawable 画一个黑圆为例),大家日后自定义按照这个模板思路来套即可,效果如下:
这里写图片描述
模板套路代码结构如下:

//自定义 Drawable 基本模板(实现一个黑圆Drawable)
public class RoundDrawable extends Drawable {
    private Paint mPaint;

    public RoundDrawable() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }

    @Override
    public void draw(Canvas canvas) {
        Rect rect = getBounds();
        int width = rect.right - rect.left;
        int heigh = rect.bottom - rect.top;
        canvas.drawCircle(width/2, heigh/2, Math.min(width/2, heigh/2), mPaint);
    }

    @Override
    public void setAlpha(int i) {
        mPaint.setAlpha(i);
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }
}
//自定义 View 使用自定义 Drawable
public class CustomerView extends View {
    private Drawable mDrawable;

    public CustomerView(Context context) {
        this(context, null);
    }

    public CustomerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        //在自定义View中实例化一个自定义Drawable
        mDrawable = new RoundDrawable();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //自定义View获取大小后设置给自定义Drawable
        mDrawable.setBounds(getLeft(), getTop(), getRight(), getBottom());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //把我们的自定义Drawable画到自定义View上
        mDrawable.draw(canvas);
    }
}

套路啊,除过以上这个标准模式以外,我们还可以在 Android 标准控件中使用自定义的 Drawable,譬如给 ImageView 设置 setImageDrawable(customerDrawable) 也是可以的,不过注意提前设置 Bounds 大小。看完上面的套路你可能会想,这明明通过自定义 View 自己就能搞定,为毛还要自定义一个 Drawale,哈哈,淡定,你仔细想想哪个更加灵活呢,自己琢磨下吧!

套路就是标准,有了上面这几个模板套路,你会发现自定义 Drawable 其实核心就那几个方法,你要是想进阶更加有追去的模仿,不妨去看看 Android 标准实现的那些继承 Drawable 的 XXXDrawable,你会发现归一思想以后都是上面的模板套路,只是依据自己特点实现了自己的特点而已;有一个最值得观摩的官方实现就是android.support.v4.graphics.drawable.RoundedBitmapDrawable,短小精干,300 行代码,很适合模仿自定义 Drawable 学习。

有了源码流程浅析,有了套路模板,有了来龙去脉,我们还缺一个具备实用价值的自定义实例,如下实例源码( 点我 https://github.com/yanbober/DreamDrawable 找到 )演示了几种自定义 Drawable 的姿势,效果如下:

这里写图片描述

至于细节就不多说了,相信你依据模板套路是能理解的,不扯了,我去打球去了,运动最重要。

觉得对您有帮助,不妨打开支付宝扫码鼓励一下,谢谢!
这里写图片描述

【工匠若水 blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我