Skip to content

Commit

Permalink
Use material3 chips instead of a button group for the output format s…
Browse files Browse the repository at this point in the history
…elector

The material3 button group cannot handle situations where the buttons
overflow the screen's width. Instead, we'll use a chip group, which is
designed to handle multiline scenarios.

Fixes: #52
Signed-off-by: Andrew Gunnerson <[email protected]>
  • Loading branch information
chenxiaolong committed May 30, 2022
1 parent b5273b5 commit dcc87ae
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 33 deletions.
4 changes: 4 additions & 0 deletions app/magisk/updates/release/changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### Unreleased

* Change output format button group to material chips to prevent text from being cut off with narrower screen widths (Issue: #52, PR: #55, @chenxiaolong)

### Version 1.6

* Enable minification (without obfuscation) to shrink the download size by ~64% (PR: #45, @chenxiaolong)
Expand Down
140 changes: 140 additions & 0 deletions app/src/main/java/com/chiller3/bcr/ChipGroupCentered.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package com.chiller3.bcr

import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.core.view.ViewCompat
import com.google.android.material.chip.ChipGroup
import java.lang.Integer.max
import java.lang.Integer.min

/** Hacky wrapper around [ChipGroup] to make every row individually centered. */
class ChipGroupCentered(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
ChipGroup(context, attrs, defStyleAttr) {
private val _rowCountField = javaClass.superclass.superclass.getDeclaredField("rowCount")
private var rowCountField
get() = _rowCountField.getInt(this)
set(value) = _rowCountField.setInt(this, value)

init {
_rowCountField.isAccessible = true
}

constructor(context: Context, attrs: AttributeSet?) :
this(context, attrs, com.google.android.material.R.attr.chipGroupStyle)

constructor(context: Context) : this(context, null)

@SuppressLint("RestrictedApi")
override fun onLayout(sizeChanged: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
if (isSingleLine) {
return super.onLayout(sizeChanged, left, top, right, bottom)
}

val maxWidth = right - left - paddingRight - paddingLeft
var offsetTop = paddingTop
var rowStartIndex = 0

while (rowStartIndex < childCount) {
val (rowEndIndex, rowWidth, rowHeight) = getFittingRow(rowStartIndex, maxWidth)

layoutRow(
rowStartIndex..rowEndIndex,
paddingLeft + (maxWidth - rowWidth) / 2,
offsetTop,
rowCountField,
)

offsetTop += rowHeight + lineSpacing
rowStartIndex = rowEndIndex + 1
rowCountField += 1
}
}

/**
* Find the last index starting from [indexStart] that will fit in the row.
*
* @return (Index of last fitting element, width of row, height of row)
*/
@SuppressLint("RestrictedApi")
private fun getFittingRow(indexStart: Int, maxWidth: Int): Triple<Int, Int, Int> {
var indexEnd = indexStart
var childStart = 0
var rowHeight = 0

while (true) {
val child = getChildAt(indexEnd)
if (child.visibility == GONE) {
continue
}

val (marginStart, marginEnd) = getMargins(child)
val childWidth = marginStart + child.measuredWidth + marginEnd
val separator = if (indexEnd > indexStart) { itemSpacing } else { 0 }

// If even one child can't fit, force it to do so anyway
if (indexEnd != indexStart && childStart + separator + childWidth > maxWidth) {
--indexEnd
break
}

childStart += separator + childWidth
rowHeight = max(rowHeight, child.measuredHeight)

if (indexEnd == childCount - 1) {
break
} else {
++indexEnd
}
}

return Triple(indexEnd, min(childStart, maxWidth), rowHeight)
}

/**
* Lay out [childIndices] children in a row positioned at [offsetLeft] and [offsetTop].
*/
@SuppressLint("RestrictedApi")
private fun layoutRow(childIndices: IntRange, offsetLeft: Int, offsetTop: Int, rowIndex: Int) {
val range = if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
childIndices.reversed()
} else {
childIndices
}
var childStart = offsetLeft

for (i in range) {
val child = getChildAt(i)
if (child.visibility == GONE) {
child.setTag(com.google.android.material.R.id.row_index_key, -1)
continue
} else {
child.setTag(com.google.android.material.R.id.row_index_key, rowIndex)
}

val (marginStart, marginEnd) = getMargins(child)

child.layout(
childStart + marginStart,
offsetTop,
childStart + marginStart + child.measuredWidth,
offsetTop + child.measuredHeight,
)

childStart += marginStart + child.measuredWidth + marginEnd + itemSpacing
}
}

companion object {
private fun getMargins(view: View): Pair<Int, Int> {
val lp = view.layoutParams

return if (lp is MarginLayoutParams) {
Pair(lp.marginStart, lp.marginEnd)
} else {
Pair(0, 0)
}
}
}
}
46 changes: 20 additions & 26 deletions app/src/main/java/com/chiller3/bcr/FormatBottomSheetFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,24 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.chiller3.bcr.databinding.FormatBottomSheetBinding
import com.chiller3.bcr.databinding.FormatBottomSheetButtonBinding
import com.chiller3.bcr.databinding.FormatBottomSheetChipBinding
import com.chiller3.bcr.format.*
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.chip.ChipGroup
import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider

class FormatBottomSheetFragment : BottomSheetDialogFragment(),
MaterialButtonToggleGroup.OnButtonCheckedListener, LabelFormatter, Slider.OnChangeListener,
ChipGroup.OnCheckedStateChangeListener, LabelFormatter, Slider.OnChangeListener,
View.OnClickListener {
private var _binding: FormatBottomSheetBinding? = null
private val binding
get() = _binding!!

private val buttonIdToFormat = HashMap<Int, Format>()
private val formatToButtonId = HashMap<Format, Int>()
private val chipIdToFormat = HashMap<Int, Format>()
private val formatToChipId = HashMap<Format, Int>()
private lateinit var formatParamInfo: FormatParamInfo

override fun onCreateView(
Expand All @@ -42,17 +41,18 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
continue
}

val buttonBinding = FormatBottomSheetButtonBinding.inflate(
val chipBinding = FormatBottomSheetChipBinding.inflate(
inflater, binding.nameGroup, false)
val id = ViewCompat.generateViewId()
buttonBinding.root.id = id
buttonBinding.root.text = format.name
binding.nameGroup.addView(buttonBinding.root)
buttonIdToFormat[id] = format
formatToButtonId[format] = id
val id = View.generateViewId()
chipBinding.root.id = id
chipBinding.root.text = format.name
chipBinding.root.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
binding.nameGroup.addView(chipBinding.root)
chipIdToFormat[id] = format
formatToChipId[format] = id
}

binding.nameGroup.addOnButtonCheckedListener(this)
binding.nameGroup.setOnCheckedStateChangeListener(this)

refreshFormat()

Expand All @@ -67,11 +67,11 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
/**
* Update UI based on currently selected format in the preferences.
*
* Calls [refreshParam] via [onButtonChecked].
* Calls [refreshParam] via [onCheckedChanged].
*/
private fun refreshFormat() {
val (format, _) = Formats.fromPreferences(requireContext())
binding.nameGroup.check(formatToButtonId[format]!!)
binding.nameGroup.check(formatToChipId[format]!!)
}

/**
Expand Down Expand Up @@ -109,15 +109,9 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
}
}

override fun onButtonChecked(
group: MaterialButtonToggleGroup?,
checkedId: Int,
isChecked: Boolean
) {
if (isChecked) {
Preferences.setFormatName(requireContext(), buttonIdToFormat[checkedId]!!.name)
refreshParam()
}
override fun onCheckedChanged(group: ChipGroup, checkedIds: MutableList<Int>) {
Preferences.setFormatName(requireContext(), chipIdToFormat[checkedIds.first()]!!.name)
refreshParam()
}

override fun getFormattedValue(value: Float): String =
Expand All @@ -126,7 +120,7 @@ class FormatBottomSheetFragment : BottomSheetDialogFragment(),
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
when (slider) {
binding.paramSlider -> {
val format = buttonIdToFormat[binding.nameGroup.checkedButtonId]!!
val format = chipIdToFormat[binding.nameGroup.checkedChipId]!!
Preferences.setFormatParam(requireContext(), format.name, value.toUInt())
}
}
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/res/layout/format_bottom_sheet.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
android:text="@string/bottom_sheet_output_format"
android:textAppearance="?attr/textAppearanceHeadline6" />

<com.google.android.material.button.MaterialButtonToggleGroup
<com.chiller3.bcr.ChipGroupCentered
android:id="@+id/name_group"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:selectionRequired="true"
app:singleSelection="true" />
Expand Down
5 changes: 0 additions & 5 deletions app/src/main/res/layout/format_bottom_sheet_button.xml

This file was deleted.

5 changes: 5 additions & 0 deletions app/src/main/res/layout/format_bottom_sheet_chip.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

0 comments on commit dcc87ae

Please sign in to comment.