Skip to content

Instantly share code, notes, and snippets.

@akexorcist
Last active October 26, 2021 18:05
Show Gist options
  • Select an option

  • Save akexorcist/6b79e98093731b8294132004cd7a6b5e to your computer and use it in GitHub Desktop.

Select an option

Save akexorcist/6b79e98093731b8294132004cd7a6b5e to your computer and use it in GitHub Desktop.
Material Button Group for Material Button in Android
<androidx.constraintlayout.widget.ConstraintLayout ... >
<com.akexorcist.materialdesign.MaterialButtonGroup
android:id="@+id/toggleButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/button1"
style="@style/OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 1" />
<Button
android:id="@+id/button2"
style="@style/OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 2" />
<Button
android:id="@+id/button3"
style="@style/OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 3" />
</com.akexorcist.materialdesign.MaterialButtonGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
package com.akexorcist.materialdesign
import android.content.Context
import androidx.core.view.MarginLayoutParamsCompat
import androidx.core.view.ViewCompat
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.widget.LinearLayout
import com.google.android.material.shape.AbsoluteCornerSize
import com.google.android.material.shape.CornerSize
import com.google.android.material.shape.ShapeAppearanceModel
import com.google.android.material.button.MaterialButton
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.AccessibilityDelegateCompat
import android.view.ViewGroup
import java.util.*
class MaterialButtonGroup : LinearLayout {
constructor(context: Context?) : super(context, null)
constructor(context: Context?, attrs: AttributeSet?) : super(
context,
attrs,
R.attr.materialButtonToggleGroupStyle
)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
private val originalCornerData: ArrayList<CornerData> = ArrayList()
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
if (child !is MaterialButton) {
Log.e("MaterialButtonGroup", "Child views must be of type MaterialButton.")
return
}
super.addView(child, index, params)
setGeneratedIdIfNeeded(child)
setChildButton(child)
// Saves original corner data
val shapeAppearanceModel = child.shapeAppearanceModel
originalCornerData.add(
CornerData(
shapeAppearanceModel.topLeftCornerSize,
shapeAppearanceModel.bottomLeftCornerSize,
shapeAppearanceModel.topRightCornerSize,
shapeAppearanceModel.bottomRightCornerSize
)
)
ViewCompat.setAccessibilityDelegate(child, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.setCollectionItemInfo(
CollectionItemInfoCompat.obtain(
0,
1,
getIndexWithinVisibleButtons(host),
1,
false,
(host as MaterialButton).isSelected
)
)
}
})
}
override fun onViewRemoved(child: View?) {
super.onViewRemoved(child)
val indexOfChild = indexOfChild(child)
if (indexOfChild >= 0) {
originalCornerData.removeAt(indexOfChild)
}
updateChildShapes()
adjustChildMarginsAndUpdateLayout()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
updateChildShapes()
adjustChildMarginsAndUpdateLayout()
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
private fun adjustChildMarginsAndUpdateLayout() {
val firstVisibleChildIndex = getFirstVisibleChildIndex()
if (firstVisibleChildIndex == -1) {
return
}
for (i in firstVisibleChildIndex + 1 until childCount) {
val currentButton = getChildButton(i)
val previousButton = getChildButton(i - 1)
val smallestStrokeWidth = currentButton.strokeWidth.coerceAtMost(previousButton.strokeWidth)
val params: LayoutParams = buildLayoutParams(currentButton)
if (orientation == HORIZONTAL) {
MarginLayoutParamsCompat.setMarginEnd(params, 0)
MarginLayoutParamsCompat.setMarginStart(params, -smallestStrokeWidth)
params.topMargin = 0
} else {
params.bottomMargin = 0
params.topMargin = -smallestStrokeWidth
MarginLayoutParamsCompat.setMarginStart(params, 0)
}
currentButton.layoutParams = params
}
resetChildMargins(firstVisibleChildIndex)
}
private fun getChildButton(index: Int): MaterialButton {
return getChildAt(index) as MaterialButton
}
private fun resetChildMargins(childIndex: Int) {
if (childCount == 0 || childIndex == -1) {
return
}
val currentButton = getChildButton(childIndex)
val params = currentButton.layoutParams as LayoutParams
if (orientation == VERTICAL) {
params.topMargin = 0
params.bottomMargin = 0
return
}
MarginLayoutParamsCompat.setMarginEnd(params, 0)
MarginLayoutParamsCompat.setMarginStart(params, 0)
params.leftMargin = 0
params.rightMargin = 0
}
private fun updateChildShapes() {
val childCount = childCount
val firstVisibleChildIndex: Int = getFirstVisibleChildIndex()
val lastVisibleChildIndex: Int = getLastVisibleChildIndex()
for (i in 0 until childCount) {
val button: MaterialButton = getChildButton(i)
if (button.visibility == GONE) {
continue
}
val builder = button.shapeAppearanceModel.toBuilder()
val newCornerData = getNewCornerData(i, firstVisibleChildIndex, lastVisibleChildIndex)
updateBuilderWithCornerData(builder, newCornerData)
button.shapeAppearanceModel = builder.build()
}
}
private fun getFirstVisibleChildIndex(): Int {
val childCount = childCount
for (i in 0 until childCount) {
if (isChildVisible(i)) {
return i
}
}
return -1
}
private fun getLastVisibleChildIndex(): Int {
val childCount = childCount
for (i in childCount - 1 downTo 0) {
if (isChildVisible(i)) {
return i
}
}
return -1
}
private fun isChildVisible(i: Int): Boolean {
val child = getChildAt(i)
return child.visibility != GONE
}
private fun getVisibleButtonCount(): Int {
var count = 0
for (i in 0 until childCount) {
if (getChildAt(i) is MaterialButton && isChildVisible(i)) {
count++
}
}
return count
}
private fun getIndexWithinVisibleButtons(child: View?): Int {
if (child !is MaterialButton) {
return -1
}
var index = 0
for (i in 0 until childCount) {
if (getChildAt(i) === child) {
return index
}
if (getChildAt(i) is MaterialButton && isChildVisible(i)) {
index++
}
}
return -1
}
private fun setGeneratedIdIfNeeded(materialButton: MaterialButton) {
if (materialButton.id == NO_ID) {
materialButton.id = ViewCompat.generateViewId()
}
}
private fun setChildButton(materialButton: MaterialButton) {
materialButton.insetTop = 0
materialButton.insetBottom = 0
}
private fun buildLayoutParams(child: View): LayoutParams {
val layoutParams = child.layoutParams
return if (layoutParams is LayoutParams) layoutParams
else LayoutParams(layoutParams.width, layoutParams.height)
}
private fun getNewCornerData(
index: Int,
firstVisibleChildIndex: Int,
lastVisibleChildIndex: Int
): CornerData? {
val cornerData: CornerData = originalCornerData[index]
if (firstVisibleChildIndex == lastVisibleChildIndex) {
return cornerData
}
val isHorizontal = orientation == HORIZONTAL
if (index == firstVisibleChildIndex) {
return if (isHorizontal) CornerData.start(cornerData, this)
else CornerData.top(cornerData)
}
return if (index == lastVisibleChildIndex) {
if (isHorizontal) CornerData.end(cornerData, this)
else CornerData.bottom(cornerData)
} else null
}
private fun updateBuilderWithCornerData(
shapeAppearanceModelBuilder: ShapeAppearanceModel.Builder,
cornerData: CornerData?
) {
if (cornerData == null) {
shapeAppearanceModelBuilder.setAllCornerSizes(0f)
return
}
shapeAppearanceModelBuilder
.setTopLeftCornerSize(cornerData.topLeft)
.setBottomLeftCornerSize(cornerData.bottomLeft)
.setTopRightCornerSize(cornerData.topRight)
.setBottomRightCornerSize(cornerData.bottomRight)
}
}
class CornerData(
var topLeft: CornerSize,
var bottomLeft: CornerSize,
var topRight: CornerSize,
var bottomRight: CornerSize
) {
companion object {
private val noCorner: CornerSize = AbsoluteCornerSize(0f)
fun start(orig: CornerData, view: View): CornerData {
return if (view.isLayoutRtl()) right(orig) else left(orig)
}
fun end(orig: CornerData, view: View): CornerData {
return if (view.isLayoutRtl()) left(orig) else right(orig)
}
fun left(orig: CornerData): CornerData {
return CornerData(orig.topLeft, orig.bottomLeft, noCorner, noCorner)
}
fun right(orig: CornerData): CornerData {
return CornerData(noCorner, noCorner, orig.topRight, orig.bottomRight)
}
fun top(orig: CornerData): CornerData {
return CornerData(orig.topLeft, noCorner, orig.topRight, noCorner)
}
fun bottom(orig: CornerData): CornerData {
return CornerData(noCorner, orig.bottomLeft, noCorner, orig.bottomRight)
}
}
}
fun View.isLayoutRtl(): Boolean {
return ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="OutlinedButton" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="strokeColor">#DDDDDD</item>
</style>
</resources>
@akexorcist
Copy link
Copy Markdown
Author

akexorcist commented Sep 10, 2021

Because Material Component for Android has MaterialToggleButtonGroup for toggle button only. Sometime we just want the button group for non-toggle outlined button. So I created the MaterialButtonGroup which copy the code from MaterialToggleButtonGroup but exclude the toggle button logic.

001

Using MaterialButtonGroup, additional custom attribute is required.

  • strokeColor - There're some limitation about overlap transparent border color because these lines of code is for internal use only. To prevent the color with alpha overlapping in button's background stroke, use no-alpha color (e.g, #DDDDDD).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment