android中drawable顯示到view上的過程

前段時間一直整理java方面的知識了,先過渡一段時間到android上面來,後期仍是會整理java相關的東西,至於整理什麼方面的,還沒想好。好了,先不說廢話了,仍是回到正片上來,說說android中用得比較多的drawable類,drawable類是一個抽象的類,其實咱們日常開發的階段用的就是它的各類子類,好比有ColorDrawableBitmapDrawable等等,後面全部相關的Drawable都會講到。相信你們用Bitmap也是用得比較多的,那他兩有啥區別呢。java

Bitmap是專門存儲圖片的一種形式,是對位圖的每個像素的顏色存儲器,而咱們的顏色值是由ARGB來標示的,所以咱們經常用16進制的6位數表示一個顏色值,而顏色模式通常有下面幾種:android

顏色模式 說明
ARGB8888 四通道高精度(32位)
ARGB4444 四通道低精度(24位)
RGB565 三通道(16位)
Alpha8 透明通道(8位)

因此對於Bitmap的使用是須要指明Bitmap使用的顏色通道模式,通常若是沒有特殊要求最好是選擇三通道的就行。好了,關於Bitmap的說明就這麼多,仍是言歸正傳,說說drawable是怎麼一步步顯示在view上的,下面仍是經過一個簡單的例子,說明drawable的用法:canvas

定義了一個drawable文件:test_back.xml數組

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/colorPrimary" android:state_pressed="true" />
    <item android:drawable="@color/colorPrimary" android:state_selected="true" />
    <item android:drawable="@color/colorAccent" />
</selector>
複製代碼

接着在activity佈局中使用:緩存

<View
    android:id="@+id/view"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:layout_marginTop="50dp"
    android:background="@drawable/test_back" />
複製代碼

Drawable的生成

相信你們這個代碼很是熟悉了,還有個疑問就是爲何在view設置了setOnClickListener纔會有view按下的效果呢,因此帶着這些疑問之前看下這些問題,你們都知道view的全部屬性是經過定義在attrs文件中的,而view的attrs的name是View:bash

image.png

緊接着第一個屬性就是獲取background屬性: app

image.png
那我們能夠看下drawable是怎麼獲取的呢,最終會到TypedArray的getDrawableForDensity方法:

@Nullable
public Drawable getDrawableForDensity(@StyleableRes int index, int density) {   
	//省略了代碼
    return mResources.loadDrawable(value, value.resourceId, density, mTheme);
}
複製代碼

能夠看到上面調的是Resource類中的loadDrawable方法:ide

@NonNull
Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
        throws NotFoundException {
    return mResourcesImpl.loadDrawable(this, value, id, density, theme);
}
複製代碼

很簡單一句話,直接調了ResourcesImpl的loadDrawable方法:佈局

@Nullable
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
        int density, @Nullable Resources.Theme theme)
        throws NotFoundException {
    //省略代碼

    try {

        final boolean isColorDrawable;
        //省略代碼
        //若是傳過來的是#開頭的屬性值,直接返回colorDrawable
        Drawable dr;
        if (isColorDrawable) {
            dr = new ColorDrawable(value.data);
        } else {
        	//若是不是則調用該方法
            dr = loadDrawableForCookie(wrapper, value, id, density);
        }
        return dr;
    } catch (Exception e) {
        
    }
}
複製代碼

上面代碼已經到了最精簡的代碼了,前面一大堆的工做判斷有沒有緩存的drawable,若是有直接返回cachedDrawable,若是沒有接着判斷是否是以#開頭的clor顏色值,若是是直接返回colorDrawable,先不說colorDrawable,後面會講到,該節只是分析drawable的顯示流程。那我們看下loadDrawableForCookie:ui

private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
        int id, int density) {
    final String file = value.string.toString();

    final Drawable dr;
    try {
        try {
            //注意了若是drawable是一個xml文件定義的走這裏
            if (file.endsWith(".xml")) {
                final XmlResourceParser rp = loadXmlResourceParser(
                        file, id, value.assetCookie, "drawable");
                dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
                rp.close();
            } else {
                //不然從asset輸入流讀取
                final InputStream is = mAssets.openNonAsset(
                        value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                AssetInputStream ais = (AssetInputStream) is;
                dr = decodeImageDrawable(ais, wrapper, value);
            }
        } finally {
            stack.pop();
        }
    } catch (Exception | StackOverflowError e) {
    
    }
    return dr;
}
複製代碼

從上面能夠看到,咱們通常寫的xml都是用Drawable.createFromXmlForDensity方法來獲取的,接着看下該方法:

@NonNull
public static Drawable createFromXmlForDensity(@NonNull Resources r,
        @NonNull XmlPullParser parser, int density, @Nullable Theme theme)
        throws XmlPullParserException, IOException {
    AttributeSet attrs = Xml.asAttributeSet(parser);
    //省略了判斷
    Drawable drawable = createFromXmlInnerForDensity(r, parser, attrs, density, theme);
    return drawable;
}
複製代碼

該方法很簡單,直接是調用了createFromXmlInnerForDensity方法:

@NonNull
static Drawable createFromXmlInnerForDensity(@NonNull Resources r,
        @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density,
        @Nullable Theme theme) throws XmlPullParserException, IOException {
    return r.getDrawableInflater().inflateFromXmlForDensity(parser.getName(), parser, attire
            density, theme);
}
複製代碼

能夠看到經過resources.getDrawableInflater().inflateFromXmlForDensity方法返回的drawable對象,能夠直接看下resources.getDrawableInflater()返回的對象:

image.png
很一目瞭然,獲取的是DrawableInfalter對象,還記得LayoutInflater對象吧,它是用來加載佈局的,能夠看出來各類xml都是經過各類****Inlfater加載出來的,直接看DrawableInfalter的inflateFromXmlForDensity方法:

@NonNull
Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
        @NonNull AttributeSet attrs, int density, @Nullable Theme theme)
        throws XmlPullParserException, IOException {
    //若是xml中根標籤是drawable,那麼直接解析它的class屬性,通常若是是自定義drawable能夠這麼玩
    if (name.equals("drawable")) {
        name = attrs.getAttributeValue(null, "class");
        if (name == null) {
            throw new InflateException("<drawable> tag must specify class attribute");
        }
    }
    //此處是關鍵
    Drawable drawable = inflateFromTag(name);
    if (drawable == null) {
        drawable = inflateFromClass(name);
    }
    drawable.setSrcDensityOverride(density);
    //該處也很重要,後面講各類drawable的時候會講到該方法
    drawable.inflate(mRes, parser, attrs, theme);
    return drawable;
}
複製代碼

上面代碼邏輯很清晰,若是獲取到的標籤是drawable,經過class屬性獲取到drawable對象,若是標籤不是drawable經過inflateFromTag方法獲取:

private Drawable inflateFromTag(@NonNull String name) {
    switch (name) {
        case "selector":
            return new StateListDrawable();
        case "animated-selector":
            return new AnimatedStateListDrawable();
        case "level-list":
            return new LevelListDrawable();
        case "layer-list":
            return new LayerDrawable();
        case "transition":
            return new TransitionDrawable();
        case "ripple":
            return new RippleDrawable();
        case "adaptive-icon":
            return new AdaptiveIconDrawable();
        case "color":
            return new ColorDrawable();
        case "shape":
            return new GradientDrawable();
        case "vector":
            return new VectorDrawable();
        case "animated-vector":
            return new AnimatedVectorDrawable();
        case "scale":
            return new ScaleDrawable();
        case "clip":
            return new ClipDrawable();
        case "rotate":
            return new RotateDrawable();
        case "animated-rotate":
            return new AnimatedRotateDrawable();
        case "animation-list":
            return new AnimationDrawable();
        case "inset":
            return new InsetDrawable();
        case "bitmap":
            return new BitmapDrawable();
        case "nine-patch":
            return new NinePatchDrawable();
        case "animated-image":
            return new AnimatedImageDrawable();
        default:
            return null;
    }
}
複製代碼

我去,這也太明顯了吧,整個drawable的子類都放出來了,像不像工廠方法呢,是的,沒錯,DrawableInflater類就是Drawable的工廠類,經過標籤的name,返回不一樣的drawable。若是標籤的名字是drawable的話,會調用inflateFromClass方法來生成drawable的:

@NonNull
private Drawable inflateFromClass(@NonNull String className) {
    try {
        Constructor<? extends Drawable> constructor;
        synchronized (CONSTRUCTOR_MAP) {
            constructor = CONSTRUCTOR_MAP.get(className);
            if (constructor == null) {
                final Class<? extends Drawable> clazz =
                        mClassLoader.loadClass(className).asSubclass(Drawable.class);
                constructor = clazz.getConstructor();
                CONSTRUCTOR_MAP.put(className, constructor);
            }
        }
        return constructor.newInstance();
    } catch (NoSuchMethodException e) {
        final InflateException ie = new InflateException(
                "Error inflating class " + className);
        ie.initCause(e);
        throw ie;
    } catch (ClassCastException e) {
        // If loaded class is not a Drawable subclass.
        final InflateException ie = new InflateException(
                "Class is not a Drawable " + className);
        ie.initCause(e);
        throw ie;
    } catch (ClassNotFoundException e) {
        // If loadClass fails, we should propagate the exception.
        final InflateException ie = new InflateException(
                "Class not found " + className);
        ie.initCause(e);
        throw ie;
    } catch (Exception e) {
        final InflateException ie = new InflateException(
                "Error inflating class " + className);
        ie.initCause(e);
        throw ie;
    }
}
複製代碼

這裏就不作過多的解釋了,經過反射生成Drawable對象的,關於反射你們能夠看我前面寫的java反射整理 緊接着調用了drawable.inflate方法。該方法對於後面分析各類Drawable有很大的幫助。

小節

xml中定義的drawable是經過DrawableInflater生成不一樣的drawable,若是標籤直接定義drawable,去解析class屬性,class屬性是drawable的全類名;不然解析相應的標籤生成不一樣的drawable,好比ColorDrawableStateListDrawableGradientDrawable

經過上面的分析,例子中的Drawable實際是一個StateListDrawable,下面一塊兒來看看他是如何顯示到view上的

Drawable與view的關係

上面已經講了drawable是如何經過xml生成drawable,下面要將的是Drawable是怎麼做用到view上,在前面說到view中經過TypeArray.getDrawable獲取到Background是一個StateListDrawable,後面在view四個參數的構造方法中設置了background:

if (background != null) {
    setBackground(background);
}
複製代碼

接着調用了下面該方法:

public void setBackground(Drawable background) {
    //noinspection deprecation
    setBackgroundDrawable(background);
}
複製代碼
@Deprecated
public void setBackgroundDrawable(Drawable background) {
    computeOpaqueFlags();

    if (background == mBackground) {
        return;
    }

    boolean requestLayout = false;

    mBackgroundResource = 0;

    //1.銷燬以前用到的drawable
    if (mBackground != null) {
        if (isAttachedToWindow()) {
            mBackground.setVisible(false, false);
        }
        mBackground.setCallback(null);
        unscheduleDrawable(mBackground);
    }

    if (background != null) {
       	//2.須要刷新view的位置時候
        if (mBackground == null
                || mBackground.getMinimumHeight() != background.getMinimumHeight()
                || mBackground.getMinimumWidth() != background.getMinimumWidth()) {
            requestLayout = true;
        }

        mBackground = background;
        if (background.isStateful()) {
        	//3.getDrawableState主要是獲取到drawable狀態的全集
            background.setState(getDrawableState());
        }
        if (isAttachedToWindow()) {
            background.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
        }
        //添加着色的代碼
        applyBackgroundTint();

        // 對當前的drawable設置回調
        background.setCallback(this);
    } else {
        //若是當前background爲空,須要從新更新view在頁面上的位置
        mBackground = null;
        requestLayout = true;
    }

    computeOpaqueFlags();

    if (requestLayout) {
        requestLayout();
    }

    mBackgroundSizeChanged = true;
    //從新走繪製
    invalidate(true);
    invalidateOutline();
}
複製代碼

能夠看到上面先是對以前的background進行銷燬,調用了drawable.setCallback(null),能夠看到將當前view實例傳給了drawable對象:

public final void setCallback(@Nullable Callback cb) {
    mCallback = cb != null ? new WeakReference<>(cb) : null;
}
複製代碼

此處看到了沒,將view實例經過弱引用包裝起來了,防止drawable長時間不釋放view實例,因此在不用drawable的時候務必調用下setCallback(null)防止內存泄漏

緊接着註釋三處經過drawable.isStateful來判斷須要給drawale加各類狀態不,在drawable默認中實現了isStateful方法:

/**
  * 該drawable是否根據state來更改樣式
  *
  */
 public boolean isStateful() {
     return false;
 }
複製代碼

咋們看下ColorDrawable和StateListDrawable下是怎麼實現該方法的:

//stateLIstDrawable直接返回true,說明它是根據狀態改變樣式的drawable
@Override
public boolean isStateful() {
    return true;
}
複製代碼
//colorDrawable會根據mColorState.mTint.isStateful()來判斷是否是根據狀態來變樣式
//mColorState.mTint是colorStateLIst對象
@Override
public boolean isStateful() {
    return mColorState.mTint != null && mColorState.mTint.isStateful();
}
複製代碼
@Override
public boolean isStateful() {
    return mStateSpecs.length >= 1 && mStateSpecs[0].length > 0;
}
複製代碼

判斷mStateSpecs長度大於1,而且第一個length大於0

public ColorStateList(int[][] states, @ColorInt int[] colors) {
    mStateSpecs = states;
    mColors = colors;
    onColorsChanged();
}
複製代碼

mStateSpecs數組是根據states二維數組傳過來的,這裏寫一個簡單的例子來看看isStateful方法的說明:

int pressed = Color.RED;
int focused = Color.RED;
int normal = Color.BLACK;
int unable = Color.GRAY;
//顏色數組值要和狀態值對應上
int[] colors = new int[]{pressed, focused, normal, focused, unable, normal}
int[][] states = new int[6][];
//定義6個狀態的數組
states[0] = new int[]{android.R.attr.state_pressed, android.R.attr.state_en
states[1] = new int[]{android.R.attr.state_enabled, android.R.attr.state_fo
states[2] = new int[]{android.R.attr.state_enabled};
states[3] = new int[]{android.R.attr.state_focused};
states[4] = new int[]{android.R.attr.state_window_focused};
states[5] = new int[]{};
ColorStateList colorStateList = new ColorStateList(states, colors);
ColorDrawable colorDrawable = new ColorDrawable();
//能夠經過該方法設置colorStateList,先只須要知道用就行,後面會講到
colorDrawable.setTintMode(PorterDuff.Mode.ADD);
colorDrawable.setTintList(colorStateList);
View test = findViewById(R.id.test);
test.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
});
test.setBackground(colorDrawable);
boolean stateful = colorDrawable.isStateful();
Log.d(TAG, "stateful:" + stateful);
複製代碼

此時獲取到的stateful是true,再來看不加狀態值的時候,代碼改爲以下:

ColorDrawable colorDrawable = new ColorDrawable(Color.BLACK);
View test = findViewById(R.id.test);
test.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
});
test.setBackground(colorDrawable);
boolean stateful = colorDrawable.isStateful();
Log.d(TAG, "stateful:" + stateful);
複製代碼

看到上面獲取的stateful爲false,由於此時咱們沒有傳state的數組,因此驗證了上面的代碼。關於isStateful方法說明到這裏,下面繼續回到 setBackgroundDrawable方法的background.setState(getDrawableState())這一行,你們能夠看下drawable.setState方法:

public boolean setState(@NonNull final int[] stateSet) {
    if (!Arrays.equals(mStateSet, stateSet)) {
        mStateSet = stateSet;
        return onStateChange(stateSet);
    }
    return false;
}
複製代碼

該方法表示當前drawable是什麼狀態的,若是狀態不同,則會觸發到onStateChange方法,drawable默認的onStateChange是一個空的實現,所以須要子類本身實現,你們這會只須要知道這麼個流程,後面會仔細介紹drawable的子類時候再講該方法,繼續看view的getDrawableState方法:

public final int[] getDrawableState() {
      //若是已經獲取過mDrawableState的狀態值,而且mPrivateFlags等於標誌
    if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
        return mDrawableState;
    } else {
        mDrawableState = onCreateDrawableState(0);
        mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
        return mDrawableState;
    }
}
複製代碼

剛開始mDrawableState變量爲空,那麼此時經過onCreateDrawableState方法來獲取mDrawableState

protected int[] onCreateDrawableState(int extraSpace) {
    int[] drawableState;
    int privateFlags = mPrivateFlags;
    int viewStateIndex = 0;
    //下面這些操做都是經過當前view的狀態來給viewStateIndex設置不一樣狀態的值
    if ((privateFlags & PFLAG_PRESSED) != 0) viewStateIndex |= StateSet.VIEW_STATE_PRESSED
    if ((mViewFlags & ENABLED_MASK) == ENABLED) viewStateIndex |= StateSet.VIEW_STATE_ENAB
    if (isFocused()) viewStateIndex |= StateSet.VIEW_STATE_FOCUSED;
    if ((privateFlags & PFLAG_SELECTED) != 0) viewStateIndex |= StateSet.VIEW_STATE_SELECT
    if (hasWindowFocus()) viewStateIndex |= StateSet.VIEW_STATE_WINDOW_FOCUSED;
    if ((privateFlags & PFLAG_ACTIVATED) != 0) viewStateIndex |= StateSet.VIEW_STATE_ACTIV
    if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested &&
            ThreadedRenderer.isAvailable()) {
        viewStateIndex |= StateSet.VIEW_STATE_ACCELERATED;
    }
    if ((privateFlags & PFLAG_HOVERED) != 0) viewStateIndex |= StateSet.VIEW_STATE_HOVERED
    final int privateFlags2 = mPrivateFlags2;
    if ((privateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0) {
        viewStateIndex |= StateSet.VIEW_STATE_DRAG_CAN_ACCEPT;
    }
    if ((privateFlags2 & PFLAG2_DRAG_HOVERED) != 0) {
        viewStateIndex |= StateSet.VIEW_STATE_DRAG_HOVERED;
    }
    //獲取到上面的不一樣狀態的值後,獲取到當前drawable的狀態,以此來設置drawable在不一樣狀態下的樣式
    drawableState = StateSet.get(viewStateIndex);
   
    if (extraSpace == 0) {
        return drawableState;
    }
    //省略代碼
}
複製代碼

上面的操做是經過全局mPrivateFlags標誌獲取到各類狀態下的索引,而且位運算或放到viewStateIndex中,最後經過StateSet.get(viewStateIndex)賦值給drawableState。關於StateSet.get(viewStateIndex)獲取到的都是R.attr.state_****,關於經過view的狀態設置drawable的狀態就是這麼來的,下面繼續回到view的setBackgroundDrawable方法,接着會調用applyBackgroundTint方法,

private void applyBackgroundTint() {
    if (mBackground != null && mBackgroundTint != null) {
        //若是view設置了backgroundTint屬性,那麼mBackgroundTint就不會爲空
        final TintInfo tintInfo = mBackgroundTint;
        if (tintInfo.mHasTintList || tintInfo.mHasTintMode) {
            mBackground = mBackground.mutate();
            if (tintInfo.mHasTintList) {
                //傳入colorStateList實現效果
                mBackground.setTintList(tintInfo.mTintList);
            }
            if (tintInfo.mHasTintMode) {
                //設置tintMode
                mBackground.setTintMode(tintInfo.mTintMode);
            }
            if (mBackground.isStateful()) {
                mBackground.setState(getDrawableState());
            }
        }
    }
}
複製代碼

看到上面的代碼是否是很熟悉上面事例中setTintListsetTintMode的使用,沒錯上面代碼也能夠經過xml來實現setTintList的效果:

//定義一個drawable文件,實質是一個StateListDrawable,名字叫test.xml:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/holo_red_dark" android:state_enabled="true" android:state_pressed="true" />
    <item android:drawable="@android:color/holo_red_dark" android:state_enabled="true" android:state_focused="true" />
</selector>
複製代碼

在相應的view佈局上使用以下:

<View
    android:id="@+id/test"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@color/black"
    android:backgroundTint="@drawable/test"
    android:backgroundTintMode="add"/>
複製代碼

簡單來講,backgroundTint是往background上着色,至關於網上塗層,後面細講drawable的時候,會說到他的各類子類的狀況是如何控制backgroundTint的。繼續回到view的setBackgroundDrawable方法上來,上面說完了applyBackgroundTint方法,後面緊接着到了background.setCallback(this),此處是控制drawable繪製的回調,將view當前的實例傳給drawable。此時drawable的setState、setTintList、setCallback都已經完成了,緊接着就是繪製了,所以在最後調用了invalidate(true),最終會觸發view的從新繪製了。

view繪製怎麼繪製drawable

你們知道view的繪製方法在draw---->onDraw,draw裏面作的就是系統的一些繪製,而onDraw是view的子類繪製的方法,因此咱們通常寫的繪製都是在onDraw裏面,下面來看看draw裏面繪製background,能夠看到draw裏面有一句drawBackground(canvas):

private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    //設置drawable的大小,這個很重要
    setBackgroundBounds();

    //省略代碼
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    //最終會走drawable.draw方法,所以drawable的繪製,實際上是drawable本身完成的
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}
複製代碼

上面繪製drawable很簡單,首先調用了setBackgroundBounds方法,而後調用了drawable.draw(canvas),最終的繪製仍是交給了drawable本身去繪製。下面看下setBackgroundBounds方法:

void setBackgroundBounds() {
    if (mBackgroundSizeChanged && mBackground != null) {
        //設置drawable的大小,大小其實就是view的大小
        mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
}
複製代碼

能夠看到setBackgroundBounds實際上是調用了mBackground.setBounds方法,在drawable的setBounds方法中只是設置了全局的mBounds變量:

public void setBounds(int left, int top, int right, int bottom) {
    Rect oldBounds = mBounds;

    if (oldBounds == ZERO_BOUNDS_RECT) {
        oldBounds = mBounds = new Rect();
    }

    if (oldBounds.left != left || oldBounds.top != top ||
            oldBounds.right != right || oldBounds.bottom != bottom) {
        if (!oldBounds.isEmpty()) {
            // first invalidate the previous bounds
            invalidateSelf();
        }
        mBounds.set(left, top, right, bottom);
        onBoundsChange(mBounds);
    }
}
複製代碼

因此說drawable中只是設置了個全局變量mBounds,供子類使用,因此從這裏看得出來,若是要使用drawable必須調用drawable的setBounds方法,要否則在繪製drawable的時候顯示不出來。到最後就是background的繪製了,也就是drawable.draw(canvas)方法,能夠看出來drawable是不負責繪製的,繪製工做都交給了本身的子類。

view點擊的時候drawable狀態改變

view的點擊都是在ontouchEvent裏面,咱們主要看關鍵點就行

//手指在擡起的時候,而且mPrivateFlags等於PFLAG_PRESSED標誌
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
    setPressed(false);
}
複製代碼

這裏能夠看出來,若是手擡起來了,而且狀態是PFLAG_PRESSED,調用了setPressed(false),看看該方法:

public void setPressed(boolean pressed) {
    //這句標明在按下的時候needsRefresh=true,由於剛開始mPrivateFlags&PFLAG_PRESSED!=PFLAG_PRESSED
    //所以後面爲false,當pressed=true的時候,那麼needsRefresh=true,反之pressed=false的時候,
    //所以此時mPrivateFlags&PFLAG_PRESSED=PFLAG_PRESSED,所以needsRefresh仍是爲true
    //因此在按下和鬆開view的時候needsRefresh都爲true
    final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
    //若是按下了mPrivateFlags|PFLAG_PRESSED,那此時mPrivateFlags=PFLAG_PRESSED了
    //反之鬆開的時候mPrivateFlags!=PFLAG_PRESSED了
    if (pressed) {
        mPrivateFlags |= PFLAG_PRESSED;
    } else {
        mPrivateFlags &= ~PFLAG_PRESSED;
    }
    //若是needsRefresh爲true,此時須要刷新drawable
    if (needsRefresh) {
        refreshDrawableState();
    }
    dispatchSetPressed(pressed);
}
複製代碼

註釋寫得很清楚了,擡起和按下view都會觸發refreshDrawableState方法,而且在按下的時候mPrivateFlags= PFLAG_PRESSED,擡起的時候mPrivateFlags!= PFLAG_PRESSED,下面來看看refreshDrawableState方法:

public void refreshDrawableState() {
    mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
    drawableStateChanged();

    ViewParent parent = mParent;
    if (parent != null) {
        parent.childDrawableStateChanged(this);
    }
}
複製代碼

接着看drawableStateChanged方法:

protected void drawableStateChanged() {
    //能夠看到先去根據getDrawableState獲取到drawable的狀態,而後調用了各個drawable的setState方法
    final int[] state = getDrawableState();
    boolean changed = false;
    //首先就是設置background的state
    final Drawable bg = mBackground;
    if (bg != null && bg.isStateful()) {
        changed |= bg.setState(state);
    }

    //省略代碼
    //foreground的state,後面會講到foreground水波效果是怎麼造成的
    final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
    if (fg != null && fg.isStateful()) {
        changed |= fg.setState(state);
    }
    //省略代碼
    
    //最後刷新view來調用view的draw方法,最後調用drawable的draw方法
    if (changed) {
        invalidate();
    }
}
複製代碼

前面講過,getDrawableState方法會根據當前view的狀態的flag值,去StateSet類中去找各個狀態下對應的drawable的state,找到後,調用drawable的setState方法,在setState中會經過mStateSet和傳進來的stateSet對比,若是二者相等,那麼觸發drawable的onStateChange方法,後面講drawable的時候細講,先明白是設置state就行。上面能夠看到foreground設置背景也是這麼幹的,後面會介紹foreground是怎麼來的。最後調用了invalidate方法,從新繪製drawable。上面在整個ontouchEvent的up事件中調用了setPress(false),那何時調用的setPress(true)的呢,很簡單,在view的down事件中觸發的,回到ontouchEvent裏面,在裏面有這麼一句if (clickable || (viewFlags & TOOLTIP) == TOOLTIP)也就是說只有在clickable爲true的狀況下,drawable點擊纔有效果,因此在開篇的例子中,只有設置了view的監聽器,drawable點擊樣式才起效果。關於view的事件傳遞能夠看我以前寫的該篇android中view事件傳遞,在view的down事件中有這麼一句setPressed(true, x, y),也就是會觸發drawable按下的時候樣式。 在上面咱們提到過drawable.,setbackCall(this),這句代碼,此處是把view的事例傳給了drawable,在drawale的invalidateSelf方法中會回調到view:

//該方法是drawale的重繪方法,回調到view的invalidateDrawable方法
public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}
複製代碼

看下view中回調的invalidateDrawable方法:

@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
    if (verifyDrawable(drawable)) {
        final Rect dirty = drawable.getDirtyBounds();
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;

        invalidate(dirty.left + scrollX, dirty.top + scrollY,
                dirty.right + scrollX, dirty.bottom + scrollY);
        rebuildOutline();
    }
}
複製代碼

看到了沒,實際上調用了view的重繪,而view的重繪又會調用drawable的draw方法,所以上面的drawable.setbackCall(this),最終是繪製本身。好了,介紹到這裏,drawable顯示到view上基本梳理完了,後面還會介紹foreground是怎麼有波紋點擊效果,以及介紹drawable在xml中定義的state_****是如何加載到drawale的state上來的,這個就涉及到xml解析了,其實跟xml佈局的解析是差很少的,後面還會介紹各類drawable的使用,以及如何自定義一個drawable。

總結

  • xml中的各類drawable,其實每個標籤對應了各類drawable,該工做交給了DrawableInflater來完成的
  • 解析到各類drawable後,會觸發drawable的inflate方法,後面會講drawable的state獲取時會講到
  • view中backgroundforeground其實都是對應的drawable,若是咱們設置的background只是一個color,那麼此時background就是一個colorDrawable,若是是一個selector標籤的話,此時是一個StateListDrawable
  • background的isStateful方法判斷drawable是否是帶有state的drawble。
  • drawable的setState方法是設置drawable當前的state的
  • drawable.setTintList是設置drawable的着色,它的實質是給paint設置setColorFilter,關於ColorFilter有三個子類,後面會講到
  • drawable.setTintMode是設置上面着色的模式,關於模式後面也會講到
  • 在view按下和鬆開的時候會經過view的FLAG_PRESSED來給drawble設置state_pressed狀態,最終調用到drawable的draw方法,drawable的draw方法須要子類本身去實現。
  • drawable.setCallback(this)是將view的事例傳到drawable的WeakReference弱引用包裝起來了,在invalidateSelf方法中會觸發callback的invalidateDrawable方法,從而回調到view的重繪,而最終會從新調用了drawable的draw方法,從而達到drawble的從新繪製。

關於drawable的打造能夠看我另一篇實戰仿蘋果版小黃車(ofo)app主頁菜單效果

相關文章
相關標籤/搜索