上一篇文章写了 Android Drawable 基础常识 , 今天写写对于 android.graphics.drawable 的源码分析以及在开发中可以用上的例子。
一、自定义Drawable VS 自定义View
在实际项目中,我们在会有一些自定义控件,常常是写自定义View来实现,其实使用自定义的Drawable也可以实现大部分的效果,那么现在谈谈自定义Drawable VS 自定义View 。
Paste_Image.png自定义Drawable VS 自定义View的好处:轻量级,更容易实现,提高UI性能优化
先简单看代码
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();
}
}
网友评论