A year ago, a library of highly extensible select buttons was written in Java. Single control to achieve single, multiple, menu selection, and the selection mode can be dynamically expanded.

A year later, a new requirement came to the library, and the project code was fully kotlinized. The hard-line insertion of some Java code was incompatible, and Java’s redundant syntax reduced the readability of the code, so the decision was made to refactor the code in Kotlin, adding some new features while refactoring. This article shares the refactoring process.

The extensibility of the select button is mainly reflected in four aspects:

  1. Option button layout is extensible
  2. Option button styles are extensible
  3. The selected style is extensible
  4. The selection pattern is extensible

Extend the layout

The native radio button goes throughRadioButton+ RadioGroupImplementation, they must be parent-child in the layout, whileRadioGroupInherited fromLinearLayout, radio buttons can only be rolled out horizontally or vertically, which limits the variety of radio button layouts, such as the following triangular layout, which is difficult to achieve with native controls:

To break this restriction, radio buttons are no longer part of a parent control. They are independent and can be arranged in any layout file. The Activity layout file in the diagram looks like this (pseudo-code) :

<androidx.constraintlayout.widget.ConstraintLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Selector age"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <test.taylor.AgeSelector
        android:id="@+id/selector_teenager"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/title"
        app:layout_constraintStart_toStartOf="parent"/>

    <test.taylor.AgeSelector
        android:id="@+id/selector_man"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toStartOf="@id/selector_old_man"
        app:layout_constraintTop_toBottomOf="@id/selector_teenager"
        app:layout_constraintStart_toStartOf="parent"/>

    <test.taylor.AgeSelector
        android:id="@+id/selector_old_man"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/selector_teenager"
        app:layout_constraintStart_toEndOf="@id/selector_man"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

AgeSelector represents a specific button, which in this case is a “picture above, text below” radio button. It inherits from abstract selectors.

Extension style

From a business perspective, what a Selector looks like is a frequent point of variation, so the “build button style” behavior is designed as an abstract function onCreateView() of a Selector that subclasses can override to extend.

public abstract class Selector extends FrameLayout{

    public Selector(Context context) {
        super(context);
        initView(context, null);
    }

    private void initView(Context context, AttributeSet attrs) {
        // Initialize the button algorithm framework
        View view = onCreateView();
        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(view, params);
    }
    
    // How to build button view and defer to subclass implementation
    protected abstract View onCreateView(a);
}
Copy the code

Selector inherits from FrameLayout and instantiates it by building a button view and adding that view to your layout as a child. Subclasses extend the button style by overriding onCreateView() :

public class AgeSelector extends Selector {
    @Override
    protected View onCreateView(a) {
        View view = LayoutInflater.from(this.getContext()).inflate(R.layout.age_selector, null);
        returnview; }}Copy the code

The style of AgeSelector is defined in XML.

The style of the selected button is also a business change point. In the same way, you can design a Selector like this:

// The abstract button implements the click event
public abstract class Selector extends FrameLayout implements View.OnClickListener {

    public Selector(Context context) {
        super(context);
        initView(context, null);
    }
    
    private void initView(Context context, AttributeSet attrs) {
        View view = onCreateView();
        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(view, params);
        // Set the click event
        this.setOnClickListener(this);
    }
    
    @Override
    public void onClick(View v) {
        // The original selected state
        boolean isSelect = this.isSelected();
        // Reverse the selected status
        this.setSelected(! isSelect);// Show the effect of the selected state switchonSwitchSelected(! isSelect);return! isSelect; }// The button is selected to defer the effect of state changes to the subclass
    protected abstract void onSwitchSelected(boolean isSelect);
}
Copy the code

Abstract the effect of the selected button state change into an algorithm, deferred to the subclass implementation:

public class AgeSelector extends Selector {
    // The radio button selects the background
    private ImageView ivSelector;
    private ValueAnimator valueAnimator;

    @Override
    protected View onCreateView(a) {
        View view = LayoutInflater.from(this.getContext()).inflate(R.layout.selector, null);
        ivSelector = view.findViewById(R.id.iv_selector);
        return view;
    }

    @Override
    protected void onSwitchSelected(boolean isSelect) {
        if (isSelect) {
            playSelectedAnimation();
        } else{ playUnselectedAnimation(); }}// Play the unselected animation
    private void playUnselectedAnimation(a) {
        if (ivSelector == null) {
            return;
        }
        if(valueAnimator ! =null) { valueAnimator.reverse(); }}// Play the selected animation
    private void playSelectedAnimation(a) {
        if (ivSelector == null) {
            return;
        }
        valueAnimator = ValueAnimator.ofInt(0.255);
        valueAnimator.setDuration(800);
        valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                ivSelector.setAlpha((int) animation.getAnimatedValue()); }}); valueAnimator.start(); }}Copy the code

AgeSelector defines a background color gradient animation when the selected state changes.

Function type variables replace inheritance

In the abstract button control, the “button style” and “button selection state transition” are abstracted into algorithms, the implementation of which is deferred to subclasses, in such a way as to extend the button style and behavior.

One consequence of inheritance is that the number of classes expands. Is there any way to extend button styles and behaviors without inheritance?

The build button style member method onCreateView() can be designed as a member variable of type View, whose value can be changed by setting the value function. But button selected state transformation is a behavior, and in Java behavior is expressed only by means, so you can only change behavior by inheritance.

Kotlin has a type called a function type, which allows you to store behavior in a variable:

class Selector @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    FrameLayout(context, attrs, defStyleAttr) {
    
    // Select the behavior of the state transition, which is a lambda
    var onSelectChange: ((Selector, Boolean) - >Unit)? = null
    // Whether the button is selected
    var isSelecting: Boolean = false

    // Button style
     var contentView: View? = null
        set(value) { field = value value? .let {// When the button style is assigned, add it to the Selector as a subview
                addView(it, LayoutParams(MATCH_PARENT, MATCH_PARENT))
            }
        }
    
    // The change button is selected
    fun setSelect(select: Boolean) {
        showSelectEffect(select)
    }
    
    // Show the effect of the selected state transition
    fun showSelectEffect(select: Boolean) {
        // If the selected state changes, the selected state transition behavior is performed
        if(isSelecting ! = select) { onSelectChange? .invoke(this, select)
        }
        isSelecting = select
    }
}
Copy the code

The selected style and behavior are both abstracted as a member variable that can be dynamically extended simply by assigning values, no inheritance required:

// Build the button instance
val selector = Selector {
    layout_width = 90
    layout_height = 50
    contentView = ageSelectorView
    onSelectChange = onAgeSelectStateChange
}

// Build the button style
private val ageSelectorView: ConstraintLayout
    get() = ConstraintLayout {
        layout_width = match_parent
        layout_height = match_parent
        
        // Button to select background
        ImageView {
            layout_id = "ivSelector"
            layout_width = 0
            layout_height = 30
            top_toTopOf = "ivContent"
            bottom_toBottomOf = "ivContent"
            start_toStartOf = "ivContent"
            end_toEndOf = "ivContent"
            background_res = R.drawable.age_selctor_shape
            alpha = 0f
        }

        // Button image
        ImageView {
            layout_id = "ivContent"
            layout_width = match_parent
            layout_height = 30
            center_horizontal = true
            src = R.drawable.man
            top_toTopOf = "ivSelector"
        }

        // Button text
        TextView {
            layout_id = "tvTitle"
            layout_width = match_parent
            layout_height = wrap_content
            bottom_toBottomOf = parent_id
            text = "man"
            gravity = gravity_center_horizontal
        }
    }

// The button selects the behavior
private val onAgeSelectStateChange = { selector: Selector, select: Boolean ->
    // Select the background according to the selected state change button
    selector.find<ImageView>("ivSelector")? .alpha =if (select) 1f else 0f
}
Copy the code

When you build the Selector instance, you specify its style and select the transform effect. (This applies to the DSL simplify build code, which can be described here.)

Extended selected mode

A single Selector already works fine, but for multiple selectors to form a single or multiple selection mode, you need a manager to synchronize the selection state between them. The Java version of the manager is as follows:

public class SelectorGroup {
    // Select the mode
    public interface ChoiceAction {
        void onChoose(Selector selector, SelectorGroup selectorGroup, StateListener stateListener);
    }
    
    // Select the status listener
    public interface StateListener {
        void onStateChange(String groupTag, String tag, boolean isSelected);
    }
    
    // Select the pattern instance
    private ChoiceAction choiceMode;
    // Select the status listener instance
    private StateListener onStateChangeListener;
    // Map for the last selected button
    private HashMap<String, Selector> selectorMap = new HashMap<>();

    // Inject the selected mode
    public void setChoiceMode(ChoiceAction choiceMode) {
        this.choiceMode = choiceMode;
    }

    // Set the selected status listener
    public void setStateListener(StateListener onStateChangeListener) {
        this.onStateChangeListener = onStateChangeListener;
    }

    // Get the previously selected button
    public Selector getPreSelector(String groupTag) {
        return selectorMap.get(groupTag);
    }
    
    // Change the selected state of the specified button
    public void setSelected(boolean selected, Selector selector) {
        if (selector == null) {
            return;
        }
        // Remember the selected button
        if (selected) {
            selectorMap.put(selector.getGroupTag(), selector);
        }
        // The trigger button selects the style change
        selector.setSelected(selected);
        if(onStateChangeListener ! =null) { onStateChangeListener.onStateChange(selector.getGroupTag(), selector.getSelectorTag(), selected); }}// Cancel the previously selected button
    private void cancelPreSelector(Selector selector) {
        // Each button has a group id to identify which group it belongs to
        String groupTag = selector.getGroupTag();
        // Get the previously selected button in the group and deselect it
        Selector preSelector = getPreSelector(groupTag);
        if(preSelector ! =null) {
            preSelector.setSelected(false); }}// When a button is clicked, the click event is passed to SelectorGroup via this function
    void onSelectorClick(Selector selector) {
        // Delegate the click event to the selection mode
        if(choiceMode ! =null) {
            choiceMode.onChoose(selector, this, onStateChangeListener);
        }
        // Record the selected button in the Map
        selectorMap.put(selector.getGroupTag(), selector);
    }
    
    // The scheduled radio mode
    public class SingleAction implements ChoiceAction {
        @Override
        public void onChoose(Selector selector, SelectorGroup selectorGroup, StateListener stateListener) {
            cancelPreSelector(selector);
            setSelected(true, selector); }}// Scheduled multiple choice mode
    public class MultipleAction implements ChoiceAction {
        @Override
        public void onChoose(Selector selector, SelectorGroup selectorGroup, StateListener stateListener) {
            booleanisSelected = selector.isSelected(); setSelected(! isSelected, selector); }}}Copy the code

SelectorGroup abstracts the selected pattern into the interface ChoiceAction, which can be dynamically extended with setChoiceMode().

SelectorGroup also preorders two selection modes: single and multiple.

  1. Radio options can be understood as: when a button is clicked, select the current one and deselect the previous one.
  2. Multiple selection can be understood as: when a button is clicked, the current selected state is unconditionally reversed.

A Selector holds an instance of SelectorGroup to pass button click events to for unified management:

public abstract class Selector extends FrameLayout implements View.OnClickListener {
    // Button group label
    private String groupTag;
    // Button manager
    private SelectorGroup selectorGroup;
    
    // Set the group label and manager
    public Selector setGroup(String groupTag, SelectorGroup selectorGroup) {
        this.selectorGroup = selectorGroup;
        this.groupTag = groupTag;
        return this;
    }
    
    @Override
    public void onClick(View v) {
        // Pass the click event to the manager
        if(selectorGroup ! =null) {
            selectorGroup.onSelectorClick(this); }}}Copy the code

Then you can implement radio selection like this:

SelectorGroup singleGroup = new SelectorGroup();
singleGroup.setChoiceMode(SelectorGroup.SingleAction);
selector1.setGroup("single", singleGroup);
selector2.setGroup("single", singleGroup);
selector3.setGroup("single", singleGroup);
Copy the code

Menu selection can also be implemented like this:

SelectorGroup orderGroup = new SelectorGroup();
orderGroup.setStateListener(new OrderChoiceListener());
orderGroup.setChoiceMode(new OderChoiceMode());
/ / before the food groups
selector1_1.setGroup("starters", orderGroup);
selector1_2.setGroup("starters", orderGroup);
/ / staple food group
selector2_1.setGroup("main", orderGroup);
selector2_2.setGroup("main", orderGroup);
/ / soup group
selector3_1.setGroup("soup", orderGroup);
selector3_2.setGroup("soup", orderGroup);

// Menu selection: select one within a group and multiple across groups
private class OderChoiceMode implements SelectorGroup.ChoiceAction {

    @Override
    public void onChoose(Selector selector, SelectorGroup selectorGroup, SelectorGroup.StateListener stateListener) {
        cancelPreSelector(selector, selectorGroup);
        selector.setSelected(true);
        if(stateListener ! =null) {
            stateListener.onStateChange(selector.getGroupTag(), selector.getSelectorTag(), true); }}// Cancel the previously selected group button
    private void cancelPreSelector(Selector selector, SelectorGroup selectorGroup) {
        Selector preSelector = selectorGroup.getPreSelector(selector.getGroupTag());
        if(preSelector ! =null) {
            preSelector.setSelected(false); }}}Copy the code

Change the interface in Java to a lambda and store it in a variable of function type, thus eliminating the need to inject functions. The Kotlin version of SelectorGroup looks like this:

class SelectorGroup {
    companion object {
        // Static implementation of radio mode
        var MODE_SINGLE = { selectorGroup: SelectorGroup, selector: Selector ->
            selectorGroup.run {
                // Find the previously selected ones in the same group and deselect themfindLast(selector.groupTag)? .let { setSelected(it,false)}// Select the current button
                setSelected(selector, true)}}// Static implementation of multiple selection mode
        varMODE_MULTIPLE = { selectorGroup: SelectorGroup, selector: Selector -> selectorGroup.setSelected(selector, ! selector.isSelecting) } }// An ordered set of all currently selected buttons (some scenarios require memorization of the order in which buttons are selected)
    private var selectorMap = LinkedHashMap<String, MutableSet<Selector>>()

    // The current selected mode (function type)
    var choiceMode: ((SelectorGroup, Selector) -> Unit)? = null

    // Select the state change listener and call back all the selected buttons (function type)
    var selectChangeListener: ((List<Selector>/*selected set*/) - >Unit)? = null

    // Selector passes the click event to the SelectorGroup via this method
    fun onSelectorClick(selector: Selector) {
        // Delegate the click event to the selected modechoiceMode? .invoke(this, selector)
    }

    // Find all the selected buttons for the specified group
    fun find(groupTag: String) = selectorMap[groupTag]

    // Look for the last selected button in the group based on the group label
    fun findLast(groupTag: String)= find(groupTag)? .takeUnless { it.isNullOrEmpty() }? .last()// Change the selected state of the specified button
    fun setSelected(selector: Selector, select: Boolean) {
        // Create, delete, or append the selected button to the Map
        if(select) { selectorMap[selector.groupTag]? .also { it.add(selector) } ? : also { selectorMap[selector.groupTag] = mutableSetOf(selector) } }else{ selectorMap[selector.groupTag]? .also { it.remove(selector) } }// Show the selected effect
        selector.showSelectEffect(select)
        // Triggers the selected status listener
        if(select) { selectChangeListener? .invoke(selectorMap.flatMap { it.value }) } }// Release the held selected control
    fun clear(a) {
        selectorMap.clear()
    }
}
Copy the code

Then you can use SelectorGroup like this:

// Build manager
val singleGroup = SelectorGroup().apply {
    choiceMode = SelectorGroup.MODE_SINGLE
    selectChangeListener = { selectors: List<Selector>->
        // You can get all the selected buttons here}}// Build radio button 1
Selector {
    tag = "old-man"
    group = singleGroup
    groupTag = "age"
    layout_width = 90
    layout_height = 50
    contentView = ageSelectorView
}

// Build radio button 2
Selector {
    tag = "young-man"
    group = singleGroup
    groupTag = "age"
    layout_width = 90
    layout_height = 50
    contentView = ageSelectorView
}
Copy the code

The two buttons built have the same groupTag and SelectorGroup, so they belong to the same group and are in radio mode.

Dynamic binding of data

A button in a project usually corresponds to a “data”, such as in the following scenario:

Both the packet data and the button data in the diagram are returned by the server. When you click Create group, you want to get the ID of each option in the selectChangeListener. So how do you bind data to a Selector?

You can, of course, do this by inheritance, adding a specific business data type to the Selector subclass. But is there a more general solution?

In the ViewModel designed a dynamic extended attributes for its method, to apply it in the Selector (details can be set to read the source code knowledge | dynamic extension class and bind the life cycle of the new way)

class Selector @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    FrameLayout(context, attrs, defStyleAttr) {

    // Container for storing service data
    private vartags = HashMap<Any? , Closeable? > ()// Get business data (overloaded value operator)
    operator fun <T : Closeable> get(key: Key<T>): T? = (tags.getOrElse(key, { null })) as T

    // Add business data (overloading the set operator)
    operator fun <T : Closeable> set(key: Key<T>, closeable: Closeable) {
        tags[key] = closeable
    }
    
    // Clear all service data
    private fun clear(a){ group? .clear() tags.forEach { entry -> closeWithException(entry.value) } }// Clean up the business data when the control is decoupled from the window
    override fun onDetachedFromWindow(a) {
        super.onDetachedFromWindow()
        clear()
    }

    // Clear a single service data
    private fun closeWithException(closable: Closeable?). {
        try{ closable? .close() }catch (e: Exception) {
        }
    }
    
    // Key of the service data
    interface Key<E : Closeable>
}
Copy the code

Add a Map member to the Selector to hold the business data. The business data is declared as a subtype of Closeable. The purpose is to abstract the various resource cleanup actions to the close() method. The Selector overrides onDetachedFromWindow() and iterates over each of the business data and calls their close(), freeing the business data resource when its lifecycle ends.

Selector also overrides the set and value operators to simplify code for accessing business data:

// Game properties entity class
data class GameAttr( var name: String, var id: String ): Closeable {
    override fun close(a) {
        name = null
        id = null}}// Build a game attribute instance
val attr = GameAttr("Gold"."id-298")

// The key that matches the game attribute entity
val key = object : Selector.Key<GameAttr> {}

// Build the options group
val gameSelectorGroup by lazy {
    SelectorGroup().apply {
        // Select mode (omitted)
        choiceMode = { selectorGroup, selector -> ... }
        // Select the callback
        selectChangeListener = { selecteds ->
            // Iterate through all the selected options
            selecteds.forEach { s ->
                // Access the game properties bound to each option (using the value operator)
                Log.v("test"."${s[key].name} is selected")}}}}// Build options
Selector {
    tag = attr.name
    groupTag = "Matching segment"
    group = gameSelectorGroup
    layout_width = 70
    layout_height = 32
    // Bind the game properties (using the set operator)
    this[key] = attr
}
Copy the code

Because the operator is overloaded, the code for binding and getting game properties is shorter.

If you use generics, you have to be strong, right?

Binding to theSelectorThe data is designed to be generic, and the business layer can only be used if it is forcibly converted into a specific type. Is there any way not to forcibly convert the data in the business layer?

The CoroutineContext key carries the type information:

public interface CoroutineContext {
    public interface Key<E : Element>
    public operator fun <E : Element> get(key: Key<E>): E?
}
Copy the code

And each concrete subtype of CoroutineContext corresponds to a static key instance:

public interface Job : CoroutineContext.Element {
    public companion object Key : CoroutineContext.Key<Job> {}
}
Copy the code

In this way, a concrete subtype can be obtained without a strong turn:

coroutineContext[Job]// Return Job instead of CoroutineContext
Copy the code

Mimicking CoroutineContext, a generic interface is designed for the key of a business Selector:

interface Key<E : Closeable>
Copy the code

To bind data to a Selector, you need to build a “key instance” :

val key = object : Selector.Key<GameAttr> {}
Copy the code

The incoming key carries type information, which can be preempted in the value method and returned to the business layer for use:

// The specific type of the value is specified by the parameter key and is returned to the business layer after being strong-handed
operator fun <T : Closeable> get(key: Key<T>): T? = (tags.getOrElse(key, { null })) as T
Copy the code

With the help of a DSL it is easy to dynamically build the selection button from the data. The interface code shown in the previous Gif is as follows:

// Game properties collection entity class
data class GameAttrs(
    vartitle: String? .// Option group title
    var attrs: List<GameAttrName>? // Option group content
)

// A simplified single game properties entity class (which is bound to a Selector)
data class GameAttrName(
    var name: String?
) : Closeable {
    override fun close(a) {
        name = null}}Copy the code

These are the two data entity classes used in the Demo. In the real project, they should be returned by the server. For simplicity, simulate some data locally:

val gameAttrs = listOf(
    GameAttrs(
        "Regional", listOf(
            GameAttrName("WeChat"),
            GameAttrName("QQ")
        )
    ),
    GameAttrs(
        "Mode", listOf(
            GameAttrName("Qualifying"),
            GameAttrName("Normal mode"),
            GameAttrName("Entertainment mode"),
            GameAttrName("Game communication")
        )
    ),
    GameAttrs(
        "Matching segment", listOf(
            GameAttrName("Bronze and silver"),
            GameAttrName("Gold"),
            GameAttrName("Platinum"),
            GameAttrName("Diamond"),
            GameAttrName("Star yao"),
            GameAttrName("The king")
        )
    ),
    GameAttrs(
        "Number of teams.", listOf(
            GameAttrName("Three line"),
            GameAttrName("Five rows"))))Copy the code

Finally, use DSL to dynamically build the selection button:

// Vertical layout
LinearLayout {
    layout_width = match_parent
    layout_height = 573
    orientation = vertical

    // Iterate through the game collection, dynamically adding options groupsgameAttrs? .forEach { gameAttr ->// Add the option group title
        TextView {
            layout_width = wrap_content
            layout_height = wrap_content
            textSize = 14f
            textColor = "#ff3f4658"
            textStyle = bold
            text = gameAttr.title
        }

        // Wrap the container control
        LineFeedLayout {
            layout_width = match_parent
            layout_height = wrap_content
            
            // Iterate through the game properties and dynamically add the option buttongameAttr.attrs? .forEachIndexed { index, attr -> Selector { layout_id = attr.name tag = attr.name groupTag = gameAttr.title// Set the controller for the button
                    group = gameSelectorGroup
                    // Specify a view for the button
                    contentView = gameAttrView
                    // Set the selected effect converter for the button
                    onSelectChange = onGameAttrChange
                    layout_width = 70
                    layout_height = 32
                    // Bind data to the button and update the view
                    bind = Binder(attr) { _, _ ->
                        this[gameAttrKey] = attr
                        find<TextView>("tvGameAttrName")? .text = attr.name } } } } } }Copy the code

The button view, button controller and button effect converter are defined as follows:

// The key corresponding to the game attribute
val gameAttrKey = object : Selector.Key<GameAttrName> {}

// Build the game properties view
val gameAttrView: TextView?
        get() = TextView {
            layout_id = "tvGameAttrName"
            layout_width = 70
            layout_height = 32
            textSize = 12f
            textColor = "#ff3f4658"
            background_res = R.drawable.bg_game_attr
            gravity = gravity_center
            padding_top = 7
            padding_bottom = 7
        }

// When the button status changes, change the background color and button font color
private val onGameAttrChange = { selector: Selector, select: Boolean ->
    selector.find<TextView>("tvGameAttrName")? .apply { background_res =if (select) R.drawable.bg_game_attr_select else R.drawable.bg_game_attr
        textColor = if (select) "#FFFFFF" else "#3F4658"
    }
    Unit
}

// Build the button controller
private val gameSelectorGroup by lazy {
    SelectorGroup().apply {
        choiceMode = { selectorGroup, selector ->
            // Set all groups except matching segment option group to radio
            if(selector.groupTag ! ="Matching segment") { selectorGroup.apply { findLast(selector.groupTag)? .let { setSelected(it,false) }
                }
                selectorGroup.setSelected(selector, true)}// Set Matching segment option Group to multiple
            else{ selectorGroup.setSelected(selector, ! selector.isSelecting) } }// When the selected button changes, it will be called back here
        selectChangeListener = { selecteds ->
            selecteds.forEach { s->
                Log.v("test"."${s[gameAttrKey]? .name} is selected")}}}}Copy the code

talk is cheap, show me the code

The full code is available here

Recommended reading

There are some unexpanded details, such as “Building a Layout DSL,” “How ViewModel dynamically extends properties,” “Applying data binding to DSLS,” and “Overloading operators.” They can be explained in detail by clicking the following link:

  1. Android custom controls | high extensible radio button (never quarreled with the product manager)
  2. Android custom controls | using strategy pattern extend radio buttons and product managers to become good friends
  3. Android custom controls | source there is treasure in the automatic line feed control
  4. Android performance optimization | to shorten the building layout is 20 times (below)
  5. Reading knowledge source long | dynamic extension class and bind the new way of the life cycle