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:
- Option button layout is extensible
- Option button styles are extensible
- The selected style is extensible
- The selection pattern is extensible
Extend the layout
The native radio button goes throughRadioButton
+ RadioGroup
Implementation, they must be parent-child in the layout, whileRadioGroup
Inherited 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.
- Radio options can be understood as: when a button is clicked, select the current one and deselect the previous one.
- 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 theSelector
The 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:
- 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
- Android custom controls | source there is treasure in the automatic line feed control
- Android performance optimization | to shorten the building layout is 20 times (below)
- Reading knowledge source long | dynamic extension class and bind the new way of the life cycle