Mejorando el “Doble panel” en Android

Siguendo con la serie de casos en mejorar las interfaces en Android, esta vez le toca al doble panel que solemos ver en las aplicaciones cuando las usamos en una tablet. No voy a entrar en detalle de cómo implementar un doble panel con Fragments ya que en la web hay más que suficiente información para hacerlo.

De lo que si vamos a hablar es de cómo mejorar el aspecto y darle al usuario una mejor experiencia. Para lograr esto lo que vamos a hacer es copiar lo que hacen Google con sus aplicaciones, com Gmail o la misma aplicación de Contactos que viene en IceCream Sandwich y que se ve algo así :

ics_effects

El simple agregado de un gradiente a modo de “dropshadow” entre los paneles y el indicador de selección cambian radicalmente la experiencia de usuario, por más simple que parezca el cambio, pueden verlo al final del post donde dejo el antes y el después. Ahora bien, lo simple no siempre es fácil :), por lo que para lograr este efecto tuve que trabajar un poco. Empecemos por la sombra.

Existen varias formas de lograrla, desde las más simples como usar un FrameLayour con un bitmap como overlay hasta backgrounds complicados. Sin embargo se pueden obtener efectos raros en versiones nuevas de Android, ya que la ActionBar también tiene un dropshadow. Estas técnicas además pueden hacer complicado la segunda parte que es mostrar el selector.

La forma más prolija que encontré es meterse en el ciclo de render y aprovechar el método “dispatchDraw(Canvas canvas)”. Este método es el encargado de dibujar un View, por lo que si lo sobreescribimos podemos dibujar cosas antes o despues del render original (que lo llamaremos con “super.dispatchDraw(canvas)”). Como es un método protegido, lo que debemos hacer entonces es nuestra propia View, en este caso basta con hacer nuestra propia ListView :

;
public class ShadowListView extends ListView {
  public ShadowListView(Context context) {
    super(context);
  }

  private void prepareShadow() {
    // Load drawables and initialize stuff
  }

  @Override
  protected void dispatchDraw(Canvas canvas) {
    // Draw the children
    super.dispatchDraw(canvas);

    if (!initializes)
      prepareShadow();

    drawShadow(canvas);
  }

  private void drawShadow(Canvas canvas) {
    canvas.save();
    canvas.translate(getWidth() - 20, 0);
    drawable.setBounds(0, 0, 20, getHeight());
    drawable.draw(canvas);
    canvas.restore();
  }
}

Lo siguientes es utilizarla en nuestro layout directamente en lugar de usar <ListView> usar <ShadowListView>. Con esto lo que hacemos es dibujar en el borde derecho, desde arriba hasta abajo con un ancho de “20” la sombra, haciendo parecer que el panel derecho está por arriba y que le hace “sombra” a la lista, cool :), no?.

Lo siguiente a lograr es dibujar el triángulo y queremos tener en cuanta varias cosas : debe estar dibujado por encima de la sombra (así el efecto queda bien) y debe coincidir en la posición del elemento seleccionado en la lista, si hay alguno.

El primer problema con el que me encontré es que quise utilizar “getSelectedView” para obtener la View seleccionada. Lamentablemente no pude entender por qué no funciona como parecería indicar el nombre :D. Por lo que fui por un camino diferente : como ya tengo mi clase de ListView, puedo sobreescribir otros métodos, como por ejemplo “performItemClick” para saber cuando un item fue “clickeado” (ya sea via touch o via teclado). Cuando eso pasa me guardo esa posición para futuro :

@Override
public boolean performItemClick(View view, int position, long id) {
  selectedPosition = position;
  return super.performItemClick(view, position, id);
}

Y luego puedo definir el método que dibuja la flecha :

private void drawArrow(Canvas canvas) {
  View selectedView = getChildAt(selectedPosition - getFirstVisiblePosition());
  if (selectedView != null) {
    selectedView.getDrawingRect(rect);

    offsetDescendantRectToMyCoords(selectedView, rect);

    int h = selectedView.getHeight();

    canvas.save();
    canvas.translate(getWidth() - h, rect.top);
    arrow.setBounds(0, 0, h, h);
    arrow.draw(canvas);
    canvas.restore();
  }
}

Lo primero que hago es ver que vista está seleccionada. Para eso, como ListView puede reutilizar un View para optimizar el render, debo tener en cuenta el View que se está mostrando. Si tengo una vista obtengo su recta de de dibujado. Esta recta está en coordenadas de la vista, por lo que tengo que trasladar esas coordenadas a mi espacio de coordenadas, y eso android me permite hacerlo fácil llamando a “offsetDescendantRectToMyCoords”.

Por último dibujo la flecha que es un PNG cuadrado que tiene la flecha con su propio dropshadow. El paso final es llamar a drawArrow despues de drawShadow en el dispatchDraw, de manera que si dibujo la flecha, esta se dibuja por arriba.

El resultado final, antes y después :

table_sin_efectos tablet_conf_efectos

Mejorando las UI en Android

En los últimos días me puse a ver varios de los videos de Google I/O del año pasado referidos al diseño de interfaces y diseño gráfico de android. La verdad es que con muy poco se puede cambiar el aspecto radicalmente, y la experiencia termina siendo muy superior (y si tenemos suerte podemos tener una mayor user base lo que significa más ingresos :D).

Para probar diferentes consejos decidí mejorar la interfaz de mi lector del diario Río Negro, el cual era bastante feo y se puede ver al final del post lado a lado con la imagen luego de mejorar el diseño.
La idea era resaltar un poco más los títulos y acomodar el layout. Lo primero que hice fue agregar “paddings”, de manera de descomprimir los diferentes elementos de la lista. Luego de un par de pruebas y error el valor de “10dp” fue el ganador.Una vez hecho eso tire los LinearLayout que usaba y converti a RelativeLayout lo que además hizo que quede más natural al rotar o usar la aplicación en otra resolución, ya que los “floats” se adapan mucho mejor una vez que uno entiende bien que opción va en cada caso :D.Lo último fue mejorar el render de la imagen. Como primer paso decidí cropear las imágenes a un tamaño único, para que tenga todo más sentido : da la sensación de uniformidad y distrae mucho menos si queremos ignorar las imágenes.El último punto fue hacer mas “fancy” la imagen poniéndole bordes redondeados. Para eso hice un custom View que cache la imagen usando una versión modificada de esta técnica.La imagen original y el resultado final a continuación :

Todavía no está subida a Google Play esta modificación pero lo estará en los próximos días. Mientras si pueden bajar la versión actual desde el market ;).


Get it on Google Play

Participando en Facebook Hacker Cup

Este año me acordé a tiempo de este evento (y la verdad que este año estuve usando por primera vez mi cuenta de Facebook :P) así que decidí probar a ver que onda.

La primer impresión : se complica rendir bien teniendo familia (o una vida de por si :P); no se puede salir apenas aparecen los problemas a lo loco a cualquier hora a resolverlos.

Desde el punto de vista técnico, los dos primeros problemas fueron muy fáciles. “Beautiful strings” fue trivial realmente, con pensar 2 o 3 min salía. El problema “Balanced Smileys” donde se buscaba determinar balanceo de () con al dificultad de que había caritas felices y tristes fue un poco mas arduo de probar, más que nada por pensar en posibilidades locas para estar seguro de tener todos los casos. En ambos problemas el primer envío de mi solución paso bien, así que realmente para estar fuera de training nada mal :).

El problema real llegó en el tercer problema, el que más puntos da. “Find the Min” es un problema conceptualmente fácil. Me complico un poco al principio entender por el idioma (“which is *not* contained in the previous *k* values of m.” no fue una frase trivial de entender para mi :D). El desafío del problema era principalmente la optimización, ya que se trabajaba mucho con arrays y en algunos casos se podía ir el tiempo al demonio.

En mi implementación (que veré de postear en algún momento cuando cierre el concurso) pude resolver los ejemplos que dan y algunos casos más que invente grandes en unos 2 a 3 min. Sin embargo no conté con casos tan complicados como los que venían cuando uno se bajaba el archivo posta para participar y lamentablemente llegué unos 5 o 6 minutos tarde, por lo que no pude subir mi solución. Facebook nos da una ventana de 6 minutos desde que bajamos el archivo de prueba hasta subir la solución. Yo pase de los 10 y ahí me quede.

Calculo que eso me elimina para la siguiente face, pero uno nunca sabe :P, tal vez me vaya mejor el año que viene :D. O en el Google Code Jam que viene dentro de poco ;).

Otro año que se va …

Hace un año aproximadamente hice mi último update :D, la verdad es que 2012 me agarró con muy pocas ganas de escribir, aunque espero que eso durante 2013 cambie.

Este año ha sido un gran año realmente, tanto laboral como personal. Tal vez el logro más destacable sera que finalmente pude presentar mi tesis de Ingenería en Informática (la cual estaré publicando la semana que viene calculo), haciendo que “automáticamente” pueda poner un “Ing.” en la firma de los mails :P.

El año que se va también me vio alejado de mis colaboraciones al software libre, otra cosa que quiero ver de cambiar un poco más en el 2013. Veremos …

Feliz 2013

Oregano tiene nuevo lider

Luego ya de muchisimos años (parece eterno) alguien con pilas apareció Marc Lorber, quien va a ser el nuevo lider y main developer de Oregano, proyecto que salio de este grupo alguna vez. Marc estaba completando el port a Gtk3 y al nuevo canvas además de tener pilas para arreglar los bugs históricos que arrastramos hace tiempo :).

El main git fue movido a https://github.com/marc-lorber/oregano por lo que es la nueva fuente oficial.

Saludos y Feliz 2012 para todos!

Git branch & Subversion detection en el prompt de Bash

Simple, agregar en el .bashrc lo siguiente :

function parse_git_branch {
  git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* (.*)/(1)/'
}

function detect_svn {
  test -d .svn && echo "(svn)"
}

function proml {
  local         RED="[33[0;31m]"
  local   LIGHT_RED="[33[1;31m]"
  local      YELLOW="[33[0;33m]"
  local LIGHT_GREEN="[33[1;32m]"
  local       WHITE="[33[1;37m]"
  local  LIGHT_GRAY="[33[0;37m]"
  local LIGHT_PURPLE="[33[1;34m]"
  case $TERM in
    xterm*)
    TITLEBAR='[33]0;u@h:w07]'
    ;;
    *)
    TITLEBAR=""
    ;;
  esac

PS1="${TITLEBAR}
$LIGHT_PURPLEw$YELLOW$(parse_git_branch)$(detect_svn)
$LIGHT_GRAY$ "
PS2='> '
PS4='+ '
}
proml

Y se ve algo como (sin colores, sorry 🙂 :

~/src/some_git_project(master)$ 
~/src/some_svn_project(svn)$

Filtro de Contenido para T!

Hoy estuve leyendo este post donde los dueños de Taringa declara que “no podemos investigar el destino final de cada uno de los 20.000 post diarios”. Lejos de ser algo “colectivamente inteligente” como declaración, es una terrible boludez :). Con aplicar algunos filtros automáticos, permitir que cualquiera de forma rápida y fácil reporte los contenidos ilegales y actuando más o menos rápido en su eliminación uno se cubre el 90% del orto. El otro 10% hay que pelearlo por carta documento porque hay gente muy al pedo ahí afuera, pero ni en pedo llegamos a un juicio oral teniendo un poco de criterio :).

El problema es que filtrar a mano es caro, y además filtrar hace que nuestros usuarios bajen pues no tiene ya toda la piratería al alcance de la mano ;). Pero veamos como hacer un filtro para determinar por heurística los posts que posiblemente sean ilegales, de manera de poder revisar a mano solo los que tal ve son malos, y así bajar el trabajo necesario y por ahí bajar el costo y aumentar el profit :).

No lo voy a explicar muy en detalle, pero básicamente el programa siguiente hace un scraping de la home de T! y agarra los ‘Ultimos posts’. Para cada uno de ellos, lee el contenido y aplica una heurística muy básica para ver si es potencialmente ilegal :

require 'rubygems'
require 'net/http'
require 'hpricot'

AGENT = "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.10) Gecko/20100915 Ubuntu/10.04 (lucid) Firefox/3.6.10"

url = URI.parse("http://www.taringa.net/")
http = Net::HTTP.new("www.taringa.net")
html = http.request_get('/', {'User-Agent' => AGENT})

doc = Hpricot(html.body)
(doc/"html/body/div[2]/div[4]/div[4]/div/div[2]/ul/li/a").each do |link|
  text = http.request_get(link.attributes['href'], {'User-Agent' => AGENT})
  doc1 = Hpricot(text.body)
  content = (doc1/"html/body/div[2]/div[4]/div[4]/div/div[2]/div[2]").first.to_s

  if (content =~ /megaupload/i) || (content =~ /free/i) || (content =~ /mediafire/i) || (content =~ /fileserve/i)
    puts "#{link.inner_html.strip} posible post ilegal"
  else
    if (content =~ /Este post es privado/i)
        puts "#{link.inner_html.strip} es privado. Al menos si es ilegal no lo ve todo el mundo :)"
    else
      puts "#{link.inner_html.strip} parece legal."
    end
  end
end

Con esto podemos saber :

  • Cuantos posts parecen legales
  • Cuantos parecen ilegales
  • Cuantos no son públicos, por lo que podemos revisarlos o no, no es tan terrible

Con un poco de bash sacamos las stats :

gazer@Max:~$ ruby tfilter.rb > post.txt
54 post.txt
gazer@Max:~$ grep 'posible post ilegal' post.txt | wc -l
2

Solo el 3.7% de los posts analizados parecería ser ilegales, por lo que en lugar de tener que revisar 20.000 posts por día solo deberia revisar 740, el número es otro. Si de esos ademas sumás el report del user, seguro no te quedan mucho más y hasta por ahí lo podés moderar mientras desayunas :).

Obviamente mi filtro se basa solamente en linksharing (que es el 90% del problema de T! y del uso que sus usuarios le dan al site). Seguramente haya servicios de upload que no puse, pero para demostrar que filtrar el contenido es una terrible pelotudez y que Taringa! no lo hace porque no le conviene, alcanza y sobra :).

Posts analizados