/**
- 回调接口类,若要实现自定义动画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 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 的方法是十分有限的,所以不要被那么多方法蒙蔽了双眼。
【工匠若水 http://blog.youkuaiyun.com/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 绘制来用的而已,所以通过上面两大节源码分析,我想你至少脑袋中已经产生了下面这个抽象的不能再抽象的对比图,然后也能恍然大悟吧。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
为了方便有学习需要的朋友,我把资料都整理成了视频教程(实际上比预期多花了不少精力)
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
- 无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!
- 我希望每一个努力生活的IT工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,没有人能随随便便成功。
加油,共勉。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
[外链图片转存中…(img-4D1Qzknp-1713756443305)]
最后
为了方便有学习需要的朋友,我把资料都整理成了视频教程(实际上比预期多花了不少精力)
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
- 无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!
- 我希望每一个努力生活的IT工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,没有人能随随便便成功。
加油,共勉。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!