android.graphics.Shader

本文详细探讨了Android中Shader着色器的基本概念、成员、使用方法及其实现案例,包括BitmapShader、LinearGradient、RadialGradient、SweepGradient和ComposeShader等子类的功能与应用。通过实例展示了如何利用Shader着色器实现纹理、渐变色、放射状颜色分布、螺旋式颜色扩散以及复合着色器等效果,为开发者提供了丰富的视觉呈现手段。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        Shader我们不妨把它翻译成着色器,它是一个基本类,用来在进行绘制操作时提供颜色,它的子类通过函数paint.setShader(shader)将自身安装在paint当中,然后paint在画任何东西(除了Bitmap)的时候就会在该Shader中取颜色。我们不妨将canvas看成是画布,paint看成是画笔,画笔是要沾取颜料才能画画的,没有设置Shader的时候,画笔是在Color中沾取颜色的,每次沾取一个颜色,想换别的颜色还得重新沾。设置了Shader之后,画笔就在Shader中自动沾取颜色,每画一个点都跟Shader中对应的颜色一样。这要涉及到如何对应的问题。

      先看Shader中的成员,首先它有个内部枚举类,Shader.TileMode,该枚举有三个值,CLAMP,MIRROR,REPEAT,看下sdk中对这三个值的解释分别是,

CLAMP   重复Shader边缘的颜色,当paint要画超出了Shader范围的地方时。(这个paint是指安装了Shader的paint)

MIRROR 水平和垂直的使用Shader的镜像图片,这样使图片依然是连接的。

REPEAT  重复的使用Shader的图片。

在实际使用中我们发现其实这三个模式都是当paint要画超出了Shader范围的地方时,paint该如何取色,第一个是反复在shader的边缘取颜色,第二个是从shader的右边往左边取色或者从下边往上边取色,就是反方向开始取色。第三个是继续从shader的左边往右边或者上边往下边取色。

    Shader里边有三个函数,

public Shader()
public boolean getLocalMatrix(Matrix localM)
public void setLocalMatrix(Matrix localM)

第一个是构造函数,第二个是获取shader中的matrix,如果获取的不是标准matrix,那么就返回true,第三个是设置一个matrix给shader,如果matrix为null,那么就设置一个标准矩阵给shader。

下面我们来看shader的5个子类,

 

BitmapShader

该类的简介是使用该着色器可以使图片画起来有纹理(texture,注意这个词,很多安卓游戏引擎里边画图片就是做了Texture这么一个类),它可以设置重复模式和镜像模式。

该类剩下的内容就是一个构造函数,再也没别的什么介绍了,

public BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)

bitmap是该着色器中要用到的图片,tileX表示在x方向要选择何种模式,tileY表示在y方向要选择何种模式。

看下如何应用吧

 Bitmap b1,b2; 
 BitmapShader bs1;

b1=BitmapFactory.decodeResource(this.getContext().getResources(), R.drawable.tx);

bs1=new BitmapShader(b1,Shader.TileMode.MIRROR,Shader.TileMode.MIRROR);

canvas.drawColor(Color.WHITE);

 Paint paint=new Paint();
 paint.setShader(bs1);
 canvas.drawRect(0, 0, 240, 300, paint);

选择的图片尺寸是144x192,这时候可以看到在x轴超出144的地方画的内容跟<144的地方成镜像显示,两者结合无缝,y轴也是如此,当选择Shader.TileMode.REPEAT模式的时候在x轴超出144的地方和<144的地方画的内容是一样的,中间明显有根线。选择Shader.TileMode.CLAMP时,超出144的地方就全是144那个位置的颜色了。

当然你也可以画别的尺寸的别的形状的东东,比如canvas.drawCircle(100f, 100f, 30f, paint);画出来的就是该图片在对应位置的显示,显示的是一个圆形的图片,有锯齿,不过只要设置paint.setAntiAlias(true);锯齿就没了。这样的话可以通过bitmapshader画出任何你想要的图片的部分,不像BitmapRegionDecoder只能截取矩形,比canvas的clip也要灵活很多。当然bitmapshader还有很多用法,跟ShapeDrawable连在一起用的时候也比较多.

LinearGradient

这是将颜色线性分布在一条线上的shader,有两个初始化函数

public LinearGradient(float x0, float y0, float x1, float y1, int[] colors, float[] positions,Shader.TileMode tile)
public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1,Shader.TileMode tile)

x0,y0起始位置,x1,y1终点位置,colors需要铺洒在直线上的颜色数组,positions表示colors中的颜色各自占的比重,如果为null,则colors的颜色会均匀的洒在直线上。

tile还是表示shader模式,不过这个比起bitmapshader没那么好理解了,就是画布上的点颜色如何与shader对应,超出shader的范围又该如何重复或者镜像。

当直线是与x轴或者y轴平行的话这个也很好理解,当不平行的时候,我们试着画一个全屏矩形,可以发现颜色分布成三角形似的分布,同一个斜率的直线颜色一样。调用举例

 int colors[]=new int[]{Color.RED,Color.GREEN,Color.BLUE};
 float positions[]=new float[]{0.1f,0.2f,0.7f};
// LinearGradient lg=new LinearGradient(0,0,20,100,Color.RED,Color.GREEN,Shader.TileMode.REPEAT);
 LinearGradient lg=new LinearGradient(0,0,20,100,colors,positions,Shader.TileMode.REPEAT); 
 Paint paint=new Paint();
 paint.setShader(lg);
 paint.setAntiAlias(true);
 canvas.drawRect(0, 0, 240, 320, paint);

RadialGradient

这个是着色器以一个原点为中心,放射性的向周围铺上各种颜色,每一个以原点为中心,相同半径的圆周线颜色是一样的,初始化函数照样有两个

public RadialGradient (float x, float y, float radius, int[] colors, float[] positions, Shader.TileMode tile)
public RadialGradient (float x, float y, float radius, int color0, int color1, Shader.TileMode tile)

且看一个例子吧

 RadialGradient rg=new RadialGradient(50.f,50.f,50.f,Color.RED,Color.GREEN,Shader.TileMode.CLAMP);

 Paint paint=new Paint();
 paint.setShader(rg);

 paint.setAntiAlias(true);
 canvas.drawCircle(50.f, 50.f, 50.f, paint);

这时候我们可以看到显示的图片,并且可以看到颜色如何分布,就是感觉怪怪的,貌似这样的图形没什么用。只要改变下两个颜色的值和坐标位置,就可发现用它可以画出球形来:

    int red = (int)(100 + Math.random() * 155);
    int green = (int)(100 + Math.random() * 155);
    int blue = (int)(100 + Math.random() * 155);
    int color = 0xff000000 | red << 16 | green << 8 | blue;
    int darkColor = 0xff000000 | red/4 << 16 | green/4 << 8 | blue/4;   
 RadialGradient rg=new RadialGradient(37.5f,12.5f,50.f,color,darkColor,Shader.TileMode.CLAMP);

 Paint paint=new Paint();
 paint.setShader(rg);

 paint.setAntiAlias(true);
 canvas.drawCircle(50.f, 25.f, 50.f, paint);

这时候画出来的图形可以明显看出是个球。

SweepGradient

该shader的作用是选定一个中心点,从中心点像四周均匀的扩散颜色值。与RadialGradient的扩散不一样,它虽然也是以圆周的方式扩散,但是每条圆周线上均匀的分布着选中的那几种颜色,除了均匀分布,当然也可以以选定的比重分布

两个初始化函数

public SweepGradient (float cx, float cy, int[] colors, float[] positions)
public SweepGradient (float cx, float cy, int color0, int color1)

它的初始化只有中心点,没有半径,就是以中心点向四周无线扩散。positions还是用于设置各自颜色比重,范围[0,1],如果里边的值都一样,会得到不想得到的效果,最好设置成null,这样函数会自动排铺颜色。看应用:

 SweepGradient sg=new SweepGradient(100,100,Color.RED,Color.GREEN);

 Paint paint=new Paint();
 paint.setShader(sg);

 paint.setAntiAlias(true);
 canvas.drawCircle(100.f, 100.f, 50.f, paint);

可以看到显示效果是绿色沿着圆周慢慢的转化到红色,红绿之间一条明显的线分割开。之前说到Shader类的时候看到有两个函数,可以看出Shader有个matrix,那么这个matrix是用来干什么的呢,在SweepGradient例子中用了下,发现它就是用来改变Shader的点的坐标的,Shader中的坐标都会经过一个matrix的变换后再应用到paint中,

我设置的是translate改变,加了三句代码

 Matrix matirx=new Matrix();

 matirx.setTranslate(20, 20);
 sg.setLocalMatrix(matirx);

ComposeShader

这是一个联合Shader,将两个Shader以Xfermode或者PorterDuff.Mode的模式联合在一起组成一个着色器,看初始函数

public ComposeShader (Shader shaderA, Shader shaderB, Xfermode mode)
public ComposeShader (Shader shaderA, Shader shaderB, PorterDuff.Mode mode)

参数很明朗,一看就知,写个例子应用下吧

 SweepGradient sg=new SweepGradient(100,100,Color.RED,Color.GREEN);

 LinearGradient lg=new LinearGradient(0,0,20,100,Color.RED,Color.GREEN,Shader.TileMode.REPEAT);

ComposeShader cs=new ComposeShader(lg,sg,PorterDuff.Mode.SRC_ATOP);

 Paint paint=new Paint();
 paint.setShader(cs);

 paint.setAntiAlias(true);
 canvas.drawCircle(100.f, 100.f, 50.f, paint);

这样画出来的东东跟单独设置sg的图貌似一样啊,而且我把sg和lg的位置换了下发现画的图就跟单独用lg的图一样了,我看初始化函数里边对参数的解释是shaderA在模式里边被看成dst,shaderB被看成src,看来谁在后边就像谁啊,不过也可能是模式选择的问题。

顺带说说PorterDuff.Mode这个东东,原来在colorfilter里边用过,当时没怎么明白是怎么回事,这回算懂点了,它是有两个颜色通过该mode来进行变换,比如dst,src两种颜色,把两种颜色都拆分成 (Da,Dc),(Sa,Sc)前面表示alpha值,后边表示颜色值,都是int型,然后经过设置的变换后比如[Sa, Sa * Dc + Sc * (1 - Da)],得到新的alpha,color,经过组合后画出在画布上。  

import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Camera; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; import android.graphics.Shader; import android.util.AttributeSet; import android.view.View; import android.view.animation.LinearInterpolator; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; /** * @Description: 网格线 * @Author: alex * @CreateDate: 2025/7/9 16:03 */ public class LyricGridView extends View { private Paint gridPaint; private Paint controlPointPaint; private Path gridPath; private int gridSize = 100; // 网格尺寸 (行数和列数) private LinearGradient gradient; private List<PointF> controlPoints = new ArrayList<>(); private List<Boolean> randomizedPoints = new ArrayList<>(); private float rotationAngle = 15f; // 网格旋转角度(度) private float floatAmplitude = 20f; // 浮动幅度 private float floatPhase = 0f; // 浮动相位 private ValueAnimator floatAnimator; // 浮动动画控制器 Camera camera = new Camera(); Matrix canvasMatrix = new Matrix(); public LyricGridView(Context context) { super(context); init(); } public LyricGridView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { // 网格线画笔设置 gridPaint = new Paint(Paint.ANTI_ALIAS_FLAG); gridPaint.setStyle(Paint.Style.STROKE); gridPaint.setStrokeWidth(1f); gridPaint.setColor(Color.parseColor("#80FFFFFF")); // 半透明白色 // 控制点画笔设置 controlPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG); controlPointPaint.setStyle(Paint.Style.FILL); controlPointPaint.setColor(Color.parseColor("#cccccc")); gridPath = new Path(); // 初始化浮动动画 setupFloatAnimation(); } private void setupFloatAnimation() { floatAnimator = ValueAnimator.ofFloat(0, 2 * (float) Math.PI); floatAnimator.setDuration(5000); // 3秒完成一个完整周期 floatAnimator.setRepeatCount(ValueAnimator.INFINITE); floatAnimator.setInterpolator(new LinearInterpolator()); floatAnimator.addUpdateListener(animation -> { floatPhase = (float) animation.getAnimatedValue(); invalidate(); // 触发重绘 }); floatAnimator.start(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 设置渐变的方向是从右到左 gradient = new LinearGradient( w, 0, // 起始点(右侧) 0, 0, // 结束点(左侧) Color.parseColor("#cccccc"), Color.parseColor("#00000000"), // 黑色 -> 透明 Shader.TileMode.CLAMP ); // 更新画笔的着色器 gridPaint.setShader(gradient); generateControlPoints(w, h); buildBezierGrid(w, h); } private void generateControlPoints(int width, int height) { controlPoints.clear(); randomizedPoints.clear(); // 清空随机标记 float cellWidth = width / (float) gridSize; float cellHeight = height / (float) gridSize; // 生成网格点并添加随机偏移作为贝塞尔控制点 for (int row = 0; row <= gridSize; row++) { for (int col = 0; col <= gridSize; col++) { float x = col * cellWidth; float y = row * cellWidth; // 添加随机偏移(实际应用中可替换为自定义控制点) float offsetX = (float) (Math.random() * 10 - 5); float offsetY = (float) (Math.random() * 10 - 5); boolean isRandomized = false; // 边界约束 if (col == 0 || col == gridSize) offsetX = 0; if (row == 0 || row == gridSize) offsetY = 0; // 只有50%的点会被随机偏移 if (Math.random() > 0.95) { isRandomized = true; } offsetY = 0; offsetX = 0; controlPoints.add(new PointF(x + offsetX, y + offsetY)); randomizedPoints.add(isRandomized); // 记录随机状态 } } } private void buildBezierGrid(int width, int height) { gridPath.reset(); // 绘制横向曲线 for (int row = 0; row <= gridSize; row++) { for (int col = 0; col < gridSize; col++) { int index = row * (gridSize + 1) + col; PointF start = controlPoints.get(index); PointF end = controlPoints.get(index + 1); if (col == 0) { gridPath.moveTo(start.x, start.y); } gridPath.lineTo(end.x, end.y); } } // 绘制纵向曲线 for (int col = 0; col <= gridSize; col++) { for (int row = 0; row < gridSize; row++) { int index = row * (gridSize + 1) + col; PointF start = controlPoints.get(index); PointF end = controlPoints.get(index + gridSize + 1); if (row == 0) { gridPath.moveTo(start.x, start.y); } gridPath.lineTo(end.x, end.y); } } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 保存当前画布状态 canvas.save(); // 计算浮动偏移量 (使用正弦函数创造波浪效果) float floatOffset = floatAmplitude * (float) Math.sin(floatPhase * 0.5f); // 计算中心点坐标 float centerX = getWidth() / 2f; float centerY = getHeight() / 2f; camera.save(); camera.rotateX(40f); camera.getMatrix(canvasMatrix); camera.restore(); // 应用3D旋转(保持水平位置不变) canvasMatrix.preTranslate(-centerX, 0); canvasMatrix.postTranslate(centerX, 0); // 应用3D变换到Canvas canvas.concat(canvasMatrix); // 应用旋转变换(以中心点为轴心) // canvas.rotate(rotationAngle, centerX, centerY); canvas.translate((getWidth() / (float) gridSize) * 10, -(getWidth() / (float) gridSize) * 8); canvas.translate(floatOffset, 0); // 添加垂直浮动 // 绘制贝塞尔曲线网格 gridPaint.setAlpha(150); // 增加透明度 canvas.drawPath(gridPath, gridPaint); // // 绘制控制点(可选) // for (PointF point : controlPoints) { // canvas.drawCircle(point.x, point.y, 4, controlPointPaint); // } // 获取屏幕宽度的一半 float screenHalf = getWidth() / 3f; // 只绘制被随机偏移的控制点 for (int i = 0; i < controlPoints.size(); i++) { PointF point = controlPoints.get(i); if (randomizedPoints.get(i) && point.x > screenHalf) { // 只绘制被随机偏移的点 canvas.drawCircle(point.x, point.y, 1, controlPointPaint); } } // 恢复画布状态 canvas.restore(); } // 设置网格密度 public void setGridSize(int size) { this.gridSize = Math.max(2, size); requestLayout(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (floatAnimator != null) { floatAnimator.cancel(); } } public void setDrawingEnabled(boolean enabled) { if (enabled) { setVisibility(VISIBLE); if (floatAnimator != null && !floatAnimator.isRunning()) { setupFloatAnimation(); } } else { setVisibility(GONE); if (floatAnimator != null) { floatAnimator.cancel(); } } } } 3d旋转后网格变形的原因
最新发布
07-13
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值