StateListDrawable初始化、繪製、setColorFilter講解

xiangcman發表於2019-09-16

該篇是繼上兩篇文章分析的,主要分析了background到view顯示的流程,以及後面也分析了foregrounds在view上顯示的過程,後面也介紹了foreground顯示水波效果,以及如何自定義foreground的水波效果顏色,如果還沒有看前面兩節的內容,大家先看看前兩節的內容:

StateListDrawable的state初始化

還記得在第一篇介紹drawable顯示到view的過程說過,通過xml各種標籤名生成drawable的時候,後面繼續呼叫了drawable的inflate方法吧,我們們就順著這個方向看下inflate方法做了啥,直接看StateListDrawable下面的inflate方法:

@Override
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
        throws XmlPullParserException, IOException {
    final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.StateListDrawable);
    super.inflateWithAttributes(r, parser, a, R.styleable.StateListDrawable_visible);
    updateStateFromTypedArray(a);
    updateDensity(r);
    a.recycle();
    //方法很重要,用來獲取xml中的屬性
    inflateChildElements(r, parser, attrs, theme);
    //獲取完屬性之後,觸發state的改變
    onStateChange(getState());
}
複製程式碼

看註釋一,呼叫了inflateChildElements方法:

private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
        Theme theme) throws XmlPullParserException, IOException {
    final StateListState state = mStateListState;
    final int innerDepth = parser.getDepth() + 1;
    int type;
    int depth;
    //遍歷裡面的每一個item標籤
    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
            && ((depth = parser.getDepth()) >= innerDepth
            || type != XmlPullParser.END_TAG)) {
        if (type != XmlPullParser.START_TAG) {
            continue;
        }
        //如果裡面的標籤不是item直接跳出該處迴圈
        if (depth > innerDepth || !parser.getName().equals("item")) {
            continue;
        }
        final TypedArray a = obtainAttributes(r, theme, attrs,
                R.styleable.StateListDrawableItem);
        //如果當前屬性值直接是一個drawable的話,而不是一個xml檔案,直接返回drawable
        Drawable dr = a.getDrawable(R.styleable.StateListDrawableItem_drawable);
        a.recycle();
        //拿到item下面的各種狀態值
        final int[] states = extractStateSet(attrs);
        if (dr == null) {
            //如果drawable屬性是單獨的xml檔案,還得繼續去解析drawable下面的xml檔案
            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
        }
        //最後將不同的狀態加到StateListState裡面
        state.addStateSet(states, dr);
    }
}
複製程式碼

上面程式碼是解析每一個item標籤,如果標籤裡面drawable的值直接是一個值,而不是一個drawable的xml檔案的時候直接返回dr,通過extraStateSet方法,將各個狀態下對應的state是true或者false的狀態值獲取到:

int[] extractStateSet(AttributeSet attrs) {
    int j = 0;
    final int numAttrs = attrs.getAttributeCount();
    int[] states = new int[numAttrs];
    for (int i = 0; i < numAttrs; i++) {
        final int stateResId = attrs.getAttributeNameResource(i);
        switch (stateResId) {
            case 0:
                break;
            //如果屬性是drawable或者id直接不要
            case R.attr.drawable:
            case R.attr.id:
                continue;
            default:
                //通過屬性的布林值,返回對應state_***的整型值
                states[j++] = attrs.getAttributeBooleanValue(i, false)
                        ? stateResId : -stateResId;
        }
    }
    states = StateSet.trimStateSet(states, j);
    return states;
}
複製程式碼

上面方法如果item標籤裡面有drawable或者是id的屬性,直接不要;如果獲取的state_*** 是true,那麼返回它對應的state_*** 對應的id,如果為false,則返回它對應的-id。

獲取到對應的state_***的對應的id之後放到陣列states裡面。如果上面定義的drawable值不能通過上面獲取到,那麼通過Drawable.createFromXmlInner方法獲取。最後將各個state下對應的drawable,新增到StateListState裡面,StateListStateStateListDrawable的子類,它將每一個state下的drawable儲存下來,放到父類的mDrawables變數裡面,將state放到mStateSets裡面,下面我們通過一個例子來說說上面的api: 寫了一個selector的drawable,名字是test_back.xml

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

三種狀態,對應的都是colorDrawable,然後在佈局中給view設定背景:

StateListDrawable初始化、繪製、setColorFilter講解
然後通過debug,可以看下程式碼跟蹤的情況:

StateListDrawable初始化、繪製、setColorFilter講解
第一次獲取的dr是colorDrawable,然後我們看下對應的狀態id:

StateListDrawable初始化、繪製、setColorFilter講解
對應的id是16842919,這個怎麼看對應的state_*** 呢,我們可以android.R.attr下面找到對應的state_*** 可以看到:

StateListDrawable初始化、繪製、setColorFilter講解
正好對應的id是state_press的id。看到這裡,第二次迴圈應該是state_selected對應的id吧:

StateListDrawable初始化、繪製、setColorFilter講解
哈哈哈,還真的是state_pressed對應的id,第三次對應的就是普通時候的state了。第三次對應的states是空的,因為在正常情況下在extractStateSet方法裡面獲取的stateResId=0,因此直接跳出迴圈extractStateSet方法的迴圈了,所以下面通過日誌列印到不同狀態下state的drawable如下:

final View view2 = findViewById(R.id.view2);
Drawable background = view2.getBackground();
StateListDrawable.DrawableContainerState constantState =
        (StateListDrawable.DrawableContainerState) background.getConstantState();
Drawable[] children = constantState.getChildren();
for (int i = 0; i < children.length; i++) {
    Drawable child = children[i];
    if (child instanceof ColorDrawable) {
        ColorDrawable colorDrawable = (ColorDrawable) child;
        int color = colorDrawable.getColor();
        Log.d(TAG, "color:" + toHexEncoding(color));
    } else {
        Log.d(TAG, "drawable:" + children[i]);
    }
}
複製程式碼

StateListDrawable初始化、繪製、setColorFilter講解
在color.xml中定義的幾個顏色值如下:

<resources>
    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorAccent">#D81B60</color>
</resources>
複製程式碼

所以更加說明了,在StateListDrawable.StateListState類通過addStateSet方法,將不同狀態下對應的drawable放到mDrawable[]陣列裡面,但是有人好奇了,為什麼在列印日誌裡面,除了前面三個狀態下的drawable都是colorDrawable,而後面7個是為空呢,其實這個當時我也很好奇,為什麼輸出的長度是10個,翻開StateSet類發現,所有關於state_***的屬性總共10個:

StateListDrawable初始化、繪製、setColorFilter講解
所以我在想是不是有什麼地方做了mDrawable長度的限制,果然在StateListDrawable.StateListState類呼叫addStateSet方法的時候,呼叫了父類DrawableContaineraddChild方法的時候有這麼一句:

StateListDrawable初始化、繪製、setColorFilter講解
第一次addState的時候,mNumChildren=0,這個時候mDrawables.length=0,此時呼叫了growArray方法:

public void growArray(int oldSize, int newSize) {
    //newSize=10,oldSize=0
    Drawable[] newDrawables = new Drawable[newSize];
    //arraycopy是將mDrawables拷貝到newDrawables裡面,所以此時mDrawables的長度=10
    System.arraycopy(mDrawables, 0, newDrawables, 0, oldSize);
    mDrawables = newDrawables;
}
複製程式碼

從這裡不難看出,最終是將長度=10的newDrawables作為拷貝的陣列,放到了mDrawables裡面。所以印證了上面日誌上輸出的長度=10的列印結果。

StateListDrawable的繪製過程

還記得我們在第一節android中drawable顯示到view上的過程的時候,說過view在action_down和action_up的時候會觸發drawable的setState方法:

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

傳進來的是當前的state陣列狀態,mStateSet表示當前drawable正在執行的state,mStateSet預設是一個空的陣列,因此Arrays.equals(mStateSet, stateSet)肯定不相等,所以會進到if,將stateSet賦給了mStateSet,回撥給了onStateChange方法,drawable該方法下面是個空方法,因此可以看得出來狀態的改變交給了子類去完成:

@Override
protected boolean onStateChange(int[] stateSet) {
    //呼叫了父類的onStateChange方法
    final boolean changed = super.onStateChange(stateSet);
    //通過傳來的state在R檔案中的int值來獲取idx
    int idx = mStateListState.indexOfStateSet(stateSet);
    if (idx < 0) {
        idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
    }
    return selectDrawable(idx) || changed;
}

//父類的onStateChange方法
@Override
protected boolean onStateChange(int[] state) {
    //剛開始mLastDrawable和mCurrDrawable為空
    if (mLastDrawable != null) {
        return mLastDrawable.setState(state);
    }
    if (mCurrDrawable != null) {
        return mCurrDrawable.setState(state);
    }
    return false;
}
複製程式碼

可以看到上面onStateChange方法先是呼叫了父類的onStateChange方法,然後通過mStateListState.indexOfStateSet獲取到idx值,最後呼叫了父類的selectDrawable(idx)方法,通過日誌我們在按下的時候獲取到日誌如下:

StateListDrawable初始化、繪製、setColorFilter講解
通過android.R.attr檔案找到了對應的state_***:

StateListDrawable初始化、繪製、setColorFilter講解
mStateListState.indexOfStateSet中做的工作是如果找到了StateListDrawable中和傳過來的state有對應關係,直接返回StateListDrawable.StateListStatemStateSets二維陣列的索引。 這裡可以看到對應的ids=0:

StateListDrawable初始化、繪製、setColorFilter講解
我們可以做個驗證,將xml中的state_pressed狀態放在後面的位置,再來看下日誌:

StateListDrawable初始化、繪製、setColorFilter講解
這裡我把pressed的item放到了第二個位置,然後通過debug日誌繼續可以看到:

StateListDrawable初始化、繪製、setColorFilter講解
看到了吧,獲取到的idx=1,正好對應了selector裡面第二個item,也就是state_pressed對應的位置。

這裡提個醒哈,如果將預設的item放到了第二個位置,按下的item放到第三個位置,按下的時候直接不顯示按下的drawable了,為啥呢,這是因為 int idx = mStateListState.indexOfStateSet(stateSet);這句返回的idx=1,正好返回的drawable是第二個位置的item,所以按下的時候,還是顯示正常情況下的drawable。

上面最後呼叫了selectDrawable方法,該方法在父類裡面:

public boolean selectDrawable(int index) {
    if (index == mCurIndex) {
        return false;
    }

    final long now = SystemClock.uptimeMillis();

    //預設情況下mDrawableContainerState.mExitFadeDuration=0
    if (mDrawableContainerState.mExitFadeDuration > 0) {
        if (mLastDrawable != null) {
            mLastDrawable.setVisible(false, false);
        }
        if (mCurrDrawable != null) {
            mLastDrawable = mCurrDrawable;
            mLastIndex = mCurIndex;
            mExitAnimationEnd = now + mDrawableContainerState.mExitFadeDuration;
        } else {
            mLastDrawable = null;
            mLastIndex = -1;
            mExitAnimationEnd = 0;
        }
    } else if (mCurrDrawable != null) {
        mCurrDrawable.setVisible(false, false);
    }
    //實際上看這裡就行,index始終是>=0的,並且mDrawableContainerState.mNumChildren=10
    if (index >= 0 && index < mDrawableContainerState.mNumChildren) {
        //獲取到當前的drawable
        final Drawable d = mDrawableContainerState.getChild(index);
        //將d賦給mCurrDrawable
        mCurrDrawable = d;
        mCurIndex = index;
        if (d != null) {
            if (mDrawableContainerState.mEnterFadeDuration > 0) {
                mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration;
            }
            initializeDrawableForDisplay(d);
        }
    } else {
        mCurrDrawable = null;
        mCurIndex = -1;
    }
    //預設是等於0的
    if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) {
        if (mAnimationRunnable == null) {
            mAnimationRunnable = new Runnable() {
                @Override public void run() {
                    animate(true);
                    invalidateSelf();
                }
            };
        } else {
            unscheduleSelf(mAnimationRunnable);
        }
        // Compute first frame and schedule next animation.
        animate(true);
    }
    //這裡會觸發自己的draw方法
    invalidateSelf();

    return true;
}
複製程式碼

上面這麼多的程式碼,其實只需要看這段邏輯就行: if (index >= 0 && index < mDrawableContainerState.mNumChildren),可以看到將當前獲取到的drawable賦值給了mCurrDrawable變數,在最後觸發了invalidateSelf方法,如果看過我寫的第一節android中drawable顯示到view上的過程,一定會知道,最後會觸發到StateListDrawable的draw方法,draw方法在DrawContainer裡面:

@Override
public void draw(Canvas canvas) {
    if (mCurrDrawable != null) {
        mCurrDrawable.draw(canvas);
    }
    if (mLastDrawable != null) {
        mLastDrawable.draw(canvas);
    }
}
複製程式碼

說白了,正常情況下按下和抬起的時候,用到了上面陣列中的第二個和第三個colorDrawable,將drawable賦值給了mCurrDrawable,所以最終會繪製成mCurrDrawable的樣子。

在上面selectDrawable方法中有這麼一句if (mDrawableContainerState.mExitFadeDuration > 0),該if可以通過xml或者setExitFadeDuration方法來實現:

StateListDrawable初始化、繪製、setColorFilter講解

StateListDrawable初始化、繪製、setColorFilter講解
英語不好的筒子們,可以看下翻譯該方法啥意思:在drawable離開時淡入淡出的時間間隔

可以看下設定該屬性之後的效果,這是按下一會兒和鬆開的時候效果:

StateListDrawable初始化、繪製、setColorFilter講解
通過該效果分析下過程,回到剛才的selectDrawable方法,

public boolean selectDrawable(int index) {
    if (index == mCurIndex) {
        return false;
    }

    final long now = SystemClock.uptimeMillis();

    //此時會走這裡
    if (mDrawableContainerState.mExitFadeDuration > 0) {
        if (mLastDrawable != null) {
            mLastDrawable.setVisible(false, false);
        }
        //第二次mCurrDrawable不為空,
        if (mCurrDrawable != null) {
            mLastDrawable = mCurrDrawable;
            mLastIndex = mCurIndex;
            //設定動畫維持的時間
            mExitAnimationEnd = now + mDrawableContainerState.mExitFadeDuration;
        } else {
            mLastDrawable = null;
            mLastIndex = -1;
            mExitAnimationEnd = 0;
        }
    } else if (mCurrDrawable != null) {
        mCurrDrawable.setVisible(false, false);
    }
   
    //預設是等於0的
    if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) {
        if (mAnimationRunnable == null) {
            mAnimationRunnable = new Runnable() {
                @Override public void run() {
                    //動畫執行的地方
                    animate(true);
                    invalidateSelf();
                }
            };
        } else {
            //釋放任務
            unscheduleSelf(mAnimationRunnable);
        }
        //此處會觸發動畫執行
        animate(true);
    }
    //這裡會觸發自己的draw方法
    invalidateSelf();

    return true;
}
複製程式碼

如果設定了mDrawableContainerState.mExitFadeDuration > 0,mCurrDrawable是正常state下的drawable,因此會將currentDrawable賦值給lastDrawable,此時mExitAnimationEnd就是我們設定的淡入淡出的時間,緊接著就是在animate方法裡面觸發mAnimationRunnable的執行:

void animate(boolean schedule) {
    mHasAlpha = true;

    final long now = SystemClock.uptimeMillis();
    boolean animating = false;
    //此處是設定drawable進入的時候動畫,如果設定了enterAnimationEnd屬性才會走這裡
    //實際上進入的動畫是不斷改變mCurrDrawable的動畫,而此時mLastDrawable是空的 
    //所以可以想象下,當按下的時候,按下的drawable的alpha從0到255
    //當抬起的時候,正常的drawable也會從0到255
    if (mCurrDrawable != null) {
        if (mEnterAnimationEnd != 0) {
            if (mEnterAnimationEnd <= now) {
                mCurrDrawable.setAlpha(mAlpha);
                mEnterAnimationEnd = 0;
            } else {
                int animAlpha = (int)((mEnterAnimationEnd-now)*255)
                        / mDrawableContainerState.mEnterFadeDuration;
                mCurrDrawable.setAlpha(((255-animAlpha)*mAlpha)/255);
                animating = true;
            }
        }
    } else {
        mEnterAnimationEnd = 0;
    }
    if (mLastDrawable != null) {
        if (mExitAnimationEnd != 0) {
            if (mExitAnimationEnd <= now) {
                mLastDrawable.setVisible(false, false);
                mLastDrawable = null;
                mLastIndex = -1;
                mExitAnimationEnd = 0;
            } else {
                //實際上就是不斷改變mLastDrawable的透明度,透明度到了mExitAnimationEnd的時候就為0,
                //所以到了最後就只能看到mCurrDrawable
                int animAlpha = (int)((mExitAnimationEnd-now)*255)
                        / mDrawableContainerState.mExitFadeDuration;
                mLastDrawable.setAlpha((animAlpha*mAlpha)/255);
                animating = true;
            }
        }
    } else {
        mExitAnimationEnd = 0;
    }

    if (schedule && animating) {
        scheduleSelf(mAnimationRunnable, now + 1000 / 60);
    }
}
複製程式碼

上面如果mExitAnimationEnd>0,lastDrawable是正常狀態下的drawable,此時lastDrawable的alpha值從255到0,看到的效果就是按下的時候正常的drawable(lastDrawable)慢慢地變淡,等now到了mExitAnimationEnd時候,按下的drawable才會顯示。而抬起的時候,此時lastDrawable是按下的drawable,此時lastDrawable的alpha值從255到0,所以看到的效果就是抬起時按下的drawable慢慢變淡。

如果mEnterAnimationEnd>0,此時lastDrawable為空,只繪製currentDrawable,而此時currentDrawable的alpha值從0到255,所以按下的時候,按下的drawable會從淡到顯示。當抬起的時候,此時currentDrawable是正常情況的drawable,因此會出現正常drawable從淡到顯示。

enterFadeDuration設定要顯示的drawable的淡入時間。 exitFadeDuration設定當前drawable離開的淡出時間。

關於enterFadeDuration在上面例子中沒有演示,大家可以自己嘗試該屬性。

上面例子中,currentDrawable和lastDrawable實際都是colorDrawable,因為我們在例子中定義的drawable只是一個顏色值,因此我們們看看colorDrawable實際繪製的過程,在說colorDrawable的繪製之前,我們先來回憶下在第一節講android中drawable顯示到view上的過程applyBackgroundTint方法當時提過是用來著色用的,其實它的實質是通過paint.setColorFilter(new PorterDuffColorFilter(color,mode))來實現的,下面先度娘看下paint.setColorFilter能做些啥事:

setColorFilter

ColorFiler一共有三個子類,分別是ColorMatrixColorFilterLightingColorFilterPorterDuffColorFilter

ColorMatrixColorFilter

是一個顏色矩陣器,它需要一個ColorMatrix物件,ColorMatrix需要設定顏色矩陣,下面通過一個demo來說明該問題:

public class ColorFilterView extends View {
    private static final String TAG = ColorFilterView.class.getSimpleName();
    Paint mPaint;

    public ColorFilterView(Context context) {
        this(context, null);
    }

    public ColorFilterView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ColorFilterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        int color = Color.parseColor("#666666");//R=102,G=102,B=102,A=255
        int red = Color.red(color);
        int green = Color.green(color);
        int blue = Color.blue(color);
        Log.d(TAG, "red:" + red);
        Log.d(TAG, "green:" + green);
        Log.d(TAG, "blue:" + blue);
        Log.d(TAG, "alpha:" + Color.alpha(color));
        mPaint.setColor(color);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d(TAG, "onDraw");
        // 生成色彩矩陣
        ColorMatrix colorMatrix = new ColorMatrix(new float[]{
                0.5f, 0, 0, 0, 0,//R=0.5*102+0*102+0*102+0*255+0=51
                0, 0.5f, 0, 0, 0,//G=0*102+0.5*102+0*102+0*255+0=51
                0, 0, 0.5f, 0, 0,//B=0*102+0*102+0.5*102+0*255+0=51
                0, 0, 0, 1, 0,//B=0*102+0*102+0*102+1*255+0=255
        });
        ColorMatrix colorMatrix1 = new ColorMatrix(new float[]{
                1, 0, 0, 0, 0,//R=1*102+0*102+0*102+0*255+0=102
                0, 1, 0, 0, 0,//G=0*102+1*102+0*102+0*255+0=102
                0, 0, 1, 0, 0,//B=0*102+0*102+1*102+0*255+0=102
                0, 0, 0, 1, 0,//A=0*102+0*102+0*102+1*255+0=255
        });
        mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));

        // 設定畫筆顏色為自定義顏色

        // 繪製圓環 (x座標,y座標,半徑,畫筆)
        canvas.drawCircle(240, 600 / 2, 200, mPaint);
    }
}
複製程式碼

例子中的顏色值是#666666,顏色值R=102,G=102,B=102,A=255, 在colorMatrix運算的時候,矩陣第一行算出來的結果是R的值,演算法過程是每一行的每一個數分別與顏色值相乘然後相加。所以第一個矩陣算出來的結果:R=51,G=51,B=51,A=255,對應顏色的16進位制是#333333;在第二個矩陣中,每一行每一個數相乘的顏色位正好是原來的102的值,因此第二個顏色矩陣算出來的值還是原來的顏色值。 這裡借用網上一張圖:

StateListDrawable初始化、繪製、setColorFilter講解

下面來介紹下在bitmap情況下,ColorMatrix是怎麼工作的,先上一個demo看下,圖片就用android studio自帶的:

StateListDrawable初始化、繪製、setColorFilter講解
這裡寫了一個紅色通道的colorMatrix,意思是如果你想讓圖片偏向哪個顏色,對應的通道就儘量大點,最後一列表示圖片的飽和度:

StateListDrawable初始化、繪製、setColorFilter講解
這裡寫了幾種情況,第一個是原始圖片,第二個是紅色通道生成的,第三個是紅色通道+100的飽和度生成的,第四個是藍色通道生成的,第五個是綠色通道生成的,第六個透明度通道生成的,透明度是0.5。

//紅色通道
ColorMatrix colorMatrixR = new ColorMatrix(new float[]{
        1, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 0, 1, 0,
});
//綠色通道
ColorMatrix colorMatrixG = new ColorMatrix(new float[]{
        0, 0, 0, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 0, 1, 0,
});
//紅色通道+100的飽和度
ColorMatrix colorMatrixRB = new ColorMatrix(new float[]{
        1, 0, 0, 0, 100,
        0, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 0, 1, 0,
});
//藍色通道
ColorMatrix colorMatrixB = new ColorMatrix(new float[]{
        0, 0, 0, 0, 0,
        0, 0, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 0, 0, 1, 0,
});
//透明度0.5的通道
ColorMatrix colorMatrixA = new ColorMatrix(new float[]{
        1, 0, 0, 0, 0,
        0, 1, 0, 0, 0,
        0, 0, 1, 0, 0,
        0, 0, 0, 0.5f, 0,
});
 // LightingColorFilter lightingColorFilter = new LightingColorFilter(0x0000ff, 0x000000);
mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrixR));
// 設定畫筆顏色為自定義顏色
// 繪製圓環 (x座標,y座標,半徑,畫筆)
  canvas.drawCircle(240, 600 / 2, 200, mPaint);
canvas.drawBitmap(bitmap, 100, 100, null);
canvas.drawBitmap(bitmap, 300, 100, mPaint);
mPaint.setColorFilter(null);
mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrixRB));
canvas.drawBitmap(bitmap, 500, 100, mPaint);
mPaint.setColorFilter(null);
mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrixB));
canvas.drawBitmap(bitmap, 700, 100, mPaint);
mPaint.setColorFilter(null);
mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrixG));
canvas.drawBitmap(bitmap, 900, 100, mPaint);
mPaint.setColorFilter(null);
mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrixA));
canvas.drawBitmap(bitmap, 100, 300, mPaint);
複製程式碼

colorMatrix其他的幾個方法:

setRGB2YUV將通道設定偏向紅色通道

setSaturation設定飽和度

setScale設定各個通道的縮放比

setRotate

StateListDrawable初始化、繪製、setColorFilter講解
該註釋說明如果引數axis=0,設定紅色通道旋轉的角度,axis=1,設定綠色通道旋轉的角度,axis=2,設定藍色通道的旋轉角度。下面也是將每個api跑了一遍demo:

StateListDrawable初始化、繪製、setColorFilter講解

//圖二
ColorMatrix colorMatrix1 = new ColorMatrix();
colorMatrix1.setSaturation(100);//設定每個通道的飽和度

//圖三
ColorMatrix colorMatrix2 = new ColorMatrix();
colorMatrix2.setRGB2YUV();//設定偏向紅色通道

//圖四
ColorMatrix colorMatrix3 = new ColorMatrix();
colorMatrix3.setYUV2RGB();//也是偏向紅色通道

//圖五
ColorMatrix colorMatrix4 = new ColorMatrix();
colorMatrix4.setScale(50, 50, 50, 50);//指定每一個通道放大的倍數

//圖六,因為是圍繞G通道,圍繞那個通道旋轉,就偏向旋轉的通道
ColorMatrix colorMatrix5 = new ColorMatrix();
colorMatrix5.setRotate(0, 180);//圍繞某一個通道進行旋轉多少度,1.是圍繞G通道,0.是圍繞R通道,2.是圍繞B通道
複製程式碼

關於ColorMatrix就說這麼多,大家只要記住計算公式就行,需要偏向那個通道,就設定那個通道的值偏大,其餘的通道就不用管或者調小。

LightingColorFilter

是一個光照顏色過濾器,他只有一個帶兩個引數的構造器,第一個引數是色彩倍增,第二個引數是色彩增加,兩個引數都是16進位制的顏色值,下面看看如何使用:

//光照colorFilter
//第一個引數的顏色值,通過ARGB每兩位設定通道的倍數
//第二個引數設定每一個通道的偏移量
//下面應該可以看出來繪製的顏色是偏向R通道的,因此呈現出紅色
LightingColorFilter lightingColorFilter = new LightingColorFilter(0x66660000, 0x00000000);
mPaint.setColorFilter(null);
mPaint.setColorFilter(lightingColorFilter);
canvas.drawBitmap(bitmap, 300, 300, mPaint);
複製程式碼

很簡單,如果你想讓顏色偏向什麼通道顏色,直接將通道顏色值設定大點,偏移量引數也可以適當的設定相應的通道。關於LightingColorFilter就說這麼多,基本知道兩個引數的含義就ok。

PorterDuffColorFilter

它是我們今天drawable中用到的著色器colorFilter,PorterDuffColorFilter提供了兩個引數,第一個引數是16進位制顏色值,第二個引數是PorterDuff.Mode,表示顏色混合模式,而在模式裡面又分為dst和src,src表示當前PorterDuffColorFilter第一個引數的顏色,dst表示在繪製用到paint時候的圖案,下面的demo以繪製bitmap作為dst:

//繪製的顏色是src,dst是圖片區域
PorterDuffColorFilter srcPorterDuffColorFilter = new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC);
mPaint.setColorFilter(null);
mPaint.setColorFilter(srcPorterDuffColorFilter);
canvas.drawBitmap(bitmap, 500, 300, mPaint);
//DST模式下只顯示圖片
PorterDuffColorFilter porterDuffColorFilter = new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.DST);
mPaint.setColorFilter(null);
mPaint.setColorFilter(porterDuffColorFilter);
canvas.drawBitmap(bitmap, 700, 300, mPaint);
複製程式碼

下面來看看android中真實案例用到了PorterDuffColorFilter的繪圖重疊,其實在imageView中的setColorFilter正是用到PorterDuffColorFilter的api,最終呼叫了drawable.setColorFilter:

StateListDrawable初始化、繪製、setColorFilter講解

StateListDrawable初始化、繪製、setColorFilter講解
上面執行出來的結果是專案中用到PorterDuffColorFilter的特性,通過設定mode為MULTIPLY,顏色值為Color.GRAYMULTIPLY模式表示取src和dst的交集,並且src在上面,dst在下面,通過兩者的複合達到遮罩效果。該demo樣式是在recyclerView的holder中使用的,部分程式碼如下:

public class ManagerBookShelfHolder extends ViewHolderImpl<BookShelfItem> {

    private ImageView bookCoverimg;
    private TextView bookName;
    private ImageView select;

    @Override
    protected int getItemLayoutId() {
        return R.layout.book_shelf_item;
    }

    @Override
    public void initView() {
        bookCoverimg = findById(R.id.bookCoverimg);
        bookName = findById(R.id.bookName);
        select = findById(R.id.select);
    }

    @Override
    public void onBind(BookShelfItem data, int pos) {
        Glide.with(getContext()).load(data.bookCoverimg).listener(new RequestListener<Drawable>() {
            @Override
            public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                return false;
            }

            @Override
            public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                bookCoverimg.setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY);
                return false;
            }
        }).into(bookCoverimg);
        if (data.select) {
            select.setImageResource(R.mipmap.select);
            bookCoverimg.clearColorFilter();
        } else {
            bookCoverimg.setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY);
            select.setImageResource(R.mipmap.un_select);
        }
        bookName.setText(data.bookName);
        select.setVisibility(View.VISIBLE);

    }

}
複製程式碼

關於其他的mode就都不嘗試了,大家自己根據下面的mode圖自己嘗試繪製能體會到:

StateListDrawable初始化、繪製、setColorFilter講解

其實在android中paint.setXmode方法中,也有此mode的應用,關於paint.setXmode的應用,我在仿蘋果版小黃車(ofo)app主頁選單效果中有使用到,大家如果喜歡該文章,可以關注下文章。

好了,關於ColorFilter就說這麼多,我們再簡單的回到drawable的setColorFilter看下:

public void setColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode) {
    if (getColorFilter() instanceof PorterDuffColorFilter) {
        PorterDuffColorFilter existing = (PorterDuffColorFilter) getColorFilter();
        if (existing.getColor() == color && existing.getMode() == mode) {
            return;
        }
    }
    setColorFilter(new PorterDuffColorFilter(color, mode));
}
複製程式碼

預設getColorFilter()肯定不是PorterDuffColorFilter型別的,所以會走setColorFilter(new PorterDuffColorFilter(color, mode)),因此順著找到對應的setColorFilter,該方法在drawable中是個抽象的方法,因此可以看下colorDrawable裡面的setColorFilter方法:

@Override
public void setColorFilter(ColorFilter colorFilter) {
    mPaint.setColorFilter(colorFilter);
}
複製程式碼

好吧,如此簡單呼叫了piant.setColorFilter,最終會由view觸發到drawable的繪製。

TODO

  • 本來是想將StateListDrawable和RippleDrawable放在一起講的,限於篇幅,將RippleDrawable放到後面部分說水波效果的實現,以及如何實現定義view的時候水波效果。
  • ColorMatrixColorFilter延伸講解。

如果還不熟悉drawable在view上的顯示流程還不熟悉,請看之前的兩節內容:

相關文章