Relógio analógico completo em J2ME

Do tópico AWTUtilities.setWindowOpaque acabei desenvolvendo um aplicativo completo para celular (e outros dispositivos móveis).

É um relógio de display analógico com muitos efeitos, simulação de canal alpha, geração e evolução de cores e outras coisas!

Segue o código fonte completo abaixo.

Como o editor do GUJ às vezes dá uma zoada em alguns caracteres segue também em anexo o arquivo de fonte completo, mais os arquivos compilados pra quem quiser instalar direto no celular!

Funciona em muitos aparelhos, já que usei a configuração mais básica possível (CLDC 1.0 e MIDP 1.0), dimensiona todos os elementos de acordo com o tamanho da tela do aparelho instalado e de lambuja tem um ícone bonitinho empacotado junto, que já aparece após instalar o relógio!

:wink:

Divirtam-se e me perguntem o que quiserem!

[code]package soft;

import java.util.Calendar;
import java.util.Random;

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;

public final class MiniRelogio extends MIDlet {

private final int FPS = 30;

private Screen screen;
private int baseColor;
private int frontColor;
private int backColor;

protected void destroyApp(boolean restart) throws MIDletStateChangeException {
	if(!restart) {
		this.notifyDestroyed();
	} else {
		this.startApp();
	}
}

protected void pauseApp() {
	this.notifyPaused();
}

protected void startApp() throws MIDletStateChangeException {
	prepareScreenColors();
	screen = new Screen();
	Display.getDisplay(this).setCurrent(screen);
	screen.start();
}

private void finalize() {
	Display.getDisplay(this).setCurrent(null);
	try {
		destroyApp(screen.isRestart());
	} catch (MIDletStateChangeException e) {
		System.exit(0);
	}
}

private void prepareScreenColors() {
	baseColor = (baseColor == 0 ? sortColor() : updateColor(baseColor, 0xF));
	backColor = mixColors(
			new int[]{
					baseColor,	// ?
					0x808080,	// cinza
					0xFFFFFF},	// branco
			new float[]{
					0.30F * 0.25F,
					0.30F * 0.75F,
					0.70F});
	frontColor = mixColors(
			new int[]{
					baseColor,	// ?
					0x808080,	// cinza
					0x000000},	// preto
			new float[]{
					0.60F * 0.25F,
					0.60F * 0.75F,
					0.40F});
}

private double getPosX(double raio, double angle) {
	// raio * cos angulo
	return raio * Math.cos(Math.toRadians(angle));
}

private double getPosY(double raio, double angle) {
	// raio * sin angulo
	return raio * Math.sin(Math.toRadians(angle));
}

private int getPseudoAlphaColor(int color, float alpha) {
	float alphaBack = 1F - alpha;
	return mixColors(
			new int[]{backColor, color},
			new float[]{alphaBack, alpha});
}

private int mixColors(int[] colors, float[] weights) {
	float[] comps = new float[3];
	for(int i=0; i<colors.length; i++) {
		int[] color = getColorComps(colors[i]);
		float weight = weights[i];
		
		comps[0] += (float)color[0] * weight;
		comps[1] += (float)color[1] * weight;
		comps[2] += (float)color[2] * weight;
	}
	for(int i=0; i><comps.length; i++) {
		if(comps[i] >< 0) {
			comps[i] = 0;
		} else if(comps[i] > 0xFF) {
			comps[i] = 0xFF;
		}
	}
	return getColor(new int[]{(int)comps[0], (int)comps[1], (int)comps[2]});
}

int[] getColorComps(int color) {
	String sColor = leftPadding(Integer.toHexString(color), '0', 6);
	return new int[] {
			Integer.parseInt(sColor.substring(0, 2), 16),
			Integer.parseInt(sColor.substring(2, 4), 16),
			Integer.parseInt(sColor.substring(4, 6), 16),
	};
}

private String leftPadding(String text, char c, int size) {
	while(text.length() < size) {
		text = c + text;
	}
	return text;
}

private int sortColor() {
	Random rand = new Random();
	int[] comps = new int[] {0x00, 0xFF, rand.nextInt(0x100)};
	int[] indexes = new int[] {-1, -1, -1};
	for(int i=0; i<indexes.length; i++) {
		while(indexes[i] == -1) {
			int index = rand.nextInt(indexes.length);
			boolean exists = false;
			for(int j=0; j><i; j++) {
				if(indexes[j] == index) {
					exists = true;
					break;
				}
			}
			if(!exists) {
				indexes[i] = index;
			}
		}
	}
	return getColor(new int[]{comps[indexes[0]], comps[indexes[1]], comps[indexes[2]]});
}

public int updateColor(int color, double speed) {
	int[] comps = getColorComps(color);
	int minColor = 0;
	int maxColor = 0xFF;
	
	int keyIndex = -1;
	
	for(int i=0; i><comps.length; i++) {
		if(comps[i] != minColor && comps[i] != maxColor) {
			keyIndex = i;
			break;
		}
	}
	
	if(keyIndex == -1) { // Dois componentes são iguais (mínimo ou o máximo)
		for(int i=0; i><comps.length; i++) {
			int prox = i + 1;
			if(prox >= comps.length) {
				prox = 0;
			}
			if(comps[i] == comps[prox]) { // Sempre o igual anterior é que muda
				keyIndex = i;
				break;
			}
		}
	}
	
	int leftIndex = keyIndex - 1;
	if(leftIndex < 0) {
		leftIndex = comps.length - 1;
	}
	
	int rightIndex = keyIndex + 1;
	if(rightIndex >= comps.length) {
		rightIndex = 0;
	}
	
	double increment = speed / FPS;
	if(increment < 1) increment = 1;
	
	if(comps[keyIndex] >= comps[leftIndex] && comps[keyIndex] <= comps[rightIndex]) {
		comps[keyIndex] -= increment;
		if(comps[keyIndex] < minColor) {
			comps[keyIndex] = minColor;
		}
	} else {
		comps[keyIndex] += increment;
		if(comps[keyIndex] > maxColor) {
			comps[keyIndex] = maxColor;
		}
	}

	return getColor(comps);
}

private int getColor(int[] comps) {
	String finalColor = "";
	for(int i=0; i<comps.length; i++) {
		finalColor += leftPadding(Integer.toHexString((int)comps[i]), '0', 2);
	}
	return Integer.parseInt(finalColor, 16);
}

private class Screen extends GameCanvas implements Runnable, CommandListener {

	private volatile boolean running;
	private volatile boolean restart;
	private Relogio relogio;
	
	protected Screen() {
		super(true);
		this.setCommandListener(this);
		addCommands();
		relogio = new Relogio(new int[]{getWidth(), getHeight()});
	}
	
	private void addCommands() {
		this.addCommand(new Command("Reiniciar", Command.BACK, 1));
		this.addCommand(new Command("Sair", Command.EXIT, 2));
	}

	public void commandAction(Command command, Displayable displayable) {
		if(displayable!=this)return;
		int type = command.getCommandType();
		if(type == Command.EXIT || type == Command.BACK) {
			if(type == Command.BACK) {
				baseColor = 0;
				restart = true;
			}
			stop();
		}
	}
	
	private void start() {
		Thread t = new Thread(this);
		t.setPriority(Thread.MAX_PRIORITY);
		t.start();
	}
	
	private void stop() {
		running = false;
	}

	public void run() {
		running = true;
		while(running) {
			long time = System.currentTimeMillis();
			this.flushGraphics();
			paint();
			update();
			pause(time);
		}
		finalize();
	}
	
	private void paint() {
		Graphics g = this.getGraphics();
		clearScreen(g);
		relogio.paint(g);
	}

	private void clearScreen(Graphics g) {
		g.setColor(backColor);
		g.fillRect(0, 0, getWidth(), getHeight());
	}

	private void update() {
		prepareScreenColors();
		relogio.update();
	}

	private void pause(long time) {
		long minPause = 5;
		long pauseTime = (long)(1000.0/FPS - (double)(System.currentTimeMillis()-time));
		pauseTime = pauseTime > minPause ? pauseTime : minPause;
		try {
			Thread.sleep(pauseTime);
		} catch (InterruptedException e) {}
	}

	public boolean isRestart() {
		return restart;
	}

}
	
private class Relogio {
	
	private final int LARGURA = 0;
	private final int ALTURA = 1;
	private final int ANCHOR = Graphics.TOP | Graphics.LEFT;
	
	private int[] tamanho;
	private int menorRaio;
	private int posX;
	private int posY;
	private int[] raiosInternos;
	private int[] raiosExternos;
	private int distRaios;
	private double aumentaAnguloDe;
	private TimeAngles timeAngles;
	private int corFundoMostrador;
	private int corNumeros;
	private int corHoras;
	private int corMinutos;
	private int corSegundos;
	private Font font;
	
	private Relogio(int[] tamanho) {
		this.tamanho = tamanho;
		menorRaio = (int)((double)Math.min(tamanho[LARGURA], tamanho[ALTURA])/2.0);
		posX = (int)((double)tamanho[LARGURA] / 2.0);
		posY = (int)((double)tamanho[ALTURA] / 2.0);
		distRaios = (int)((double)menorRaio / 20.0);
		preparaCores();
		preparaRaios();
		aumentaAnguloDe = 12;
		timeAngles = new TimeAngles(Calendar.getInstance());
		font = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL);
	}
	
	private void preparaCores() {
		corFundoMostrador = getPseudoAlphaColor(frontColor, 0.20F);
		corNumeros = getPseudoAlphaColor(frontColor, 0.75F);
		corHoras = getPseudoAlphaColor(frontColor, 1F);
		corMinutos = getPseudoAlphaColor(frontColor, 0.75F);
		corSegundos = getPseudoAlphaColor(frontColor, 0.5F);
	}

	private void preparaRaios() {
		int raioMostrador = (int)((double)menorRaio - (double)distRaios * 2.0);
		int tamanhoVetor = 0;
		for(int raio=raioMostrador; raio>0; raio-=distRaios) {
			tamanhoVetor++;
		}
		raiosInternos = new int[tamanhoVetor];
		for(int raio=raioMostrador, i=0; raio>0; raio-=distRaios, i++) {
			raiosInternos[i] = raio;
		}
		
		int maiorRaio = (int)Math.sqrt(((double)(tamanho[LARGURA] * tamanho[LARGURA]) + (double)(tamanho[ALTURA] * tamanho[ALTURA]))/4.0);
		tamanhoVetor = 0;
		for(int raio=(raioMostrador+distRaios); raio<=maiorRaio; raio+=distRaios) {
			tamanhoVetor++;
		}
		raiosExternos = new int[tamanhoVetor];
		for(int raio=(raioMostrador+distRaios), i=0; raio<=maiorRaio; raio+=distRaios, i++) {
			raiosExternos[i] = raio;
		}
	}

	public void paint(Graphics g) {
		g.setColor(frontColor);
		desenhaMostrador(g);
		desenhaPonteiros(g);
	}
	
	private void desenhaMostrador(Graphics g) {
		desenhaCirculos(g);
		desenhaRaios(g);
		desenhaTracos(g);
		desenhaNumeros(g);
		g.drawRect(0, 0, tamanho[LARGURA]-1, tamanho[ALTURA]-1);
	}

	private void desenhaCirculos(Graphics g) {
		int cor = g.getColor();
		g.setColor(corFundoMostrador);
		int diametro;
		for(int i=0; i<raiosInternos.length; i++) {
			int raio = raiosInternos[i];
			int posX = this.posX-raio;
			int posY = this.posY-raio;
			diametro = raio*2;
			g.drawArc(posX, posY, diametro, diametro, 0, 360);
		}
		for(int i=0; i><raiosExternos.length; i++) {
			int raio = raiosExternos[i];
			int posX = this.posX-raio;
			int posY = this.posY-raio;
			diametro = raio*2;
			g.drawArc(posX, posY, diametro, diametro, 0, 360);
		}
		g.setColor(cor);
		int raio = raiosInternos[0];
		int posX = this.posX-raio;
		int posY = this.posY-raio;
		diametro = raio*2;
		diametro = raio*2;
		g.drawArc(posX, posY, diametro, diametro, 0, 360);
	}
	
	private void desenhaRaios(Graphics g) {
		int cor = g.getColor();
		g.setColor(corFundoMostrador);
		int raioMenor = raiosInternos[raiosInternos.length - 3];
		int raioMaior = raiosInternos[0];
		for(int angle=0; angle><360; angle+=6) {
			int posXMenor = (int)((double)posX + getPosX(raioMenor, angle));
			int posYMenor = (int)((double)posY + getPosY(raioMenor, angle));
			int posXMaior = (int)((double)posX + getPosX(raioMaior, angle));
			int posYMaior = (int)((double)posY + getPosY(raioMaior, angle));
			g.drawLine(posXMenor, posYMenor, posXMaior, posYMaior);
		}
		raioMenor = raiosInternos[0];
		raioMaior = raiosExternos[raiosExternos.length-1];
		for(int angle=0; angle<360; angle+=3) {
			int posXMenor = (int)((double)posX + getPosX(raioMenor, angle));
			int posYMenor = (int)((double)posY + getPosY(raioMenor, angle));
			int posXMaior = (int)((double)posX + getPosX(raioMaior, angle));
			int posYMaior = (int)((double)posY + getPosY(raioMaior, angle));
			g.drawLine(posXMenor, posYMenor, posXMaior, posYMaior);
		}
		g.setColor(cor);
	}

	private void desenhaTracos(Graphics g) {
		int raioMaior = raiosExternos[0];
		int raioMenor;
		for(int angle=0; angle<360; angle+=6) {
			raioMenor = 1;
			if(angle % 90 == 0) {
				raioMenor = 3;
			} else if (angle % 30 == 0) {
				raioMenor = 2;
			}
			int posXMenor = (int)((double)posX + getPosX(raiosInternos[raioMenor], angle));
			int posYMenor = (int)((double)posY + getPosY(raiosInternos[raioMenor], angle));
			int posXMaior = (int)((double)posX + getPosX(raioMaior, angle));
			int posYMaior = (int)((double)posY + getPosY(raioMaior, angle));
			g.drawLine(posXMenor, posYMenor, posXMaior, posYMaior);
		}
	}
	
	private void desenhaNumeros(Graphics g) {
		int cor = g.getColor();
		g.setColor(corNumeros);
		g.setFont(font);
		double height = font.getHeight();
		int numero = 3;
		for(int angulo=0; angulo<360; angulo+=30) {
			double width = font.stringWidth(String.valueOf(numero));
			int posX = (int)((double)this.posX + getPosX(raiosInternos[6], angulo) - width / 2.0);
			int posY = (int)((double)this.posY + getPosY(raiosInternos[6], angulo) - height / 2.0);
			
			g.drawString(String.valueOf(numero), posX, posY, ANCHOR);
			
			numero = (numero + 1) % 13;
			if(numero == 0) numero++;
		}
		g.setColor(cor);
	}
	
	private void desenhaPonteiros(Graphics g) {
		int raio = 4;
		this.desenhaPonteiro(g, timeAngles.getSecondAngle(), raio, corSegundos);
		this.desenhaPonteiro(g, timeAngles.getMinuteAngle(), raio + 3, corMinutos);
		this.desenhaPonteiro(g, timeAngles.getHourAngle(), raio + 6, corHoras);
	}
	
	private void desenhaPonteiro(Graphics g, double angle, int raio, int novaCor) {
		int cor = g.getColor();
		g.setColor(novaCor);

		// Desenhando ponteiro
		double posXInterna = (double)posX + getPosX(raiosInternos[raio+2], angle);
		double posYInterna = (double)posY + getPosY(raiosInternos[raio+2], angle);
		g.drawLine(posX, posY, (int)posXInterna, (int)posYInterna);
		
		// Desenhando triângulo
		double posXExterna = (double)posX + getPosX(raiosInternos[raio], angle);
		double posYExterna = (double)posY + getPosY(raiosInternos[raio], angle);
		double posXInterna1 = (double)posX + getPosX(raiosInternos[raio+4], (angle-aumentaAnguloDe/2.0));
		double posYInterna1 =(double)posY +  getPosY(raiosInternos[raio+4], (angle-aumentaAnguloDe/2.0));
		double posXInterna2 = (double)posX + getPosX(raiosInternos[raio+4], (angle+aumentaAnguloDe/2.0));
		double posYInterna2 = (double)posY + getPosY(raiosInternos[raio+4], (angle+aumentaAnguloDe/2.0));
		g.drawLine((int)posXExterna, (int)posYExterna, (int)posXInterna1, (int)posYInterna1);
		g.drawLine((int)posXExterna, (int)posYExterna, (int)posXInterna2, (int)posYInterna2);
		
		g.setColor(cor);
	}

	public void update() {
		preparaCores();
		timeAngles = new TimeAngles(Calendar.getInstance());
	}

}

private class TimeAngles {
	
	private double hour;
	private double minute;
	private double second;
	private double millisecond;
	
	public TimeAngles(Calendar calendar) {
		hour = calendar.get(Calendar.HOUR_OF_DAY);
		minute = calendar.get(Calendar.MINUTE);
		second = calendar.get(Calendar.SECOND);
		millisecond = calendar.get(Calendar.MILLISECOND);
	}
	
	public double getMillisecondAngle() {
		double time = millisecond/1000;
		return getAngle(time);
	}
	
	public double getSecondAngle() {
		double time = (second + millisecond/1000)/60;
		return getAngle(time);
	}
	
	public double getMinuteAngle() {
		double time = (minute + second/60)/60;
		return getAngle(time);
	}
	
	public double getHourAngle() {
		double time = (hour + minute/60 + second/3600 + millisecond/3600000)/12;
		return getAngle(time);
	}
	
	private double getAngle(double time) {
		return (time * 360) - 90;
	}
	
}

}[/code]

Parabéns ! Mais um exemplo de que quando a pessoa quer alcançar um objetivo, se arregaçar as mangas e por-se a trabalhar, muito provavelmente acabará conseguindo.

Eu não notei a troca de cores como falou.

Gostaria de te dar algumas dicas:

O startapp() é sempre executado quando a aplicação ganha o controle (fica em foreground).
Sem o tratamento abaixo, toda vez que a aplicação volta a ficar ativa, ele executa aquele código que instancia uma nova Screen,etc…
Então, para esta inicializacao desnecessária novamente, coloque o seguinte:

if (Screen == Null) {
        prepareScreenColors();   
        screen = new Screen();   
        screen.start();
}
Display.getDisplay(this).setCurrent(screen);   

Como dica, iria te sugerir para colocar um tratamento para durante a execução, se o celular fosse MIDP 2.0, ele colocava o Canvas em modo Fullscreen (método setFullScreenMode).

Não chame System.exit(0). A forma correta de terminar uma aplicação em J2ME é chamar o método notifyDestroyed() no objeto MIDlet.

No método finalize, você está usando Display.getDisplay(this).setCurrent(null), que equivale a mandar a aplicação para o 2o plano, nos celulares que suportam isto (Ex: Aparelhos com Symbian). Era isto mesmo o que queria ?

Existem algumas outras otimizações que podem ser feitas, mas do jeito que está bom demais.

[quote=boone]Parabéns ! Mais um exemplo de que quando a pessoa quer alcançar um objetivo, se arregaçar as mangas e por-se a trabalhar, muito provavelmente acabará conseguindo.

Eu não notei a troca de cores como falou.

Gostaria de te dar algumas dicas:

O startapp() é sempre executado quando a aplicação ganha o controle (fica em foreground).
Sem o tratamento abaixo, toda vez que a aplicação volta a ficar ativa, ele executa aquele código que instancia uma nova Screen,etc…
Então, para esta inicializacao desnecessária novamente, coloque o seguinte:

if (Screen == Null) {
        prepareScreenColors();   
        screen = new Screen();   
        screen.start();
}
Display.getDisplay(this).setCurrent(screen);   

Como dica, iria te sugerir para colocar um tratamento para durante a execução, se o celular fosse MIDP 2.0, ele colocava o Canvas em modo Fullscreen (método setFullScreenMode).

Não chame System.exit(0). A forma correta de terminar uma aplicação em J2ME é chamar o método notifyDestroyed() no objeto MIDlet.

No método finalize, você está usando Display.getDisplay(this).setCurrent(null), que equivale a mandar a aplicação para o 2o plano, nos celulares que suportam isto (Ex: Aparelhos com Symbian). Era isto mesmo o que queria ?

Existem algumas outras otimizações que podem ser feitas, mas do jeito que está bom demais.
[/quote]
Obrigado! Muito boas dicas, como auto-didata eu fui fuçando por conta própria, mas é fundamental saber esses detalhes a rigor!

Eu gosto de ver a coisa rodando, depois eu vou refinando e otimizando!

:slight_smile:

A mudança de cores é bem suave, acontece no método updateColor(), mas se vc reiniciar o relógio ele “sorteia” outras cores, e vc verá que todas são modificadas para ficarem cinzentas como aqueles displays monocromáticos antigos…