该部分主要基于Canvas的绘图系统介绍一些常见的特效。
1 扭曲特效
这里使用Canvas的drawBitmapmesh对图像的局部进行扭曲特效处理。该效果主要是在APP上显示“水波荡漾”、“红旗飘扬”等扭曲效果,非常灵活。drawBitmapmesh定义如下:
public void drawBitmapMesh (Bitmap bitmap,
int meshWidth,int meshHeight,
float[] verts,int vertOffset,
int[] colors,int colorOffset,Paint paint)
主要有几个关键参数(其他参数相对好理解一些):
- meshWidth 和 meshHeight:在横向/纵向把图片切分成多少个格子。
- verts:该参数是一个长度len为(meshWidth+1)*(meshHeight+1)*2的数组,记录了扭曲后位图的各个顶点,记录格式为:(x1,y1)、(x2,y2)、(x3,y3)、。。。该方法主要是通过这些数组元素来控制对bitmap位图的扭曲效果(在下面实战demo中会进一步了解)。
drawBitmapmesh的实际扭曲效果图如下所示:
关于drawBitmapmesh方法更多解读可参照文档: Android Canvas drawBitmapMesh详解。
@2 drawBitmapmesh实战(drawBitmapmesh效果展示)
实现功能:触摸屏幕,在触摸处展示drawBitmapmesh的扭曲效果。程序运行效果如下所示:
关于该程序,自定义View的关键代码如下所示:
public class MyView extends View {
private static final String TAG = "MyView";
private Matrix initMatrix = new Matrix();
private int h,w;
//为drawBitmapMesh构建的相关变量,start
private final static int WIDTH =20;
private final static int HEIGHT =20;
private final static int COUNT = (WIDTH+1)*(HEIGHT+1);
private float[] verts = new float[COUNT*2];
private float[] origs = new float[COUNT*2];
//为drawBitmapMesh构建的相关变量,end
public Bitmap bitmap = null;
float offsetStartX=0.0f,offsetStartY=0.0f;//居中显示调整
public MyView(Context context) {
super(context);
init(context);
}
private void init(Context context){
//1 获取bitmap
initMatrix.reset();
initMatrix.setScale(1.0f,1.0f);
try {
InputStream isImage = context.getAssets().open("test6.png");
bitmap = BitmapFactory.decodeStream(isImage);
w = bitmap.getWidth();
h = bitmap.getHeight();
bitmap = Bitmap.createBitmap(bitmap,0,0, w, h, initMatrix, true);
} catch (IOException e) {
e.printStackTrace();
}
//2 按照drawBitmapMesh规定方式构建origs数组(处理数据)和verts数组(保存原始数据)
int index=0;
for(int i=0;i<HEIGHT+1;i++){
float fy=(h*i)/HEIGHT;
for(int j=0;j<WIDTH+1;j++){
float fx=(w*j)/WIDTH;
//记录格式为:(x1,y1)、(x2,y2)、(x3,y3),以此类推
origs[index*2+0]=verts[index*2+0]=fx;
origs[index*2+1]=verts[index*2+1]=fy;
index++;
}
}
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.WHITE);
//居中显示
offsetStartX = (canvas.getWidth()-bitmap.getWidth())*1.0f/2;
offsetStartY = (canvas.getHeight()-bitmap.getHeight())*1.0f/2;
canvas.translate(offsetStartX,offsetStartY);
//5 bitmap绘制,当vert数组变化时该图片中局部会做相应的调整。
canvas.drawBitmapMesh(bitmap,WIDTH,HEIGHT,verts,0,null,0,null);
}
private void wrap(float x,float y){
//4 关键算法,扭曲效果设计,根据输入的事件坐标构建扭曲效果。
for(int i=0;i<COUNT*2;i+=2){
float dx = x-origs[i];
float dy = y-origs[i+1];
float d = (float)Math.sqrt(dx*dx+dy*dy);
float pull = 200000.f/(d*d*d);
if(pull>=1){
verts[i] = x;
verts[i+1] = y;
}else{
verts[i] = origs[i]+dx*pull;
verts[i+1] = origs[i+1]+dy*pull;
}
}
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//3 为配合居中显示,这里做了坐标校正。
wrap(event.getX()-offsetStartX,event.getY()-offsetStartY);
return true;
}
}
在MainActivity中实现代码为:
public class MainActivity extends AppCompatActivity {
private static String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new MyView(this));
}
}
2 shader特效
@1 shader概念基础与派生类解读
这里主要是使用Paint画笔的填充模式展示shader的填充效果。在Android APP中,Shader类(更多关于Shader类解读查看文档:Android Shader类详解)是一个抽象类,它的派生类如下:
- BitmapShader:Android BitmapShader类详解 ,位图平铺。
- LinearGradient:Android LinearGradient类详解 ,线性渐变。
- RadialGradient:Android RadialGradient类详解 ,圆形渐变。
- SweepGradient:Android SweepGradient类详解 ,角度渐变。
- ComposeGradient:Android ComposeShader类详解 ,组合效果。
shader是着色器的含义(就像皮肤一样,只不过我们看到的一个人穿的衣服还要依赖于环境,比如光照、阴影等,也就是说shader可以理解为 衣服在一个人身上 整体呈现的最终效果),更多关于shader的解读可查看文章:OpenGL基础(01)基础知识中的第二部分即可。
@2 shader效果实战(展示5种shader派生类特效)
实现功能:通过按键响应5种shader效果,效果如下所示:
关于该程序,自定义View的关键代码如下所示:
public class MyView extends View {
private static final String TAG = "MyView";
public Paint paint = new Paint();
private Path path = new Path();
public MyView(Context context,AttributeSet attrs){
super(context,attrs);
init();
}
private void init(){
paint.setAntiAlias(true);//抗锯齿
paint.setColor(Color.RED);//画笔颜色
paint.setStyle(Paint.Style.FILL);//填充模式
paint.setStrokeWidth(4f);//设置画笔粗细度
paint.setTextSize(48f);
}
public void setShader(Shader shader){
paint.setShader(shader);
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.WHITE);
drawsth(canvas,paint,path,440f);
}
//测试:绘制基本图形、Path不规则图形和文字
private void drawsth(Canvas canvas, Paint paint,Path path,float offset){
//paint绘制圆形,参数:圆心坐标(x,y)半径r,画笔paint
canvas.drawCircle(100f+offset,100f,90f,paint);
//paint绘制矩形,参数:左上角坐标(x,y),右下角坐标(x,y),画笔paint
canvas.drawRect(10f+offset,200f,210f+offset,400f,paint);
//paint绘制圆角矩形,参数:左上角坐标(x,y),右下角坐标(x,y),x方向上的圆角半径,y方向上的圆角半径,画笔paint
canvas.drawRoundRect(10f+offset,410f,210f+offset,610f,40f,40f,paint);
//paint绘制椭圆,参数:左上角坐标(x,y),右下角坐标(x,y)[参数表示矩形内切的椭圆],画笔paint
canvas.drawOval(10f+offset,620f,210f+offset,720f,paint);
//Path绘制三角形,moveto表示第1个坐标,lineto分别表示第2、3个坐标。以此类推。
path.moveTo(110f+offset,730f);
path.lineTo(10f+offset,930f);
path.lineTo(210f+offset,930f);
path.close();
canvas.drawPath(path,paint);
//Path绘制不规则多点多边形
path.moveTo(10f+offset,950f);
path.lineTo(50f+offset,1000f);
path.lineTo(200f+offset,970f);
path.lineTo(150f+offset,1070f);
path.lineTo(10f+offset,1110f);
path.lineTo(210f+offset,1130f);
path.close();
canvas.drawPath(path,paint);
//注意:这个坐标(x,y)并不是文字的左上角,而是一个与左下角比较接近的位置。
canvas.drawText("测试文字",10f+offset,1190f,paint);
}
}
在MainActivity中实现代码如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static String TAG = "MainActivity";
private Shader[] shaders = new Shader[5];
private int[] colors = new int[]{Color.RED,Color.GREEN,Color.BLUE};
private MyView myView;
private Button btn1,btn2,btn3,btn4,btn5;
private Bitmap bitmap = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.shaderlayout);
Button btn1 = findViewById(R.id.btn1);
Button btn2 = findViewById(R.id.btn2);
Button btn3 = findViewById(R.id.btn3);
Button btn4 = findViewById(R.id.btn4);
Button btn5 = findViewById(R.id.btn5);
myView = findViewById(R.id.myView);
try {
InputStream isImage = getAssets().open("test6.png");
bitmap = BitmapFactory.decodeStream(isImage);
} catch (IOException e) {
e.printStackTrace();
}
//创建5种shader的派生类对象,分别测试效果
shaders[0] = new BitmapShader(bitmap,Shader.TileMode.REPEAT, Shader.TileMode.MIRROR);
shaders[1] = new LinearGradient(0f,0f,100f,100f,colors,null, Shader.TileMode.REPEAT);
shaders[2] = new RadialGradient(100f,100f,80f,colors,null, Shader.TileMode.REPEAT);
shaders[3] = new SweepGradient(160f,160f,colors,null);
shaders[4] = new ComposeShader(shaders[1],shaders[2], PorterDuff.Mode.ADD);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn1:myView.setShader(shaders[0]);break;
case R.id.btn2:myView.setShader(shaders[1]);break;
case R.id.btn3:myView.setShader(shaders[2]);break;
case R.id.btn4:myView.setShader(shaders[3]);break;
case R.id.btn5:myView.setShader(shaders[4]);break;
default:break;
}
}
}
关于该程序,shaderlayout.xml 布局参考文件如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="visible">
<Button
android:id="@+id/btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="20"
android:onClick="onClick"
android:text="@string/BitmapShader" />
<Button
android:id="@+id/btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="20"
android:onClick="onClick"
android:text="@string/LinearGradient" />
<Button
android:id="@+id/btn3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="20"
android:onClick="onClick"
android:text="@string/RadialGradient" />
<Button
android:id="@+id/btn4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="20"
android:onClick="onClick"
android:text="@string/SweepGradient" />
<Button
android:id="@+id/btn5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="20"
android:onClick="onClick"
android:text="@string/ComposeGradient" />
</LinearLayout>
<com.ags.myapplication.MyView
android:id="@+id/myView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/ic_launcher" />
</LinearLayout>