This is the first in a series of articles.

  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

The business scenario

Go to the weekly demand meeting in high spirits. In order to push more accurately, user information needs to be collected, so the following interface is designed for the product:

Unexpectedly, the day before the release, I suddenly felt that the collection granularity was not fine enough, so I wanted to increase the 4 options to 6. In the face of this sudden, unexpected change in requirements, the design and DEVELOPMENT teams were strongly opposed.

For the design, it’s not just about adding two images. If you use the previous layout design, you won’t be able to fit 6 options on the screen, so you need to redesign the layout. After the design sister’s overtime efforts, the final design drawing is changed to this:For development… Radio button with two titles? Two headings or different colors? The title is gonna change color after I select it? Not afraid not afraid, don’t say tomorrow will be sent version, it is also possible to send tonight. Because I have a custom radio control, this interface change, only need to change 2 layout files. (The company encourages the value of embracing change, and writing code that “embraces change” is the best response to development)

How to define the abstract of radio buttons?

In native abstraction, radio controls contain two concepts:

  1. The radio setRadioGroup
  2. The radio buttonRadioButton

The limitation of the native abstraction is that RadioGroup and RadioButton are parent to child, that is, RadioGroup must be an explicit ViewGroup type, which restricts the layout of radioButtons.

If the radio group is not oneViewIs it possible to liberate this layer of constraints?

To keep the answer to this question in suspense, let’s take a look at how abstract radio buttons are, leaving aside radio groups.

Radio buttons should contain the following basic features:

  1. It’s a View, and it’s clickable
  2. There are two states (selected and unselected) that correspond to different views

Just inherit from View and use view.isSelected () to implement both features. The code is as follows:

public abstract class Selector extends FrameLayout implements View.OnClickListener {

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

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

    public Selector(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context, attrs);
    }

    private void initView(Context context, AttributeSet attrs) {
        // Implement feature 1: clickable
        this.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        // Implement feature 2: Click to change the selected state
        boolean isSelect = switchSelector();
    }

    // Reverse the selected status
    public boolean switchSelector(a) {
        boolean isSelect = this.isSelected();
        this.setSelected(! isSelect);return !isSelect;
    }
}
Copy the code

To meet business scenarios, additional features need to be added: The relative layout of elements in buttons can be customized

Additional features change as business needs change, and you can encapsulate this layer of change with a template method pattern: initializing the algorithm framework by Selector definition, and delaying real interface initialization to subclasses.

  • In this business scenario, though, the layout of the radio button elements is: image on top, text on bottom. What happens next time? So defining the element layout should be left as an abstract functionSelectorSubclass implementation.
  • To achieve the selected gradient,SelectorProvide timing for options to change.
  • A button contains some basic properties, such as the button name and button icon. Write these properties as custom properties and pass them to the subclass for resolution as follows:
public abstract class Selector extends FrameLayout implements View.OnClickListener {

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

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

    public Selector(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context, attrs);
    }

    // Template method
    private void initView(Context context, AttributeSet attrs) {
        // Read from the defined attribute
        if(attrs ! =null) {
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Selector);
            int tagResId = typedArray.getResourceId(R.styleable.Selector_tag, 0);
            tag = context.getString(tagResId);
            // Pass the custom property to the subclass
            onObtainAttrs(typedArray);
            typedArray.recycle();
        } else{tag ="defaultThe tag "; }// Build the button view
        View view = onCreateView();
        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(view, params);
        this.setOnClickListener(this);
    }
    
    public void onObtainAttrs(TypedArray typedArray) {}

    // Subclasses implement this function to define the layout of radio button elements
    protected abstract View onCreateView(a);

    @Override
    public void onClick(View v) {
        boolean isSelect = switchSelector();
    }

    public boolean switchSelector(a) {
        boolean isSelect = this.isSelected();
        this.setSelected(! isSelect);// When an option is changedonSwitchSelected(! isSelect);return! isSelect; }// Options changed
    protected abstract void onSwitchSelected(boolean isSelect);
}
Copy the code

Since Selector is an abstract class, it must be abstracted by a subclass. The following code is the demo’s implementation of the age radio button:

public class AgeSelector extends Selector {
    // Declare the controls that the button contains
    private TextView tvTitle;
    private ImageView ivIcon;
    private ImageView ivSelector;
    private ValueAnimator valueAnimator;
    
    @Override
    public void onObtainAttrs(TypedArray typedArray) {
        // Parse custom attributes
        text = typedArray.getString(R.styleable.Selector_text);
        iconResId = typedArray.getResourceId(R.styleable.Selector_img, 0);
        indicatorResId = typedArray.getResourceId(R.styleable.Selector_indicator, 0);
        textColor = typedArray.getColor(R.styleable.Selector_text_color, Color.parseColor("#FF222222"));
        textSize = typedArray.getInteger(R.styleable.Selector_text_size, 15);
    }

    private void onBindView(String text, int iconResId, int indicatorResId, int textColor, int textSize) {
        // Bind custom properties to the control
        if(tvTitle ! =null) {
            tvTitle.setText(text);
            tvTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);
            tvTitle.setTextColor(textColor);
        }
        if(ivIcon ! =null) {
            ivIcon.setImageResource(iconResId);
        }
        if(ivSelector ! =null) {
            ivSelector.setImageResource(indicatorResId);
            ivSelector.setAlpha(0); }}@Override
    protected View onCreateView(a) {
        // Build a custom button layout
        View view = LayoutInflater.from(this.getContext()).inflate(R.layout.age_selector, null);
        tvTitle = view.findViewById(R.id.tv_title);
        ivIcon = view.findViewById(R.id.iv_icon);
        ivSelector = view.findViewById(R.id.iv_selector);
        onBindView(text, iconResId, indicatorResId, textColor, textSize);
        return view;
    }

    @Override
    protected void onSwitchSelected(boolean isSelect) {
        // Animate a radio button when its state changes
        if (isSelect) {
            playSelectedAnimation();
        } else{ playUnselectedAnimation(); }}private void playUnselectedAnimation(a) {
        if (ivSelector == null) {
            return;
        }
        if(valueAnimator ! =null) { valueAnimator.reverse(); }}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

The layout file of the radio button is as follows:


      
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_selector"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/tv_title"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="spread"
        app:layout_constraintVertical_weight="122" />

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.026"
        app:layout_constraintWidth_percent="81" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:gravity="center_horizontal|bottom"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/iv_selector"
        app:layout_constraintVertical_chainStyle="spread"
        app:layout_constraintVertical_weight="28" />
</android.support.constraint.ConstraintLayout>
Copy the code

How do you define the radio group abstraction?

Wait, there’s something wrong! If you run the above code, you will see that each Selector works fine (with a gradient animation when the selected state changes), but multiple selectors can be selected at the same time, they are not mutually exclusive…

The reason is that the Selector abstraction only cares about its own selected state, and it doesn’t know the state of any other Selector.

So native controls need the role of RadioGroup, which acts as the father and keeps track of each child!

But we don’t want a ViewGroup parent, because it’s too much of a control, and the kids can’t lay it out and it’s too limited.

Then make an invisible father! In fact, what fathers do is not “when one child is selected, the other child is unselected”?

Have the idea to do it, the code is as follows:

public class SelectorGroup {
    // To save the last selected button
    private HashMap<String, Selector> selectorMap = new HashMap<>();

    // Get the last selected button
    public Selector getPreSelector(String groupTag) {
        return selectorMap.get(groupTag);
    }

    // Deselect the last selected button
    private void cancelPreSelector(Selector selector) {
        String groupTag = selector.getGroupTag();
        Selector preSelector = getPreSelector(groupTag);
        if(preSelector ! =null) {
            preSelector.setSelected(false); }}// When a radio group button is clicked, the click event is passed to the radio group via this method
    void onSelectorClick(Selector selector) {
        selector.setSelected(true);
        cancelPreSelector(selector);
        // Save the selected button in the mapselectorMap.put(selector.getGroupTag(), selector); }}Copy the code

In order for SelectorGroup to manage the selected and cancelled status changes after the button is clicked, we need to pass the button click event to SelectorGroup, and then modify the radio button code as follows:

public abstract class Selector extends FrameLayout implements View.OnClickListener {
    @Override
    public void onClick(View v) {
        // Pass the click event to SelectorGroup, which calls setSelected() to manage the selected and cancelled status of the button
        if(selectorGroup ! =null) {
            selectorGroup.onSelectorClick(this); }}@Override
    public void setSelected(boolean selected) {
        boolean isPreSelected = isSelected();
        super.setSelected(selected);
        if (isPreSelected != selected) {
            onSwitchSelected(selected);
        }
    }

    public boolean switchSelector(a) {
        boolean isSelect = this.isSelected();
        this.setSelected(! isSelect);// When an option is changedonSwitchSelected(! isSelect);return! isSelect; }// Options changed
    protected abstract void onSwitchSelected(boolean isSelect);
}
Copy the code

Now you can use a custom radio button like this:

public class MainActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView(a) {
        SelectorGroup singleGroup = new SelectorGroup();
        singleGroup.setChoiceMode(SelectorGroup.MODE_SINGLE_CHOICE);
        singleGroup.setStateListener(new SingleChoiceListener());
        ((Selector) findViewById(R.id.single10)).setGroup("single", singleGroup);
        ((Selector) findViewById(R.id.single20)).setGroup("single", singleGroup);
        ((Selector) findViewById(R.id.single30)).setGroup("single", singleGroup);
    }

    private class SingleChoiceListener implements SelectorGroup.StateListener {

        @Override
        public void onStateChange(String tag, boolean isSelected) {
            Toast.makeText(MainActivity.this, tag + " is selected", Toast.LENGTH_SHORT).show(); }}}Copy the code

More and more

In addition to being able to respond quickly to changing requirements,SelectorMore customizations can be made. The following image shows a three option one radio component with the options separated into two rows to form a triangle with a gradient selection effect.

  • In contrast, native controlsRadioButtonIt has the following limitations:
    1. Cannot customize button selected animation effect
    2. Cannot customize button relative layout.

RadioGroup inherits from LinearLayout, so radioButtons can only be lined up horizontally or vertically.

  • In this articleSelectorYou can do this very easily.

talk is cheap ,show me the code

Recommended reading

Write code so you can become good friends with product managers — strategy mode in practice