¿Cómo crear una aplicación de pintura en Android?

Todos alguna vez usamos MS-Paint en nuestra infancia, y cuando el sistema pasó de los escritorios a nuestras manos, comenzamos a garabatear en Instagram Stories, Hike, WhatsApp y muchas más aplicaciones similares. Pero, ¿alguna vez has pensado en cómo se dieron vida a estas funcionalidades? Entonces, en este artículo, discutiremos el enfoque básico utilizado por dichas aplicaciones y crearemos una réplica básica de dichas aplicaciones. qué

El enfoque

  1. En el día a día, si queremos crear un dibujo primero necesitamos un Canvas sobre el que trabajar. Entonces, en nuestra aplicación, primero crearemos un lienzo donde el usuario puede dibujar sus dibujos. Y para eso, necesitamos crear una vista personalizada donde el usuario pueda simplemente arrastrar el dedo para dibujar los trazos. Para lograr esto, creamos una clase DrawView que extiende la clase View del SDK estándar de Android.
  2. Luego necesitaremos un pincel que actúe como herramienta para ayudarnos a dibujar sobre el lienzo. Ahora, dado que necesitamos diferentes pinceles para diferentes colores y diferentes anchos del trazo, crearemos un modelo, es decir, una clase llamada Trazo con atributos como el color del trazo, el ancho del trazo, la visibilidad del trazo, etc. Cada objeto de esta clase representará un pincel distinto que dibuja un trazo único en el lienzo.
  3. Para llevar un registro de todos y cada uno de los trazos que el usuario ha dibujado en el Canvas, crearemos un ArrayList de tipo Stroke. Este ArrayList nos ayudará a deshacer el Trazo que el usuario ha dibujado por error en el Lienzo.
  4. Ahora, al final, cuando el usuario haya terminado con el dibujo, es posible que desee guardar esa pintura para cualquier uso posterior. Por lo tanto, proporcionamos la opción Guardar, que permitirá al usuario guardar el lienzo actual en forma de PNG o JPEG.

Lista de métodos

Antes de saltar al código, aquí hay algunos de los métodos que usaremos para construir nuestra aplicación:

Escribe

Método

Descripción

vacío setDither(interpolación booleana)

El tramado afecta la reducción de muestreo de los colores que son de

mayor precisión que la precisión del dispositivo.

vacío setAntiAlias ​​(aa booleano)

AntiAliasing suaviza los bordes de lo que se dibuja pero

tiene poco efecto en el interior de la forma.

vacío setStyle (estilo Paint.Style) Este método controla la 
vacío setStrokeCap ​​(Tapa de Paint.Cap)

Este método cambia la geometría del punto final de la

línea según el argumento Por ejemplo, ROUND, SQUARE, BUTT.

vacío void setStrokeJoin (Paint.Join unión) Este método establece que la pintura se una a REDONDO, BISEL, INGLETE
vacío setAlfa (int a)

Es un método auxiliar que solo asigna el color 

valor alfa, dejando sus valores r,g,b sin cambios. 

Los resultados no están definidos si el valor alfa está fuera del rango [0..255]

vacío invalidar()

Este método llama al método anulado onDraw(). 

Siempre que queramos actualizar la pantalla, en nuestro caso el Canvas,

llamamos a invalidate(), que además llama internamente al método onDraw().

En t Lienzo.guardar()

Este método guarda el estado actual del lienzo. 

para que podamos volver a él más tarde

vacío Lienzo.restaurar()

Este método revierte los ajustes del lienzo a 

la última vez que se llamó a canvas.save().

vacío Path.quadTo (flotante x1, flotante y1, flotante x2, flotante y2)

Este método suaviza las curvas usando una línea cuadrática.

(x1,y1) es el punto de control en una curva cuadrática y (x2,y2) 

son el punto final de una curva cuadrática.

Ahora, comencemos a construir la aplicación. Esta aplicación no requiere ningún permiso especial. Así que deja AndroidManifest.xml como predeterminado.

Implementación paso a paso

Paso 1: Crear un nuevo proyecto

Para crear un nuevo proyecto en Android Studio, consulte Cómo crear/iniciar un nuevo proyecto en Android Studio . Tenga en cuenta que seleccione Java como lenguaje de programación.

Paso 2: Agregar la dependencia en gradle.build

Esta biblioteca se utiliza para agregar una paleta de colores a nuestra aplicación para que el usuario pueda seleccionar cualquier color de su elección.

implementación ‘petrov.kristiyan:colorpicker-library:1.1.10’

Paso 3: trabajar con el archivo activity_main.xml

Vaya a la aplicación > res > diseño > actividad_principal.xml y agregue el siguiente código a ese archivo. A continuación se muestra el código para el archivo  activity_main.xml .

XML

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
     
    <LinearLayout
        android:id="@+id/linear"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
 
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
 
            <ImageButton
                android:id="@+id/btn_undo"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:src="@drawable/ic_undo"
                android:text="Undo" />
 
            <ImageButton
                android:id="@+id/btn_save"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:src="@drawable/ic_floppy_disk"
                android:text="Save" />
 
            <ImageButton
                android:id="@+id/btn_color"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:src="@drawable/ic_colorpicker"
                android:text="Color" />
 
            <ImageButton
                android:id="@+id/btn_stroke"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:src="@drawable/ic_paint_brush"
                android:text="Stroke" />
        </LinearLayout>
 
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
 
            <com.google.android.material.slider.RangeSlider
                android:id="@+id/rangebar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:visibility="gone" />
           
        </LinearLayout>
       
    </LinearLayout>
 
    <com.raghav.paint.DrawView
        android:id="@+id/draw_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/linear"
        android:layout_centerInParent="true" />
     
</RelativeLayout>

Paso 4: crea la clase Stroke

Consulte Cómo crear clases en Android Studio . Y nombra la clase como Stroke . A continuación se muestra el código para el archivo Stroke.java

Java

import android.graphics.Path;
 
public class Stroke {
 
    // color of the stroke
    public int color;
     
    // width of the stroke
    public int strokeWidth;
     
    // a Path object to
    // represent the path drawn
    public Path path;
 
    // constructor to initialise the attributes
    public Stroke(int color, int strokeWidth, Path path) {
        this.color = color;
        this.strokeWidth = strokeWidth;
        this.path = path;
    }
}

Paso 5: crea la clase DrawView

Del mismo modo, cree una nueva clase Java y nombre la clase como DrawView . A continuación se muestra el código para el archivo DrawView.java

Java

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.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
 
import java.util.ArrayList;
 
public class DrawView extends View {
 
    private static final float TOUCH_TOLERANCE = 4;
    private float mX, mY;
    private Path mPath;
     
    // the Paint class encapsulates the color
    // and style information about
    // how to draw the geometries,text and bitmaps
    private Paint mPaint;
    
    // ArrayList to store all the strokes
    // drawn by the user on the Canvas
    private ArrayList<Stroke> paths = new ArrayList<>();
    private int currentColor;
    private int strokeWidth;
    private Bitmap mBitmap;
    private Canvas mCanvas;
    private Paint mBitmapPaint = new Paint(Paint.DITHER_FLAG);
 
    // Constructors to initialise all the attributes
    public DrawView(Context context) {
        this(context, null);
    }
 
    public DrawView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint();
         
        // the below methods smoothens
        // the drawings of the user
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        
        // 0xff=255 in decimal
        mPaint.setAlpha(0xff);
 
    }
 
    // this method instantiate the bitmap and object
    public void init(int height, int width) {
 
        mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(mBitmap);
 
        // set an initial color of the brush
        currentColor = Color.GREEN;
         
        // set an initial brush size
        strokeWidth = 20;
    }
 
    // sets the current color of stroke
    public void setColor(int color) {
        currentColor = color;
    }
 
    // sets the stroke width
    public void setStrokeWidth(int width) {
        strokeWidth = width;
    }
 
    public void undo() {
        // check whether the List is empty or not
        // if empty, the remove method will return an error
        if (paths.size() != 0) {
            paths.remove(paths.size() - 1);
            invalidate();
        }
    }
 
    // this methods returns the current bitmap
    public Bitmap save() {
        return mBitmap;
    }
 
    // this is the main method where
    // the actual drawing takes place
    @Override
    protected void onDraw(Canvas canvas) {
        // save the current state of the canvas before,
        // to draw the background of the canvas
        canvas.save();
         
        // DEFAULT color of the canvas
        int backgroundColor = Color.WHITE;
        mCanvas.drawColor(backgroundColor);
 
        // now, we iterate over the list of paths
        // and draw each path on the canvas
        for (Stroke fp : paths) {
            mPaint.setColor(fp.color);
            mPaint.setStrokeWidth(fp.strokeWidth);
            mCanvas.drawPath(fp.path, mPaint);
        }
        canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
        canvas.restore();
    }
 
    // the below methods manages the touch
    // response of the user on the screen
 
    // firstly, we create a new Stroke
    // and add it to the paths list
    private void touchStart(float x, float y) {
        mPath = new Path();
        Stroke fp = new Stroke(currentColor, strokeWidth, mPath);
        paths.add(fp);
 
        // finally remove any curve
        // or line from the path
        mPath.reset();
         
        // this methods sets the starting
        // point of the line being drawn
        mPath.moveTo(x, y);
        
        // we save the current
        // coordinates of the finger
        mX = x;
        mY = y;
    }
 
    // in this method we check
    // if the move of finger on the
    // screen is greater than the
    // Tolerance we have previously defined,
    // then we call the quadTo() method which
    // actually smooths the turns we create,
    // by calculating the mean position between
    // the previous position and current position
    private void touchMove(float x, float y) {
        float dx = Math.abs(x - mX);
        float dy = Math.abs(y - mY);
 
        if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
            mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
            mX = x;
            mY = y;
        }
    }
 
    // at the end, we call the lineTo method
    // which simply draws the line until
    // the end position
    private void touchUp() {
        mPath.lineTo(mX, mY);
    }
 
    // the onTouchEvent() method provides us with
    // the information about the type of motion
    // which has been taken place, and according
    // to that we call our desired methods
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
 
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touchStart(x, y);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                touchMove(x, y);
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                touchUp();
                invalidate();
                break;
        }
        return true;
    }
}

Paso 6: trabajar con el archivo MainActivity.java

Vaya al archivo MainActivity.java y consulte el siguiente código. A continuación se muestra el código del archivo MainActivity.java . Se agregan comentarios dentro del código para comprender el código con más detalle. 

Java

import android.content.ContentValues;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageButton;
 
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
 
import com.google.android.material.slider.RangeSlider;
 
import java.io.OutputStream;
 
import petrov.kristiyan.colorpicker.ColorPicker;
 
public class MainActivity extends AppCompatActivity {
     
    // creating the object of type DrawView
    // in order to get the reference of the View
    private DrawView paint;
     
    // creating objects of type button
    private ImageButton save, color, stroke, undo;
     
    // creating a RangeSlider object, which will
    // help in selecting the width of the Stroke
    private RangeSlider rangeSlider;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        // getting the reference of the views from their ids
        paint = (DrawView) findViewById(R.id.draw_view);
        rangeSlider = (RangeSlider) findViewById(R.id.rangebar);
        undo = (ImageButton) findViewById(R.id.btn_undo);
        save = (ImageButton) findViewById(R.id.btn_save);
        color = (ImageButton) findViewById(R.id.btn_color);
        stroke = (ImageButton) findViewById(R.id.btn_stroke);
 
        // creating a OnClickListener for each button,
        // to perform certain actions
 
        // the undo button will remove the most
        // recent stroke from the canvas
        undo.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                paint.undo();
            }
        });
         
        // the save button will save the current
        // canvas which is actually a bitmap
        // in form of PNG, in the storage
        save.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                 
                // getting the bitmap from DrawView class
                Bitmap bmp = paint.save();
                 
                // opening a OutputStream to write into the file
                OutputStream imageOutStream = null;
 
                ContentValues cv = new ContentValues();
                 
                // name of the file
                cv.put(MediaStore.Images.Media.DISPLAY_NAME, "drawing.png");
                 
                // type of the file
                cv.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
                 
                // location of the file to be saved
                cv.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
 
                // get the Uri of the file which is to be created in the storage
                Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cv);
                try {
                    // open the output stream with the above uri
                    imageOutStream = getContentResolver().openOutputStream(uri);
                     
                    // this method writes the files in storage
                    bmp.compress(Bitmap.CompressFormat.PNG, 100, imageOutStream);
                     
                    // close the output stream after use
                    imageOutStream.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        // the color button will allow the user
        // to select the color of his brush
        color.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                final ColorPicker colorPicker = new ColorPicker(MainActivity.this);
                colorPicker.setOnFastChooseColorListener(new ColorPicker.OnFastChooseColorListener() {
                    @Override
                    public void setOnFastChooseColorListener(int position, int color) {
                        // get the integer value of color
                        // selected from the dialog box and
                        // set it as the stroke color
                        paint.setColor(color);
                    }
                    @Override
                    public void onCancel() {
                        colorPicker.dismissDialog();
                    }
                })
                        // set the number of color columns
                        // you want  to show in dialog.
                        .setColumns(5)
                        // set a default color selected
                        // in the dialog
                        .setDefaultColorButton(Color.parseColor("#000000"))
                        .show();
            }
        });
        // the button will toggle the visibility of the RangeBar/RangeSlider
        stroke.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (rangeSlider.getVisibility() == View.VISIBLE)
                    rangeSlider.setVisibility(View.GONE);
                else
                    rangeSlider.setVisibility(View.VISIBLE);
            }
        });
 
        // set the range of the RangeSlider
        rangeSlider.setValueFrom(0.0f);
        rangeSlider.setValueTo(100.0f);
         
        // adding a OnChangeListener which will
        // change the stroke width
        // as soon as the user slides the slider
        rangeSlider.addOnChangeListener(new RangeSlider.OnChangeListener() {
            @Override
            public void onValueChange(@NonNull RangeSlider slider, float value, boolean fromUser) {
                paint.setStrokeWidth((int) value);
            }
        });
 
        // pass the height and width of the custom view
        // to the init method of the DrawView object
        ViewTreeObserver vto = paint.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                paint.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = paint.getMeasuredWidth();
                int height = paint.getMeasuredHeight();
                paint.init(height, width);
            }
        });
    }
}

Nota: 

Para los archivos de recursos dibujables, puede encontrarlos en el siguiente enlace de GitHub.

Enlace del proyecto Github: https://github.com/raghavtilak/Paint

Producción:

Alcance futuro

Hay muchas cosas que puede agregar a este proyecto como: 

  1. Agregar una máscara al objeto pintado, es decir, crear un efecto de desenfoque o relieve en el trazo.
  2. Adición de animaciones a la aplicación.
  3. Agregar un selector de color para el lienzo, es decir, cambiar el color del lienzo del color blanco predeterminado según los requisitos del usuario.
  4. Agregar un botón para compartir, para compartir directamente el dibujo en varias aplicaciones.
  5. Agregar una funcionalidad de borrador que borra la ruta/trazo específico en el que se arrastra el borrador.
  6. Agregar un selector de forma, mediante el cual un usuario puede seleccionar directamente cualquier forma en particular de la lista y puede arrastrar en la pantalla para crear esa forma.
  7. Mejora de la interfaz de usuario mediante la adición de BottomSheet, vectores, etc.

“Cualquiera puede poner pintura en un lienzo, pero solo un verdadero maestro puede darle vida a la pintura”, terminamos de construir nuestra aplicación, ahora dibuja algunas pinturas increíbles en este lienzo y conviértete en un “verdadero maestro”. 

Nota:

  1. No llame directamente al método getMeasuredWidth(), getMeasuredHeight() ya que estos pueden devolver el valor 0. Debido a que una Vista tiene su propio ciclo de vida, Adjunto->Medido->Diseño->Dibujar, entonces cuando llame a estos métodos, la vista podría no se han inicializado por completo. Por lo tanto, se recomienda utilizar ViewTreeObserver, que se activa en el mismo momento en que se ha diseñado o dibujado la vista en la pantalla.
  2. En caso de que haya habilitado el modo oscuro en su dispositivo de prueba, es posible que vea un pequeño efecto de falla al cambiar la vista personalizada del modo oscuro al claro. Dado que la aplicación no está optimizada para DarkMode.

Publicación traducida automáticamente

Artículo escrito por raghav14 y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *