爱哥地址:http://blog.csdn.net/aigestudio/article/details/41799811
着色器。有五个子类。五个子类中最异类的是BitmapShader,因为只有它允许我们载入一张图片来给图像着色。
BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMod tileY)
第二个和第三个参数是用来指定模式的。有三种模式:CLAMP、MIRROR和REPETA。后两种模式效果图:
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); } }
效果图:
public class BrickView extends View { private Paint mFillPaint, mStrokePaint;// 填充和描边的画笔 private BitmapShader mBitmapShader;// Bitmap着色器 private float posX, posY;// 触摸点的XY坐标 public BrickView(Context context, AttributeSet attrs) { super(context, attrs); // 初始化画笔 initPaint(); } /** * 初始化画笔 */ private void initPaint() { /* * 实例化描边画笔并设置参数 */ mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); mStrokePaint.setColor(0xFF000000); mStrokePaint.setStyle(Paint.Style.STROKE); mStrokePaint.setStrokeWidth(5); // 实例化填充画笔 mFillPaint = new Paint(); /* * 生成BitmapShader */ Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.brick); mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); mFillPaint.setShader(mBitmapShader); } @Override public boolean onTouchEvent(MotionEvent event) { /* * 手指移动时获取触摸点坐标并刷新视图 */ if (event.getAction() == MotionEvent.ACTION_MOVE) { posX = event.getX(); posY = event.getY(); invalidate(); } return true; } @Override protected void onDraw(Canvas canvas) { // 设置画笔背景色 canvas.drawColor(Color.DKGRAY); /* * 绘制圆和描边 */ canvas.drawCircle(posX, posY, 300, mFillPaint); canvas.drawCircle(posX, posY, 300, mStrokePaint); } }
线性渐变,是用来画渐变的。
LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, Shader.TileMode tile)
x0和y0表示渐变的起点坐标,x1和y1表示渐变的终点坐标,这两点都是相对于屏幕坐标系而言的。color0和color1表示起点颜色和终点颜色。当使用REPATE模式时的效果图:
上面只有两种颜色渐变,那我们能不能定义多种颜色渐变呢?看另一个构造函数
LinearGradient(float x0, float y0, float x1, float y1, int[] colors, float[] positions, Shader.TileMode tile)colors数组定义所有渐变的颜色,positions表示渐变的相对区域,其取值只有0到1,。比如定义一个[0, 0.1f, 0.5f, 0.7f]表示第一个颜色的渐变起点坐标在整个区域(left, top, right, bottom定义的渐变区域)的起点,而终点则在渐变区域长度*10%的地方,而第二个颜色则从渐变区域10%到50%的地方以此类推。positions为空时,表示各种颜色渐变会均分整个渐变区域。
我们可以通过线性渐变和混合模式一起制作我们常见的图片倒影效果:
public class ReflectView extends View { private Bitmap mSrcBitmap, mRefBitmap;// 位图 private Paint mPaint;// 画笔 private PorterDuffXfermode mXfermode;// 混合模式 private int x, y;// 位图起点坐标 public ReflectView(Context context, AttributeSet attrs) { super(context, attrs); // 初始化资源 initRes(context); } /* * 初始化资源 */ private void initRes(Context context) { // 获取源图 mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.gril); // 实例化一个矩阵对象 Matrix matrix = new Matrix(); matrix.setScale(1F, -1F); // 生成倒影图 mRefBitmap = Bitmap.createBitmap(mSrcBitmap, 0, 0, mSrcBitmap.getWidth(), mSrcBitmap.getHeight(), matrix, true); int screenW = MeasureUtil.getScreenSize((Activity) context)[0]; int screenH = MeasureUtil.getScreenSize((Activity) context)[1]; x = screenW / 2 - mSrcBitmap.getWidth() / 2; y = screenH / 2 - mSrcBitmap.getHeight() / 2; // ……………………………… mPaint = new Paint(); mPaint.setShader(new LinearGradient(x, y + mSrcBitmap.getHeight(), x, y + mSrcBitmap.getHeight() + mSrcBitmap.getHeight() / 4, 0xAA000000, Color.TRANSPARENT, Shader.TileMode.CLAMP)); // ……………………………… mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); } @Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); canvas.drawBitmap(mSrcBitmap, x, y, null); int sc = canvas.saveLayer(x, y + mSrcBitmap.getHeight(), x + mRefBitmap.getWidth(), y + mSrcBitmap.getHeight() * 2, null, Canvas.ALL_SAVE_FLAG); canvas.drawBitmap(mRefBitmap, x, y + mSrcBitmap.getHeight(), null); mPaint.setXfermode(mXfermode); canvas.drawRect(x, y + mSrcBitmap.getHeight(), x + mRefBitmap.getWidth(), y + mSrcBitmap.getHeight() * 2, mPaint); mPaint.setXfermode(null); canvas.restoreToCount(sc); } }
梯度渐变,也称为扫描式渐变,因其效果有点类似雷达的扫描效果。
</pre><pre name="code" class="java">SweepGradient(float cx, float cy, int color0, int color1)
SweepGradient(float cx, float cy, int[] colors, float[] positions)
使用
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));
对应效果图:
径向渐变。就是圆形中心向四周渐变的效果。
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)
模拟单反相机的暗角效果,压暗图片周围的颜色亮度提亮中心,让整张图片的中心突出来。比如之前提到的BlurMaskFilter向内模糊可得到类似效果,但是BlurMaskFilter计算出来的像素太生硬,这里用RadialGradient实现下:
public class DreamEffectView extends View { private Paint mBitmapPaint, mShaderPaint;// 位图画笔和Shader图形的画笔 private Bitmap mBitmap;// 位图 private PorterDuffXfermode mXfermode;// 图形混合模式 private int x, y;// 位图起点坐标 private int screenW, screenH;// 屏幕宽高 public DreamEffectView(Context context, AttributeSet attrs) { super(context, attrs); // 初始化资源 initRes(context); // 初始化画笔 initPaint(); } /** * 初始化资源 * * @param context * 丢你螺母 */ private void initRes(Context context) { // 获取位图 mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.gril); // 实例化混合模式 mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN); screenW = MeasureUtil.getScreenSize((Activity) context)[0]; screenH = MeasureUtil.getScreenSize((Activity) context)[1]; x = screenW / 2 - mBitmap.getWidth() / 2; y = screenH / 2 - mBitmap.getHeight() / 2; } /** * 初始化画笔 */ private void initPaint() { // 实例化画笔 mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 去饱和、提亮、色相矫正 mBitmapPaint.setColorFilter(new ColorMatrixColorFilter(new float[] { 0.8587F, 0.2940F, -0.0927F, 0, 6.79F, 0.0821F, 0.9145F, 0.0634F, 0, 6.79F, 0.2019F, 0.1097F, 0.7483F, 0, 6.79F, 0, 0, 0, 1, 0 })); // 实例化Shader图形的画笔 mShaderPaint = new Paint(); // 设置径向渐变,渐变中心当然是图片的中心也是屏幕中心,渐变半径我们直接拿图片的高度但是要稍微小一点 // 中心颜色为透明而边缘颜色为黑色 mShaderPaint.setShader(new RadialGradient(screenW / 2, screenH / 2, mBitmap.getHeight() * 7 / 8, Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP)); } @Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); // 新建图层 int sc = canvas.saveLayer(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), null, Canvas.ALL_SAVE_FLAG); // 绘制混合颜色 canvas.drawColor(0xcc1c093e); // 设置混合模式 mBitmapPaint.setXfermode(mXfermode); // 绘制位图 canvas.drawBitmap(mBitmap, x, y, mBitmapPaint); // 还原混合模式 mBitmapPaint.setXfermode(null); // 还原画布 canvas.restoreToCount(sc); // 绘制一个跟图片大小一样的矩形 canvas.drawRect(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), mShaderPaint); } }前后效果图:
组合Shader。就是两个Shader组合到一起。
ComposeShader (Shader shaderA, Shader shaderB, Xfermode mode)
ComposeShader (Shader shaderA, Shader shaderB, PorterDuff.Mode mode)区别是一个用PorterDuff的混合模式,而另一个用XferMode的混合模式。
上面我们为美女图片加暗角时只是单纯的使用了径向渐变,实际效果并不好,因为我们的径向渐变是个圆形的,但我们的图片实际上是个竖向矩形的,直接往上面“盖”一个径向渐变实际效果简而言之应该是这样:
可见渐变坡度太缓了,能不能改下让它拉伸下变成一个竖向的椭圆形呢?比如下图这样的:
public class DreamEffectView extends View { private Paint mBitmapPaint, mShaderPaint;// 位图画笔和Shader图形的画笔 private Bitmap mBitmap, darkCornerBitmap;// 源图的Bitmap和我们自己画的暗角Bitmap private PorterDuffXfermode mXfermode;// 图形混合模式 private int x, y;// 位图起点坐标 private int screenW, screenH;// 屏幕宽高 public DreamEffectView(Context context, AttributeSet attrs) { super(context, attrs); // 初始化资源 initRes(context); // 初始化画笔 initPaint(); } /** * 初始化资源 * * @param context * 丢你螺母 */ private void initRes(Context context) { // 获取位图 mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.gril); // 实例化混合模式 mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN); screenW = MeasureUtil.getScreenSize((Activity) context)[0]; screenH = MeasureUtil.getScreenSize((Activity) context)[1]; x = screenW / 2 - mBitmap.getWidth() / 2; y = screenH / 2 - mBitmap.getHeight() / 2; } /** * 初始化画笔 */ private void initPaint() { // 实例化画笔 mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 去饱和、提亮、色相矫正 mBitmapPaint.setColorFilter(new ColorMatrixColorFilter(new float[] { 0.8587F, 0.2940F, -0.0927F, 0, 6.79F, 0.0821F, 0.9145F, 0.0634F, 0, 6.79F, 0.2019F, 0.1097F, 0.7483F, 0, 6.79F, 0, 0, 0, 1, 0 })); // 实例化Shader图形的画笔 mShaderPaint = new Paint(); // 根据我们源图的大小生成暗角Bitmap darkCornerBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888); // 将该暗角Bitmap注入Canvas Canvas canvas = new Canvas(darkCornerBitmap); // 计算径向渐变半径 float radiu = canvas.getHeight() * (2F / 3F); // 实例化径向渐变 RadialGradient radialGradient = new RadialGradient(canvas.getWidth() / 2F, canvas.getHeight() / 2F, radiu, new int[] { 0, 0, 0xAA000000 }, new float[] { 0F, 0.7F, 1.0F }, Shader.TileMode.CLAMP); // 实例化一个矩阵 Matrix matrix = new Matrix(); // 设置矩阵的缩放 matrix.setScale(canvas.getWidth() / (radiu * 2F), 1.0F); // 设置矩阵的预平移 matrix.preTranslate(((radiu * 2F) - canvas.getWidth()) / 2F, 0); // 将该矩阵注入径向渐变 radialGradient.setLocalMatrix(matrix); // 设置画笔Shader mShaderPaint.setShader(radialGradient); // 绘制矩形 canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mShaderPaint); } @Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); // 新建图层 int sc = canvas.saveLayer(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), null, Canvas.ALL_SAVE_FLAG); // 绘制混合颜色 canvas.drawColor(0xcc1c093e); // 设置混合模式 mBitmapPaint.setXfermode(mXfermode); // 绘制位图 canvas.drawBitmap(mBitmap, x, y, mBitmapPaint); // 还原混合模式 mBitmapPaint.setXfermode(null); // 还原画布 canvas.restoreToCount(sc); // 绘制我们画好的径向渐变图 canvas.drawBitmap(darkCornerBitmap, x, y, null); } }再看效果图:
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); // 设置着色器 mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); // 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); } }当上面绘制矩形用的是canvas.drawRect(left, top, right, bottom, mPaint);时,显示效果如下:
而当使用canvas.drawRect(0, 0, screenX*2, screenY*2, mPaint);时,显示效果如下:
第一张图只是在屏幕中间画出一个矩形,第二张图是在全屏幕上画出一个矩形,其他都一样,可以看出第一张图中显示的其实是第二张图的中间部分,BitmapShader是从画布的左上方开始着色,然后将图像的边缘拉伸。那怎么改变这种着色方式呢?由此引出另一个很重要的类Matrix。
接着上面绘制图形,我们使用Matrix:
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个单位。
除了平移外,缩放、旋转、错切、透视都是需要一个中心点作为参考的,如果没有平移,默认图形的[0, 0]点,平移只需要指定移动的距离即可,平移操作会改变中心点的位置!这点非常重要!记住了!
大家在理解Matrix时要把它想象成一个容器,什么容器呢?存放我们变化信息的容器,Matrix的所有方法都是针对其自身的!!!当我们把所有的变换操作做完后再“一次性地”注入我们想要的地方。
android提供了很多setXXX、preXXX和postXXX方法就是Android为我们封装好的针对不同运算的“档位”,那该怎么用呢?
// 实例一个矩阵对象 Matrix matrix = new Matrix(); // 设置矩阵变换 matrix.setTranslate(500, 500); // 设置Shader的变换矩阵 bitmapShader.setLocalMatrix(matrix); // 设置着色器 mPaint.setShader(bitmapShader);
// 设置矩阵变换 matrix.setTranslate(500, 500); matrix.setRotate(5);
其实是这样,在我们new了一个Matrix对象后,这个Matrix对象中已经就为我们封装了一组原始数据:
float[]{ 1, 0, 0 0, 1, 0 0, 0, 1 }而我们的setXXX()执行的操作是把原本Matrix对象中的数据重置,重新设置新的数据:
matrix.setTranslate(500, 500);后数据变为:
float[]{ 1, 0, 500 0, 1, 500 0, 0, 1 }而如果再旋转呢?比如我们上面的:
matrix.setTranslate(500, 500); matrix.setRotate(5);那旋转的数据就会直接覆盖掉我们平移的数据:
float[]{ cos, sin, 0 sin, cos, 0 0, 0, 1 }
3)preXXX和postXXX
再来看看另外的两个方法:matrix.setTranslate(500, 500); matrix.preRotate(5);和
matrix.setTranslate(500, 500); matrix.postRotate(5);
好像没啥区别吧?其实这是一个谁先谁后的问题,preXXX和postXXX我们之前说过一个是前乘一个是后乘,那么具体表现是怎样的呢?非常简单,比如我有如下代码:
matrix.preScale(0.5f, 1); matrix.setScale(1, 0.6f); matrix.postScale(0.7f, 1); matrix.preTranslate(15, 0);那么Matrix的计算过程即为:translate(15, 0)->scale(1, 0.6f)->scale(0.7f, 1),因为set会重置数据,所以第一句代码直接忽略了。同样的,对于类似的变换:
matrix.preScale(0.5f, 1); matrix.preTranslate(10, 0); matrix.postScale(0.7f, 1); matrix.postTranslate(15, 0);其计算过程为:translate(10, 0)->scale(0.5f, 1)->scale(0.7f, 1)->translate(15, 0)。
那么对于上图的计算结果是一样的吗?这里我教给大家一个方法去验证,Matrix有一个getValues方法可获取当前Matrix的变换浮点数组,即我们之前说的矩阵:
/* * 新建一个9个单位长度的浮点数组 * 因为我们的Matrix矩阵是9个单位长的对吧 */ float[] fs = new float[9]; // 将从matrix中获取到的浮点数组装载进我们的fs里 matrix.getValues(fs); Log.d("Aige", Arrays.toString(fs));// 输出看看呗!