Relógio analógico completo em J2ME

2 respostas
Fox_McCloud

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!

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;
		}
		
	}
	
}

2 Respostas

P

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.

Fox_McCloud

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.


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…

Criado 5 de maio de 2009
Ultima resposta 6 de mai. de 2009
Respostas 2
Participantes 2