-
-
Save programmerr47/7fdd35517c9ae4a62125a4879fafdbd4 to your computer and use it in GitHub Desktop.
View that prevents children collision with scales
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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