UI not only acts as a way for the users to interact with the product, it also serves as a tool to show the brand of a company or a solo entrepreneur.
In Android, we have tools to achieve that, like custom views. Instead of using the default views available in an Android project, you can customize everything to give your Android app a unique look and feel.
We want to make these custom views reusable. For example, you can make a library and use it in several apps, with the drawback that you need to be careful because any change to the library can affect every app in an unexpected way.
We are going to see how to do this, by customizing a Switch Slider button. You can check an introduction on how to adjust some settings of this button if you are interested in the following post.
How to build custom views
A custom view is basically composed of three parts.
- Kotlin or Java class where the view is inflated
- XML file that act as source of the view
- An attributes enumeration to define the properties of the custom view
For this example, we are going to inflate a ConstraintLayout
, which act as a container for the Switch and a TextView
. This means the class, our new custom view, needs to inherit from ConstraintLayout
.
Here is where the attributes enumeration comes into play. We are going to use it in the constructor of our class. We want to do this because this is going to allow us to configure the custom view from an XML file too. For example, our custom Switch is going to have these properties, which are defined in a file called attrs.xml
in the res/values
folder.
<declare-styleable name="CustomSwitch">
<attr name="switch_type" format="enum">
<enum name="vanilla" value="1" />
<enum name="small" value="2" />
<enum name="big" value="3" />
<enum name="pill" value="4" />
</attr>
<attr name="anim_text" format="boolean" />
<attr name="manual" format="boolean" />
<attr name="textVisible" format="boolean" />
<attr name="text" format="string" />
</declare-styleable>
XMLAnd now in the custom class, we need to override the constructors, and in those that come with an AttributeSet
variable, we are going to read the properties defined earlier like so.
class CustomSwitch : ConstraintLayout {
private lateinit var manager: CustomSwitchManager
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
readAttrs(attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
readAttrs(attrs)
}
private fun readAttrs(attrs: AttributeSet) {
with(context.theme.obtainStyledAttributes(attrs, R.styleable.CustomSwitch, 0, 0)) {
inflateView(
getSwitchTypeFromAttr(this),
getBoolean(R.styleable.CustomSwitch_anim_text, false),
getBoolean(R.styleable.CustomSwitch_manual, false),
getBoolean(R.styleable.CustomSwitch_textVisible, false),
getString(R.styleable.CustomSwitch_text).orEmpty()
)
recycle()
}
}
private fun getSwitchTypeFromAttr(attributes: TypedArray): CustomSwitchType =
when (attributes.getInt(R.styleable.CustomSwitch_switch_type, 1)) {
1 -> Vanilla
2 -> CustomThumbSmallSwitch
3 -> CustomThumbBigSwitch
else -> CustomTrackPillSwitch
}
}
KotlinNotice the call to recycle()
, that is creating a cache of these attributes to improve performance whenever another of our CustomView
object is inflated.
Now, in the XML file where the view is being used, we could use it like any other view.
<com.molidev8.customswitchsamples.view.customSwitch.CustomSwitch
android:id="@+id/myCustomSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:anim_text="true"
app:manual="true"
app:text="Bluetooth"
app:switch_type="pill"
app:textVisible="true" />
KotlinWith that, we can use the view programmatically in Kotlin, Java and through XMLs.
Applying the factory pattern to our custom views
In order to make these variations of our custom views reusable, we are going to apply the Factory Pattern. We are going to end up with the following classes.
CustomSwitchManager: this interface defines the methods that every CustomView
implementation is going to need. Our CustomSwitchFactory
is going to be the responsible to provide this manager.
interface CustomSwitchManager {
fun initView(
context: Context,
viewGroup: ViewGroup,
animText: Boolean,
manual: Boolean,
textVisible: Boolean,
text: String
)
fun setAnimText(shouldAnim: Boolean)
fun setManual(shouldBeManual: Boolean)
fun setTextVisible(shouldBeVisible: Boolean)
fun setText(text: String)
}
KotlinCustomSwitchType: this is a sealed class
to modelate the types of every CustomSwitch
implementation. In case we add another type, the compiler is going to launch an error if we don’t update with a new branch in a when
, so we are covered with runtime errors of this kind.
sealed class CustomSwitchType {
object Vanilla : CustomSwitchType()
object CustomThumbSmallSwitch : CustomSwitchType()
object CustomThumbBigSwitch : CustomSwitchType()
object CustomTrackPillSwitch : CustomSwitchType()
}
KotlinCustomSwitchFactory: returns whatever exact implementation of the CustomView
we ask for, using the type.
object CustomSwitchFactory {
fun build(type: CustomSwitchType): CustomSwitchManager = when (type) {
CustomSwitchType.CustomThumbBigSwitch -> CustomSwitchThumbBig()
CustomSwitchType.CustomThumbSmallSwitch -> CustomSwitchThumbSmall()
CustomSwitchType.CustomTrackPillSwitch -> CustomSwitchThumbPill()
CustomSwitchType.Vanilla -> CustomSwitchVanilla()
}
}
KotlinCustomSwitch: this is the class we talked about before that inherits from ConstraintLayout
. If you read the code carefully, you must have seen a function called inflateView
. This is where the CustomSwitchManager
is going to call to the implementation for each of the CustomSwitch
created, so that the ViewBinding is applied and the configuration of the view is done.
class CustomSwitch : ConstraintLayout {
private lateinit var manager: CustomSwitchManager
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
readAttrs(attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
readAttrs(attrs)
}
private fun readAttrs(attrs: AttributeSet) {
with(context.theme.obtainStyledAttributes(attrs, R.styleable.CustomSwitch, 0, 0)) {
inflateView(
getSwitchTypeFromAttr(this),
getBoolean(R.styleable.CustomSwitch_anim_text, false),
getBoolean(R.styleable.CustomSwitch_manual, false),
getBoolean(R.styleable.CustomSwitch_textVisible, false),
getString(R.styleable.CustomSwitch_text).orEmpty()
)
recycle()
}
}
private fun getSwitchTypeFromAttr(attributes: TypedArray): CustomSwitchType =
when (attributes.getInt(R.styleable.CustomSwitch_switch_type, 1)) {
1 -> Vanilla
2 -> CustomThumbSmallSwitch
3 -> CustomThumbBigSwitch
else -> CustomTrackPillSwitch
}
private fun inflateView(
type: CustomSwitchType,
animText: Boolean,
manual: Boolean,
textVisible: Boolean,
text: String
) {
manager = CustomSwitchFactory.build(type)
removeAllViews()
manager.initView(context, this, animText, manual, textVisible, text)
}
}
KotlinNow the only classes we have left are the different implementations of our CustomSwitchManager
. Here is an example of one of them.
class CustomSwitchThumbPill : CustomSwitchManager {
private lateinit var binding: CustomSwitchThumbPillBinding
override fun initView(
context: Context,
viewGroup: ViewGroup,
animText: Boolean,
manual: Boolean,
textVisible: Boolean,
text: String
) {
binding = CustomSwitchThumbPillBinding.inflate(LayoutInflater.from(context), viewGroup, true)
setText(text)
}
override fun setAnimText(shouldAnim: Boolean) {
// do something
}
override fun setManual(shouldBeManual: Boolean) {
// do something
}
override fun setTextVisible(shouldBeVisible: Boolean) {
// do something
}
override fun setText(text: String) {
binding.switchHint.text = text
}
}
KotlinThat’s all! Now we have custom views that can be configured in the XML files, with working Android Studio previews.
Here is the repo in case you want to check it out.
Featured image by Sigmund on Unsplash
If you want to read more content like this without ads and support me, don’t forget to check my profile, or give Medium a chance by becoming a member to access unlimited stories from me and other writers. It’s only $5 a month and if you use this link I get a small commission.