@linux1s1s
2017-01-22T08:25:06.000000Z
字数 6831
阅读 5314
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;@Overridepublic 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 {...//默认实现,重新绘制该视图本身@Overridepublic 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() 方法
@Overridepublic void invalidateChild(View child, Rect dirty) {invalidateChildInParent(null, dirty);}@Overridepublic ViewParent invalidateChildInParent(int[] location, Rect dirty) {if (!mWillDrawSoon && (intersected || mIsAnimating)) {scheduleTraversals();}}
下面的流程都可以在 Android View 分析初步(中) 对于 ViewRootImpl.requestLayout(...) 这部分说明查看到。需要说明的是在 Android View 分析初步(下) 中的View 三部曲,其中 draw环节中重绘背景部分就是这里的更换背景的最后环节。
这里几乎把Drawable更换背景的原理说明清楚了,如果不清楚请自行读读源代码。
