Android Mansions of Madness Dice Roller Tutorial Part1
Building a Minimum Viable Product dice roller Android app using the basic Android constructs.
Vector
RelativeLayout
, ConstraintLayout
, LinearLayout
]ListView
and ArrayAdapter
A little while ago, I got into this board game Mansions of Madness. The game is a bit like the classic Clue where players roam around a house trying to solve some mystery. It’s an awesome game that I highly recommend. Anyway, the game uses dice rolls to resolve actions, and other game events. Oddly, players sometimes have to roll more dice than the game includes (6)! I decided that this was a perfect opportunity to build a custom dice roller app. In this tutorial, I will use basic Android components to build a Mansions of Madness dice roller.
This dice app is specifically designed for Mansions of Madness gameplay so first I’ll outline how the game uses dice.
To keep things simple, the app will be a vertical, scrollable list of dice. The app will have 3 buttons to trigger functions “Roll Dice”, “Add Dice” and “Remove Dice”. Each Dice will have a corresponding ‘Hold’ and ‘Change’.
The Android platform is always shifting, making tutorials like these obsolete over time. For reference, my Dev environment:
Android Environment
All of these names can be changed after project creation though it can get cumbersome to chase all the name references if the project gets complex.
Android has many versions. With each release, the platform changes. This basically means there are lots of Android Devices out in the world with different versions. This becomes a headache for app developers because depending on what libraries the app uses, the app may be incompatible with certain devices. The tradeoff here is that app using the new Android libraries cannot run on older devices. If the app must run on older devices, the app must use some of the older Android constructs.
The initial template actually doesn’t matter too much for this app. The template code is sometimes useful because it prepopulates the layout and initial classes with some code. Since I’m not going to use any of this, I chose Empty Activity.
The Android Project creation dialog initializes the project with basic constructs:
Gradle is a framework to facilitate building projects. On Wikipedia
The Android Manifest describe the app to Android. Application properties such as permissions, and Activites. Details can be found on the Android documentation page.
To start, I used a simple online SVG editor called Clker to draw out the dice faces as SVG’s.
Next, I import them into my project using Android Studio’s Asset Studio.
Devices have different resolutions and dimensions. Predicting the resolution and dimension of the device the app runs is difficult. Scaling Jpegs can result in blurry or grainy graphics. One way to tackle this is for graphics is to use SVG’s. There are numerous articles online discussing SVG’s but for reference, please check out the Wikipedia SVG article for more information.
Android offers two ways to describe layouts: programmatic and xml layouts. Describing complex layouts programmaticly is pretty difficult so most people generally avoid that. Writing xml may not be all that fun, but Android Studio does offer several tools to ease the pain. a preview tool and a WSYWIG layout editor. But even with the editor, diving into the xml is nearly unavoidable.
The Android Studio Empty Activity template starts us off with a ConstraintLayout
root layout element. We’ll need two components in this app: Dice area and Controller area. The dice area will be a scrollable dice list and the controller will be the 3 buttons “ADD” “REMOVE” “ROLL”.
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>
<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>
<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"></Button>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
For those unfamiliar with xml, the above may look like gibberish. Explaining xml is outside the scope of this tutorial, but Google, YouTube, and Wikipedia are great resources for those looking for more information. For this layout I’m using classes ListView
, Button
, LinearLayout
, and ConstraintLayout
. The details around their attributes can be found on the Android documentation page.
I was looking for a Layout
that easily describes a fixed height bottom area(for Buttons) and a top area (for dice) that filled up available screen space. LinearLayout
spaces out its sub elements using weights making it unsuitable. RelativeLayout
does not offer the ability to ‘fill remaining space’ also making it unsuitable.
Visually, LinearLayout
looks pretty close to what I need. However, LinearLayout
is for static list of elements rather than dynamic lists. For this app, the Dice list can have anywhere between 0 and 25 dice, making ListView is a better candidate.
The 0dp
value is specific to ConstraintLayout
that indicates that it should fill the remaining space of the parent.
As described above, each dice row includes 2 buttons and a dice image.
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"></ImageView>
<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>
The button sits at the vertical center of the row and some distance from each edge. I felt that the design would be cleaner if the design separated the actual button element and its position in the layout. So for my app, I use the FrameLayout
to specify the position and center the button in that layout.
String and Dimension values allow us to not write configuration Strings and Integers directly into code. For our small app, maybe not a big deal.
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
and ArrayAdapter
At this point, I’ve initialized our Android Project with an Empty MainActivity and mocked out some layouts. Next, I’ll get into the logic and code. To start, I’d like to get into some more Android specific Java classes. ListView
is a basic layout class for rendering visual lists. The Android framework separates the visual components (ListView
) and data components (List<Dice>
) by employing an Adapter Pattern. In our case, all the adapter does is map the data(Dice
) to some visual layout(dice_row.xml
). In this case, a layout xml file describes the layout.
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;
}
}
}
The app will represent the dice state with Dice Objects. The Dice object has two properties things: dice value [Blank, Magnify, Star], and whether the dice is ‘held’. Functionally, the Dice has a roll method that will randomly select a dice face. Finally, I add a method that changes the dice value to the next on the list.
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];
}
}
In this step I map button clicks to logic. The Android platform offers a couple ways to do this. One way is to specify an attribute from the layout file. Another is to programmatically set the onClickListener
. In our app, use attribute approach for the three top level buttons and programmatically set the listener for the row buttons.
addDice
if dice count is less than 25, adds a new Dice object to the Dice list.
Design
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"></Button>
....
Logic
MainActivity.java
....
public void addDice(View view) {
if(diceList.size()< MAX_DICE_COUNT) {
diceAdapter.add(new Dice());
}
}
....
removeDice
if Dice list is not empty, removes the last dice from the list
Design
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"></Button>
....
Logic
MainActivity.java
....
public void removeDice(View view) {
if(!diceList.isEmpty()) {
int lastIndex = diceList.size() - 1;
diceAdapter.remove(diceAdapter.getItem(lastIndex));
}
}
....
rollDice
Rerolls the value of every dice on he list that hasn’t been marked for holding.
Design
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"></Button>
....
Logic
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
?The roll button changes the diceValue for the corresponding Dice object. Due to View/Data Adapter Pattern separation, the Dice row layout does not automatically re-render unless triggered. Calling notifyDataSetChanged
redraws the view.
Clicking hold button will set the dice’s hold flag.
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();
}
});
....
Clicking hold button will change the dice’s value and update interface.
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();
}
});
....