Dibujo de un pulgar hueco redondeado sobre arco

Quiero crear un gráfico redondeado que muestre un rango de valores de mi aplicación. Los valores se pueden clasificar en 3 categorías: baja, media, alta – que están representadas por 3 colores: azul, verde y rojo (respectivamente).

Por encima de este rango, quiero mostrar los valores realmente medidos – en forma de "pulgar" sobre la parte del rango relevante:

Ver la foto adjunta

La posición del pulgar blanco sobre el arco de rango puede cambiar, según los valores medidos.

Actualmente, soy capaz de dibujar la gama de 3 colores dibujando 3 arcos sobre el mismo centro, dentro del método onDraw de la vista:

width = (float) getWidth(); height = (float) getHeight(); float radius; if (width > height) { radius = height / 3; } else { radius = width / 3; } paint.setAntiAlias(true); paint.setStrokeWidth(arcLineWidth); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStyle(Paint.Style.STROKE); center_x = width / 2; center_y = height / 1.6f; left = center_x - radius; float top = center_y - radius; right = center_x + radius; float bottom = center_y + radius; oval.set(left, top, right, bottom); //blue arc paint.setColor(colorLow); canvas.drawArc(oval, 135, 55, false, paint); //red arc paint.setColor(colorHigh); canvas.drawArc(oval, 350, 55, false, paint); //green arc paint.setColor(colorNormal); canvas.drawArc(oval, 190, 160, false, paint); 

Y este es el resultado del arco:

Arco de corriente

Mi pregunta es, ¿cómo puedo:

  1. Crear un gradiente suave entre esos 3 colores (intenté usar SweepGradient pero no me dio el resultado correcto).
  2. Cree el pulgar blanco de la superposición como se muestra en la imagen, para que pueda controlar dónde mostrarlo.

  3. Animar este pulgar blanco sobre mi arco de rango.

Nota: el rango de 3 colores es estático – por lo que otra solución puede ser simplemente tomar el dibujable y pintar el pulgar blanco sobre él (y animarlo), así que estoy abierto a escuchar una solución así 🙂

Yo usaría máscaras para sus dos primeros problemas.

1. Crear un gradiente suave

El primer paso sería dibujar dos rectángulos con un gradiente lineal. El primer rectángulo contiene los colores azul y verde, mientras que el segundo rectángulo contiene verde y rojo como se ve en la siguiente imagen. Marcé la línea donde ambos rectángulos se tocan negro para aclarar que son de hecho dos rectángulos diferentes.

primer paso

Esto se puede lograr utilizando el siguiente código (extracto):

 // Both color gradients private Shader shader1 = new LinearGradient(0, 400, 0, 500, Color.rgb(59, 242, 174), Color.rgb(101, 172, 242), Shader.TileMode.CLAMP); private Shader shader2 = new LinearGradient(0, 400, 0, 500, Color.rgb(59, 242, 174), Color.rgb(255, 31, 101), Shader.TileMode.CLAMP); private Paint paint = new Paint(); // ... @Override protected void onDraw(Canvas canvas) { float width = 800; float height = 800; float radius = width / 3; // Arc Image Bitmap.Config conf = Bitmap.Config.ARGB_8888; // See other config types Bitmap mImage = Bitmap.createBitmap(800, 800, conf); // This creates a mutable bitmap Canvas imageCanvas = new Canvas(mImage); // Draw both rectangles paint.setShader(shader1); imageCanvas.drawRect(0, 0, 400, 800, paint); paint.setShader(shader2); imageCanvas.drawRect(400, 0, 800, 800, paint); // /Arc Image // Draw the rectangle image canvas.save(); canvas.drawBitmap(mImage, 0, 0, null); canvas.restore(); } 

Como su objetivo es tener un arco de color con casquillos redondeados, a continuación necesitamos definir el área de los dos rectángulos que deben ser visibles para el usuario. Esto significa que la mayoría de los dos rectángulos serán enmascarados lejos y por lo tanto no visibles. En cambio, lo único que queda es el área de arco.

El resultado debería tener este aspecto:

segundo paso

Con el fin de lograr el comportamiento necesario se define una máscara que sólo revela el área de arco dentro de los rectángulos. Para ello hacemos un uso setXfermode método setXfermode de Paint . Como argumento usamos diferentes instancias de un PorterDuffXfermode .

 private Paint maskPaint; private Paint imagePaint; // ... // To be called within all constructors private void init() { // I encourage you to research what this does in detail for a better understanding maskPaint = new Paint(); maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); imagePaint = new Paint(); imagePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); } @Override protected void onDraw(Canvas canvas) { // @step1 // Mask Bitmap mMask = Bitmap.createBitmap(800, 800, conf); Canvas maskCanvas = new Canvas(mMask); paint.setColor(Color.WHITE); paint.setShader(null); paint.setStrokeWidth(70); paint.setStyle(Paint.Style.STROKE); paint.setStrokeCap(Paint.Cap.ROUND); paint.setAntiAlias(true); final RectF oval = new RectF(); center_x = 400; center_y = 400; oval.set(center_x - radius, center_y - radius, center_x + radius, center_y + radius); maskCanvas.drawArc(oval, 135, 270, false, paint); // /Mask canvas.save(); // This is new compared to step 1 canvas.drawBitmap(mMask, 0, 0, maskPaint); canvas.drawBitmap(mImage, 0, 0, imagePaint); // Notice the imagePaint instead of null canvas.restore(); } 

2. Cree el pulgar blanco de la superposición

Esto soluciona su primer problema. El segundo se puede lograr usando máscaras de nuevo, aunque esta vez queremos lograr algo diferente. Antes, queríamos mostrar sólo un área específica (el arco) de la imagen de fondo (siendo los dos rectángulos). Esta vez queremos hacer lo contrario: Definimos una imagen de fondo (el pulgar) y enmascaramos su contenido interno, de modo que sólo el golpe parece permanecer. Aplicado a la imagen de arco, el pulgar superpone el arco de color con un área de contenido transparente.

Así que el primer paso sería dibujar el pulgar. Utilizamos un arco para esto con el mismo radio que el arco de fondo, pero con ángulos diferentes, resultando en un arco mucho más pequeño. Pero debido a que el pulgar debe "rodear" el arco de fondo, su anchura de trazo tiene que ser mayor que el arco de fondo.

 @Override protected void onDraw(Canvas canvas) { // @step1 // @step2 // Thumb Image mImage = Bitmap.createBitmap(800, 800, conf); imageCanvas = new Canvas(mImage); paint.setColor(Color.WHITE); paint.setStrokeWidth(120); final RectF oval2 = new RectF(); center_x = 400; center_y = 400; oval2.set(center_x - radius, center_y - radius, center_x + radius, center_y + radius); imageCanvas.drawArc(oval2, 270, 45, false, paint); // /Thumb Image canvas.save(); canvas.drawBitmap(RotateBitmap(mImage, 90f), 0, 0, null); canvas.restore(); } public static Bitmap RotateBitmap(Bitmap source, float angle) { Matrix matrix = new Matrix(); matrix.postRotate(angle); return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true); } 

El resultado del código se muestra a continuación.

tercer paso

Así que ahora que tenemos un pulgar que superpone el arco de fondo, necesitamos definir la máscara que elimina la parte interna del pulgar, de modo que el arco de fondo vuelva a ser visible nuevamente.

Para lograr esto usamos básicamente los mismos parámetros que antes para crear otro arco, pero esta vez el ancho del trazo tiene que ser idéntico al ancho utilizado para el arco de fondo ya que esto marca el área que queremos eliminar dentro del pulgar.

Utilizando el siguiente código, la imagen resultante se muestra en la imagen 4.

cuarto paso

 @Override protected void onDraw(Canvas canvas) { // @step1 // @step2 // Thumb Image // ... // /Thumb Image // Thumb Mask mMask = Bitmap.createBitmap(800, 800, conf); maskCanvas = new Canvas(mMask); paint.setColor(Color.WHITE); paint.setStrokeWidth(70); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); final RectF oval3 = new RectF(); center_x = 400; center_y = 400; oval3.set(center_x - radius, center_y - radius, center_x + radius, center_y + radius); maskCanvas.drawBitmap(mImage, 0, 0, null); maskCanvas.drawArc(oval3, 270, 45, false, paint); // /Thumb Mask canvas.save(); canvas.drawBitmap(RotateBitmap(mMask, 90f), 0, 0, null); // Notice mImage changed to mMask canvas.restore(); } 

3. Animar el pulgar blanco

La última parte de su pregunta sería animar el movimiento del arco. No tengo una solución sólida para esto, pero tal vez puede guiarlo en una dirección útil. Yo intentaría lo siguiente:

Primero defina el pulgar como un ImageView que forma parte de su gráfico de arco completo. Al cambiar los valores seleccionados de su gráfico, gire la imagen del pulgar alrededor del centro del arco de fondo. Debido a que queremos animar el movimiento, simplemente establecer la rotación de la imagen del pulgar no sería adecuada. En su lugar, utilizamos un tipo de RotateAnimation como así:

 final RotateAnimation animRotate = new RotateAnimation(0.0f, -90.0f, // You have to replace these values with your calculated angles RotateAnimation.RELATIVE_TO_SELF, // This may be a tricky part. You probably have to change this to RELATIVE_TO_PARENT 0.5f, // x pivot RotateAnimation.RELATIVE_TO_SELF, 0.5f); // y pivot animRotate.setDuration(1500); animRotate.setFillAfter(true); animSet.addAnimation(animRotate); thumbView.startAnimation(animSet); 

Esto está lejos de ser definitiva, supongo, pero muy bien puede ayudarle en su búsqueda de la solución necesaria. Es muy importante que sus valores de pivote tengan que referirse al centro de su arco de fondo, ya que éste es el punto en el que su imagen de pulgar debe girar alrededor.

He probado mi código (completo) con el API de nivel 16 y 22, 23, por lo que espero que esta respuesta al menos le da nuevas ideas sobre cómo resolver sus problemas.

Tenga en cuenta que las operaciones de asignación dentro del método onDraw son una mala idea y deben evitarse. Por simplicidad no seguí este consejo. También el código debe ser utilizado como una guía en la dirección correcta y no ser simplemente copia y pegado, ya que hace un uso intensivo de números mágicos y generalmente no sigue buenas normas de codificación.

  1. Me gustaría cambiar un poco de la forma en que dibuja su punto de vista, mirando el diseño original, en lugar de dibujar 3 tapas que dibujaría sólo una línea, de esa manera el SweepGradient funcionará.

  2. Este migth sea un pedacito complicado, usted tiene 2 opciones:

    • Crear un Path con 4 arcos
    • Dibujar 2 arcos-uno es el blanco grande (lleno de blanco por lo que todavía quiere utilizar Paint.Style.STROKE ) y otro en la parte superior de que lo hacen relleno transparente, se puede lograr con PorterDuff xfermode, probablemente le llevará par de Intenta hasta que usted consigue que sin limpiar el círculo verde también.
  3. Me imagino que desea animar la posición del pulgar, por lo que sólo tiene que utilizar la Animation simple que invalidan la vista y dibujar la posición vista del pulgar en consecuencia.

Espera que esto ayude

  • Diseño desplazable en Android
  • Cómo dibujar un ShapeDrawable más pequeño dentro de otro shapeDrawable programaticamente
  • EditText ellipsize (tres puntos ...)
  • Cómo obtener una referencia al grupo de vista principal
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.