@linux1s1s
2017-01-22T16:25:06.000000Z
字数 6831
阅读 4928
AndroidDrawable
2015-05
我们知道,一般在设置用户点击效果的时候,会对这个View设置drawable,在布局文件中引用这个xml文件或者在代码中setBackgroundDrawable的时候使用此xml就可以实现控件按下或有焦点等不同状态的效果。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ad_close_clicked" android:state_pressed="true"/>
<item android:drawable="@drawable/ad_close_clicked" android:state_focused="true"/>
<item android:drawable="@drawable/ad_close"/>
</selector>
你知道Android是怎么实现这个功能的吗?这篇博客我们就来回答这个问题。
在点击某个View的时候会触发 onTouchEvent
事件,这个毫无疑问,接下来我们就从这个方法入手
public boolean onTouchEvent(MotionEvent event) {
...
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
...
if (isInScrollingContainer) {
...
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
setPressed(true, x, y);
}
break;
...
}
...
}
上面代码片段重点列出了 ACTION_DOWN
我们来分别看看if判断的两个分支,首先看看 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
继续跟踪这个runnable
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout());
}
}
最终执行了第8行 setPressed(true, x, y)
。
接下来分析另外一个if分支,直接执行setPressed(true, x, y)
,所以无论怎样,都会执行这个setPressed方法,接下来分析这个方法好了,还是在 View
这个类里
private void setPressed(boolean pressed, float x, float y) {
if (pressed) {
drawableHotspotChanged(x, y);
}
setPressed(pressed);
}
接着继续跟踪下去
public void setPressed(boolean pressed) {
final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
if (needsRefresh) {
refreshDrawableState();
}
dispatchSetPressed(pressed);
}
上面代码片段,重点看第10行,needsRefresh
这个条件是成立的,pressed
传进来的值为true,并且mPrivateFlags
是 PFLAG_PRESSED
所以会调用 refreshDrawableState()
。所以由上面的分析可以推出:当View状态标志位发生变化时会调用refreshDrawableList()方法去更新对应的背景Drawable对象。
//路径:\frameworks\base\core\java\android\view\View.java
/* Call this to force a view to update its drawable state. This will cause
* drawableStateChanged to be called on this view. Views that are interested
* in the new state should call getDrawableState.
*/
//主要功能是根据当前的状态值去更换对应的背景Drawable对象
public void refreshDrawableState() {
mPrivateFlags |= DRAWABLE_STATE_DIRTY;
//所有功能在这个函数里去完成
drawableStateChanged();
...
}
/* This function is called whenever the state of the view changes in such
* a way that it impacts the state of drawables being shown.
*/
// 获得当前的状态属性--- 整型集合 ; 调用Drawable类的setState方法去获取资源。
protected void drawableStateChanged() {
//该视图对应的Drawable对象,通常对应于StateListDrawable类对象
Drawable d = mBGDrawable;
if (d != null && d.isStateful()) { //通常都是成立的
//getDrawableState()方法主要功能:会根据当前View的状态属性值,将其转换为一个整型集合
//setState()方法主要功能:根据当前的获取到的状态,更新对应状态下的Drawable对象。
d.setState(getDrawableState());
}
}
/*Return an array of resource IDs of the drawable states representing the
* current state of the view.
*/
public final int[] getDrawableState() {
if ((mDrawableState != null) && ((mPrivateFlags & DRAWABLE_STATE_DIRTY) == 0)) {
return mDrawableState;
} else {
//根据当前View的状态属性值,将其转换为一个整型集合,并返回
mDrawableState = onCreateDrawableState(0);
mPrivateFlags &= ~DRAWABLE_STATE_DIRTY;
return mDrawableState;
}
}
通过这段代码我们可以明白View内部是如何获取更新后的状态值以及动态获取对应的背景Drawable对象,主要通过 setState()
方法去完成的。这里从 setState()
方法开始梳理一下流程:
android.graphics.drawable.Drawable.java
// 如果状态值发生了改变,就回调onStateChange方法
public boolean setState(final int[] stateSet) {
if (!Arrays.equals(mStateSet, stateSet)) {
mStateSet = stateSet;
return onStateChange(stateSet);
}
return false;
}
在Drawable类中onStateChange()
方法
protected boolean onStateChange(int[] state) {
return false;
}
这个方法在很多子类中都经过了重写,这里讨论的是Drawable话题,所以我们只看一下和这个话题相关的StateListDrawable类的重写方法
//状态值发生了改变,我们需要找出第一个吻合的当前状态的Drawable对象
protected boolean onStateChange(int[] stateSet) {
//要找出第一个吻合的当前状态的Drawable对象所在的索引位置, 具体匹配算法请自己深入源码看看
int idx = mStateListState.indexOfStateSet(stateSet);
...
//获取对应索引位置的Drawable对象
if (selectDrawable(idx)) {
return true;
}
...
}
该函数的主要功能: 根据新的状态值,从StateListDrawable实例对象中,找到第一个完全吻合该新状态值的索引下标处 ;继而,调用selectDrawable()方法去获取索引下标的当前Drawable对象。接着继续往下看第7行selectDrawable(idx)
方法调用父类:android.graphics.drawable.DrawableContainer,这个方法比较庞大,我们只看部分细节
public boolean selectDrawable(int idx)
{
if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {
//获取对应索引位置的Drawable对象
Drawable d = mDrawableContainerState.mDrawables[idx];
...
mCurrDrawable = d; //mCurrDrawable即使当前Drawable对象
mCurIndex = idx;
...
} else {
...
}
//请求该View刷新自己,这个方法我们稍后讲解。
invalidateSelf();
return true;
}
该函数的主要功能是选择当前索引下标处的Drawable对象,并保存在mCurrDrawable中.接着看第14行invalidateSelf()
方法
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
进入回调,那么看一下这个若引用回调在哪里写入的。
public final void setCallback(Callback cb) {
mCallback = new WeakReference<Callback>(cb);
}
对外提供了一个set接口,先不管是谁会写入这个接口,我们先搞清楚这个接口的内部情况
public static interface Callback {
/**
* Called when the drawable needs to be redrawn. A view at this point
* should invalidate itself (or at least the part of itself where the
* drawable appears).
*
* @param who The drawable that is requesting the update.
*/
public void invalidateDrawable(Drawable who);
/**
* A Drawable can call this to schedule the next frame of its
* animation. An implementation can generally simply call
* {@link android.os.Handler#postAtTime(Runnable, Object, long)} with
* the parameters <var>(what, who, when)</var> to perform the
* scheduling.
*
* @param who The drawable being scheduled.
* @param what The action to execute.
* @param when The time (in milliseconds) to run. The timebase is
* {@link android.os.SystemClock#uptimeMillis}
*/
public void scheduleDrawable(Drawable who, Runnable what, long when);
/**
* A Drawable can call this to unschedule an action previously
* scheduled with {@link #scheduleDrawable}. An implementation can
* generally simply call
* {@link android.os.Handler#removeCallbacks(Runnable, Object)} with
* the parameters <var>(what, who)</var> to unschedule the drawable.
*
* @param who The drawable being unscheduled.
* @param what The action being unscheduled.
*/
public void unscheduleDrawable(Drawable who, Runnable what);
}
只有四个方法,其中比较常用的是invalidateDrawable
方法,而当我们回过头来看看View这个顶层类会发现其实View自身实现了这个接口。
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
...
//默认实现,重新绘制该视图本身
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
if (verifyDrawable(drawable)) {
final Rect dirty = drawable.getDirtyBounds();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
//重新请求绘制该View,即重新调用该View的draw()方法
invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
mPrivateFlags3 |= PFLAG3_OUTLINE_INVALID;
}
}
}
我们接着invalidate()
方法继续分析下去
public void invalidate(int l, int t, int r, int b) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
}
然后调用ViewRootImpl类的 invalidateChild()
方法
@Override
public void invalidateChild(View child, Rect dirty) {
invalidateChildInParent(null, dirty);
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
if (!mWillDrawSoon && (intersected || mIsAnimating)) {
scheduleTraversals();
}
}
下面的流程都可以在 Android View 分析初步(中) 对于 ViewRootImpl.requestLayout(...)
这部分说明查看到。需要说明的是在 Android View 分析初步(下) 中的View 三部曲,其中 draw
环节中重绘背景部分就是这里的更换背景的最后环节。
这里几乎把Drawable更换背景的原理说明清楚了,如果不清楚请自行读读源代码。