This is the first in a series of articles.
- Android custom controls | high extensible radio button (never quarreled with the product manager)
- 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:
- The radio set
RadioGroup
- The radio button
RadioButton
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 oneView
Is 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:
- It’s a View, and it’s clickable
- 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 function
Selector
Subclass implementation. - To achieve the selected gradient,
Selector
Provide 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,Selector
More 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 controls
RadioButton
It has the following limitations:- Cannot customize button selected animation effect
- Cannot customize button relative layout.
RadioGroup inherits from LinearLayout, so radioButtons can only be lined up horizontally or vertically.
- In this article
Selector
You 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