写点什么

Android 应用层开发 Drawable 的一些叨叨絮,跨平台移动开发答案

发布于: 2 小时前
  • 设置滤镜效果,类似 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 @Null


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


able ConstantState getConstantState() {return null;}


......


}


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


【工匠若水 http://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) {


/*



    */


    ......


    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 的一个特殊实现子类而已。


    【工匠若水 http://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 的一些叨叨絮,跨平台移动开发答案