/ android  pathmeasure  getpostan  drawBitmapMesh  bitmap弯曲  bitmap变形 

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


image.png

箭头方向应该是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);
    }
}


发布评论

热门评论区: