StateListDrawable初始化、绘制、setColorFilter讲解

StateListDrawable初始化、绘制、setColorFilter讲解

Android小彩虹2021-08-25 2:16:01230A+A-

该篇是继上两篇文章分析的,主要分析了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设置背景:

然后通过debug,可以看下代码跟踪的情况:

第一次获取的dr是colorDrawable,然后我们看下对应的状态id:

对应的id是16842919,这个怎么看对应的state_*** 呢,我们可以android.R.attr下面找到对应的state_*** 可以看到:

正好对应的id是state_press的id。看到这里,第二次循环应该是 state_selected对应的id吧:

哈哈哈,还真的是 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]);
    }
}

在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个:

所以我在想是不是有什么地方做了mDrawable长度的限制,果然在 StateListDrawable.StateListState类调用 addStateSet方法的时候,调用了父类 DrawableContaineraddChild方法的时候有这么一句:

第一次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)方法,通过日志我们在按下的时候获取到日志如下:

通过android.R.attr文件找到了对应的state_***:

mStateListState.indexOfStateSet中做的工作是如果找到了StateListDrawable中和传过来的state有对应关系,直接返回 StateListDrawable.StateListStatemStateSets二维数组的索引。 这里可以看到对应的ids=0:

我们可以做个验证,将xml中的state_pressed状态放在后面的位置,再来看下日志:

这里我把pressed的item放到了第二个位置,然后通过debug日志继续可以看到:

看到了吧,获取到的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方法来实现:

英语不好的筒子们,可以看下翻译该方法啥意思: 在drawable离开时淡入淡出的时间间隔

可以看下设置该属性之后的效果,这是按下一会儿和松开的时候效果:

通过该效果分析下过程,回到刚才的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的值,因此第二个颜色矩阵算出来的值还是原来的颜色值。 这里借用网上一张图:

下面来介绍下在bitmap情况下,ColorMatrix是怎么工作的,先上一个demo看下,图片就用android studio自带的:

这里写了一个红色通道的colorMatrix,意思是如果你想让图片偏向哪个颜色,对应的通道就尽量大点,最后一列表示图片的饱和度:

这里写了几种情况,第一个是原始图片,第二个是红色通道生成的,第三个是红色通道+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

该注释说明如果参数axis=0,设置红色通道旋转的角度,axis=1,设置绿色通道旋转的角度,axis=2,设置蓝色通道的旋转角度。下面也是将每个api跑了一遍demo:

//图二
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:

上面运行出来的结果是项目中用到 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图自己尝试绘制能体会到:

其实在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上的显示流程还不熟悉,请看之前的两节内容:

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们