Add AI Spinner to Player List Items

Given that we’re making a game here, it’d be helpful to have a way for people to play against computer-controlled players. To do that, we’re going to create some AI players, then give users the ability to choose which of those AI players they’re facing. In this part, we’re only going to worry about giving the AI players names, and we’ll leave the actual logic for Chapter 4, Update LiveData with Conditional Game Logic. We’re going to create an AI class with a number of possible AI players, then update the player items to include these AI players. Whether a user is shown a name text field for a player or the AI Spinner will depend on the state of the SwitchCompat on that player item. Before we get to the UI, let’s create the AI class.

Create the AI Class

Our AI will be another data class, this time living in the game package. For the time being, it’ll only contain one property, name, a toString method that returns that name, and a companion object with a List of AI called basicAI. In here, we’ll declare eight AI players to be used in the Spinner. The code looks like this:

 package​ ​dev.mfazio.pennydrop.game
 
 data class​ AI(​val​ name: String) {
 
 override​ ​fun​ ​toString​() = name
 
 companion​ ​object​ {
  @JvmStatic
 val​ basicAI = listOf(
  AI(​"TwoFace"​),
  AI(​"No Go Noah"​),
  AI(​"Bail Out Beulah"​),
  AI(​"Fearful Fred"​),
  AI(​"Even Steven"​),
  AI(​"Riverboat Ron"​),
  AI(​"Sammy Sixes"​),
  AI(​"Random Rachael"​)
  )
  }
 }

This class doesn’t have too much, but a few things may be new for you. The companion object syntax denotes an object that is associated with a given class. There’s only one instance of this object per class (meaning it’s a singleton) and functions/values/variables in here can be accessed in a similar way to how we would reference static types. In this case, we can get basicAI elsewhere by referencing AI.basicAI.

Since the new toString function is just going to return the name value on the AI instance, we can use expression syntax to directly assign name to toString. That is equivalent to this:

 override​ ​fun​ ​toString​(): String {
 return​ name
 }

The last piece that may look weird is the @JvmStatic annotation on basicAI. Adding this annotation tells the compiler to create an additional static get method for basicAI. This will allow us to reference basicAI in a static way within a player list item, which we’ll see in a bit. Since this is all we need in the AI class currently, we’re going to add the Spinner list inside player_list_item.xml.

Add AI Spinner in player_list_item.xml

I keep mentioning the <Spinner> tag, which is what we sometimes call a “drop-down list” or “select” in HTML. A <Spinner> contains some number of items and allows the user to select one of those items. Our AI <Spinner> is going to live in the same spot as the name <EditText>, and we’ll toggle which one we’re displaying using the <SwitchCompat> associated with the player item. As a result, the constraints for the <Spinner> will match up with the <EditText> on all four sides. We’ll also need another android:visibility attribute on both the <EditText> and <Spinner> based on the player’s value for isHuman.

Finally, once we get the conditional logic completed, we can handle the AI items themselves. Normally, we’d need to create some kind of SpinnerAdapter to include our items, but we’re going to take a different approach thanks to the Data Binding library. The android:entries attribute will allow us to bind an object as the list of items in the <Spinner>.

In our case, we could use AI.basicAI or AI.getBasicAI() for that android:entries attribute. The reason there are two options is because the former value is the Kotlin property while the latter is a getter method that’s generated for use with Java. Now, we normally want to go with the Kotlin approaches all over, but if you use AI.basicAI, Android Studio will complain (even though it works). Instead, you can avoid the error by using the Java getter syntax in our binding.

No matter the approach you take with the list of AI objects, we need to update NewPlayer to include a new Int variable—selectedAIPosition.

 data class​ NewPlayer(
 var​ playerName: String = ​""​,
 val​ isHuman: ObservableBoolean = ObservableBoolean(​true​),
 val​ canBeRemoved: Boolean = ​true​,
 val​ canBeToggled: Boolean = ​true​,
 var​ isIncluded: ObservableBoolean = ObservableBoolean(!canBeRemoved),
»var​ selectedAIPosition: Int = -1
 )

This will track the index of the selected AI (retrieved from AI.basicAI or AI.getBasicAI). In our XML, we’ll use the android:selectedItemPosition attribute with selectedAIPosition. All these pieces together will look like this in the layout file:

 <Spinner
  android:id=​"@+id/spinner_ai_name"
  android:layout_width=​"0dp"
  android:layout_height=​"wrap_content"
  android:entries=​"@{AI.getBasicAI()}"
  android:selectedItemPosition=​"@={player.selectedAIPosition}"
  android:visibility=​"@{player.isHuman ? View.INVISIBLE : View.VISIBLE}"
  app:layout_constraintBottom_toBottomOf=​"@id/edit_text_player_name"
  app:layout_constraintEnd_toStartOf=​"@id/switch_player_type"
  app:layout_constraintStart_toEndOf=​"@id/checkbox_player_active"
  app:layout_constraintTop_toTopOf=​"@id/edit_text_player_name"​ />

We established that we can use AI.basicAI or AI.getBasicAI to get our list of AI options, but we haven’t established why they’re available yet. It’s because we added the @JvmStatic annotation in the AI class. Without that, the Data Binding library wouldn’t have been able to figure out how to send in the data. To use AI.basicAI or AI.getBasicAI, we also need to import the AI type inside our <data> block:

 <data>
 
  <import type=​"android.view.View"​ />
» <import type=​"dev.mfazio.pennydrop.game.AI"​ />
 
  <variable
  name=​"player"
  type=​"dev.mfazio.pennydrop.types.NewPlayer"​ />
 </data>

Also of note is that android:selectedItemPosition is a two-way binding expression to our new selectedAIPosition property on NewPlayer. Otherwise, this item is similar to the <EditText> we created earlier. Speaking of that <EditText>, don’t forget to include the android:visibility attribute there:

 <EditText
  android:id=​"@+id/edit_text_player_name"
  android:layout_width=​"0dp"
  android:layout_height=​"wrap_content"
  android:layout_margin=​"5dp"
  android:hint=​"@string/player_name"
  android:inputType=​"textPersonName"
  android:text=​"@={player.playerName}"
  android:visibility=​"@{player.isHuman ? View.VISIBLE : View.INVISIBLE}"
  app:layout_constraintEnd_toStartOf=​"@id/switch_player_type"
  app:layout_constraintStart_toEndOf=​"@id/checkbox_player_active"
  app:layout_constraintTop_toTopOf=​"parent"​ />

Since we’re now getting an AI from the front end (or at least the index for one), we can get the currently selected AI object via a new function called selectedAI. This function will return null either if the isHuman is true or the AI at the given index can’t be found.

 data class​ NewPlayer(
 var​ playerName: String = ​""​,
 var​ isIncluded: ObservableBoolean = ObservableBoolean(​false​),
 val​ isHuman: ObservableBoolean = ObservableBoolean(​true​),
 val​ canBeRemoved: Boolean = ​true​,
 val​ canBeToggled: Boolean = ​true​,
»var​ selectedAIPosition: Int = -1
 ) {
 
»fun​ ​selectedAI​() = ​if​ (!isHuman.​get​()) {
» AI.basicAI.getOrNull(selectedAIPosition)
» } ​else​ {
»null
» }
 }

Now the form works as we need it to for the game, but we could make this a bit prettier and user friendly. Let’s go ahead and add a couple of nice-looking UI features: disabling excluded players and iconography for the <SwitchCompat>. Both changes will give the app a more polished feel.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.111.85