Skip to content

Horizontal Selector

RealWearDevBot edited this page Aug 28, 2020 · 1 revision

Description

Horizontal Selector is a speech enabled horizontal list view that can be scrolled by the user moving their head ("head-tracking"). The basic design draws from the Android Recycler View. Like the Recycler View, the Horizontal Selector requires two more components to function properly: a view holder and an adapter. Additionally, a customized fragments can be supplied by the developer to populate a View Pager included in the Horizontal Selector in cases where additional views need to be displayed to a user.

Example

https://github.com/realwear/UXLibrary-Example/blob/master/app/src/main/java/com/realwear/uxlibrary_example/horizontalselectorexample/HorizontalSelectorExampleActivity.kt

Usage

The basic Horizontal Selector component can be created in an XML layout file or programmatically.

<com.realwear.ux.viewgroup.HorizontalSelector
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

or

val horizontalSelector = HorizontalSelector(context)

Simply instantiating the Horizontal Selector alone is not enough, as you need to provide the information for what will be shown and how it will be shown by the component. This is done by supplying both a View Holder and a Horizontal Selector Adapter.

View Holder

View Holder is an open class included in the UX Library that needs to be used when populated a Horizontal Selector. The View Holder is where the developer provides what the layout will be for each list item in the Horizontal Selector. The View Holder class can be used directly or extended from.

inner class ExampleViewHolder(itemView: View) : ViewHolder(itemView) {
    val textView: TextView = itemView.findViewById(R.id.text_view)
}

Horizontal Selector Adapter

Horizontal Selector Adapter is an abstract class that provides the capability for attaching view holders to the Horizontal Selector, as well as determining the behavior of each list item in the Horizontal Selector. This is modeled after the Recycler View Adapter. A developer must extend from the HorizontalSelectorAdapter class and can also designate what View Holder the adapter will be using:

inner class ExampleAdapter(context: Context) : 
    HorizontalSelectorAdapter<ExampleViewHolder>(context) {

When the class is created, the developer will have to override the abstract functions included in the adapter. These functions will allow the developer to control how the view holders are created, how they are attached to the Horizontal Selector, how they should be populated with information, and how they should react when their voice command is triggered.

/**
* Returns a View Holder of the designated type to be constructed when populate
* the list items.
*/
override fun onCreateViewHolder(parent: ViewGroup): ExampleViewHolder {}

/**
* Returns the total number of items in the data set held by the adapter.
*/
override fun getItemCount(): Int {}

/**
* Updates the contents of the View Holder item view to display the data as
* decided by the developer.
*/
override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) {}

/**
* Gets the command used at the specified [position].
*/
override fun getCommand(position: Int): String? {}

/**
* Gets the state at the specified [position].
*/
override fun getState(position: Int): String? {}

/**
* Called when the voice command at [position] is triggered.
*/
override fun onCommand(position: Int) {}

/**
* Gets the view pager fragment at the specified [position].
*/
override fun getViewPagerFragment(position: Int): 
    HorizontalSelector.ViewPagerFragment? {}

The below example of a complete Horizontal Selector Adapter shows how an entire Horizontal Selector can be created with information from an array. The example array is using elements from an enum class, also included in this code snippet at the top. Notice how each override function contains the custom logic supplied by the developer to control the content and behavior of each list item.

enum class TestColor(val resource: Int) {
    Black(Color.BLACK),
    Red(Color.RED),
    Yellow(Color.YELLOW),
    Green(Color.GREEN),
    Cyan(Color.CYAN),
    Blue(Color.BLUE),
    Gray(Color.GRAY),
    White(Color.WHITE),
    Brown(Color.rgb(120, 79, 23)),
    Orange(Color.rgb(255, 127, 0)),
    Violet(Color.rgb(117, 7, 135))
}

inner class ExampleAdapter(context: Context) :
    HorizontalSelectorAdapter<ExampleViewHolder>(context) {
    private val arr = arrayOf(
        TestColor.Black,
        TestColor.Brown,
        TestColor.Red,
        TestColor.Orange,
        TestColor.Yellow,
        TestColor.Green,
        TestColor.Cyan,
        TestColor.Blue,
        TestColor.Violet,
        TestColor.Gray,
        TestColor.White
    )

    override fun onCreateViewHolder(parent: ViewGroup): ExampleViewHolder {
        return ExampleViewHolder(
            layoutInflater.inflate(R.layout.example_view_holder, parent, false))
    }

    override fun getItemCount(): Int {
        return arr.size
    }

    override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) {
        holder.textView.text = "$position"
        holder.textView.setBackgroundColor(arr[position].resource)
    }

    override fun getCommand(position: Int): String? {
        return arr[position].name
    }

    override fun getState(position: Int): String? {
        return "#${Integer.toHexString(arr[position].resource)}"
    }

    override fun onCommand(position: Int) {
        Log.i(
            HorizontalSelectorExampleActivity::class.simpleName,
                "Voice command triggered for position: $position")
    }

    override fun getViewPagerFragment(position: Int): 
        HorizontalSelector.ViewPagerFragment? {
        //
        // An example of how to set distinct ViewPagerFragments to different 
        // positions. The first and second list items will use custom 
        // ViewPagerFragments, and the rest will return null to produce default 
        // behavior (no fragment appears on selection).
        //
        return when (position) {
            0 -> ColorOptionFragment(arr[position].name)
            1 -> ColorLevelFragment(arr[position].name)
            else -> null
        }
    }
}

View Pager and View Pager Fragment

Horizontal Selector includes a View Pager, which allows the user to display fragments to the user when list items are selected. A common use case of the Horizontal Selector is that selecting a list item will trigger another, more specific menu to be shown to the user. For example, the list item may have a voice command “Volume”, and when it is selected, a new menu will be shown to the user to “Set Volume 1-10”. View Pager allows you as the developer to create the layout and behavior for a fragment that would be serve as the second menu.

To populate this View Pager, the developer needs to extend from ViewPagerFragment with a custom class. The developer can include any and all information in this subclass about what the ViewPagerFragment should display, what behavior it should include, etc. Below is shown an example ViewPagerFragment which gives the user the option to choose a “level” for a color. (Notice that this custom ViewPagerFragment was included in the previous code example of Horizontal Selector Adapter above in line 69.)

class ColorLevelFragment(private val color: String) :
        HorizontalSelector.ViewPagerFragment(R.layout.fragment_color_level) {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val titleButton = ContinuousRadioButton(this.requireContext())
        titleButton.text = String.format(
            resources.getString(R.string.color_level_fragment_text), color)
        titleButton.titleOnly = true
        color_level_radio_group.addView(titleButton)
    }

        for (i in 1..10) {
            val newButton = ContinuousRadioButton(this.requireContext())
            newButton.voiceCommand = "$color Level $i"
            newButton.text = "$i"
            color_level_radio_group.addView(newButton)
        }
    }
}

If the developer has no need for an extra view to be displayed to the user as a fragment when a list item is selected, the developer simply needs to have the getViewPagerFragment() abstract function in HorizontalSelectorAdapter return null. In this case, when the list item is selected and no ViewPagerFragment is supplied, nothing will be shown to the user.

Examples:

https://github.com/realwear/UXLibrary-Example/blob/master/app/src/main/java/com/realwear/uxlibrary_example/horizontalselectorexample/ColorLevelFragment.kt

https://github.com/realwear/UXLibrary-Example/blob/master/app/src/main/java/com/realwear/uxlibrary_example/horizontalselectorexample/ColorOptionFragment.kt

Additional Info

Certain information about the state of the Horizontal Selector can be obtained and/or set by the developer programmatically:

  • Focused Index: the index number of the list item currently closest to the center of the screen

horizontal_selector.focusedItemIndex

  • Selected Item: the index number of the list item that is selected. When a list item is selected, the item moves to the center of the screen, becomes “focused” (see above), and any ViewPagerFragments supplied for this list item will be displayed to the user. Additionally, head scrolling and all voice commands for the Horizontal Selector will be paused for a period of 5 seconds.
// Select the item at position 5 in the Horizontal Selector.
horizontal_selector.selectListItem(5)

// Deselect the currently selected list item. Headtracking and voice commands
// for the Horizontal Selector will resume.
horizontal_selector.deselectListItem()
  • Center Border: the bordered outline in the center of the Horizontal Selector with the rounded corners which visually aids the user in recognizing which list item is in the center and focused. This border can be removed or inserted as needed by the developer.
// Remove center border view.
horizontal_selector.setCenterBorderVisibility(View.GONE)

// Insert center border view.
horizontal_selector.setCenterBorderVisibility(View.VISIBLE)
  • Command and State: each list item has two text views below the view holder that provide two pieces information about that list item: its "command" and "state". The command is the voice command associated with that item. The state is more flexible in its definition, and it is up to the developer to decide what to place there. The state can also be left blank. The text views will be populated with strings as set by the developer for each list item in the Horizontal Selector Adapter:
inner class ExampleAdapter(context: Context) :
    HorizontalSelectorAdapter<ExampleViewHolder>(context) {
    ...
    override fun getCommand(position: Int): String? {
        // Logic for deciding the Command string at [position].
    }

    override fun getState(position: Int): String? {
        // Logic for deciding the State string at [position].
    }
}

An example use case for the command and state strings is a list item that lets the user change the volume on the device. The command might be "Volume" and the state might be "Level %d", which would be displayed as "Level 1" or "Level 3" or simply "Level [d]" and adapt as the user changes the volume on the device.

Pause Timer: built into the Horizontal Selector is the behavior that when a list item is selected, the Horizontal Selector will pause for 5 seconds to allow the user to react to whatever behavior has been triggered by the item selection. This means that head tracking is stopped and all voice commands for the Horizontal Selector are unregistered for those 5 seconds.

The developer can additionally restart that 5 second timer in a View Pager Fragment with the function notifyFragmentAction(). The use case for this is to allow a user to again have more time to react to whatever behavior has been triggered by an action in the View Pager Fragment. In the below example, we are calling notifyFragmentAction every time the checked state for a Radio Group changes and restarting the 5 second timer.

class ColorLevelFragment(private val color: String) :
        HorizontalSelector.ViewPagerFragment(R.layout.fragment_color_level) {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        color_level_radio_group.setOnCheckedChangeListener { 
                _, _ -> notifyFragmentAction() 
        }
}

Clone this wiki locally