转载爱哥自定义View系列--Paint详解
上图是paint中的各种set方法
这些属性大多我们都可以见名知意,很好理解,即便如此,哥还是带大家过一遍逐个剖析其用法,其中会不定穿插各种绘图类比如Canvas、Xfermode、ColorFilter等等的用法。
set(Paint src)
顾名思义为当前画笔设置一个画笔,说白了就是把另一个画笔的属性设置Copy给我们的画笔,不累赘了
setARGB(int a, int r, int g, int b)
不扯了,别跟我说不懂
setAlpha(int a)
同上
setAntiAlias(boolean aa)
这个上一节我们用到了,打开抗锯齿,不过我要说明一点,抗锯齿是依赖于算法的,算法决定抗锯齿的效率,在我们绘制棱角分明的图像时,比如一个矩形、一张位图,我们不需要打开抗锯齿。
setColor(int color)
不扯
setColorFilter(ColorFilter filter)
设置颜色过滤,什么意思呢?就像拿个筛子把颜色“滤”一遍获取我们想要的色彩结果,感觉像是扯蛋白说一样是不是?没事我们慢慢说你一定会懂,这个方法需要我们传入一个ColorFilter参数同样也会返回一个ColorFilter实例,那么ColorFilter类是什么呢?追踪源码进去你会发现其里面很简单几乎没有:
ColorMatrixColorFilter、LightingColorFilter和PorterDuffColorFilter,也就是说我们在setColorFilter(ColorFilter filter)的时候可以直接传入这三个子类对象作为参数,那么这三个子类又是什么东西呢?首先我们来看看
ColorMatrixColorFilter
中文直译为色彩矩阵颜色过滤器,要明白这玩意你得先了解什么是色彩矩阵。在Android中图片是以RGBA像素点的形式加载到内存中的,修改这些像素信息需要一个叫做ColorMatrix类的支持,其定义了一个4x5的float[]类型的矩阵:
ColorMatrix colorMatrix = new ColorMatrix(new float[]{ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, });
其中,第一行表示的R(红色)的向量,第二行表示的G(绿色)的向量,第三行表示的B(蓝色)的向量,最后一行表示A(透明度)的向量,这一顺序必须要正确不能混淆!这个矩阵不同的位置表示的RGBA值,其范围在0.0F至2.0F之间,1为保持原图的RGB值。每一行的第五列数字表示偏移值,何为偏移值?顾名思义当我们想让颜色更倾向于红色的时候就增大R向量中的偏移值,想让颜色更倾向于蓝色的时候就增大B向量中的偏移值,这是最最朴素的理解,但是事实上色彩偏移的概念是基于白平衡来理解的,什么是白平衡呢?说得简单点就是白色是什么颜色!如果大家是个单反爱好者或者会些PS就会很容易理解这个概念,在单反的设置参数中有个色彩偏移,其定义的就是白平衡的色彩偏移值,就是当你去拍一张照片的时候白色是什么颜色的,在正常情况下白色是(255, 255, 255, 255)但是现实世界中我们是无法找到这样的纯白物体的,所以在我们用单反拍照之前就会拿一个我们认为是白色的物体让相机记录这个物体的颜色作为白色,然后拍摄时整张照片的颜色都会依据这个定义的白色来偏移!而这个我们定义的“白色”(比如:255, 253, 251, 247)和纯白(255, 255, 255, 255)之间的偏移值(0, 2, 4, 8)我们称之为白平衡的色彩偏移。
那么说了这么多,这玩意到底有啥用呢?我们来做个test!还是接着昨天那个圆环,不过我们今天把它改成绘制一个圆并且去掉线程动画的效果因为我们不需要:
public class CustomView extends View { private Paint mPaint;// 画笔 private Context mContext;// 上下文环境引用 public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; // 初始化画笔 initPaint(); } /** * 初始化画笔 */ private void initPaint() { // 实例化画笔并打开抗锯齿 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); /* * 设置画笔样式为描边,圆环嘛……当然不能填充不然就么意思了 * * 画笔样式分三种: * 1.Paint.Style.STROKE:描边 * 2.Paint.Style.FILL_AND_STROKE:描边并填充 * 3.Paint.Style.FILL:填充 */ mPaint.setStyle(Paint.Style.FILL); // 设置画笔颜色为自定义颜色 mPaint.setColor(Color.argb(255, 255, 128, 103)); /* * 设置描边的粗细,单位:像素px 注意:当setStrokeWidth(0)的时候描边宽度并不为0而是只占一个像素 */ mPaint.setStrokeWidth(10); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制圆形 canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2, 200, mPaint); } }
运行下是一个橙红色的圆~~是不是有点萝卜头国旗帜的感脚?
下面我们为Paint设置一个色彩矩阵:
// 生成色彩矩阵 ColorMatrix colorMatrix = new ColorMatrix(new float[]{ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, }); mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
再次运行发现没变化啊!!!草!!!是不是感觉被我坑了?如果你真的那么认为我只能说你压根就没认真看上面的文字,我说过什么?值为1时表示什么?表示不改变原色彩的值!!这时我们改变色彩矩阵:
// 生成色彩矩阵 ColorMatrix colorMatrix = new ColorMatrix(new float[]{ 0.5F, 0, 0, 0, 0, 0, 0.5F, 0, 0, 0, 0, 0, 0.5F, 0, 0, 0, 0, 0, 1, 0, }); mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
再次运行:
是不是明显不一样了?颜色变深了便淳厚了!我们通过色彩矩阵与原色彩的计算得出的色彩就是这样的。那它们是如何计算的呢?其实说白了就是矩阵之间的运算乘积:
矩阵ColorMatrix的一行乘以矩阵MyColor的一列作为矩阵Result的一行,这里MyColor的RGBA值我们需要转换为[0, 1]。那么我们依据此公式来计算下我们得到的RGBA值是否跟我们计算得出来的圆的RGBA值一样:
我们计算得出最后的RGBA值应该为:0.5, 0.25, 0.2, 1;
有兴趣的童鞋可以去PS之类的绘图软件里试试看正不正确对不对~~~这里就不演示了!看完这里有朋友又会说了,这玩意有毛线用啊!改个颜色还这么复杂!劳资直接setColor多爽!!没错,你这样想是对的,因为毕竟我们只是一个颜色,可是如果是一张图片呢????一张图片可有还几十万色彩呢!!!你麻痹你跟我说setColor?那么我们换张图片来试试呗!看看是什么样的效果:
public class CustomView extends View { private Paint mPaint;// 画笔 private Context mContext;// 上下文环境引用 private Bitmap bitmap;// 位图 private int x,y;// 位图绘制时左上角的起点坐标 public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; // 初始化画笔 initPaint(); //初始化资源 initRes(context); } /** * 初始化画笔 */ private void initPaint() { // 实例化画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); } /** * 初始化资源 */ private void initRes(Context context) { // 获取位图 bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a); /* * 计算位图绘制时左上角的坐标使其位于屏幕中心 * 屏幕坐标x轴向左偏移位图一半的宽度 * 屏幕坐标y轴向上偏移位图一半的高度 */ x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2; y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制位图 canvas.drawBitmap(bitmap, x, y, mPaint); } }
如代码所示我们清除了所有的画笔属性设置因为没必要,从资源获取一个Bitmap绘制在画布上:
一张灰常漂亮的风景图,好!现在我们来为我们的画笔添加一个颜色过滤:
// 生成色彩矩阵 ColorMatrix colorMatrix = new ColorMatrix(new float[]{ 0.5F, 0, 0, 0, 0, 0, 0.5F, 0, 0, 0, 0, 0, 0.5F, 0, 0, 0, 0, 0, 1, 0, }); mPaint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
大家看到还是刚才那个色彩矩阵,运行下看看什么效果呢:
变暗了对吧!没意思,我们来点更刺激的,改下ColorMatrix矩阵:
ColorMatrix colorMatrix = new ColorMatrix(new float[]{ 0.33F, 0.59F, 0.11F, 0, 0, 0.33F, 0.59F, 0.11F, 0, 0, 0.33F, 0.59F, 0.11F, 0, 0, 0, 0, 0, 1, 0, });
噢!变灰了!还是没意思!继续改:
ColorMatrix colorMatrix = new ColorMatrix(new float[]{ -1, 0, 0, 1, 1, 0, -1, 0, 1, 1, 0, 0, -1, 1, 1, 0, 0, 0, 1, 0, });
哟呵!!是不是有点类似PS里反相的效果?我们常看到的图片都是RGB的,颠覆一下思维,看看BGR的试试:
ColorMatrix colorMatrix = new ColorMatrix(new float[]{ 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, });
这样红色的变成了蓝色而蓝色的就变成了红色,继续改:
ColorMatrix colorMatrix = new ColorMatrix(new float[]{ 0.393F, 0.769F, 0.189F, 0, 0, 0.349F, 0.686F, 0.168F, 0, 0, 0.272F, 0.534F, 0.131F, 0, 0, 0, 0, 0, 1, 0, });
是不是有点类似于老旧照片的感脚?继续:
ColorMatrix colorMatrix = new ColorMatrix(new float[]{ 1.5F, 1.5F, 1.5F, 0, -1, 1.5F, 1.5F, 1.5F, 0, -1, 1.5F, 1.5F, 1.5F, 0, -1, 0, 0, 0, 1, 0, });
类似去色后高对比度的效果,继续:
ColorMatrix colorMatrix = new ColorMatrix(new float[]{ 1.438F, -0.122F, -0.016F, 0, -0.03F, -0.062F, 1.378F, -0.016F, 0, 0.05F, -0.062F, -0.122F, 1.483F, 0, -0.02F, 0, 0, 0, 1, 0, });
饱和度对比度加强,好了不演示了……累死我了!截图粘贴上传!!
这些各种各样的图像效果在哪见过?PS?对的!还有各种拍照软件拍摄后的特效处理!大致原理都是这么来的!有人会问爱哥你傻逼么!这么多参数怎么玩!谁记得!而且TMD用参数调颜色?我映像中都是直接在各种绘图软件(比如PS)里拖进度条的!这怎么玩!淡定!如我所说很多时候你压根不需要了解太多原理,只需站在巨人的丁丁上即可,所以稍安勿躁!再下一个系列教程“设计色彩”中爱哥教你玩转色彩并且让设计和开发无缝结合!
ColorMatrixColorFilter和ColorMatrix就是这么个东西,ColorMatrix类里面也提供了一些实在的方法,比如setSaturation(float sat)设置饱和度,而且ColorMatrix每个方法都用了阵列的计算,如果大家感兴趣可以自己去深挖来看不过我是真心不推荐的~~~
下面我们来看看ColorFilter的另一个子类
LightingColorFilter
顾名思义光照颜色过滤,这肯定是跟光照是有关的了~~该类有且只有一个构造方法:
LightingColorFilter (int mul, int add)
这个方法非常非常地简单!mul全称是colorMultiply意为色彩倍增,而add全称是colorAdd意为色彩添加,这两个值都是16进制的色彩值0xAARRGGBB。这个方法使用也是非常的简单。还是拿上面那张图片来说吧,比如我们想要去掉绿色:
public class CustomView extends View { private Paint mPaint;// 画笔 private Context mContext;// 上下文环境引用 private Bitmap bitmap;// 位图 private int x, y;// 位图绘制时左上角的起点坐标 public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; // 初始化画笔 initPaint(); // 初始化资源 initRes(context); } /** * 初始化画笔 */ private void initPaint() { // 实例化画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 设置颜色过滤 mPaint.setColorFilter(new LightingColorFilter(0xFFFF00FF, 0x00000000)); } /** * 初始化资源 */ private void initRes(Context context) { // 获取位图 bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a); /* * 计算位图绘制时左上角的坐标使其位于屏幕中心 * 屏幕坐标x轴向左偏移位图一半的宽度 * 屏幕坐标y轴向上偏移位图一半的高度 */ x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2; y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制位图 canvas.drawBitmap(bitmap, x, y, mPaint); } }
运行后你会发现绿色确实是没了但是原来偏绿的部分现在居然成了红色,为毛!敬请关注下一系列设计色彩文章!!哈哈哈!!当LightingColorFilter(0xFFFFFFFF, 0x00000000)的时候原图是不会有任何改变的,如果我们想增加红色的值,那么LightingColorFilter(0xFFFFFFFF, 0x00XX0000)就好,其中XX取值为00至FF。那么这个方法有什么存在的意义呢?存在必定合理,这个方法存在一定是有它可用之处的,前些天有个盆友在群里问点击一个图片如何直接改变它的颜色而不是为他多准备另一张点击效果的图片,这种情况下该方法就派上用场了!如下图一个灰色的星星,我们点击后让它变成黄色
代码如下,注释很清楚我就不再多说了:
public class CustomView extends View { private Paint mPaint;// 画笔 private Context mContext;// 上下文环境引用 private Bitmap bitmap;// 位图 private int x, y;// 位图绘制时左上角的起点坐标 private boolean isClick;// 用来标识控件是否被点击过 public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; // 初始化画笔 initPaint(); // 初始化资源 initRes(context); setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { /* * 判断控件是否被点击过 */ if (isClick) { // 如果已经被点击了则点击时设置颜色过滤为空还原本色 mPaint.setColorFilter(null); isClick = false; } else { // 如果未被点击则点击时设置颜色过滤后为黄色 mPaint.setColorFilter(new LightingColorFilter(0xFFFFFFFF, 0X00FFFF00)); isClick = true; } // 记得重绘 invalidate(); } }); } /** * 初始化画笔 */ private void initPaint() { // 实例化画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); } /** * 初始化资源 */ private void initRes(Context context) { // 获取位图 bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a2); /* * 计算位图绘制时左上角的坐标使其位于屏幕中心 * 屏幕坐标x轴向左偏移位图一半的宽度 * 屏幕坐标y轴向上偏移位图一半的高度 */ x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2; y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制位图 canvas.drawBitmap(bitmap, x, y, mPaint); } }
运行后点击星星即可变成黄色再点击变回灰色,当我们不想要颜色过滤的效果时,setColorFilter(null)并重绘视图即可!那么为什么要叫光照颜色过滤呢?原因很简单,因为它所呈现的效果就像有色光照在物体上染色一样~~~哎,不说这方法了,看下一个也是最后一个ColorFilter的子类。
PorterDuffColorFilter
PorterDuffColorFilter跟LightingColorFilter一样,只有一个构造方法
PorterDuffColorFilter(int color, PorterDuff.Mode mode)
这个构造方法也接受两个值,一个是16进制表示的颜色值这个很好理解,而另一个是PorterDuff内部类Mode中的一个常量值,这个值表示混合模式。那么什么是混合模式呢?混合混合必定是有两种东西混才行,第一种就是我们设置的color值而第二种当然就是我们画布上的元素了!,比如这里我们把Color的值设为红色,而模式设为PorterDuff.Mode.DARKEN变暗:
public class CustomView extends View { private Paint mPaint;// 画笔 private Context mContext;// 上下文环境引用 private Bitmap bitmap;// 位图 private int x, y;// 位图绘制时左上角的起点坐标 public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; // 初始化画笔 initPaint(); // 初始化资源 initRes(context); } /** * 初始化画笔 */ private void initPaint() { // 实例化画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 设置颜色过滤 mPaint.setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.DARKEN)); } /** * 初始化资源 */ private void initRes(Context context) { // 获取位图 bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a); /* * 计算位图绘制时左上角的坐标使其位于屏幕中心 * 屏幕坐标x轴向左偏移位图一半的宽度 * 屏幕坐标y轴向上偏移位图一半的高度 */ x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2; y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制位图 canvas.drawBitmap(bitmap, x, y, mPaint); } }
我们尝试在画布上Draw刚才的那张图片看看:
变暗了……也变红了……这就是PorterDuff.Mode.DARKEN模式给我们的效果,当然PorterDuff.Mode还有其他很多的混合模式,大家可以尝试,但是这里要注意一点,PorterDuff.Mode中的模式不仅仅是应用于图像色彩混合,还应用于图形混合,比如PorterDuff.Mode.DST_OUT就表示裁剪混合图,如果我们在PorterDuffColorFilter中强行设置这些图形混合的模式将不会看到任何对应的效果,关于图形混合我们将在下面详解。
setXfermode(Xfermode xfermode)
[只对图像有效,drawBitmapXXX()]
Xfermode国外有大神称之为过渡模式,这种翻译比较贴切但恐怕不易理解,大家也可以直接称之为图像混合模式,因为所谓的“过渡”其实就是图像混合的一种,这个方法跟我们上面讲到的setColorFilter蛮相似的
同理可得其必然有一定的子类去实现一些方法供我们使用,查看API文档发现其果然有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode,这三个子类实现的功能要比setColorFilter的三个子类复杂得多,主要是是涉及到图像处理的一些知识可能对大家来说会比较难以理解,不过我会尽量以通俗的方式阐述它们的作用,那好先来看看我们的第一个子类
AvoidXfermode
首先我要告诉大家的是这个API因为不支持硬件加速在API 16已经过时了(大家可以在HardwareAccel查看那些方法不支持硬件加速)……如果想在高于API 16的机子上测试这玩意,必须现在应用或手机设置中关闭硬件加速,在应用中我们可以通过在AndroidManifest.xml文件中设置application节点下的android:hardwareAccelerated属性为false来关闭硬件加速:
<application android:allowBackup="true" android:hardwareAccelerated="false" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" > <activity android:name="com.aigestudio.customviewdemo.activities.MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>
AvoidXfermode只有一个含参的构造方法AvoidXfermode(int opColor, int tolerance, AvoidXfermode.Mode mode),其具体实现和ColorFilter一样都被封装在C/C++内,它怎么实现我们不管我们只要知道这玩意怎么用就行对吧。AvoidXfermode有三个参数,第一个opColor表示一个16进制的可以带透明通道的颜色值例如0x12345678,第二个参数tolerance表示容差值,那么什么是容差呢?你可以理解为一个可以标识“精确”或“模糊”的东西,待会我们细讲,最后一个参数表示AvoidXfermode的具体模式,其可选值只有两个:AvoidXfermode.Mode.AVOID或者AvoidXfermode.Mode.TARGET,两者的意思也非常简单,我们先来看
AvoidXfermode.Mode.TARGET
在该模式下Android会判断画布上的颜色是否会有跟opColor不一样的颜色,比如我opColor是红色,那么在TARGET模式下就会去判断我们的画布上是否有存在红色的地方,如果有,则把该区域“染”上一层我们画笔定义的颜色,否则不“染”色,而tolerance容差值则表示画布上的像素和我们定义的红色之间的差别该是多少的时候才去“染”的,比如当前画布有一个像素的色值是(200, 20, 13),而我们的红色值为(255, 0, 0),当tolerance容差值为255时,即便(200, 20, 13)并不等于红色值也会被“染”色,容差值越大“染”色范围越广反之则反,空说无凭我们来看看具体的实现和效果:
public class CustomView extends View { private Paint mPaint;// 画笔 private Context mContext;// 上下文环境引用 private Bitmap bitmap;// 位图 private AvoidXfermode avoidXfermode;// AV模式 private int x, y, w, h;// 位图绘制时左上角的起点坐标 public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; // 初始化画笔 initPaint(); // 初始化资源 initRes(context); } /** * 初始化画笔 */ private void initPaint() { // 实例化画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); /* * 当画布中有跟0XFFFFFFFF色不一样的地方时候才“染”色 */ avoidXfermode = new AvoidXfermode(0XFFFFFFFF, 0, AvoidXfermode.Mode.TARGET); } /** * 初始化资源 */ private void initRes(Context context) { // 获取位图 bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.a); /* * 计算位图绘制时左上角的坐标使其位于屏幕中心 * 屏幕坐标x轴向左偏移位图一半的宽度 * 屏幕坐标y轴向上偏移位图一半的高度 */ x = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 - bitmap.getWidth() / 2; y = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 - bitmap.getHeight() / 2; w = MeasureUtil.getScreenSize((Activity) mContext)[0] / 2 + bitmap.getWidth() / 2; h = MeasureUtil.getScreenSize((Activity) mContext)[1] / 2 + bitmap.getHeight() / 2; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 先绘制位图 canvas.drawBitmap(bitmap, x, y, mPaint); // “染”什么色是由我们自己决定的 mPaint.setARGB(255, 211, 53, 243); // 设置AV模式 mPaint.setXfermode(avoidXfermode); // 画一个位图大小一样的矩形 canvas.drawRect(x, y, w, h, mPaint); } }
在高于API 16的测试机上会得到一个矩形的色块(API 16+的都类似,改ROM和关闭了硬件加速的除外):
我们再用低于API 16(或高于API 16但关闭了硬件加速)的测试机运行就会得到另一个不同的效果:
大家可以看到,在我们的模式为TARGET容差值为0的时候此时只有当图片中像色颜色值为0XFFFFFFFF的地方才会被染色,而其他地方不会有改变
AvoidXfermode(0XFFFFFFFF, 0, AvoidXfermode.Mode.TARGET):
而当容差值为255的时候只要是跟0XFFFFFFFF有点接近的地方都会被染色
而另外一种模式
AvoidXfermode.Mode.AVOID
则与TARGET恰恰相反,TARGET是我们指定的颜色是否与画布的颜色一样,而AVOID是我们指定的颜色是否与画布不一样,其他的都与TARGET类似
AvoidXfermode(0XFFFFFFFF, 0, AvoidXfermode.Mode.AVOID):
当模式为AVOID容差值为0时,只有当图片中像素颜色值与0XFFFFFFFF完全不一样的地方才会被染色
AvoidXfermode(0XFFFFFFFF, 255, AvoidXfermode.Mode.AVOID):
当容差值为255时,只要与0XFFFFFFFF稍微有点不一样的地方就会被染色
那么这玩意究竟有什么用呢?比如说当我们只想在白色的区域画点东西或者想把白色区域的地方替换为另一张图片的时候就可以采取这种方式!
Xfermode的第二个子类
PixelXorXfermode
与AvoidXfermode一样也在API 16过时了,该类也提供了一个含参的构造方法PixelXorXfermode(int opColor),该类的计算实现很简单,从官方给出的计算公式来看就是:op ^ src ^ dst,像素色值的按位异或运算,如果大家感兴趣,可以自己用一个纯色去尝试,并自己计算异或运算的值是否与得出的颜色值一样,这里我就不讲了,Because it was deprecated and useless。
Xfermode的最后一个子类也是惟一一个没有过时且沿用至今的子类
PorterDuffXfermode
该类同样有且只有一个含参的构造方法PorterDuffXfermode(PorterDuff.Mode mode),这个PorterDuff.Mode大家看后是否会有些面熟,它跟上面我们讲ColorFilter时候用到的PorterDuff.Mode是一样的!麻雀虽小五脏俱全,虽说构造方法的签名列表里只有一个PorterDuff.Mode的参数,但是它可以实现很多酷毙的图形效果!!而PorterDuffXfermode就是图形混合模式的意思,其概念最早来自于SIGGRAPH的Tomas Proter和Tom Duff,混合图形的概念极大地推动了图形图像学的发展,延伸到计算机图形图像学像Adobe和AutoDesk公司著名的多款设计软件都可以说一定程度上受到影响,而我们PorterDuffXfermode的名字也来源于这俩人的人名组合PorterDuff,那PorterDuffXfermode能做些什么呢?我们先来看一张API DEMO里的图片:
这张图片从一定程度上形象地说明了图形混合的作用,两个图形一圆一方通过一定的计算产生不同的组合效果,在API中Android为我们提供了18种(比上图多了两种ADD和OVERLAY)模式:
来定义不同的混合效果,这18种模式Android还为我们提供了它们的计算方式比如LIGHTEN的计算方式为[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)],其中Sa全称为Source alpha表示源图的Alpha通道;Sc全称为Source color表示源图的颜色;Da全称为Destination alpha表示目标图的Alpha通道;Dc全称为Destination color表示目标图的颜色,细心的朋友会发现“[……]”里分为两部分,其中“,”前的部分为“Sa + Da - Sa*Da”这一部分的值代表计算后的Alpha通道而“,”后的部分为“Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)”这一部分的值代表计算后的颜色值,图形混合后的图片依靠这个矢量来计算ARGB的值,如果大家感兴趣可以查看维基百科中对Alpha合成的解释:http://en.wikipedia.org/wiki/Alpha_compositing。作为一个猿,我们不需要知道复杂的图形学计算但是一定要知道这些模式会为我们提供怎样的效果,当大家看到上面API DEMO给出的效果时一定会觉得PorterDuffXfermode其实就是简单的图形交并集计算,比如重叠的部分删掉或者叠加等等,事实上呢!PorterDuffXfermode的计算绝非是根据于此!上面我们也说了PorterDuffXfermode的计算是要根据具体的Alpha值和RGB值的,既然如此,我们就来看一个比API DEMO稍微复杂的例子来更有力地说明PorterDuffXfermode是如何工作而我们又能用它做些什么,在这个例子中我将用到两个带有Alpha通道的渐变图形Bitmap:
我们将在不同的模式下混合这两个Bitmap来看看这两个渐变色的颜色值在不同的混合模式下究竟发生了什么?先看看我们的测试代码:
@TargetApi(Build.VERSION_CODES.HONEYCOMB) public class PorterDuffView extends View { /* * PorterDuff模式常量 * 可以在此更改不同的模式测试 */ private static final PorterDuff.Mode MODE = PorterDuff.Mode.ADD; private static final int RECT_SIZE_SMALL = 400;// 左右上方示例渐变正方形的尺寸大小 private static final int RECT_SIZE_BIG = 800;// 中间测试渐变正方形的尺寸大小 private Paint mPaint;// 画笔 private PorterDuffBO porterDuffBO;// PorterDuffView类的业务对象 private PorterDuffXfermode porterDuffXfermode;// 图形混合模式 private int screenW, screenH;// 屏幕尺寸 private int s_l, s_t;// 左上方正方形的原点坐标 private int d_l, d_t;// 右上方正方形的原点坐标 private int rectX, rectY;// 中间正方形的原点坐标 public PorterDuffView(Context context, AttributeSet attrs) { super(context, attrs); // 实例化画笔并设置抗锯齿 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 实例化业务对象 porterDuffBO = new PorterDuffBO(); // 实例化混合模式 porterDuffXfermode = new PorterDuffXfermode(MODE); // 计算坐标 calu(context); } /** * 计算坐标 * * @param context * 上下文环境引用 */ private void calu(Context context) { // 获取包含屏幕尺寸的数组 int[] screenSize = MeasureUtil.getScreenSize((Activity) context); // 获取屏幕尺寸 screenW = screenSize[0]; screenH = screenSize[1]; // 计算左上方正方形原点坐标 s_l = 0; s_t = 0; // 计算右上方正方形原点坐标 d_l = screenW - RECT_SIZE_SMALL; d_t = 0; // 计算中间方正方形原点坐标 rectX = screenW / 2 - RECT_SIZE_BIG / 2; rectY = RECT_SIZE_SMALL + (screenH - RECT_SIZE_SMALL) / 2 - RECT_SIZE_BIG / 2; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 设置画布颜色为黑色以便我们更好地观察 canvas.drawColor(Color.BLACK); // 设置业务对象尺寸值计算生成左右上方的渐变方形 porterDuffBO.setSize(RECT_SIZE_SMALL); /* * 画出左右上方两个正方形 * 其中左边的的为src右边的为dis */ canvas.drawBitmap(porterDuffBO.initSrcBitmap(), s_l, s_t, mPaint); canvas.drawBitmap(porterDuffBO.initDisBitmap(), d_l, d_t, mPaint); /* * 将绘制操作保存到新的图层(更官方的说法应该是离屏缓存)我们将在1/3中学习到Canvas的全部用法这里就先follow me */ int sc = canvas.saveLayer(0, 0, screenW, screenH, null, Canvas.ALL_SAVE_FLAG); // 重新设置业务对象尺寸值计算生成中间的渐变方形 porterDuffBO.setSize(RECT_SIZE_BIG); // 先绘制dis目标图 canvas.drawBitmap(porterDuffBO.initDisBitmap(), rectX, rectY, mPaint); // 设置混合模式 mPaint.setXfermode(porterDuffXfermode); // 再绘制src源图 canvas.drawBitmap(porterDuffBO.initSrcBitmap(), rectX, rectY, mPaint); // 还原混合模式 mPaint.setXfermode(null); // 还原画布 canvas.restoreToCount(sc); } }
代码中我们使用到了View的离屏缓冲,也通俗地称之为层,这个概念很简单,我们在绘图的时候新建一个“层”,所有的绘制操作都在该层上而不影响该层以外的图像,比如代码中我们在绘制了画布颜色和左右上方两个方形后就新建了一个图层来绘制中间的大正方形,这个方形和左右上方的方形是在两个不同的层上的:
注:图中所显示色彩效果与我们的代码不同,上图只为演示图层概念
当我们绘制完成后要通过restore将所有缓冲(层)中的绘制操作还原到画布以结束绘制,具体关于画布的知识在自定义控件其实很简单1/3,这里就不多说了,下面我们看具体各种模式的计算效果
PS:Src为源图像,意为将要绘制的图像;Dis为目标图像,意为我们将要把源图像绘制到的图像……是不是感脚很拗口 = = !Fuck……意会意会~~
PorterDuff.Mode.ADD
计算方式:Saturate(S + D);Chinese:饱和相加
从计算方式和显示的结果我们可以看到,ADD模式简单来说就是对图像饱和度进行相加,这个模式在应用中不常用,我唯一一次使用它是通过代码控制RGB通道的融合生成图片。
PorterDuff.Mode.CLEAR
计算方式:[0, 0];Chinese:清除
清除图像,很好理解不扯了。
PorterDuff.Mode.DARKEN
计算方式:[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)];Chinese:变暗
这个模式计算方式目测很复杂,其实效果很好理解,两个图像混合,较深的颜色总是会覆盖较浅的颜色,如果两者深浅相同则混合,如图,黄色覆盖了红色而蓝色和青色因为是跟透明混合所以不变。细心的朋友会发现青色和黄色之间有一层类似橙色的过渡色,这就是混合的结果。在实际的测试中源图和目标图的DARKEN混合偶尔会有相反的结果比如红色覆盖了黄色,这源于Android对颜色值“深浅”的定义,我暂时没有在官方查到有关资料不知道是否与图形图像学一致。DARKEN模式的应用在图像色彩方面比较广泛我们可以利用其特性来获得不同的成像效果,这点与之前介绍的ColorFilter有点类似。
该模式处理过后,会感觉效果变暗,即进行对应像素的比较,取较暗值,如果色值相同则进行混合;
从算法上看,alpha值变大,色值上如果都不透明则取较暗值,非完全不透明情况下使用上面算法进行计算,受到源图和目标图对应色值和alpha值影响;
PorterDuff.Mode.DST
计算方式:[Da, Dc];Chinese:只绘制目标图像
如Chinese所说,很好理解。
PorterDuff.Mode.DST_ATOP
计算方式:[Sa, Sa * Dc + Sc * (1 - Da)];Chinese:在源图像和目标图像相交的地方绘制目标图像而在不相交的地方绘制源图像
PorterDuff.Mode.DST_IN
计算方式:[Sa * Da, Sa * Dc];Chinese:只在源图像和目标图像相交的地方绘制目标图像
PorterDuff.Mode.DST_OUT
计算方式:[Da * (1 - Sa), Dc * (1 - Sa)];Chinese:只在源图像和目标图像不相交的地方绘制目标图像
PorterDuff.Mode.DST_OVER
计算方式:[Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc];Chinese:在源图像的上方绘制目标图像
这个就不说啦,就是两个图片谁在上谁在下的意思
PorterDuff.Mode.LIGHTEN
计算方式:[Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)];Chinese:变亮
可以和 DARKEN 对比起来看,DARKEN 的目的是变暗,LIGHTEN 的目的则是变亮,如果在均完全不透明的情况下 ,色值取源色值和目标色值中的较大值
PorterDuff.Mode.MULTIPLY
计算方式:[Sa * Da, Sc * Dc];Chinese:正片叠底
该模式通俗的计算方式很简单,源图像素颜色值乘以目标图像素颜色值除以255即得混合后图像像素的颜色值,该模式在设计领域应用广泛,因为其特性黑色与任何颜色混合都会得黑色,在手绘的上色、三维动画的UV贴图绘制都有应用,具体效果大家自己尝试我就不说了
PorterDuff.Mode.OVERLAY
计算方式:未给出;Chinese:叠加
这个模式没有在官方的API DEMO中给出,谷歌也没有给出其计算方式,在实际效果中其对亮色和暗色不起作用,也就是说黑白色无效,它会将源色与目标色混合产生一种中间色,这种中间色生成的规律也很简单,如果源色比目标色暗,那么让目标色的颜色倍增否则颜色递减。
PorterDuff.Mode.SCREEN
计算方式:[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc];Chinese:滤色
计算方式我不解释了,滤色产生的效果我认为是Android提供的几个色彩混合模式中最好的,它可以让图像焦媃幻化,有一种色调均和的感觉:
PorterDuff.Mode.SRC
计算方式:[Sa, Sc];Chinese:显示源图
只绘制源图,SRC类的模式跟DIS的其实差不多就不多说了
PorterDuff.Mode.SRC_ATOP
计算方式:[Da, Sc * Da + (1 - Sa) * Dc];Chinese:在源图像和目标图像相交的地方绘制源图像,在不相交的地方绘制目标图像
PorterDuff.Mode.SRC_IN
计算方式:[Sa * Da, Sc * Da];Chinese:只在源图像和目标图像相交的地方绘制源图像
PorterDuff.Mode.SRC_OUT
计算方式:[Sa * (1 - Da), Sc * (1 - Da)];Chinese:只在源图像和目标图像不相交的地方绘制源图像
PorterDuff.Mode.SRC_OVER
计算方式:[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc];Chinese:在目标图像的顶部绘制源图像
PorterDuff.Mode.XOR
计算方式:[Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc];Chinese:在源图像和目标图像重叠之外的任何地方绘制他们,而在重叠的地方不绘制任何内容
一张详细的图,看了就明白
更多资料请参考
http://www.youtube.com/watch?v=duefsFTJXzc&feature=results_video&playnext=1&list=PL01724209851DF753
http://ssp.impulsetrain.com/porterduff.html
http://stackoverflow.com/questions/8280027/what-does-porterduff-mode-mean-in-android-graphics-what-does-it-do
Paint类中我们还有一个方法没讲
setShader(Shader shader)
这个方法呢其实也没有什么特别的,那么为什么我们要把它单独分离出来讲那么异类呢?难道它贿赂了我吗?显然不是的,哥视金钱如粪土(我的要求很低,只需要一克反物质即可)!怎么可能做出如此下三滥的事情!之所以要把这货单独拿出来是为了引出Android在图形变换中非常重要的一个类!这个类是什么呢?我也先不说,咱还是先来看看Shader:
Shader类呢也是个灰常灰常简单的类,它有五个子类,像PathEffect一样每个子类都实现了一种Shader,Shader在三维软件中我们称之为着色器,其作用嘛就像它的名字一样是来给图像着色的或者更通俗的说法是上色!这么说该懂了吧!再不懂去厕所哭去!这五个Shader里最异类的是BitmapShader,因为只有它是允许我们载入一张图片来给图像着色,那我们还是先来看看这个怪胎吧
BitmapShader
只有一个含参的构造方法BitmapShader (Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)而其他的四个兄弟姐妹呢都有两个!它只有一个蛋,又一魂谈!那好吧,我们来看看它是什么个效果,顺便呢也学习一下Shader的用法先,来看我们熟悉的代码:
public class ShaderView extends View { private static final int RECT_SIZE = 400;// 矩形尺寸的一半 private Paint mPaint;// 画笔 private int left, top, right, bottom;// 矩形坐上右下坐标 public ShaderView(Context context, AttributeSet attrs) { super(context, attrs); // 获取屏幕尺寸数据 int[] screenSize = MeasureUtil.getScreenSize((Activity) context); // 获取屏幕中点坐标 int screenX = screenSize[0] / 2; int screenY = screenSize[1] / 2; // 计算矩形左上右下坐标值 left = screenX - RECT_SIZE; top = screenY - RECT_SIZE; right = screenX + RECT_SIZE; bottom = screenY + RECT_SIZE; // 实例化画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); // 获取位图 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a); // 设置着色器 mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); } @Override protected void onDraw(Canvas canvas) { // 绘制矩形 canvas.drawRect(left, top, right, bottom, mPaint); } }
如果上面我们没有设置Shader:
mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
那么我们Draw出来的图像一定是一个位于屏幕正中黑色的正方形,但是我们设置了Shader后还是一样的吗?看看效果:
我靠!这什么玩意!罪过罪过!真是看不懂!别急,Shader.TileMode里有三种模式:CLAMP、MIRROR和REPETA,我们看看其他两种模式是什么效果呢:
mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR));
诶?这效果还能接受,我们还能看得出一点效果,说白了就是上下左右的镜像而已,那再看看REPETA:
mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
这个就更简单了,明显的一个重复效果,而REPEAT也就是重复的意思,同理MIRROR也就是镜像的意思,这个很好理解吧。那第一个CLAMP模式究竟特么的是什么东西呢?看效果根本看不出来,我们不妨换个思维,BitmapShader (Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)的第一个参数是位图这个很显然,而后两个参数则分别表示XY方向上的着色模式,既然可以分开设置,那么我们是不是可以这样设置一个Shader呢?
mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.MIRROR));
也就是说我们在X轴方向上采取CLAMP模式而Y轴方向上采取MIRROR模式,那么这样肯定是可行的撒:
大家可以看到图像分为两部分左边呢Y轴镜像了,而右边像是被拉伸了一样怪怪的!其实CLAMP的意思就是边缘拉伸的意思,比如上图中左边Y轴镜像了,而右边会紧挨着左边将图像边缘上的第一个像素沿X轴复制!产生一种被拉伸的效果!就像扯蛋,不过这里扯的不是蛋而是图像边缘的第一个像素,就是这么简单。但是!作为一个严谨的男人必须要又一个严谨的态度!这时我就会想,如果两种模式互换会怎样呢?比如:
mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.MIRROR, Shader.TileMode.CLAMP));
这样来看,应该是X轴会镜像而Y轴会拉伸对吧,看看效果:
这…………好像跟我们想象中的不大一样唉……是我们做错了吗?不是的,结合上一个例子大家有没有注意BitmapShader是先应用了Y轴的模式而X轴是后应用的!所以着色是先在Y轴拉伸了然后再沿着X轴重复对吧(阴笑ing……)?!
来看看另一个Shader叫做
LinearGradient
线性渐变,顾名思义这锤子玩意就是来画渐变的,实际上Shader的五个子类中除了上面我们说的那个怪胎,还有个变形金刚ComposeShader外其余三个都是渐变只是效果不同而已,而这个LinearGradient线性渐变一说大家估计都懂,先来看张效果图:
是不是秒懂了!恩,说明你头脑简单,这个实现也很简单,具体代码跟上面的BitmapShader一样只是把BitmapShader换成了LinearGradient而已:
mPaint.setShader(new LinearGradient(left, top, right, bottom, Color.RED, Color.YELLOW, Shader.TileMode.REPEAT));
上面我们提到过除了BitmapShader外其他子类都有两个构造方法,上面我们用到了
LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, Shader.TileMode tile)
这是LinearGradient最简单的一个构造方法,参数虽多其实很好理解x0和y0表示渐变的起点坐标而x1和y1则表示渐变的终点坐标,这两点都是相对于屏幕坐标系而言的,而color0和color1则表示起点的颜色和终点的颜色,这些即便是213也能懂 - - ……Shader.TileMode上面我们给的是REPEAT重复但是并没有任何效果,这时因为我们渐变的起点和终点都落在了图形的两端,整个渐变shader已经填充了图形所以不起作用,如果我们改改,把终点坐标变一下:
mPaint.setShader(new LinearGradient(left, top, right - RECT_SIZE, bottom - RECT_SIZE, Color.RED, Color.YELLOW, Shader.TileMode.REPEAT));
此时我们渐变终点坐标落在了图形的终点上,根据我们的REPEAT模式,会呈现一个渐变重复的效果:
仅仅两种颜色的渐变根本无法满足我们身体的欲望,太单调乏味!我们是不是可以定义多种颜色渐变呢?答案是必须的,LinearGradient的另一个构造方法
LinearGradient(float x0, float y0, float x1, float y1, int[] colors, float[] positions, Shader.TileMode tile)
就为我们实现了这么一个功能:
mPaint.setShader(new LinearGradient(left, top, right, bottom, new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, new float[] { 0, 0.1F, 0.5F, 0.7F, 0.8F }, Shader.TileMode.MIRROR));
前面四个参数也是定义坐标的不扯了colors是一个int型数组,我们用来定义所有渐变的颜色,positions表示的是渐变的相对区域,其取值只有0到1,上面的代码中我们定义了一个[0, 0.1F, 0.5F, 0.7F, 0.8F],意思就是红色到黄色的渐变起点坐标在整个渐变区域(left, top, right, bottom定义了渐变的区域)的起点,而终点则在渐变区域长度 * 10%的地方,而黄色到绿色呢则从渐变区域10%开始到50%的地方以此类推,positions可以为空:
mPaint.setShader(new LinearGradient(left, top, right, bottom, new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, null, Shader.TileMode.MIRROR));
为空时各种颜色的渐变将会均分整个渐变区域:
SweepGradient
的意思是梯度渐变,也称之为扫描式渐变,因为其效果有点类似雷达的扫描效果,他也有两个构造方法:
SweepGradient(float cx, float cy, int color0, int color1)
其实都跟LinearGradient差不多的,简直没什么可说的,直接上效果跳过无聊的讲解:
mPaint.setShader(new SweepGradient(screenX, screenY, Color.RED, Color.YELLOW));
RadialGradient
环形渲染,简单点就是个圆形中心向四周渐变的效果
RadialGradient (float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)
RadialGradient (float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)
ComposeShader
就是组合Shader的意思,顾名思义就是两个Shader组合在一起作为一个新Shader……老掉牙的剧情是吧!同样,这锤子玩意也有两个构造方法
ComposeShader (Shader shaderA, Shader shaderB, Xfermode mode)
ComposeShader (Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
两个都差不多的,只不过一个指定了只能用PorterDuff的混合模式而另一个只要是Xfermode下的混合模式都没问题!
实例
最上面的是BitmapShader效果图;第二排的左边是LinearGradient的效果图;第二排的右边是RadialGradient的效果图;第三排的左边是ComposeShader的效果图(LinearGradient与RadialGradient的混合效果);第三排的右边是SweepGradient的效果图。
MyView.Java源码 package com.example.android_imageshader; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ComposeShader; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.RadialGradient; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.SweepGradient; import android.graphics.drawable.BitmapDrawable; import android.view.View; @SuppressLint({ "DrawAllocation", "DrawAllocation", "DrawAllocation" }) public class MyView extends View { Bitmap mBitmap = null; //Bitmap对象 Shader mBitmapShader = null; //Bitmap渲染对象 Shader mLinearGradient = null; //线性渐变渲染对象 Shader mComposeShader = null; //混合渲染对象 Shader mRadialGradient = null; //环形渲染对象 Shader mSweepGradient = null; //梯度渲染对象 public MyView(Context context) { super(context); //加载图像资源 mBitmap = ((BitmapDrawable) getResources(). getDrawable(R.drawable.snow)).getBitmap(); //创建Bitmap渲染对象 mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.MIRROR); //创建线性渲染对象 int mColorLinear[] = {Color.RED, Color.GREEN, Color.BLUE, Color.WHITE}; mLinearGradient = new LinearGradient(0, 0, 100, 100, mColorLinear, null, Shader.TileMode.REPEAT); //创建环形渲染对象 int mColorRadial[] = {Color.GREEN, Color.RED, Color.BLUE, Color.WHITE}; mRadialGradient = new RadialGradient(350, 325, 75, mColorRadial, null, Shader.TileMode.REPEAT); //创建混合渲染对象 mComposeShader = new ComposeShader(mLinearGradient, mRadialGradient, PorterDuff.Mode.DARKEN); //创建梯形渲染对象 int mColorSweep[] = {Color.GREEN, Color.RED, Color.BLUE, Color.YELLOW, Color.GREEN}; mSweepGradient = new SweepGradient(370, 495, mColorSweep, null); } public void onDraw(Canvas canvas) { super.onDraw(canvas); Paint mPaint = new Paint(); canvas.drawColor(Color.GRAY); //背景置为灰色 //绘制Bitmap渲染的椭圆 mPaint.setShader(mBitmapShader); canvas.drawOval(new RectF(90, 20, 90+mBitmap.getWidth(), 20+mBitmap.getHeight()), mPaint); //绘制线性渐变的矩形 mPaint.setShader(mLinearGradient); canvas.drawRect(10, 250, 250, 400, mPaint); //绘制环形渐变的圆 mPaint.setShader(mRadialGradient); canvas.drawCircle(350, 325, 75, mPaint); //绘制混合渐变(线性与环形混合)的矩形 mPaint.setShader(mComposeShader); canvas.drawRect(10, 420, 250, 570, mPaint); //绘制梯形渐变的矩形 mPaint.setShader(mSweepGradient); canvas.drawRect(270, 420, 470, 570, mPaint); } }
注意BitmapShader是从画布的左上方开始着色,回到我们刚才的问题,这个着色方式必须是这样的么?显然不是!在Shader类中有一对setter和getter方法:setLocalMatrix(Matrix localM)和getLocalMatrix(Matrix localM)我们可以利用它们来设置或获取Shader的变换矩阵,比如上面的例子我还是绘制成一个边长为800的矩形:
public class ShaderView extends View { private static final int RECT_SIZE = 400;// 矩形尺寸的一半 private Paint mPaint;// 画笔 private int left, top, right, bottom;// 矩形坐上右下坐标 private int screenX, screenY; public ShaderView(Context context, AttributeSet attrs) { super(context, attrs); // 获取屏幕尺寸数据 int[] screenSize = MeasureUtil.getScreenSize((Activity) context); // 获取屏幕中点坐标 screenX = screenSize[0] / 2; screenY = screenSize[1] / 2; // 计算矩形左上右下坐标值 left = screenX - RECT_SIZE; top = screenY - RECT_SIZE; right = screenX + RECT_SIZE; bottom = screenY + RECT_SIZE; // 实例化画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 获取位图 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a); // 实例化一个Shader BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); // 实例一个矩阵对象 Matrix matrix = new Matrix(); // 设置矩阵变换 matrix.setTranslate(left, top); // 设置Shader的变换矩阵 bitmapShader.setLocalMatrix(matrix); // 设置着色器 mPaint.setShader(bitmapShader); // mPaint.setShader(new LinearGradient(left, top, right - RECT_SIZE, bottom - RECT_SIZE, Color.RED, Color.YELLOW, Shader.TileMode.MIRROR)); // mPaint.setShader(new LinearGradient(left, top, right, bottom, new int[] { Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE }, null, Shader.TileMode.MIRROR)); // mPaint.setShader(new SweepGradient(screenX, screenY, Color.RED, Color.YELLOW)); // mPaint.setShader(new SweepGradient(screenX, screenY, new int[] { Color.GREEN, Color.WHITE, Color.GREEN }, null)); } @Override protected void onDraw(Canvas canvas) { // 绘制矩形 canvas.drawRect(left, top, right, bottom, mPaint); // canvas.drawRect(0, 0, screenX * 2, screenY * 2, mPaint); } }
不一样的是我在给画笔设置着色器前为我们的着色器设置了一个变换矩阵,让我们的Shader依据自身的坐标→平移left个单位↓平移top个单位,也就是说原本shader的原点应该是画布(注意不是屏幕!这里只是刚好画布更屏幕重合了而已!切记!)的左上方[0,0]的位置,通过变换移至了[left,top]的位置,如果没问题,Shader此时应该是刚好是从我们矩形的左上方开始着色:
Matrix
Matrix是一个3 x 3的矩阵,他对图片的处理分为四个基本类型:
1、Translate————平移变换
2、Scale————缩放变换
3、Rotate————旋转变换
4、Skew————错切变换
在Android的API里对于每一种变换都提供了三种操作方式:set(用于设置Matrix中的值)、post(后乘,根据矩阵的原理,相当于左乘)、pre(先乘,相当于矩阵中的右乘)。默认时,这四种变换都是围绕(0,0)点变换的,当然可以自定义围绕的中心点,通常围绕中心点。
首先说说平移,在对图片处理的过程中,最常用的就是对图片进行平移操作,该方法为setTranslate(),平移意味着在x轴和y轴上简单地移动图像。setTranslate方法采用两个浮点数作为参数,表示在每个轴上移动的数量。第一个参数是图像将在x轴上移动的数量,而第二个参数是图像将在y轴上移动的数量。在x轴上使用正数进行平移将向右移动图像,而使用负数将向左移动图像。在y轴上使用正数进行平移将向下移动图像,而使用负数将向上移动图像。
再看缩放,Matrix类中另一个有用的方法是setScale方法。它采用两个浮点数作为参数,分别表示在每个轴上所产生的缩放量。第一个参数是x轴的缩放比例,而第二个参数是y轴的缩放比例。如:matrix.setScale(1.5f,1);
比较复杂的就是图片的旋转了,内置的方法之一是setRotate方法。它采用一个浮点数表示旋转的角度。围绕默认点(0,0),正数将顺时针旋转图像,而负数将逆时针旋转图像,其中默认点是图像的左上角,如:
Matrix matrix = new Matrix();
matrix.setRotate(15);
另外,也可以使用旋转的角度及围绕的旋转点作为参数调用setRotate方法。选择图像的中心点作为旋转点,如:
matrix.setRotate(15,bmp.getWidth()/2,bmp.getHeight()/2);
对于错切变换,在数学上又称为Shear mapping(可译为“剪切变换”)或者Transvection(缩并),它是一种比较特殊的线性变换。错切变换的效果就是让所有点的x坐标(或者y坐标)保持不变,而对应的y坐标(或者x坐标)则按比例发生平移,且平移的大小和该点到x轴(或y轴)的垂直距离成正比。错切变换,属于等面积变换,即一个形状在错切变换的前后,其面积是相等的。
对于程序中,一个特别有用的方法对是setScale和postTranslate,它们允许跨单个轴(或者两个轴)翻转图像。如果以一个负数缩放,那么会将该图像绘制到坐标系统的负值空间。由于(0,0)点位于左上角,使用x轴上的负数会导致向左绘制图像。因此我们需要使用postTranslate方法,将图像向右移动,如:
matrix.setScale(-1, 1);
matrix.postTranslate(bmp.getWidth(),0);
可以在y轴上做同样的事情,翻转图像以使其倒置。通过将图像围绕两个轴上的中心点旋转180°,可以实现相同的效果,如
matrix.setScale(1, -1);
matrix.postTranslate(0, bmp.getHeight());
注意每一次setXXX方法都会把前面的变化重置
举例:
1.matrix.preScale(0.5f, 1);
2.matrix.preTranslate(10, 0);
3.matrix.postScale(0.7f, 1);
4.matrix.postTranslate(15, 0);
等价于:
translate(10, 0) -> scale(0.5f, 1) -> scale(0.7f, 1) -> translate(15, 0)
注意:后调用的pre操作先执行,而后调用的post操作则后执行。
set方法一旦调用即会清空之前matrix中的所有变换,例如:
1.matrix.preScale(0.5f, 1);
2.matrix.setScale(1, 0.6f);
3.matrix.postScale(0.7f, 1);
4.matrix.preTranslate(15, 0);
等价于
translate(15, 0) -> scale(1, 0.6f) -> scale(0.7f, 1)
matrix.preScale (0.5f, 1)将不起作用。
可以使用Matrix的getValues(float[])方法去验证自己不确定的东西,同时呢,我们也可以使用Matrix的setValues(float[])方法来直接给Matrix设置一个矩阵数组
好了,对Matrix的一个简单介绍就到这里,正如我所说,Matrix的应用是相当广泛的,不仅仅是在我们的Shader,我们的canvas也有setMatrix(matrix)方法来设置矩阵变换,更常见的是在ImageView中对ImageView进行变换,当我们手指在屏幕上划过一定的距离后根据这段距离来平移我们的控件,根据两根手指之间拉伸的距离和相对于上一次旋转的角度来缩放旋转我们的图片:
public class MatrixImageView extends ImageView { private static final int MODE_NONE = 0x00123;// 默认的触摸模式 private static final int MODE_DRAG = 0x00321;// 拖拽模式 private static final int MODE_ZOOM = 0x00132;// 缩放or旋转模式 private int mode;// 当前的触摸模式 private float preMove = 1F;// 上一次手指移动的距离 private float saveRotate = 0F;// 保存了的角度值 private float rotate = 0F;// 旋转的角度 private float[] preEventCoor;// 上一次各触摸点的坐标集合 private PointF start, mid;// 起点、中点对象 private Matrix currentMatrix, savedMatrix;// 当前和保存了的Matrix对象 private Context mContext;// Fuck…… public MatrixImageView(Context context, AttributeSet attrs) { super(context, attrs); this.mContext = context; // 初始化 init(); } /** * 初始化 */ private void init() { /* * 实例化对象 */ currentMatrix = new Matrix(); savedMatrix = new Matrix(); start = new PointF(); mid = new PointF(); // 模式初始化 mode = MODE_NONE; /* * 设置图片资源 */ Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.mylove); bitmap = Bitmap.createScaledBitmap(bitmap, MeasureUtil.getScreenSize((Activity) mContext)[0], MeasureUtil.getScreenSize((Activity) mContext)[1], true); setImageBitmap(bitmap); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN:// 单点接触屏幕时 savedMatrix.set(currentMatrix); start.set(event.getX(), event.getY()); mode = MODE_DRAG; preEventCoor = null; break; case MotionEvent.ACTION_POINTER_DOWN:// 第二个点接触屏幕时 preMove = calSpacing(event); if (preMove > 10F) { savedMatrix.set(currentMatrix); calMidPoint(mid, event); mode = MODE_ZOOM; } preEventCoor = new float[4]; preEventCoor[0] = event.getX(0); preEventCoor[1] = event.getX(1); preEventCoor[2] = event.getY(0); preEventCoor[3] = event.getY(1); saveRotate = calRotation(event); break; case MotionEvent.ACTION_UP:// 单点离开屏幕时 case MotionEvent.ACTION_POINTER_UP:// 第二个点离开屏幕时 mode = MODE_NONE; preEventCoor = null; break; case MotionEvent.ACTION_MOVE:// 触摸点移动时 /* * 单点触控拖拽平移 */ if (mode == MODE_DRAG) { currentMatrix.set(savedMatrix); float dx = event.getX() - start.x; float dy = event.getY() - start.y; currentMatrix.postTranslate(dx, dy); } /* * 两点触控拖放旋转 */ else if (mode == MODE_ZOOM && event.getPointerCount() == 2) { float currentMove = calSpacing(event); currentMatrix.set(savedMatrix); /* * 指尖移动距离大于10F缩放 */ if (currentMove > 10F) { float scale = currentMove / preMove; currentMatrix.postScale(scale, scale, mid.x, mid.y); } /* * 保持两点时旋转 */ if (preEventCoor != null) { rotate = calRotation(event); float r = rotate - saveRotate; currentMatrix.postRotate(r, getMeasuredWidth() / 2, getMeasuredHeight() / 2); } } break; } setImageMatrix(currentMatrix); return true; } /** * 计算两个触摸点间的距离 */ private float calSpacing(MotionEvent event) { float x = event.getX(0) - event.getX(1); float y = event.getY(0) - event.getY(1); return (float) Math.sqrt(x * x + y * y); } /** * 计算两个触摸点的中点坐标 */ private void calMidPoint(PointF point, MotionEvent event) { float x = event.getX(0) + event.getX(1); float y = event.getY(0) + event.getY(1); point.set(x / 2, y / 2); } /** * 计算旋转角度 * * @param 事件对象 * @return 角度值 */ private float calRotation(MotionEvent event) { double deltaX = (event.getX(0) - event.getX(1)); double deltaY = (event.getY(0) - event.getY(1)); double radius = Math.atan2(deltaY, deltaX); return (float) Math.toDegrees(radius); } }
记得在xml中设置我们MatrixImageView的scaleType="matrix":
<com.aigestudio.customviewdemo.views.MatrixImageView android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="matrix" />