Search code examples
androidandroid-layoutandroid-canvas

Stretched image in canvas when background is set in android


Ok so i was finally able to get user's signature over an image . And through all i was able to learn from canvas and saving an image + user drawings , i knew that i had to set the canvas background like so :

mDrawLayout.setBackground(d); 

But my issue is : image gets really really stretched from the original one , here s the original image :

original

heres the out put on the canvas by setting its background : output

heres my mDrawLayout class :

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();

    }
}

//************************************   draw view  *************************************************************

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

//***************************   respond to touch interaction   **************************************************

@Override
public boolean onTouchEvent(MotionEvent event) {

    canvasPaint.setColor(paintColor);
    float touchX = event.getX();
    float touchY = event.getY();
    //respond to down, move and up events

    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;
    }
    //redraw
    invalidate();
    return true;
}

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

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

heres my canvas class :

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 file :

<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>


Solution

  • View.setBackground(drawable) seems like it just applies that Drawable and scales it to fit the View. So if that View is a different aspect ratio than the bitmap, it gets stretched.

    The simplest approach would be something like this - stick your background in an ImageView behind your DrawingView, and constrain it to the same size as the DrawingView. Make the scaling CENTER_INSIDE (to make sure the whole image fits into the ImageView, while maintaining the aspect ratio) or FIT_CENTER (same, but it'll also scale up so the image touches the sides):

    
    <!-- specify this one first, so viewDraw is on top of it -->
    <ImageView
        android:id="@+id/drawingBackground"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scaleType="fitCenter"
        app:layout_constraintTop_toTopOf="@+id/viewDraw"
        app:layout_constraintBottom_toBottomOf="@+id/viewDraw"
        app:layout_constraintEnd_toEndOf="@+id/viewDraw"
        app:layout_constraintStart_toStartOf="@+id/viewDraw" />
    
    <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" />
    

    And then you can use setImageBitmap on that ImageView to display a pic.


    The other approach would be passing and storing the background bitmap in DrawingView, and drawing that to the Canvas as part of the onDraw call, using drawBitmap. That's more involved, because you'd have to create a Rect with the appropriate width and height (depending on the view's width and height) to maintain the aspect ratio, and then work out the x and y coordinates to draw it so it's centred. And you'd need to recalculate that every time a new background bitmap is provided, or whenever the onSizeChanged is called on the View.


    But passing the background bitmap into your custom view also gives you the ability to define the size of the DrawingView itself by overriding onMeasure, and basically constraining the View to fit the Bitmap's aspect ratio. The problem with the other approaches is that the DrawingView is whatever size it is, and the background is just drawn into that space filling up whatever it happens to fill.

    So that means you can draw over the empty parts of the background - and that might not be a problem! But if it is, you could override onMeasure to make use of the potential width and height your View could be, and limit one of those axes to constrain the whole View to fit your Bitmap.

    That's a little involved and I don't have time to write an example, but here's an article on onMeasure that shows you how you can control the actual size of your View, if that's a thing you want to pursue.