Android 图形特效处理
2022-02-14 本文已影响0人
gaookey
image.png
使用 Matrix 控制变换
Matrix 是 Android 提供的一个矩阵工具类,它本身并不能对图形或组件进行变换,但它可与其他 API 结合来控制图形、组件的变换。
使用 Matrix 控制图形或组件变换的步骤:
- 获取 Matrix 对象,该 Matrix 对象既可新创建,也可直接获取其它对象内封装的 Matrix 。
- 调用 Matrix 的方法进行平移、旋转、缩放、倾斜等。
- 将程序对 Matrix 所做的变换应用到指定图形或组件。
-
setTranslate(float dx, float dy)
控制 Matrix 进行平移 -
setSkew(float kx, float ky, float px, float py)
控制 Matrix 以px、py为轴心进行倾斜。kx、ky为X、Y方向上的倾斜距离。 -
setSkew(float kx, float ky)
控制 Matrix 进行倾斜。kx、ky为X、Y方向上的倾斜距离。 -
setRotate(float degrees)
控制 Matrix 进行旋转,degrees 控制旋转的角度。 -
setRotate(float degrees, float px, float py)
设置以px、py为轴心进行旋转,degrees 控制旋转的角度。 -
setScale(float sx, float sy)
设置 Matrix 进行缩放,sx、sy控制X、Y方向上的缩放比例。 -
setScale(float sx, float sy, float px, float py)
设置Matrix 以px、py为轴心进行缩放,sx、sy控制X、Y方向上的缩放比例。
MyView
public class MyView extends View {
// 初始的图片资源
private Bitmap bitmap;
// Matrix 实例
private Matrix bmMatrix = new Matrix();
// 设置倾斜度
float sx;
// 位图宽和高
private int bmWidth;
private int bmHeight;
// 缩放比例
float scale = 1.0f;
// 判断缩放还是旋转
boolean isScale;
public MyView(Context context, AttributeSet set) {
super(context, set);
// 获得位图
bitmap = ((BitmapDrawable) context.getResources().getDrawable(
R.drawable.a, context.getTheme())).getBitmap();
// 获得位图宽
bmWidth = bitmap.getWidth();
// 获得位图高
bmHeight = bitmap.getHeight();
// 使当前视图获得焦点
this.setFocusable(true);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 重置Matrix
bmMatrix.reset();
if (!isScale) {
// 倾斜Matrix
bmMatrix.setSkew(sx, 0f);
} else {
// 缩放Matrix
bmMatrix.setScale(scale, scale);
}
// 根据原始位图和Matrix创建新图片
Bitmap bitmap2 = Bitmap.createBitmap(bitmap,
0, 0, bmWidth, bmHeight, bmMatrix, true);
// 绘制新位图
canvas.drawBitmap(bitmap2, bmMatrix, null);
}
}
MainActivity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
GridLayout grid = findViewById(R.id.grid);
MyView mv = findViewById(R.id.mv);
String[] bnArray = getResources().getStringArray(R.array.bnArray);
for (String title : bnArray) {
Button bn = new Button(this);
bn.setText(title);
grid.addView(bn);
bn.setOnClickListener(view -> {
switch (getIndex(bnArray, bn.getText())) {
// 向左倾斜
case 0:
mv.isScale = false;
mv.sx += 0.1f;
mv.postInvalidate();
break;
// 向右倾斜
case 1:
mv.isScale = false;
mv.sx -= 0.1f;
mv.postInvalidate();
break;
// 放大
case 2:
mv.isScale = true;
if (mv.scale < 2.0) mv.scale += 0.1f;
mv.postInvalidate();
break;
// 缩小
case 3:
mv.isScale = true;
if (mv.scale > 0.5) mv.scale -= 0.1f;
mv.postInvalidate();
break;
}
});
}
}
// 定义一个工具方法,获取元素在数组中的出现次数
private static <T> int getIndex(T[] arr, T target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target || arr[i].equals(target)) {
return i;
}
}
return -1;
}
}
layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.myapplication.MyView
android:id="@+id/mv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" />
<GridLayout
android:id="@+id/grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginTop="200dp"
android:rowCount="4"
tools:ignore="MissingConstraints" />
</RelativeLayout>
values/arrays.xml
<resources>
<string-array name="bnArray">
<item>左倾斜</item>
<item>右倾斜</item>
<item>放大</item>
<item>缩小</item>
</string-array>
</resources>
image.png
使用 drawBitmapMesh 扭曲图像
Canvas 提供了一个 drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight, @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset, @Nullable Paint paint)
方法,该方法可以对 bitmap 进行扭曲。
-
bitmap
指定需要扭曲的源位图 -
meshWidth
该参数控制在横向上把源位图划分成多少格 -
meshHeight
该参数控制在纵向上把源位图划分成多少格 -
verts
该参数是一个长度为(meshWidth + 1) * (meshHeight + 1) * 2
的数组,它记录了扭曲后的位图各“顶点”位置。虽然它是一个一维数组,但实际上它记录的数据是形如(x0, y0)、(x1, y1)、(x2, y2)、...、(xN, yN)
格式的数组,这些数组元素控制对 bitmap 位图的扭曲效果 -
vertOffset
控制verts
数组中从第几个数组元素开始才对 bitmap 进行扭曲
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new MyView(this, R.drawable.image));
}
private class MyView extends View {
// 定义两个常量,这两个常量指定该图片横向、纵向上都被划分为20格
private final static int WIDTH = 20;
private final static int HEIGHT = 20;
// 记录该图片上包含441个顶点
private final static int COUNT = (WIDTH + 1) * (HEIGHT + 1);
// 定义一个数组,保存Bitmap上的21 * 21个点的坐标
private float[] verts = new float[COUNT * 2];
// 定义一个数组,记录Bitmap上的21 * 21个点经过扭曲后的坐标
// 对图片进行扭曲的关键就是修改该数组里元素的值
private float[] orig = new float[COUNT * 2];
private Bitmap bitmap;
MyView(Context context, int drawableId) {
super(context);
setFocusable(true);
// 根据指定资源加载图片
bitmap = BitmapFactory.decodeResource(getResources(), drawableId);
// 获取图片宽度、高度
float bitmapWidth = bitmap.getWidth();
float bitmapHeight = bitmap.getHeight();
int index = 0;
//重新计算图片尺寸
int[] screenSize = getScreenSize(context);
if (bitmapWidth > bitmapHeight) {
float ratio = (float) bitmapWidth / (float) bitmapHeight;
bitmapWidth = (float) screenSize[0];
bitmapHeight = (float) (screenSize[0] / ratio);
} else {
Double ratio = (double) bitmapHeight / (double) bitmapWidth;
bitmapHeight = (float) screenSize[1];
bitmapWidth = (float) (screenSize[1] / ratio);
}
for (int y = 0; y <= HEIGHT; y++) {
float fy = bitmapHeight * y / HEIGHT;
for (int x = 0; x <= WIDTH; x++) {
float fx = bitmapWidth * x / WIDTH;
// 初始化orig、verts数组。初始化后,orig、verts
// 两个数组均匀地保存了21 * 21个点的x,y坐标
verts[index * 2] = fx;
orig[index * 2] = verts[index * 2];
verts[index * 2 + 1] = fy;
orig[index * 2 + 1] = verts[index * 2 + 1];
index += 1;
}
}
// 设置背景色
setBackgroundColor(Color.WHITE);
}
@Override
public void onDraw(Canvas canvas) {
// 对bitmap按verts数组进行扭曲
// 从第一个点(由第5个参数0控制)开始扭曲
canvas.drawBitmapMesh(bitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);
}
// 工具方法,用于根据触摸事件的位置计算verts数组里各元素的值
private void warp(float cx, float cy) {
for (int i = 0; i < COUNT * 2; i += 2) {
float dx = cx - orig[i];
float dy = cy - orig[i + 1];
float dd = dx * dx + dy * dy;
// 计算每个坐标点与当前点(cx, cy)之间的距离
float d = (float) Math.sqrt(dd);
// 计算扭曲度,距离当前点(cx, cy)越远,扭曲度越小
float pull = 200000 / (dd * d);
// 对verts数组(保存bitmap上21 * 21个点经过扭曲后的坐标)重新赋值
if (pull >= 1) {
verts[i] = cx;
verts[i + 1] = cy;
} else {
// 控制各顶点向触摸事件发生点偏移
verts[i] = orig[i] + dx * pull;
verts[i + 1] = orig[i + 1] + dy * pull;
}
}
// 通知View组件重绘
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 调用warp方法根据触摸屏事件的坐标点来扭曲verts数组
warp(event.getX(), event.getY());
return true;
}
/**
* 获取屏幕宽度
*/
private int[] getScreenSize(Context context) {
DisplayMetrics dm = context.getResources().getDisplayMetrics();
int size[] = {dm.widthPixels, dm.heightPixels};
return size;
}
}
}
image.gif
使用 Shader 填充图形
-
BitmapShader
使用位图平铺来填充图形 -
LinearGradient
使用线性渐变来填充图形 -
RadialGradient
使用圆形渐变来填充图形 -
SweepGradient
使用角度渐变来填充图形 -
ComposeShader
使用组合效果来填充图形
MyView
public class MyView extends View {
Paint paint = new Paint();
public MyView(Context context, AttributeSet set) {
super(context, set);
paint.setColor(Color.RED);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 使用指定Paint对象画矩形
canvas.drawRect(0f, 0f, getWidth(), getHeight(), paint);
}
}
MainActivity
public class MainActivity extends AppCompatActivity {
// 声明Shader数组
private Shader[] shaders = new Shader[5];
// 声明颜色数组
private int[] colors;
private MyView myView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myView = findViewById(R.id.my_view);
// 获得Bitmap实例
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.water);
// 设置渐变的颜色组,也就是按红、绿、蓝的方式渐变
colors = new int[]{Color.RED, Color.GREEN, Color.BLUE};
// 实例化BitmapShader,x坐标方向重复图形,y坐标方向镜像图形
shaders[0] = new BitmapShader(bm, Shader.TileMode.REPEAT,
Shader.TileMode.MIRROR);
// 实例化LinearGradient
shaders[1] = new LinearGradient(0f, 0f, 100f, 100f,
colors, null, Shader.TileMode.REPEAT);
// 实例化RadialGradient
shaders[2] = new RadialGradient(100f, 100f, 80f,
colors, null, Shader.TileMode.REPEAT);
// 实例化SweepGradient
shaders[3] = new SweepGradient(160f, 160f, colors, null);
// 实例化ComposeShader
shaders[4] = new ComposeShader(shaders[1], shaders[2], PorterDuff.Mode.ADD);
Button bn1 = findViewById(R.id.bn1);
Button bn2 = findViewById(R.id.bn2);
Button bn3 = findViewById(R.id.bn3);
Button bn4 = findViewById(R.id.bn4);
Button bn5 = findViewById(R.id.bn5);
View.OnClickListener listener = source -> {
int ID = source.getId();
if (ID == R.id.bn1) {
myView.paint.setShader(shaders[0]);
} else if (ID == R.id.bn2) {
myView.paint.setShader(shaders[1]);
} else if (ID == R.id.bn3) {
myView.paint.setShader(shaders[2]);
} else if (ID == R.id.bn4) {
myView.paint.setShader(shaders[3]);
} else if (ID == R.id.bn5) {
myView.paint.setShader(shaders[4]);
}
// 重绘界面
myView.invalidate();
};
bn1.setOnClickListener(listener);
bn2.setOnClickListener(listener);
bn3.setOnClickListener(listener);
bn4.setOnClickListener(listener);
bn5.setOnClickListener(listener);
}
}
layout/activity_main.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">
<Button
android:id="@+id/bn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/bitmap" />
<Button
android:id="@+id/bn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/line" />
<Button
android:id="@+id/bn3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/radial" />
<Button
android:id="@+id/bn4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sweep" />
<Button
android:id="@+id/bn5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/composite" />
<com.example.myapplication.MyView
android:id="@+id/my_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
values/strings.xml
<resources>
<string name="app_name">My Application</string>
<string name="bitmap">位图</string>
<string name="line">线性渐变</string>
<string name="radial">圆形渐变</string>
<string name="sweep">角度渐变</string>
<string name="composite">混合</string>
</resources>
image.png
摘抄至《疯狂Android讲义(第4版)》