🎲 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:


🎯 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


🛠️ Passo 1: Configurando o Projeto no Android Studio

Configure o ambiente de forma idêntica à versão clássica:

  1. Abra o Android Studio e clique em New Project.
  2. Escolha o template Empty Views Activity.
  3. 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.
  4. 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:

  1. Consiga uma imagem de um dado D20 (você pode copiar o arquivo d20_dice.png já presente na pasta res/drawable deste projeto).
  2. 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).

🎨 Passo 3: Desenhando a Interface Imersiva (XML)

Desta vez, criaremos uma interface premium com tema escuro e elementos centralizados.

  1. Abra app > src > main > res > layout > activity_main.xml.
  2. 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.

  1. Abra o arquivo MainActivity.java (em app > src > main > java > br.com.curso.dadosrpg > MainActivity.java).
  2. 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:


📸 Resultado Esperado

Veja como sua tela deve ficar ao final deste projeto:

App Dados RPG 3D exibindo uma Falha Crítica (D20 = 1)


🏆 Desafios para você (Upgrade!)

Terminou rápido? Deixe o app ainda mais incrível com estes desafios:


📖 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();
    }
}