Construyendo una aplicación mínima de Android de Roller Dice Roller utilizando las construcciones básicas de Android.

Vector de AndroidRelativeLayout , ConstraintLayout , LinearLayout ]ListView y ArrayAdapter ListView y AdapterHace poco, me metí en este juego de mesa Mansions of Madness. El juego es un poco como la pista clásica donde los jugadores deambulan por una casa tratando de resolver un misterio. Es un juego increíble que recomiendo encarecidamente . De todos modos, el juego usa dados para resolver acciones y otros eventos del juego. ¡Curiosamente, los jugadores a veces tienen que tirar más dados de los que el juego incluye (6)! Decidí que esta era una oportunidad perfecta para construir una aplicación de dados personalizada. En este tutorial, usaré componentes básicos de Android para construir un rodillo de dados de Mansions of Madness.

Esta aplicación de dados está específicamente diseñada para el juego Mansions of Madness, así que primero describiré cómo el juego usa dados.
Para mantener las cosas simples, la aplicación será una lista vertical y desplazable de dados. La aplicación tendrá 3 botones para activar las funciones "Roll Dice", "Agregar dados" y "eliminar dados". Cada dados tendrá una 'retención' y 'cambio' correspondiente.

La plataforma Android siempre está cambiando, haciendo tutoriales como estos obsoletos con el tiempo. Como referencia, mi entorno de desarrollo:
Entorno de Android


Todos estos nombres se pueden cambiar después de la creación del proyecto, aunque puede ser engorroso perseguir todas las referencias de nombre si el proyecto se vuelve complejo.

Android tiene muchas versiones. Con cada versión, la plataforma cambia. Básicamente, esto significa que hay muchos dispositivos Android en el mundo con diferentes versiones. Esto se convierte en un dolor de cabeza para los desarrolladores de aplicaciones porque dependiendo de las bibliotecas que usa la aplicación, la aplicación puede ser incompatible con ciertos dispositivos. La compensación aquí es que esa aplicación que usa las nuevas bibliotecas de Android no puede ejecutarse en dispositivos más antiguos. Si la aplicación debe ejecutarse en dispositivos más antiguos, la aplicación debe usar algunas de las construcciones de Android más antiguas.

La plantilla inicial en realidad no importa demasiado para esta aplicación. El código de plantilla a veces es útil porque prepopula el diseño y las clases iniciales con algún código. Como no voy a usar nada de esto, elegí la actividad vacía.

El diálogo de creación de proyectos de Android inicializa el proyecto con construcciones básicas:
Gradle es un marco para facilitar los proyectos de construcción. En Wikipedia
El manifiesto Android describe la aplicación a Android. Propiedades de aplicación, como permisos y actividades. Los detalles se pueden encontrar en la página de documentación de Android.
Para comenzar, utilicé un editor SVG en línea simple llamado Clker para extraer las caras de dados como SVG.



A continuación, los importo a mi proyecto con Android Studio's Asset Studio.


Los dispositivos tienen diferentes resoluciones y dimensiones. Predecir la resolución y la dimensión del dispositivo que ejecuta la aplicación es difícil. La escala de JPEG puede provocar gráficos borrosos o granulados. Una forma de abordar esto es para los gráficos es usar SVG. Hay numerosos artículos en línea que discuten SVG, pero como referencia, consulte el artículo de Wikipedia SVG para obtener más información.
Android ofrece dos formas de describir diseños: diseños programáticos y XML. Describir diseños complejos programáticamente es bastante difícil, por lo que la mayoría de las personas generalmente evitan eso. Escribir XML puede no ser tan divertido, pero Android Studio ofrece varias herramientas para aliviar el dolor. Una herramienta de vista previa y un editor de diseño de WSYWIG. Pero incluso con el editor, sumergirse en el XML es casi inevitable.


La plantilla de actividad vacía de Android Studio nos inicia con un elemento de diseño de raíz ConstraintLayout . Necesitaremos dos componentes en esta aplicación: área de dados y área del controlador. El área de dados será una lista de dados desplazable y el controlador será los 3 botones "agregar" "eliminar" "rollo".

activity_main.xml <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ListView
android:id="@+id/dice_list"
android:layout_height="0dp"
android:layout_width="match_parent"
app:layout_constraintBottom_toTopOf="@id/button_bar"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
</ListView>
<LinearLayout
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/control_bar_height"
android:orientation="horizontal"
android:weightSum="3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/dice_list">
<Button
android:id="@+id/add_dice_button"
android:layout_gravity = "center"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add_button_label"
android:onClick="addDice"/>
<Button
android:id="@+id/rem_dice_button"
android:layout_gravity = "center"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rem_button_label"
android:onClick="removeDice"/>
<Button
android:id="@+id/roll_dice_button"
android:layout_weight="1"
android:layout_gravity = "center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/roll_button_label"
android:onClick="rollDice"/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
Para aquellos que no están familiarizados con XML, lo anterior puede parecer galimatías. Explicar que XML está fuera del alcance de este tutorial, pero Google, YouTube y Wikipedia son excelentes recursos para aquellos que buscan más información. Para este diseño, estoy usando clases ListView , Button , LinearLayout y ConstraintLayout . Los detalles sobre sus atributos se pueden encontrar en la página de documentación de Android.
Estaba buscando un Layout que describe fácilmente un área inferior de altura fija (para botones) y un área superior (para dados) que llenó el espacio de pantalla disponible. LinearLayout espacia sus sub elementos utilizando pesas, lo que lo hace inadecuado. RelativeLayout no ofrece la capacidad de 'llenar el espacio restante' también lo hace inadecuado.
Visualmente, LinearLayout se ve bastante cerca de lo que necesito. Sin embargo, LinearLayout es para la lista estática de elementos en lugar de listas dinámicas. Para esta aplicación, la lista de dados puede tener entre 0 y 25 dados, hacer que ListView es un mejor candidato.
El valor 0dp es específico para ConstraintLayout que indica que debe llenar el espacio restante del padre.
Como se describió anteriormente, cada fila de dados incluye 2 botones y una imagen de dados.

row.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="@dimen/row_height">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="@dimen/row_height"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:padding="@dimen/row_padding">
<Button
android:id="@+id/dice_change_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/change_button_label">
</Button>
</FrameLayout>
<ImageView
android:id="@+id/dice_icon"
android:layout_centerInParent="true"
android:layout_width="@dimen/image_width"
android:layout_height="@dimen/image_height"
android:src="@drawable/blank_dice"/>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="@dimen/row_height"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:padding="@dimen/row_padding">
<Button
android:id="@+id/dice_hold_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hold_button_label">
</Button>
</FrameLayout>
</RelativeLayout>
El botón se encuentra en el centro vertical de la fila y a cierta distancia de cada borde. Sentí que el diseño sería más limpio si el diseño separaba el elemento del botón real y su posición en el diseño. Entonces, para mi aplicación, uso el FrameLayout para especificar la posición y centrar el botón en ese diseño.
Los valores de cadena y dimensión nos permiten no escribir cadenas de configuración e enteros directamente en el código. Para nuestra pequeña aplicación, tal vez no sea un gran problema.
string.xml <resources>
<string name="app_name">DiceRoller</string>
<string name="hold_button_label">Hold</string>
<string name="change_button_label">Change</string>
<string name="add_button_label">ADD</string>
<string name="rem_button_label">REM</string>
<string name="roll_button_label">ROLL</string>
</resources>
dimens.xml <?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="row_height">72dp</dimen>
<dimen name="row_padding">16dp</dimen>
<dimen name="control_bar_height">72dp</dimen>
<dimen name="image_width">72dp</dimen>
<dimen name="image_height">72dp</dimen>
</resources>
ListView y ArrayAdapter En este punto, inicializé nuestro proyecto de Android con una MainActivity vacía y me burlé de algunos diseños. A continuación, entraré en la lógica y el código. Para comenzar, me gustaría entrar en algunas clases de Java específicas de Android. ListView es una clase de diseño básica para representar listas visuales. El marco de Android separa los componentes visuales ( ListView ) y los componentes de datos ( List<Dice> ) empleando un patrón de adaptador. En nuestro caso, todo lo que hace el adaptador es asignar los datos ( Dice ) a algún diseño visual ( dice_row.xml ). En este caso, un archivo XML de diseño describe el diseño.
MainActivity.java public class MainActivity extends AppCompatActivity {
DiceAdapter diceAdapter;
List <Dice> diceList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//associating activity to layout
setContentView(R.layout.activity_main);
//Setup ListView and Adapter
ListView listView = findViewById(R.id.dice_list);
diceAdapter = new DiceAdapter(this, R.layout.dice_row, diceList);
listView.setAdapter(diceAdapter);
//Initialize Data
diceAdapter.add(new Dice());
}
public class DiceAdapter extends ArrayAdapter<Dice> {
public DiceAdapter(@NonNull Context context, int resource, List<Dice> list) {
super(context, resource, list);
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.dice_row, parent, false);
}
return convertView;
}
}
}
La aplicación representará el estado de dados con objetos de dados. El objeto de dados tiene dos propiedades: valor de dados [en blanco, magnificar, estrella], y si los dados se 'retenen'. Funcionalmente, el dado tiene un método de rollo que seleccionará aleatoriamente una cara de dados. Finalmente, agrego un método que cambia el valor de dados a la siguiente en la lista.
MainActivity.java ....
public static class Dice {
public enum Face {
BLANK,
MAGNIFY,
STAR
}
public static Random random = new Random();
boolean hold = false;
Face diceVal;
Dice() {
roll();
}
public void roll() {
int num = random.nextInt(4);
if(num == 0) { //25% magify
this.diceVal = Face.MAGNIFY;
} else {
//37.5% star, 37.5% blank
if(random.nextBoolean()) {
this.diceVal = Face.BLANK;
} else {
this.diceVal = Face.STAR;
}
}
}
public void toggleHold() {
hold = !hold;
}
public void nextValue() {
int index = diceVal.ordinal();
index = (index+1) % Face.values().length;
diceVal = Face.values()[index];
}
}
En este paso, mapeo el botón Haga clic en la lógica. La plataforma Android ofrece un par de formas de hacer esto. Una forma es especificar un atributo del archivo de diseño. Otro es establecer programáticamente el onClickListener . En nuestra aplicación, use el enfoque de atributo para los tres botones de nivel superior y establezca programáticamente el oyente para los botones de fila.
addDice Si el recuento de dados es inferior a 25, agrega un nuevo objeto de dados a la lista de dados.
Diseño
activity_main.xml ....
<Button
android:id="@+id/add_dice_button"
android:layout_gravity = "center"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add_button_label"
android:onClick="addDice"/>
....
Lógica
MainActivity.java ....
public void addDice(View view) {
if(diceList.size()< MAX_DICE_COUNT) {
diceAdapter.add(new Dice());
}
}
....
removeDice si la lista de dados no está vacía, elimina los últimos dados de la lista
Diseño
activity_main.xml ....
<Button
android:id="@+id/rem_dice_button"
android:layout_gravity = "center"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rem_button_label"
android:onClick="removeDice"/>
....
Lógica
MainActivity.java ....
public void removeDice(View view) {
if(!diceList.isEmpty()) {
int lastIndex = diceList.size() - 1;
diceAdapter.remove(diceAdapter.getItem(lastIndex));
}
}
....
rollDice vuelve a revelar el valor de cada dados en la lista que no ha sido marcado para mantener.
Diseño
activity_main.xml ....
<Button
android:id="@+id/roll_dice_button"
android:layout_weight="1"
android:layout_gravity = "center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/roll_button_label"
android:onClick="rollDice"/>
....
Lógica
MainActivity.java ....
public void rollDice(View view) {
//roll all dice
for(Dice dice : diceList) {
if(!dice.hold)
dice.roll();
}
//notify adapter to update view
diceAdapter.notifyDataSetChanged();
}
....
notifyDataSetChanged ? El botón Roll cambia el dicalue para el objeto de dados correspondiente. Debido a la separación del patrón del adaptador de la vista/datos, el diseño de la fila de dados no vuelve a renderizar automáticamente a menos que se active. Llamar notifyDataSetChanged vuelve a dibujar la vista.
Haga clic en el botón Mantenerse establecerá el indicador de retención de los dados.
MainActivity.java ....
Button holdButton = convertView.findViewById(R.id.dice_hold_button);
holdButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Dice dice = diceList.get(position);
dice.toggleHold();
}
});
....
Haga clic en el botón Mantenerse cambiará el valor de los dados y la interfaz de actualización.
MainActivity.java ....
Button changeButton = convertView.findViewById(R.id.dice_change_button);
changeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Dice dice = diceList.get(position);
dice.nextValue();
diceAdapter.notifyDataSetChanged();
}
});
....