[Java] Duda con sleep()

Spacelord

Buenas.

Tengo un método que, dado un label con una imagen asignada (un icon), llamada "back", implementa un listener tal que:

  • Cuando se hace click, la imagen del icono cambia de "back" a "front".
  • El programa se pausa X segundos.
  • La imagen del icono vuelve a ser "back".

El código que tengo es este:

private void jLabelMouseClicked(java.awt.event.MouseEvent evt) {
    jLabel.setIcon(front);
    Thread.sleep(1000);
    jLabel.setIcon(back);               
}

Esto no funciona como parece a simple vista. En lugar de cambiar, esperar y volver a cambiar, es como si sleep() reseteara el bucle. Es decir, que primero espera un segundo, luego cambia a front e inmediatamente después a back, como si el sleep() se pusiera antes que los dos setIcon() en lugar de enmedio.

¿Alguien sabe por qué ocurre esto? He estado mirando la API pero no veo nada raro o que me haga pensar que estoy usando mal el método. También podría ser el sueño que tengo, no lo descarto.

P.D.: sólo hay un hilo de ejecución en todo el programa.

shaba

Te dejo esto que te puede ayudar
http://stackoverflow.com/questions/13823601/thread-sleep-change-image-java

Creo que no se llama al método draw() internamente porque esta en sleep, en el link lo explican mejor

1
Dostoievski

No sé como funcionará las llamadas al draw pero imagino que el setIcon lo que hará es encolar la acción para que cuando se vaya a pintar lo ponga y el pintado en pantalla lo hace después de haber procesado los eventos.

Después de haber visto el código del setItcon, efectivamente, encola un evento de pintado.

Spacelord

Sí, imaginaba que era eso. Buscando por ahí ya había llegado a sitios donde sugerían un timer en vez de un sleep(), pero no sabía por qué y tampoco lo explicaban. Muchas gracias a los dos.

Spacelord

Por comentar un poco la jugada, he conseguido usar bien el Timer (el de java.util, no el componente de swing), pero me da un nuevo problema: si llamo al TimerTask() por segunda vez mientras aún no ha dejado de ejecutarse (porque el delay aún no ha terminado de contar), me salta un NullPointerException:

TimerTask notMatch = new TimerTask() 
                {
                    @Override
                    public void run()
                    {
                        flippedCard.setIcon(back);
                        countFlippedCards = 0;
                        label.setIcon(back);
                        resetCards();
                    }
                };
timer.schedule(notMatch, delay);

El caso es que sé por qué salta: el método resetCards() setea a null el elemento flippedCard, que es un JLabel y atributo de la clase. Al invocar por segunda vez notMatch mientras aún está esperando a ejecutarse flippedCard vale null y de ahí la excepción.

Que en realidad problema no es, porque le puedo poner delante un glassPane y bloquear todo el input de teclado y ratón mientas dure el delay y la funcionalidad del programa sigue siendo la misma. Sdemás, así es incluso más seguro porque no tengo que preocuparme de inputs locos mientras se ejecuta el delay.

1 respuesta
B

#5 Yo soy mas fan de los juegos que te permiten introducir input en medio de las animaciones (Bejeweled Bitz) vs los juegos que se ejecutan "por turnos" (Candy Crush). La forma de conseguir esto sería que, cuando una carta se está girando front->back y la vuelves a girar, lo que haces es cancelar la animación y la marcas como "flipped".

Otra posible solución a tu problema es que flippedCard no sea un "miembro" de la clase, que no es muy buena idea cuando trabajas con hilos, sino que sea miembro del "timer de animación". Entiendo que cuando pones flippedCard a null no es porque estes "borrando" la carta, sino que es un "puntero" a la que tienes girada.

TimerTask flipCard= new TimerTask() 
{
    private jButton _card = flippedCard;
    private jLabel _label = label;
        
@Override public void run() { _card .setIcon(back); _label.setIcon(back); --countFlippedCards; // si vas a poder tener n giros a la vez, es mejor restar resetCards(); } }; timer.schedule(flipCard, delay);

Ese resetCards(), por eso no se si debería ir despues del schedule y no en el TimerTask.run, pero ahí tienes tu más contexto.

Por completitud de la respuesta añado que, en vez de crear miembros de la clase interna anónima, podrías conseguir el mismo resultado usando variables locales finales pero, personalmente, me parece más limpio (y seguramente sea más fácil de depurar) si la clase tiene una referencia a los miembros que usa.

1 respuesta
Spacelord

#6 Te explico.

Ese TimerTask() se invoca cuando, habiendo una carta girada, se gira una segunda carta que resulta ser igual a la primera. FlippedCard es, efectivamente, un JLabel que en ese momento tiene el valor de la primera carta girada, que se ha guardado para comparar el valor con la segunda. CountFlippedCards cuenta la cantidad de cartas que hay giradas en cada momento para evitar que se puedan girar más de dos simultáneamente (porque sería hacer trampa). Por eso se setean a cero y a null respectivamente cuando se ejecuta el TimerTask(). resetCards() tiene que ir dentro del run() porque es el método que hace todo esto, si lo pusiera después del run() la línea

flippedCard.setIcon(back);

Me soltaría un NullPointer porque, para ese momento, flippedCard valdría null.

Al final he resuelto el problema sin tener que recurrir al glassPane. Es tan sencillo como meter un if-else en el MouseListener que controla el comportamiento de las JLabels: si countFlippedCards>2, lo que significa que ya hay dos cartas giradas en el Frame, no se hace nada. Anoche con el sueño puse >=2 y por eso se podía girar una más habiendo dos giradas.

Muchas gracias de todas formas por la ayuda, nen, me apuntaré todo eso (sobre todo lo de cancelar la animación) por si lo puedo implementar. :D

Usuarios habituales