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
- 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.
- 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.
- 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.
- 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:
- Agregar una máscara al objeto pintado, es decir, crear un efecto de desenfoque o relieve en el trazo.
- Adición de animaciones a la aplicación.
- 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.
- Agregar un botón para compartir, para compartir directamente el dibujo en varias aplicaciones.
- Agregar una funcionalidad de borrador que borra la ruta/trazo específico en el que se arrastra el borrador.
- 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.
- 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:
- 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.
- 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.