You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android中设置Canvas背景时图片拉伸问题求助

问题描述

我终于实现了在图片上添加用户签名的功能,通过学习Canvas绘图及保存图片与用户绘制内容的相关知识,我用mDrawLayout.setBackground(d);设置Canvas背景,但遇到了严重问题:图片被强制拉伸变形,原本竖版的证件图变成了横向拉伸的状态。


现有代码

DrawingView类

public class DrawingView extends View {
public int width;
public  int height;
static Path drawPath;
private Paint drawPaint, canvasPaint;
static int paintColor = 0xFFFF0000;
private  float STROKE_WIDTH = 5f;
private Canvas drawCanvas;
private Bitmap canvasBitmap;
private boolean erase=false;


public DrawingView(Context context, AttributeSet attrs){
    super(context, attrs);
    setupDrawing();
    setErase(erase);
}
private void setupDrawing(){
    drawPath = new Path();
    drawPaint = new Paint();
    drawPaint.setColor(paintColor);
    drawPaint.setAntiAlias(true);
    drawPaint.setStrokeWidth(STROKE_WIDTH);
    drawPaint.setStyle(Paint.Style.STROKE);
    drawPaint.setStrokeJoin(Paint.Join.ROUND);
    drawPaint.setStrokeCap(Paint.Cap.ROUND);
    canvasPaint = new Paint(Paint.DITHER_FLAG);
}
public void clearDrawing()
{

    setDrawingCacheEnabled(false);


    onSizeChanged(width, height, width, height);
    invalidate();

    setDrawingCacheEnabled(true);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    width = w;
    height = h;
    canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    drawCanvas = new Canvas(canvasBitmap);
}

public void setErase(boolean isErase){
    erase=isErase;
    drawPaint = new Paint();
    if(erase) {
        setupDrawing();
        int srcColor= 0x00000000;

        PorterDuff.Mode mode = PorterDuff.Mode.CLEAR;
        PorterDuffColorFilter porterDuffColorFilter = new PorterDuffColorFilter(srcColor, mode);

        drawPaint.setColorFilter(porterDuffColorFilter);

        drawPaint.setColor(srcColor);
        drawPaint.setXfermode(new PorterDuffXfermode(mode));

    }
    else {

        setupDrawing();

    }
}

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
    canvas.drawPath(drawPath, drawPaint);
}

@Override
public boolean onTouchEvent(MotionEvent event) {

    canvasPaint.setColor(paintColor);
    float touchX = event.getX();
    float touchY = event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            drawPath.moveTo(touchX, touchY);
            break;
        case MotionEvent.ACTION_MOVE:
            drawCanvas.drawPath(drawPath, drawPaint);
            drawPath.lineTo(touchX, touchY);
            break;
        case MotionEvent.ACTION_UP:
            drawPath.lineTo(touchX, touchY);
            drawCanvas.drawPath(drawPath, drawPaint);
            drawPath.reset();
            break;
        default:
            return false;
    }
    invalidate();
    return true;
}

public int getPaintAlpha(){
    return Math.round((float)STROKE_WIDTH/255*100);
}

public void setPaintAlpha(int newAlpha){
    STROKE_WIDTH=Math.round((float)newAlpha/100*255);
    drawPaint.setStrokeWidth(newAlpha);
}
}

SignatureActivity代码

public class SignatureActivity extends AppCompatActivity {
SeekBar mThickness;
private AnexoTemporario imagePath;
private DrawingView mDrawLayout;
Button erase, draw;
private Paint drawPaint = new Paint();
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_signature);
    mThickness = findViewById(R.id.thickness);
    mDrawLayout = findViewById(R.id.viewDraw);
    erase = findViewById(R.id.erase);
    draw= findViewById(R.id.draw);
    imagePath = (AnexoTemporario) getIntent().getSerializableExtra("IMAGEURL");
    File file = new File(imagePath.getFilePath());
    Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());

    Drawable d = new BitmapDrawable(getResources(), bitmap);
    mDrawLayout.setVisibility(View.VISIBLE);
    mDrawLayout.setDrawingCacheEnabled(true);
    mDrawLayout.setEnabled(true);


    mDrawLayout.setBackground(d);
    mDrawLayout.setRotation(90);




    mThickness.setMax(50);
    mThickness.setProgress(10);
    mDrawLayout.setPaintAlpha(mThickness.getProgress());
    int currLevel = mDrawLayout.getPaintAlpha();
    mThickness.setProgress(currLevel);
    mDrawLayout.invalidate();
    erase.setOnClickListener(v -> {
        saveDrawing();
        drawPaint.setColor(Color.TRANSPARENT);
        mDrawLayout.setErase(true);

    });

    draw.setOnClickListener(v -> mDrawLayout.clearDrawing());

    mThickness.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
        }

        @Override
        public void onProgressChanged(SeekBar seekBar, int progress,
                                      boolean fromUser) {
            mDrawLayout.setPaintAlpha(mThickness.getProgress());
        }
    });
}
public void saveDrawing()
{
    Bitmap whatTheUserDrewBitmap = mDrawLayout.getDrawingCache();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    whatTheUserDrewBitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);

    GetImageTask task = new GetImageTask(whatTheUserDrewBitmap, base64string -> {

        try {
            WriteSDCard writeSDCard = new WriteSDCard(this);
            writeSDCard.writeToSDFile("assinatura.png", base64string.getBytes());

            Toast.makeText(this, "Assinatura Salva!", Toast.LENGTH_LONG).show();

            Intent intent = new Intent();
            intent.putExtra("ASSINATURA", "assinatura.png");
            setResult(Activity.RESULT_OK, intent);
            finish();
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(this, "Erro ao tentar salvar assinatura!", Toast.LENGTH_LONG).show();
        }
    });
    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

}
}

XML布局代码

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".canvas.SignatureActivity" >

<mobi.stos.gwmobile.canvas.DrawingView
    android:id="@+id/viewDraw"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:scaleType="centerInside"
    app:layout_constraintBottom_toTopOf="@+id/ll"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />


<LinearLayout
    android:id="@+id/linearLayout2"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:weightSum="2"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent">


    <Button
        android:id="@+id/erase"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/thickness"
        android:layout_weight="1"
        android:text="SALVAR"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/draw"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/erase"
        android:layout_weight="1"
        android:text="APAGAR"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</LinearLayout>

<LinearLayout
    android:id="@+id/ll"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:layout_constraintBottom_toTopOf="@+id/linearLayout2"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginBottom="10dp"
        android:text="Tamanho da linha" />

    <SeekBar
        android:id="@+id/thickness"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/viewDraw"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/viewDraw" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

问题根源与解决方案

问题原因

  1. View的背景会强制适配View尺寸,无法保持图片比例,加上setRotation(90)进一步打乱了布局逻辑
  2. scaleType属性仅对ImageView生效,对自定义View的背景无效

修复方案

放弃使用View背景,改为在DrawingView的onDraw中手动绘制背景图,通过Matrix保持图片比例并处理旋转,同时修正触摸坐标适配变换后的画布。

1. 修改DrawingView类

public class DrawingView extends View {
    // 原有成员变量
    public int width;
    public int height;
    static Path drawPath;
    private Paint drawPaint, canvasPaint;
    static int paintColor = 0xFFFF0000;
    private float STROKE_WIDTH = 5f;
    private Canvas drawCanvas;
    private Bitmap canvasBitmap;
    private boolean erase=false;

    // 新增背景图相关变量
    private Bitmap backgroundBitmap;
    private Matrix backgroundMatrix = new Matrix();

    public DrawingView(Context context, AttributeSet attrs){
        super(context, attrs);
        setupDrawing();
        setErase(erase);
    }

    // 新增设置背景图的方法
    public void setBackgroundBitmap(Bitmap bitmap) {
        backgroundBitmap = bitmap;
        calculateBackgroundMatrix();
        invalidate();
    }

    // 计算背景图的适配矩阵(保持比例+旋转)
    private void calculateBackgroundMatrix() {
        if (backgroundBitmap == null || width == 0 || height == 0) return;

        backgroundMatrix.reset();
        // 先旋转90度
        backgroundMatrix.postRotate(90, backgroundBitmap.getWidth()/2f, backgroundBitmap.getHeight()/2f);

        // 旋转后宽高互换
        float rotatedBitmapWidth = backgroundBitmap.getHeight();
        float rotatedBitmapHeight = backgroundBitmap.getWidth();
        float viewRatio = (float) width / height;
        float bitmapRatio = rotatedBitmapWidth / rotatedBitmapHeight;

        float scale;
        float translateX = 0;
        float translateY = 0;

        // 根据比例适配
        if (viewRatio > bitmapRatio) {
            scale = (float) height / rotatedBitmapHeight;
            translateX = (width - rotatedBitmapWidth * scale) / 2;
        } else {
            scale = (float) width / rotatedBitmapWidth;
            translateY = (height - rotatedBitmapHeight * scale) / 2;
        }

        backgroundMatrix.postScale(scale, scale);
        backgroundMatrix.postTranslate(translateX, translateY);
    }

    private void setupDrawing(){
        drawPath = new Path();
        drawPaint = new Paint();
        drawPaint.setColor(paintColor);
        drawPaint.setAntiAlias(true);
        drawPaint.setStrokeWidth(STROKE_WIDTH);
        drawPaint.setStyle(Paint.Style.STROKE);
        drawPaint.setStrokeJoin(Paint.Join.ROUND);
        drawPaint.setStrokeCap(Paint.Cap.ROUND);
        canvasPaint = new Paint(Paint.DITHER_FLAG);
    }

    public void clearDrawing()
    {
        setDrawingCacheEnabled(false);
        onSizeChanged(width, height, width, height);
        invalidate();
        setDrawingCacheEnabled(true);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
        canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        drawCanvas = new Canvas(canvasBitmap);
        calculateBackgroundMatrix(); // 尺寸变化时重新计算适配矩阵
    }

    public void setErase(boolean isErase){
        erase=isErase;
        drawPaint = new Paint();
        if(erase) {
            setupDrawing();
            int srcColor= 0x00000000;

            PorterDuff.Mode mode = PorterDuff.Mode.CLEAR;
            PorterDuffColorFilter porterDuffColorFilter = new PorterDuffColorFilter(srcColor, mode);

            drawPaint.setColorFilter(porterDuffColorFilter);
            drawPaint.setColor(srcColor);
            drawPaint.setXfermode(new PorterDuffXfermode(mode));

        }
        else {
            setupDrawing();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 先绘制背景图
        if (backgroundBitmap != null) {
            canvas.drawBitmap(backgroundBitmap, backgroundMatrix, canvasPaint);
        }
        // 再绘制签名
        canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
        canvas.drawPath(drawPath, drawPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 把触摸坐标转换为适配背景图后的坐标
        Matrix inverseMatrix = new Matrix();
        backgroundMatrix.invert(inverseMatrix);
        float[] touchPoint = {event.getX(), event.getY()};
        inverseMatrix.mapPoints(touchPoint);

        float touchX = touchPoint[0];
        float touchY = touchPoint[1];

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                drawPath.moveTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_MOVE:
                drawCanvas.drawPath(drawPath, drawPaint);
                drawPath.lineTo(touchX, touchY);
                break;
            case MotionEvent.ACTION_UP:
                drawPath.lineTo(touchX, touchY);
                drawCanvas.drawPath(drawPath, drawPaint);
                drawPath.reset();
                break;
            default:
                return false;
        }
        invalidate();
        return true;
    }

    public int getPaintAlpha(){
        return Math.round((float)STROKE_WIDTH/255*100);
    }

    public void setPaintAlpha(int newAlpha){
        STROKE_WIDTH=Math.round((float)newAlpha/100*255);
        drawPaint.setStrokeWidth(newAlpha);
    }
}

2. 修改SignatureActivity中的背景设置逻辑

// 替换原有setBackground和setRotation代码
// Drawable d = new BitmapDrawable(getResources(), bitmap);
// mDrawLayout.setBackground(d);
// mDrawLayout.setRotation(90);

mDrawLayout.setBackgroundBitmap(bitmap);

3. 修改XML布局,移除无效的scaleType属性

<mobi.stos.gwmobile.canvas.DrawingView
    android:id="@+id/viewDraw"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@+id/ll"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

内容的提问来源于stack exchange,提问作者Victor Pierre

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 23:51:24