[关闭]
@linux1s1s 2017-01-22T16:25:06.000000Z 字数 6831 阅读 4936

Android Drawable 分析

AndroidDrawable 2015-05


我们知道,一般在设置用户点击效果的时候,会对这个View设置drawable,在布局文件中引用这个xml文件或者在代码中setBackgroundDrawable的时候使用此xml就可以实现控件按下或有焦点等不同状态的效果。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <selector xmlns:android="http://schemas.android.com/apk/res/android">
  3. <item android:drawable="@drawable/ad_close_clicked" android:state_pressed="true"/>
  4. <item android:drawable="@drawable/ad_close_clicked" android:state_focused="true"/>
  5. <item android:drawable="@drawable/ad_close"/>
  6. </selector>

你知道Android是怎么实现这个功能的吗?这篇博客我们就来回答这个问题。

View.onTouchEvent

在点击某个View的时候会触发 onTouchEvent 事件,这个毫无疑问,接下来我们就从这个方法入手

  1. public boolean onTouchEvent(MotionEvent event) {
  2. ...
  3. switch (event.getAction()) {
  4. case MotionEvent.ACTION_DOWN:
  5. ...
  6. if (isInScrollingContainer) {
  7. ...
  8. postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
  9. } else {
  10. setPressed(true, x, y);
  11. }
  12. break;
  13. ...
  14. }
  15. ...
  16. }

上面代码片段重点列出了 ACTION_DOWN 我们来分别看看if判断的两个分支,首先看看 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 继续跟踪这个runnable

  1. private final class CheckForTap implements Runnable {
  2. public float x;
  3. public float y;
  4. @Override
  5. public void run() {
  6. mPrivateFlags &= ~PFLAG_PREPRESSED;
  7. setPressed(true, x, y);
  8. checkForLongClick(ViewConfiguration.getTapTimeout());
  9. }
  10. }

最终执行了第8行 setPressed(true, x, y)
接下来分析另外一个if分支,直接执行setPressed(true, x, y),所以无论怎样,都会执行这个setPressed方法,接下来分析这个方法好了,还是在 View 这个类里

View.setPressed()

  1. private void setPressed(boolean pressed, float x, float y) {
  2. if (pressed) {
  3. drawableHotspotChanged(x, y);
  4. }
  5. setPressed(pressed);
  6. }

接着继续跟踪下去

  1. public void setPressed(boolean pressed) {
  2. final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
  3. if (pressed) {
  4. mPrivateFlags |= PFLAG_PRESSED;
  5. } else {
  6. mPrivateFlags &= ~PFLAG_PRESSED;
  7. }
  8. if (needsRefresh) {
  9. refreshDrawableState();
  10. }
  11. dispatchSetPressed(pressed);
  12. }

上面代码片段,重点看第10行,needsRefresh 这个条件是成立的,pressed传进来的值为true,并且mPrivateFlagsPFLAG_PRESSED 所以会调用 refreshDrawableState() 。所以由上面的分析可以推出:当View状态标志位发生变化时会调用refreshDrawableList()方法去更新对应的背景Drawable对象。

  1. //路径:\frameworks\base\core\java\android\view\View.java
  2. /* Call this to force a view to update its drawable state. This will cause
  3. * drawableStateChanged to be called on this view. Views that are interested
  4. * in the new state should call getDrawableState.
  5. */
  6. //主要功能是根据当前的状态值去更换对应的背景Drawable对象
  7. public void refreshDrawableState() {
  8. mPrivateFlags |= DRAWABLE_STATE_DIRTY;
  9. //所有功能在这个函数里去完成
  10. drawableStateChanged();
  11. ...
  12. }
  13. /* This function is called whenever the state of the view changes in such
  14. * a way that it impacts the state of drawables being shown.
  15. */
  16. // 获得当前的状态属性--- 整型集合 ; 调用Drawable类的setState方法去获取资源。
  17. protected void drawableStateChanged() {
  18. //该视图对应的Drawable对象,通常对应于StateListDrawable类对象
  19. Drawable d = mBGDrawable;
  20. if (d != null && d.isStateful()) { //通常都是成立的
  21. //getDrawableState()方法主要功能:会根据当前View的状态属性值,将其转换为一个整型集合
  22. //setState()方法主要功能:根据当前的获取到的状态,更新对应状态下的Drawable对象。
  23. d.setState(getDrawableState());
  24. }
  25. }
  26. /*Return an array of resource IDs of the drawable states representing the
  27. * current state of the view.
  28. */
  29. public final int[] getDrawableState() {
  30. if ((mDrawableState != null) && ((mPrivateFlags & DRAWABLE_STATE_DIRTY) == 0)) {
  31. return mDrawableState;
  32. } else {
  33. //根据当前View的状态属性值,将其转换为一个整型集合,并返回
  34. mDrawableState = onCreateDrawableState(0);
  35. mPrivateFlags &= ~DRAWABLE_STATE_DIRTY;
  36. return mDrawableState;
  37. }
  38. }

通过这段代码我们可以明白View内部是如何获取更新后的状态值以及动态获取对应的背景Drawable对象,主要通过 setState() 方法去完成的。这里从 setState() 方法开始梳理一下流程:
android.graphics.drawable.Drawable.java

  1. // 如果状态值发生了改变,就回调onStateChange方法
  2. public boolean setState(final int[] stateSet) {
  3. if (!Arrays.equals(mStateSet, stateSet)) {
  4. mStateSet = stateSet;
  5. return onStateChange(stateSet);
  6. }
  7. return false;
  8. }

在Drawable类中onStateChange()方法

  1. protected boolean onStateChange(int[] state) {
  2. return false;
  3. }

这个方法在很多子类中都经过了重写,这里讨论的是Drawable话题,所以我们只看一下和这个话题相关的StateListDrawable类的重写方法

  1. //状态值发生了改变,我们需要找出第一个吻合的当前状态的Drawable对象
  2. protected boolean onStateChange(int[] stateSet) {
  3. //要找出第一个吻合的当前状态的Drawable对象所在的索引位置, 具体匹配算法请自己深入源码看看
  4. int idx = mStateListState.indexOfStateSet(stateSet);
  5. ...
  6. //获取对应索引位置的Drawable对象
  7. if (selectDrawable(idx)) {
  8. return true;
  9. }
  10. ...
  11. }

该函数的主要功能: 根据新的状态值,从StateListDrawable实例对象中,找到第一个完全吻合该新状态值的索引下标处 ;继而,调用selectDrawable()方法去获取索引下标的当前Drawable对象。接着继续往下看第7行selectDrawable(idx)方法调用父类:android.graphics.drawable.DrawableContainer,这个方法比较庞大,我们只看部分细节

  1. public boolean selectDrawable(int idx)
  2. {
  3. if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {
  4. //获取对应索引位置的Drawable对象
  5. Drawable d = mDrawableContainerState.mDrawables[idx];
  6. ...
  7. mCurrDrawable = d; //mCurrDrawable即使当前Drawable对象
  8. mCurIndex = idx;
  9. ...
  10. } else {
  11. ...
  12. }
  13. //请求该View刷新自己,这个方法我们稍后讲解。
  14. invalidateSelf();
  15. return true;
  16. }

该函数的主要功能是选择当前索引下标处的Drawable对象,并保存在mCurrDrawable中.接着看第14行invalidateSelf()方法

Drawable.invalidateSelf()

  1. public void invalidateSelf() {
  2. final Callback callback = getCallback();
  3. if (callback != null) {
  4. callback.invalidateDrawable(this);
  5. }
  6. }

进入回调,那么看一下这个若引用回调在哪里写入的。

  1. public final void setCallback(Callback cb) {
  2. mCallback = new WeakReference<Callback>(cb);
  3. }

对外提供了一个set接口,先不管是谁会写入这个接口,我们先搞清楚这个接口的内部情况

  1. public static interface Callback {
  2. /**
  3. * Called when the drawable needs to be redrawn. A view at this point
  4. * should invalidate itself (or at least the part of itself where the
  5. * drawable appears).
  6. *
  7. * @param who The drawable that is requesting the update.
  8. */
  9. public void invalidateDrawable(Drawable who);
  10. /**
  11. * A Drawable can call this to schedule the next frame of its
  12. * animation. An implementation can generally simply call
  13. * {@link android.os.Handler#postAtTime(Runnable, Object, long)} with
  14. * the parameters <var>(what, who, when)</var> to perform the
  15. * scheduling.
  16. *
  17. * @param who The drawable being scheduled.
  18. * @param what The action to execute.
  19. * @param when The time (in milliseconds) to run. The timebase is
  20. * {@link android.os.SystemClock#uptimeMillis}
  21. */
  22. public void scheduleDrawable(Drawable who, Runnable what, long when);
  23. /**
  24. * A Drawable can call this to unschedule an action previously
  25. * scheduled with {@link #scheduleDrawable}. An implementation can
  26. * generally simply call
  27. * {@link android.os.Handler#removeCallbacks(Runnable, Object)} with
  28. * the parameters <var>(what, who)</var> to unschedule the drawable.
  29. *
  30. * @param who The drawable being unscheduled.
  31. * @param what The action being unscheduled.
  32. */
  33. public void unscheduleDrawable(Drawable who, Runnable what);
  34. }

只有四个方法,其中比较常用的是invalidateDrawable方法,而当我们回过头来看看View这个顶层类会发现其实View自身实现了这个接口。

  1. public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
  2. ...
  3. //默认实现,重新绘制该视图本身
  4. @Override
  5. public void invalidateDrawable(@NonNull Drawable drawable) {
  6. if (verifyDrawable(drawable)) {
  7. final Rect dirty = drawable.getDirtyBounds();
  8. final int scrollX = mScrollX;
  9. final int scrollY = mScrollY;
  10. //重新请求绘制该View,即重新调用该View的draw()方法
  11. invalidate(dirty.left + scrollX, dirty.top + scrollY,
  12. dirty.right + scrollX, dirty.bottom + scrollY);
  13. mPrivateFlags3 |= PFLAG3_OUTLINE_INVALID;
  14. }
  15. }
  16. }

我们接着invalidate()方法继续分析下去

  1. public void invalidate(int l, int t, int r, int b) {
  2. final int scrollX = mScrollX;
  3. final int scrollY = mScrollY;
  4. invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
  5. }

然后调用ViewRootImpl类的 invalidateChild() 方法

  1. @Override
  2. public void invalidateChild(View child, Rect dirty) {
  3. invalidateChildInParent(null, dirty);
  4. }
  5. @Override
  6. public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
  7. if (!mWillDrawSoon && (intersected || mIsAnimating)) {
  8. scheduleTraversals();
  9. }
  10. }

下面的流程都可以在 Android View 分析初步(中) 对于 ViewRootImpl.requestLayout(...) 这部分说明查看到。需要说明的是在 Android View 分析初步(下) 中的View 三部曲,其中 draw环节中重绘背景部分就是这里的更换背景的最后环节。
这里几乎把Drawable更换背景的原理说明清楚了,如果不清楚请自行读读源代码。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注