Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
View that prevents children collision with scales
open class CollisionView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : ViewGroup(context, attrs, defStyle) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var totalHeight = 0
var maxWidth = 0
var childState = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
val childLp = child.layoutParams as MarginLayoutParams
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
totalHeight += getChildTotalHeight(child, childLp)
maxWidth = max(maxWidth, child.measuredWidth + childLp.leftMargin + childLp.rightMargin)
childState = View.combineMeasuredStates(childState, child.measuredState)
}
maxWidth = max(maxWidth + paddingStart + paddingEnd, suggestedMinimumWidth)
totalHeight = max(totalHeight + paddingTop + paddingBottom, suggestedMinimumHeight)
setMeasuredDimension(
View.resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
View.resolveSizeAndState(totalHeight, heightMeasureSpec, childState)
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if (childCount > 0) {
val fixedL = l + paddingLeft
val fixedT = t + paddingTop
val fixedB = b - paddingBottom
val parentHeight = fixedB - fixedT
val children = Array<View>(childCount) { getChildAt(it) }
val freeHeight = parentHeight - children.sumBy { getChildTotalHeight(it) }
val scales = createScales(children)
val totalScale = scales.sum()
val normalizedScales = if (totalScale <= 0) scales.map { 1f } else scales.map { it / totalScale }
val weights = createWeights(children, normalizedScales)
val buckets = createBuckets(weights, normalizedScales)
val distanceDistribution = distributeFreeHeight(buckets, freeHeight)
val distances = buildDistanceArray(distanceDistribution, buckets)
var currentT = fixedT
children.forEachIndexed { i, view ->
val lp = view.layoutParams as MarginLayoutParams
currentT += lp.topMargin
view.layout(fixedL + lp.leftMargin, currentT)
currentT += view.measuredHeight + lp.bottomMargin
if (i < children.lastIndex) currentT += distances[i]
}
}
}
private fun createScales(children: Array<View>) = FloatArray(children.size - 1) {
if (it == 0) getChildWeight(children[it]) + getChildWeight(children[it + 1]) / 2
else if (it == children.lastIndex - 1) getChildWeight(children[it]) / 2 + getChildWeight(children[it + 1])
else (getChildWeight(children[it]) + getChildWeight(children[it + 1])) / 2
}
private fun createWeights(children: Array<View>, scales: List<Float>) = FloatArray(children.size - 1) {
createWeight(children, it) / scales[it]
}
private fun createWeight(children: Array<View>, pos: Int): Int {
val childH = children[pos].measuredHeight
val nextChildH = children[pos + 1].measuredHeight
return when (pos) {
0 -> childH + nextChildH / 2
children.lastIndex - 1 -> childH / 2 + nextChildH
else -> (childH + nextChildH) / 2
}
}
private fun createBuckets(weights: FloatArray, scales: List<Float>): List<Bucket> {
val unsortedBuckets = weights.mapIndexed { i, item -> Bucket(item, scales[i], i) }
return unsortedBuckets.sortedBy { it.filled }
}
private fun distributeFreeHeight(buckets: List<Bucket>, amount: Int): IntArray {
val tempDistribution = IntArray(buckets.size)
val distribution = IntArray(buckets.size)
var remained: Int = amount
var i = 0
while (remained > 0) {
while (i < buckets.lastIndex && buckets[i].filled == buckets[i + 1].filled)
i++
if (i < buckets.lastIndex && !buckets[i + 1].filled.isInfinite()) {
val neededPerItem = buckets[i + 1].filled - buckets[i].filled
(0 until (i + 1)).forEach {
tempDistribution[it] = (neededPerItem * buckets[it].scale).toInt()
}
val needed = tempDistribution.sum(i + 1)
if (remained < needed) {
splitRemained(distribution, tempDistribution, buckets, i + 1, remained)
break
} else {
remained -= needed
(0 until (i + 1)).forEach {
distribution[it] += tempDistribution[it]
}
}
} else {
splitRemained(distribution, tempDistribution, buckets, i + 1, remained)
break
}
i++
}
return distribution
}
private fun splitRemained(distribution: IntArray, temp: IntArray, buckets: List<Bucket>, size: Int, amount: Int) {
val subTotalWeight = buckets.totalWeight(size)
val commonAmount = amount / subTotalWeight
(0 until size).forEach {
temp[it] = (commonAmount * buckets[it].scale).toInt()
}
val remainAmount = amount - temp.sum(size)
(0 until size).forEach {
distribution[it] += temp[it] + (remainAmount * buckets[it].scale).roundToInt()
}
}
private fun buildDistanceArray(distribution: IntArray, buckets: List<Bucket>): IntArray {
val distances = IntArray(distribution.size)
for (i in 0 until buckets.size) {
distances[buckets[i].position] = distribution[i]
}
return distances
}
private fun View.layout(x: Int, y: Int) =
layout(x, y, x + measuredWidth, y + measuredHeight)
private fun getChildTotalHeight(child: View, params: MarginLayoutParams = child.layoutParams as MarginLayoutParams) =
child.measuredHeight + params.topMargin + params.bottomMargin
private fun getChildWeight(child: View) = (child.layoutParams as LinearLayout.LayoutParams).weight
override fun generateDefaultLayoutParams() = LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
override fun generateLayoutParams(attrs: AttributeSet) = LinearLayout.LayoutParams(context, attrs)
override fun generateLayoutParams(p: LayoutParams) = LinearLayout.LayoutParams(p)
override fun checkLayoutParams(p: LayoutParams) = p is LinearLayout.LayoutParams
private fun IntArray.sum(size: Int): Int {
var sum = 0
forEachIndexed { i, el ->
if (i < size) {
sum += el
} else {
return sum
}
}
return sum
}
private fun Iterable<Bucket>.totalWeight(size: Int): Float {
var sum = 0.0f
forEachIndexed { i, el ->
if (i < size) {
sum += el.scale
} else {
return sum
}
}
return sum
}
private class Bucket(
val filled: Float,
val scale: Float,
val position: Int
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment