🎲 P01: Guia Passo a Passo - Dados RPG (D20 3D / Animado)
Bem-vindo à versão avançada e animada da sua primeira missão! 🚀 Neste projeto de entrada da Fase 1: Fundamentos Legados, vamos construir um Simulador de Dados D20 com Animação de Giro e Efeitos Visuais.
Em vez de apenas exibir um número estático instantaneamente, o dado vai girar na tela e trocar de números rapidamente (efeito “rolling”) antes de revelar o resultado. E caso você tire um 20 ou um 1, o app exibirá efeitos especiais de Acerto Crítico e Falha Crítica!
✅ Pré-requisitos
Antes de começar, certifique-se de já ter estudado:
- 📘 Cap 01: Seu Primeiro App (Activity, XML e Views)
- 📘 Cap 02: Sorte e Decisão em Java (Variáveis, Operadores e Random)
- 🏗️ Projeto anterior: P01: Dado RPG (D20)
🎯 Objetivo do Projeto
Criar um aplicativo imersivo com interface premium de tema escuro e elementos centralizados. Ao clicar no botão vermelho “Lançar Dado”, o dado gira 360 graus consecutivamente e altera seus valores aleatoriamente em frações de segundos para simular a rolagem realista, revelando o resultado com formatação especial para acerto crítico e falha crítica.
graph TD
A[Início / App Aberto] --> B[Clique no Botão Rolar]
B --> C{Já está rolando?}
C -- Sim --> D[Ignorar clique extra]
C -- Não --> E[Travar cliques & Iniciar Animação]
E --> F[Girar D20 com RotateAnimation]
F --> G[Enfileirar 20 números rápidos com Handler]
G --> H[Exibir Resultado Final]
H --> I{Qual o resultado?}
I -- 20 --> J[Cor Dourada & Toast: CRITICAL HIT! ⚔️]
I -- 1 --> K[Cor Cinza & Toast: CRITICAL FAIL! 💀]
I -- Outro --> L[Cor Branca padrão]
J --> M[Destravar novos cliques]
K --> M
L --> M
📖 Dicionário do Projeto
- RelativeLayout: Um layout flexível que posiciona os elementos da tela de forma relativa uns aos outros ou aos limites da tela principal.
- FrameLayout: Layout projetado para empilhar componentes uns sobre os outros (eixo Z). Colocaremos o número em cima da imagem do dado D20.
- RotateAnimation: Classe de animação usada para girar um componente em torno de si mesmo por um ângulo e tempo determinados.
- Handler / postDelayed: Mecanismo que permite enfileirar uma ação para rodar em uma linha de tempo futura após alguns milissegundos. Usado para criar o efeito realista de rolagem de dados rápidos.
- Lambda (v -> action): Uma forma simplificada e moderna (sintaxe do Java 8+) para declarar listeners de clique sem criar classes anônimas verbosas.
- Color.parseColor: Método que converte uma String contendo um código hexadecimal de cor (como
#FFD700) diretamente em uma cor aplicável na tela.
🛠️ Passo 1: Configurando o Projeto no Android Studio
Configure o ambiente de forma idêntica à versão clássica:
- Abra o Android Studio e clique em
New Project. - Escolha o template Empty Views Activity.
- Configure os dados do seu app:
- Name:
Dados RPG 3D - Package Name:
br.com.curso.dadosrpg - Language: Java (O clássico).
- Minimum SDK: API 21 (Android 5.0) ou superior.
- Name:
- Clique em Finish e aguarde o Gradle sincronizar as dependências.
🖼️ Passo 2: Importando o Recurso Visual (Dado D20)
Para esta versão, precisamos de uma bela imagem do dado de 20 faces para servir de fundo para os números:
- Consiga uma imagem de um dado D20 (você pode copiar o arquivo
d20_dice.pngjá presente na pastares/drawabledeste projeto). - Cole o arquivo na pasta de recursos do seu projeto no Android Studio:
app > src > main > res > drawable.- Dica: Certifique-se de que o nome do arquivo está em letras minúsculas e sem caracteres especiais (
d20_dice.png).
- Dica: Certifique-se de que o nome do arquivo está em letras minúsculas e sem caracteres especiais (
🎨 Passo 3: Desenhando a Interface Imersiva (XML)
Desta vez, criaremos uma interface premium com tema escuro e elementos centralizados.
- Abra
app > src > main > res > layout > activity_main.xml. - Substitua o código pelo seguinte layout moderno:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0F0F0F"
android:padding="24dp">
<!-- Header -->
<TextView
android:id="@+id/txt_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="30dp"
android:text="RPG DICE MASTER"
android:textColor="#FF4444"
android:textSize="18sp"
android:letterSpacing="0.4"
android:textStyle="bold" />
<!-- Container do Dado Animado (Sobrepõe Imagem e Texto) -->
<FrameLayout
android:layout_width="250dp"
android:layout_height="250dp"
android:layout_centerInParent="true">
<!-- Imagem do D20 no fundo -->
<ImageView
android:id="@+id/img_dado"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/d20_dice"
android:alpha="0.8"
android:scaleType="fitCenter" />
<!-- Número centralizado em cima do D20 -->
<TextView
android:id="@+id/txt_resultado"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="20"
android:textColor="#FFFFFF"
android:textSize="80sp"
android:textStyle="bold"
android:shadowColor="#FF0000"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="15" />
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_header"
android:layout_centerHorizontal="true"
android:layout_marginTop="300dp"
android:text="TOQUE PARA ROLAR"
android:textColor="#555555"
android:textSize="14sp" />
<!-- Botão de Ação -->
<Button
android:id="@+id/btn_rolar"
android:layout_width="match_parent"
android:layout_height="65dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="30dp"
android:backgroundTint="#D32F2F"
android:text="LANÇAR DADO"
android:textColor="#FFFFFF"
android:textSize="20sp"
android:textStyle="bold" />
</RelativeLayout>
🧠 Passo 4: Implementando a Lógica Animada e Críticos (Java)
Vamos codificar a lógica do lançamento. Faremos o dado rodar e exibir números temporários em altíssima velocidade antes de parar no número final.
- Abra o arquivo
MainActivity.java(emapp > src > main > java > br.com.curso.dadosrpg > MainActivity.java). - Substitua o conteúdo pelo código dinâmico a seguir:
package br.com.curso.dadosrpg;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Random;
public class MainActivity extends AppCompatActivity {
private TextView txtResultado;
private Button btnRolar;
private final Random gerador = new Random();
private boolean estaRolando = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
txtResultado = findViewById(R.id.txt_resultado);
btnRolar = findViewById(R.id.btn_rolar);
// Uso de Expressão Lambda (Sintaxe Moderna)
btnRolar.setOnClickListener(v -> simularLancamento());
}
private void simularLancamento() {
// Evita múltiplos cliques simultâneos enquanto o dado roda
if (estaRolando) return;
estaRolando = true;
// 1. Cria a animação de rotação: Gira 360 graus em torno do próprio centro (0.5f)
RotateAnimation anim = new RotateAnimation(0, 360,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
anim.setDuration(100);
anim.setRepeatCount(10); // Gira 10 vezes consecutivas
txtResultado.startAnimation(anim);
// 2. Handler para alterar os números muito rápido (simula rolagem realista)
Handler handler = new Handler();
for (int i = 0; i <= 20; i++) {
final int index = i;
handler.postDelayed(() -> {
if (index < 20) {
// Troca por um número aleatório rápido (cinza para indicar que está rolando)
txtResultado.setText(String.valueOf(gerador.nextInt(20) + 1));
txtResultado.setTextColor(Color.GRAY);
} else {
// Resultado Final do Sorteio
exibirResultadoFinal();
estaRolando = false;
}
}, i * 50); // Troca de número a cada 50 milissegundos
}
}
private void exibirResultadoFinal() {
int numero = gerador.nextInt(20) + 1;
txtResultado.setText(String.valueOf(numero));
// 3. Efeitos Especiais de RPG baseados no resultado sorteado
if (numero == 20) {
// Acerto Crítico: Cor Dourada e Toast vibrante!
txtResultado.setTextColor(Color.parseColor("#FFD700"));
ToastMessage("CRITICAL HIT! ⚔️");
} else if (numero == 1) {
// Falha Crítica: Cor Cinza Escura e Toast de falha!
txtResultado.setTextColor(Color.parseColor("#555555"));
ToastMessage("CRITICAL FAIL! 💀");
} else {
// Sucesso Padrão: Cor Branca
txtResultado.setTextColor(Color.WHITE);
}
}
private void ToastMessage(String msg) {
android.widget.Toast.makeText(this, msg, android.widget.Toast.LENGTH_SHORT).show();
}
}
🛠️ Requisitos Críticos de Configuração
1. Suporte ao AndroidX (Evita travamentos de build)
Certifique-se de que o arquivo gradle.properties na raiz do seu projeto possui estas flags essenciais de compatibilidade ativas:
android.useAndroidX=true
android.enableJetifier=true
2. Unidades de Medida corretas no XML (Sem ponto decimal)
Diferente da programação convencional, medidas de dimensões no Android no XML não toleram números flutuantes:
- ❌ Errado:
android:padding="24.5dp" - ✅ Correto:
android:padding="24dp"
📸 Resultado Esperado
Veja como sua tela deve ficar ao final deste projeto:

🏆 Desafios para você (Upgrade!)
Terminou rápido? Deixe o app ainda mais incrível com estes desafios:
- Som Realista: Adicione um arquivo
.mp3de dados rolando na pastares/rawe toque o som usando a classeMediaPlayerdo Android assim que a simulação iniciar. - Sensor de Chacoalhar: Use o sensor acelerômetro do celular (
SensorManager) para permitir que o usuário role os dados simplesmente balançando o aparelho!
📖 Gabarito Oficial de Código (Para Conferência)
Use os códigos completos abaixo para validar sua implementação ou corrigir problemas de compilação.
📄 Layout Completo (activity_main.xml)
Disponível em: app/src/main/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0F0F0F"
android:padding="24dp">
<!-- Header -->
<TextView
android:id="@+id/txt_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="30dp"
android:text="RPG DICE MASTER"
android:textColor="#FF4444"
android:textSize="18sp"
android:letterSpacing="0.4"
android:textStyle="bold" />
<!-- Container do Dado Animado -->
<FrameLayout
android:layout_width="250dp"
android:layout_height="250dp"
android:layout_centerInParent="true">
<!-- Imagem do D20 no fundo -->
<ImageView
android:id="@+id/img_dado"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/d20_dice"
android:alpha="0.8"
android:scaleType="fitCenter" />
<!-- Número centralizado -->
<TextView
android:id="@+id/txt_resultado"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="20"
android:textColor="#FFFFFF"
android:textSize="80sp"
android:textStyle="bold"
android:shadowColor="#FF0000"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="15" />
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txt_header"
android:layout_centerHorizontal="true"
android:layout_marginTop="300dp"
android:text="TOQUE PARA ROLAR"
android:textColor="#555555"
android:textSize="14sp" />
<!-- Botão de Ação -->
<Button
android:id="@+id/btn_rolar"
android:layout_width="match_parent"
android:layout_height="65dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="30dp"
android:backgroundTint="#D32F2F"
android:text="LANÇAR DADO"
android:textColor="#FFFFFF"
android:textSize="20sp"
android:textStyle="bold" />
</RelativeLayout>
📄 Lógica Java Completa (MainActivity.java)
Disponível em: app/src/main/java/br/com/curso/dadosrpg/MainActivity.java
package br.com.curso.dadosrpg;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Random;
public class MainActivity extends AppCompatActivity {
private TextView txtResultado;
private Button btnRolar;
private final Random gerador = new Random();
private boolean estaRolando = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
txtResultado = findViewById(R.id.txt_resultado);
btnRolar = findViewById(R.id.btn_rolar);
btnRolar.setOnClickListener(v -> simularLancamento());
}
private void simularLancamento() {
if (estaRolando) return;
estaRolando = true;
// Inicia a animação de rotação
RotateAnimation anim = new RotateAnimation(0, 360,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
anim.setDuration(100);
anim.setRepeatCount(10); // Gira 10 vezes rapidamente
txtResultado.startAnimation(anim);
// Handler para trocar os números rapidamente (efeito visual)
Handler handler = new Handler();
for (int i = 0; i <= 20; i++) {
final int index = i;
handler.postDelayed(() -> {
if (index < 20) {
// Troca por um número aleatório rápido
txtResultado.setText(String.valueOf(gerador.nextInt(20) + 1));
txtResultado.setTextColor(Color.GRAY);
} else {
// Resultado Final
exibirResultadoFinal();
estaRolando = false;
}
}, i * 50); // Troca a cada 50ms
}
}
private void exibirResultadoFinal() {
int numero = gerador.nextInt(20) + 1;
txtResultado.setText(String.valueOf(numero));
// Efeitos Especiais baseados no resultado
if (numero == 20) {
txtResultado.setTextColor(Color.parseColor("#FFD700")); // Dourado (Crítico!)
ToastMessage("CRITICAL HIT! ⚔️");
} else if (numero == 1) {
txtResultado.setTextColor(Color.parseColor("#555555")); // Cinza escuro (Falha!)
ToastMessage("CRITICAL FAIL! 💀");
} else {
txtResultado.setTextColor(Color.WHITE);
}
}
private void ToastMessage(String msg) {
android.widget.Toast.makeText(this, msg, android.widget.Toast.LENGTH_SHORT).show();
}
}