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); } }
发布评论
热门评论区: