Bitmap任意曲线弯曲

如上图所示,我们期望将右侧的原图Bitmap任意弯曲像左边的Bitmap一样,怎么做呢?
直入主题:
1、我们先了解一下canvas的一个核心方法
void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset, @Nullable Paint paint)
这个方法可以通过自定义Bitmap的网格位置,然后在canvas上绘制出来,其中最核心字段是 meshWidth,meshHeight, verts 这三个,分别表示网格的宽度,高度和网格对应的坐标位置(详细了解可以自行搜索drawBitmapMesh),今天我们详细了解verts中的位置如何计算,verts存储的是图片网格焦点的位置信息,那么我们可以通调整verts中的坐标信息,来达到图片弯曲的效果。

我们看这个图片,我们先定义 2 * 2的verts,用来存图片的四个顶点的坐标,并初始化改位置信息
int WIDTH = 1;
int HEIGHT = 1;
float[] verts = new float[(WIDTH + 1) * (HEIGHT + 1) * 2];
for (int x = 0; x <= WIDTH; x++) {
for (int y = 0; y <= HEIGHT; y++) {
int posX = (x * (WIDTH + 1) + y) * 2;
int poxY = posX + 1;
float widthSize = x * mWidthInterval;
float heightSize = y * mHeightInterval;
verts[posX] = orig[posX] = widthSize;
verts[poxY] = orig[poxY] = heightSize;
}
}
~~~~
//调用绘制
canvas.drawBitmapMesh(bitmap, WIDTH, HEIGHT, verts, 0, null, 0, null)我们绘制出来看下效果

图片在左上角绘制出来了,这是因为verts还是原始状态,还未进行修改
2、我们准备一个任意弯曲曲线,Path
mPath = new Path(); mPath.moveTo(100, 100); mPath.quadTo(getWidth(), 100, 100, getHeight() - 100); //在onDraw中绘制出来 canvas.drawPath(mPath, mPaint);

3、寻找点绘制逻辑,我们要将图片如下图随曲线弯曲,那么我们就要将图片一边的左边紧贴在曲线上,然后顺着垂直方向(箭头方向)依次计算每个点的坐标

箭头方向应该是A点切线的垂直方向,所以我们要计算A点垂线的角度,PathMeasure中的
boolean getPosTan(float distance, float pos[], float tan[])
方法可以帮助我们获取指定点的坐标和正切值;
由高中数学知识,正切值就是该点的斜率,所以可以得到A点的斜率为tan[1] / tan[0](看不懂可以参考这篇文章这篇文章),于是可以得到垂线(箭头方向)的斜率fK = -1 / (tan[1] / tan[0]) = 垂线的正切值,
由三角函数 sin^2 + cos^2 = 1 和 sin / cos =垂线的正切值,可以算出垂线方向 cos = Math.sqrt(1 / (k * k +1)),sin = cos * k;
所以可以算出箭头方向的坐标,当A点的坐标为x, y 时,垂线方向的坐标为 x = x + cos * bitmap网格高度,y = y + sin * bitmap网格高度
measure.getPosTan(distance, pos, tan); float fK = -1 / (tan[1] / tan[0]); float cosValue = (float) Math.sqrt(1.0 / (fK * fK + 1.0)); float sinValue = cosValue * fK; float x = pos[0] + cosValue * mHeightInterval; float y = pos[1] + sinValue * mHeightInterval;
得到了一个点,垂直方向所有点的坐标,接着依次将网格的横坐标转换成曲线坐标就可以了,,,下面是全部代码
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
import com.tencent.animation.util.BmpUtils;
import com.tencent.light.R;
public class BitmapView extends View {
private final Bitmap bitmap;
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//定义网格宽度为20
private final int WIDTH = 20;
//定义网格高度为20
private final int HEIGHT = 20;
//记录该图片上包含441个顶点
private final int COUNT = (WIDTH + 1) * (HEIGHT + 1);
//定义一个数组,保存Bitmap上的21 * 21个点坐标
private final float[] verts = new float[COUNT * 2];
//定义一个数组,记录Bitmap上的21 * 21个点经过扭曲后的坐标
//对图片进行扭曲的关键就是修改该数组里的值21*21
private final Path mPath = new Path();
private float mWidthInterval = 0;
private float mHeightInterval = 0;
private float mPosition = 0;
private PathMeasure mPathMeasure = null;
private boolean mIsRunning = false;
public BitmapView(Context context) {
this(context, null);
}
public BitmapView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
bitmap = BmpUtils.getBitmap(context, R.drawable.ic_launcher_background, 450, 90, 0);
float bitmapWidth = bitmap.getWidth();
float bitmapHeight = bitmap.getHeight();
//单个网格宽度
mWidthInterval = bitmapWidth / WIDTH;
//单个网格高度
mHeightInterval = bitmapHeight / HEIGHT;
for (int x = 0; x <= WIDTH; x++) {
for (int y = 0; y <= HEIGHT; y++) {
int posX = (x * (WIDTH + 1) + y) * 2;
int poxY = posX + 1;
float widthSize = x * mWidthInterval;
float heightSize = y * mHeightInterval;
verts[posX] = widthSize;
verts[poxY] = heightSize;
}
}
mPaint.setStrokeWidth(6f);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.RED);
setBackgroundColor(Color.BLACK);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(mPath, mPaint);
if (mIsRunning) {
handleVertPos(mPathMeasure, mPosition);
canvas.drawBitmapMesh(bitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);
}
}
private void handleVertPos(PathMeasure measure, float distance) {
for (int i = 0; i <= WIDTH; i++) {
float[] pos = new float[2];
float[] tan = new float[2];
distance += mWidthInterval;
measure.getPosTan(distance, pos, tan);
float fK = -1 / (tan[1] / tan[0]); //垂线斜率,,,两条垂直的直线,斜率之积 = -1
float cosValue = (float) Math.sqrt(1.0 / (fK * fK + 1.0)); //三角函数 sin^2 + cos^2 = 1, sin / cos = tan = 斜率
float sinValue = cosValue * fK;
int posX = 2 * i;
int posY = posX + 1;
resetValue(posX, posY, pos[0], pos[1]);
for (int j = 1; j <= HEIGHT; j++) {
float tmpWidth = mHeightInterval * j;
posX = (WIDTH + 1) * 2 * j + 2 * i;
posY = posX + 1;
resetValue(posX, posY, pos[0] + cosValue * tmpWidth, pos[1] + sinValue * tmpWidth);
}
}
}
private void resetValue(int x, int y, float xValue, float yValue) {
verts[x] = xValue;
verts[y] = yValue;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mIsRunning) return super.onTouchEvent(event);
mIsRunning = true;
if (mPathMeasure == null) {
mPath.moveTo(100, 100);
mPath.quadTo(getWidth(), 100, 100, getHeight() - 100);
mPathMeasure = new PathMeasure(mPath, false);
}
mPosition = mPathMeasure.getLength() / 10 * 3.8f;
invalidate();
return super.onTouchEvent(event);
}
}
发布评论
热门评论区: