为 Android 实施 Apple Watch 板 UI
Implementing of Apple watch board UI for Android
我需要实现一个自定义 ViewGroup
,它看起来像带有气泡的 Apple Watch 主屏幕(下面有一个屏幕截图)
ViewGroup
必须在两个方向上都可以滚动,并且它的子项必须根据它们离中心的距离来改变它们的比例。
我尝试使用 RecyclerView
和自定义 LayoutManager
来实现这一点,其中第一个元素位于中心,其他元素位于周围。但是当我尝试在滚动期间动态地实现 adding/removing 项目时,我坚持了下来。
所以,我需要任何帮助。也许有人知道现有的解决方案或有一些线索。我很乐意提供任何帮助!我还附上了我的自定义来源 LayoutManager
class AppleWatchLayoutManager : RecyclerView.LayoutManager() {
private val viewCache = SparseArray<View>()
override fun generateDefaultLayoutParams() = RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT)
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
detachAndScrapAttachedViews(recycler)
fill(recycler)
}
override fun canScrollVertically() = true
override fun canScrollHorizontally() = true
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
val delta = scrollVerticallyInternal(dy)
offsetChildrenVertical(-delta)
return delta
}
override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
val delta = scrollHorizontallyInternal(dx)
offsetChildrenHorizontal(-delta)
return delta
}
private fun fill(recycler: RecyclerView.Recycler) {
val anchorView = findAchorView()
viewCache.clear()
val childCount = childCount
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val position = getPosition(view)
viewCache.put(position, view)
}
}
var cacheSize = viewCache.size()
for (i in 0 until cacheSize) {
detachView(viewCache.valueAt(i))
}
fill(recycler, anchorView)
cacheSize = viewCache.size()
for (i in 0 until cacheSize) {
recycler.recycleView(viewCache.valueAt(i))
}
}
private fun fill(recycler: RecyclerView.Recycler, anchorView: View?) {
val anchorPosition = if (anchorView != null) getPosition(anchorView) else 0
val xOffset = if (anchorView != null) {
getDecoratedLeft(anchorView) + (getDecoratedMeasuredWidth(anchorView) / 2) - (width / 2)
} else {
0
}
val yOffset = if (anchorView != null) {
getDecoratedTop(anchorView) + (getDecoratedMeasuredHeight(anchorView) / 2) - (height / 2)
} else {
0
}
var filling = true
var round = 0
var position = anchorPosition
var scale = 0.9f
while (filling && position < itemCount) {
val sector = if (round == 0) 0.0 else 2 * PI / (6 * round)
var angle = 0.0
if (round == 0) {
filling = fillRound(recycler, round, position, angle, xOffset, yOffset, 1f)
position++
} else {
for (i in 1..(6 * round)) {
filling = filling && fillRound(recycler, round, position, angle, xOffset, yOffset, scale)
angle += sector
position++
}
}
round++
scale -= 0.1f
}
}
private fun scrollHorizontallyInternal(dx: Int): Int {
if (childCount == 0) {
return 0
}
val currentRound = getCurrentRound()
val roundsCount = getRoundsCount()
if (currentRound == roundsCount) {
val mostLeftChild = findMostLeftChild()
val mostRightChild = findMostRightChild()
if (mostLeftChild != null && mostRightChild != null) {
val viewSpan = getDecoratedRight(mostRightChild) - getDecoratedLeft(mostLeftChild)
if (viewSpan <= width) {
return 0
}
} else {
return 0
}
}
var delta = 0
if (dx < 0) {
val mostLeftChild = findMostLeftChild()
delta = if (mostLeftChild != null) {
Math.max(getDecoratedLeft(mostLeftChild), dx)
} else dx
} else if (dx > 0) {
val mostRightChild = findMostRightChild()
delta = if (mostRightChild != null) {
Math.min(getDecoratedRight(mostRightChild) - width, dx)
} else dx
}
return delta
}
private fun scrollVerticallyInternal(dy: Int): Int {
if (childCount == 0) {
return 0
}
// All views fit on screen
if (childCount == itemCount) {
val highestChild = findHighestChild()
val lowestChild = findLowestChild()
if (highestChild != null && lowestChild != null) {
val viewSpan = getDecoratedBottom(lowestChild) - getDecoratedTop(highestChild)
if (viewSpan <= height) {
return 0
}
} else {
return 0
}
}
var delta = 0
// content moves down
if (dy < 0) {
val highestChild = findHighestChild()
delta = if (highestChild != null) {
Math.max(getDecoratedTop(highestChild), dy)
} else dy
} else if (dy > 0) {
val lowestChild = findLowestChild()
delta = if (lowestChild != null) {
Math.min(getDecoratedBottom(lowestChild) - height, dy)
} else dy
}
return delta
}
private fun fillRound(recycler: RecyclerView.Recycler, round: Int, element: Int, angle: Double,
xOffset: Int, yOffset: Int, scale: Float): Boolean {
var view = viewCache[element]
if (view == null) {
view = recycler.getViewForPosition(element)
addView(view)
measureChildWithMargins(view, 0, 0)
val x = getDecoratedMeasuredWidth(view) * round * Math.cos(angle) + width / 2 + xOffset
val y = getDecoratedMeasuredHeight(view) * round * Math.sin(angle) + height / 2 + yOffset
val left = (x - getDecoratedMeasuredWidth(view) / 2).toInt()
val top = (y - getDecoratedMeasuredHeight(view) / 2).toInt()
val right = (x + getDecoratedMeasuredWidth(view) / 2).toInt()
val bottom = (y + getDecoratedMeasuredHeight(view) / 2).toInt()
layoutDecorated(view, left, top, right, bottom)
} else {
attachView(view)
viewCache.remove(element)
}
val decoratedBottom = getDecoratedBottom(view)
val decoratedTop = getDecoratedTop(view)
val decoratedLeft = getDecoratedLeft(view)
val decoratedRight = getDecoratedRight(view)
return (decoratedBottom <= height && decoratedTop >= 0) ||
(decoratedLeft >= 0 && decoratedRight <= width)
}
private fun getRoundsCount(): Int {
var itemCount = itemCount
var rounds = 0
var coeff = 1
while (itemCount > 0) {
rounds++
itemCount -= 6 * coeff
coeff++
}
return rounds
}
private fun getRoundByPosittion(position: Int): Int {
if (position == 0) {
return 0
}
if (position >= itemCount) {
throw IndexOutOfBoundsException("There's less items in RecyclerView than given position. Position is $position")
}
var elementsCount = 1
var round = 0
var coeff = 1
do {
round++
elementsCount += 6 * coeff
coeff++
} while (position > elementsCount)
return round
}
private fun getCurrentRound(): Int {
var childCount = childCount
if (childCount <= 1) {
return 0
} else if (childCount <= 7) {
return 1
}
childCount --
var round = 1
var coeff = 1
while (childCount > 0) {
childCount -= 6 * coeff
coeff++
round++
}
return round
}
private fun findHighestChild(): View? {
val childCount = childCount
if (childCount > 0) {
var highestView = getChildAt(0)
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val top = getDecoratedTop(view)
val highestViewTop = getDecoratedTop(highestView!!)
if (top < highestViewTop) {
highestView = view
}
}
}
return highestView
}
return null
}
private fun findLowestChild(): View? {
val childCount = childCount
if (childCount > 0) {
var lowestView = getChildAt(0)
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val bottom = getDecoratedBottom(view)
val lowestViewBottom = getDecoratedBottom(lowestView!!)
if (bottom > lowestViewBottom) {
lowestView = view
}
}
}
return lowestView
}
return null
}
private fun findMostLeftChild(): View? {
val childCount = childCount
if (childCount > 0) {
var mostLeftView = getChildAt(0)
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val left = getDecoratedLeft(view)
val mostLeftViewLeft = getDecoratedLeft(mostLeftView!!)
if (left < mostLeftViewLeft) {
mostLeftView = view
}
}
}
return mostLeftView
}
return null
}
private fun findMostRightChild(): View? {
val childCount = childCount
if (childCount > 0) {
var mostRightView = getChildAt(0)
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val right = getDecoratedRight(view)
val mostRightViewRight = getDecoratedRight(mostRightView!!)
if (right > mostRightViewRight) {
mostRightView = view
}
}
}
return mostRightView
}
return null
}
private fun findAchorView(): View? {
val childCount = childCount
val centerX = width / 2
val centerY = height / 2
var anchorView: View? = null
var minDistance = Int.MAX_VALUE
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val distance = distanceBetweenCenters(view, centerX, centerY)
if (distance < minDistance) {
minDistance = distance
anchorView = view
}
}
}
return anchorView
}
private fun distanceBetweenCenters(view: View, centerX: Int, centerY: Int): Int {
val viewCenterX = getDecoratedLeft(view) + getDecoratedMeasuredWidth(view) / 2
val viewCenterY = getDecoratedTop(view) + getDecoratedMeasuredHeight(view) / 2
return sqrt((centerX - viewCenterX) * (centerX - viewCenterX) * 1.0 + (centerY - viewCenterY) * (centerY - viewCenterY)).toInt()
}
}
您需要创建自己的自定义 ViewGroup 子类,它将处理所有项目的大小调整和滚动。
不需要回收视图,因为您的视图数量相对较少。
终于,我有了一个解决方案。在元素布局之前,我只是定义了一个特殊模型的集合,其中包含有关其在屏幕上的位置的信息。然后,在滚动期间我修改集合,布置现在在屏幕上的元素并回收不在屏幕上的项目。但是有一个缺点:我必须将项目大小传递给经理的构造函数以提供正确的子项填充。 itemSize
应与项目的 XML 中定义的相同。也许解决方案并不完美,但对我来说效果很好。这是 LayoutManager
.
的代码
class BubbleLayoutManager(private val itemSize: Int) : RecyclerView.LayoutManager() {
private val children = mutableListOf<Child>()
override fun generateDefaultLayoutParams() = RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT)
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
fillChildren()
detachAndScrapAttachedViews(recycler)
fillView(recycler)
}
override fun canScrollVertically() = true
override fun canScrollHorizontally() = true
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
val delta = scrollVerticallyInternal(dy)
offsetChildren(yOffset = -delta)
offsetChildrenVertical(-delta)
fillAndRecycle(recycler)
return dy
}
override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
val delta = scrollHorizontallyInternal(dx)
offsetChildren(xOffset = -delta)
offsetChildrenHorizontal(-delta)
fillAndRecycle(recycler)
return dx
}
private fun fillAndRecycle(recycler: RecyclerView.Recycler) {
val itemCount = itemCount
for (i in 0 until itemCount) {
if (i < children.size) {
val child = children[i]
val childRect = childRect(child)
val alreadyDrawn = alreadyDrawn(child)
if (!alreadyDrawn && fitOnScreen(childRect)) {
val view = recycler.getViewForPosition(i)
addView(view)
measureChildWithMargins(view, 0, 0)
layoutDecorated(view, childRect.left, childRect.top, childRect.right, childRect.bottom)
}
}
}
recycleViews(recycler)
updateScales()
}
private fun recycleViews(recycler: RecyclerView.Recycler) {
val childCount = childCount
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null && !fitOnScreen(view)) {
detachView(view)
recycler.recycleView(view)
}
}
}
private fun fillView(recycler: RecyclerView.Recycler) {
val itemCount = itemCount
for (i in 0 until itemCount) {
if (i < children.size) {
val childRect = childRect(children[i])
if (fitOnScreen(childRect)) {
val view = recycler.getViewForPosition(i)
addView(view)
measureChildWithMargins(view, 0, 0)
layoutDecorated(view, childRect.left, childRect.top, childRect.right, childRect.bottom)
}
}
}
updateScales()
}
private fun scrollVerticallyInternal(dy: Int): Int {
if (childCount == 0) {
return 0
}
val highestChild = children.minBy { it.y }
val lowestChild = children.maxBy { it.y }
if (highestChild != null && lowestChild != null) {
if (lowestChild.y + itemSize / 2 <= height && highestChild.y - itemSize / 2 >= 0) {
return 0
}
} else {
return 0
}
var delta = 0
if (dy < 0) {
delta = if (highestChild.y - itemSize / 2 < 0) {
max(highestChild.y - itemSize / 2, dy)
} else 0
} else if (dy > 0) {
delta = if (lowestChild.y + itemSize / 2 > height) {
min(lowestChild.y + itemSize / 2 - height, dy)
} else 0
}
return delta
}
private fun scrollHorizontallyInternal(dx: Int): Int {
if (childCount == 0) {
return 0
}
val mostLeftChild = children.minBy { it.x }
val mostRightChild = children.maxBy { it.x }
if (mostLeftChild != null && mostRightChild != null) {
if (mostLeftChild.x - itemSize / 2 >= 0 && mostRightChild.x + itemSize / 2 <= width) {
return 0
}
} else {
return 0
}
var delta = 0
if (dx < 0) {
delta = if (mostLeftChild.x - itemSize / 2 < 0) {
max(mostLeftChild.x - itemSize / 2, dx)
} else 0
} else if (dx > 0) {
delta = if (mostRightChild.x + itemSize / 2 > width) {
min(mostRightChild.x + itemSize / 2 - width, dx)
} else 0
}
return delta
}
private fun offsetChildren(xOffset: Int = 0, yOffset: Int = 0) {
children.forEach { it.offset(xOffset, yOffset) }
}
private fun updateScales() {
val centerX = width / 2
val centerY = height / 2
val distanceMap = sortedMapOf<Int, MutableList<Int>>()
val childCount = childCount
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val distance = distance(centerX, centerY, view.x.toInt() + view.width / 2, view.y.toInt() + view.height / 2)
val positions = distanceMap.getOrPut(distance) { mutableListOf() }
positions.add(i)
}
}
var scale = 1f
distanceMap.keys.forEach { key ->
val positions = distanceMap[key]
if (positions != null) {
for (position in positions) {
val view = getChildAt(position)
if (view != null) {
view.scaleX = scale
view.scaleY = scale
}
}
}
scale *= 0.95f
}
}
private fun distance(x1: Int, y1: Int, x2: Int, y2: Int) = sqrt(((x2 - x1) * (x2 - x1)).toFloat() + ((y2 - y1) * (y2 - y1)).toFloat()).toInt()
private fun childRect(child: Child): Rect {
val left = child.x - itemSize / 2
val top = child.y - itemSize / 2
val right = left + itemSize
val bottom = top + itemSize
return Rect(left, top, right, bottom)
}
private fun fillChildren() {
children.clear()
val centerX = width / 2
val centerY = height / 2
val itemCount = itemCount
if (itemCount > 0) {
children.add(Child(centerX, centerY))
if (itemCount > 1) {
for (i in 1 until itemCount) {
fillChildrenRelative(children[i - 1], itemCount)
}
}
}
}
private fun fillChildrenRelative(anchorChild: Child, itemCount: Int) {
var i = 0
var direction = Direction.initial()
while (i < 4 && children.size < itemCount) {
val childX = anchorChild.x + (itemSize / 2) * direction.widthMultiplier
val childY = anchorChild.y + itemSize * direction.heightMultiplier
if (!hasChild(childX, childY)) {
children.add(Child(childX, childY))
}
direction = direction.next()
i++
}
}
private fun hasChild(x: Int, y: Int) = children.any { it.x == x && it.y == y }
private fun fitOnScreen(view: View) = fitOnScreen(getViewRect(view))
private fun getViewRect(view: View) = Rect(
getDecoratedLeft(view),
getDecoratedTop(view),
getDecoratedRight(view),
getDecoratedBottom(view)
)
private fun fitOnScreen(rect: Rect): Boolean = rect.intersects(0, 0, width, height)
private fun alreadyDrawn(child: Child): Boolean {
val rect = childRect(child)
val childCount = childCount
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val viewRect = getViewRect(view)
if (viewRect.intersects(rect.left, rect.top, rect.right, rect.bottom)) {
return true
}
}
}
return false
}
private data class Child(
var x: Int,
var y: Int
) {
fun offset(xOffset: Int = 0, yOffset: Int = 0) {
x += xOffset
y += yOffset
}
}
}
// Direction.kt
internal sealed class Direction(
val widthMultiplier: Int, val heightMultiplier: Int
) {
companion object {
internal fun initial(): Direction = LeftTop
}
}
internal object LeftTop : Direction(-1, -1)
internal object RightTop : Direction(1, -1)
internal object LeftBottom : Direction(-1, 1)
internal object RightBottom : Direction(1, 1)
internal fun Direction.next() = when (this) {
is LeftTop -> RightTop
is RightTop -> LeftBottom
is LeftBottom -> RightBottom
is RightBottom -> LeftTop
}
我需要实现一个自定义 ViewGroup
,它看起来像带有气泡的 Apple Watch 主屏幕(下面有一个屏幕截图)
ViewGroup
必须在两个方向上都可以滚动,并且它的子项必须根据它们离中心的距离来改变它们的比例。
我尝试使用 RecyclerView
和自定义 LayoutManager
来实现这一点,其中第一个元素位于中心,其他元素位于周围。但是当我尝试在滚动期间动态地实现 adding/removing 项目时,我坚持了下来。
所以,我需要任何帮助。也许有人知道现有的解决方案或有一些线索。我很乐意提供任何帮助!我还附上了我的自定义来源 LayoutManager
class AppleWatchLayoutManager : RecyclerView.LayoutManager() {
private val viewCache = SparseArray<View>()
override fun generateDefaultLayoutParams() = RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT)
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
detachAndScrapAttachedViews(recycler)
fill(recycler)
}
override fun canScrollVertically() = true
override fun canScrollHorizontally() = true
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
val delta = scrollVerticallyInternal(dy)
offsetChildrenVertical(-delta)
return delta
}
override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
val delta = scrollHorizontallyInternal(dx)
offsetChildrenHorizontal(-delta)
return delta
}
private fun fill(recycler: RecyclerView.Recycler) {
val anchorView = findAchorView()
viewCache.clear()
val childCount = childCount
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val position = getPosition(view)
viewCache.put(position, view)
}
}
var cacheSize = viewCache.size()
for (i in 0 until cacheSize) {
detachView(viewCache.valueAt(i))
}
fill(recycler, anchorView)
cacheSize = viewCache.size()
for (i in 0 until cacheSize) {
recycler.recycleView(viewCache.valueAt(i))
}
}
private fun fill(recycler: RecyclerView.Recycler, anchorView: View?) {
val anchorPosition = if (anchorView != null) getPosition(anchorView) else 0
val xOffset = if (anchorView != null) {
getDecoratedLeft(anchorView) + (getDecoratedMeasuredWidth(anchorView) / 2) - (width / 2)
} else {
0
}
val yOffset = if (anchorView != null) {
getDecoratedTop(anchorView) + (getDecoratedMeasuredHeight(anchorView) / 2) - (height / 2)
} else {
0
}
var filling = true
var round = 0
var position = anchorPosition
var scale = 0.9f
while (filling && position < itemCount) {
val sector = if (round == 0) 0.0 else 2 * PI / (6 * round)
var angle = 0.0
if (round == 0) {
filling = fillRound(recycler, round, position, angle, xOffset, yOffset, 1f)
position++
} else {
for (i in 1..(6 * round)) {
filling = filling && fillRound(recycler, round, position, angle, xOffset, yOffset, scale)
angle += sector
position++
}
}
round++
scale -= 0.1f
}
}
private fun scrollHorizontallyInternal(dx: Int): Int {
if (childCount == 0) {
return 0
}
val currentRound = getCurrentRound()
val roundsCount = getRoundsCount()
if (currentRound == roundsCount) {
val mostLeftChild = findMostLeftChild()
val mostRightChild = findMostRightChild()
if (mostLeftChild != null && mostRightChild != null) {
val viewSpan = getDecoratedRight(mostRightChild) - getDecoratedLeft(mostLeftChild)
if (viewSpan <= width) {
return 0
}
} else {
return 0
}
}
var delta = 0
if (dx < 0) {
val mostLeftChild = findMostLeftChild()
delta = if (mostLeftChild != null) {
Math.max(getDecoratedLeft(mostLeftChild), dx)
} else dx
} else if (dx > 0) {
val mostRightChild = findMostRightChild()
delta = if (mostRightChild != null) {
Math.min(getDecoratedRight(mostRightChild) - width, dx)
} else dx
}
return delta
}
private fun scrollVerticallyInternal(dy: Int): Int {
if (childCount == 0) {
return 0
}
// All views fit on screen
if (childCount == itemCount) {
val highestChild = findHighestChild()
val lowestChild = findLowestChild()
if (highestChild != null && lowestChild != null) {
val viewSpan = getDecoratedBottom(lowestChild) - getDecoratedTop(highestChild)
if (viewSpan <= height) {
return 0
}
} else {
return 0
}
}
var delta = 0
// content moves down
if (dy < 0) {
val highestChild = findHighestChild()
delta = if (highestChild != null) {
Math.max(getDecoratedTop(highestChild), dy)
} else dy
} else if (dy > 0) {
val lowestChild = findLowestChild()
delta = if (lowestChild != null) {
Math.min(getDecoratedBottom(lowestChild) - height, dy)
} else dy
}
return delta
}
private fun fillRound(recycler: RecyclerView.Recycler, round: Int, element: Int, angle: Double,
xOffset: Int, yOffset: Int, scale: Float): Boolean {
var view = viewCache[element]
if (view == null) {
view = recycler.getViewForPosition(element)
addView(view)
measureChildWithMargins(view, 0, 0)
val x = getDecoratedMeasuredWidth(view) * round * Math.cos(angle) + width / 2 + xOffset
val y = getDecoratedMeasuredHeight(view) * round * Math.sin(angle) + height / 2 + yOffset
val left = (x - getDecoratedMeasuredWidth(view) / 2).toInt()
val top = (y - getDecoratedMeasuredHeight(view) / 2).toInt()
val right = (x + getDecoratedMeasuredWidth(view) / 2).toInt()
val bottom = (y + getDecoratedMeasuredHeight(view) / 2).toInt()
layoutDecorated(view, left, top, right, bottom)
} else {
attachView(view)
viewCache.remove(element)
}
val decoratedBottom = getDecoratedBottom(view)
val decoratedTop = getDecoratedTop(view)
val decoratedLeft = getDecoratedLeft(view)
val decoratedRight = getDecoratedRight(view)
return (decoratedBottom <= height && decoratedTop >= 0) ||
(decoratedLeft >= 0 && decoratedRight <= width)
}
private fun getRoundsCount(): Int {
var itemCount = itemCount
var rounds = 0
var coeff = 1
while (itemCount > 0) {
rounds++
itemCount -= 6 * coeff
coeff++
}
return rounds
}
private fun getRoundByPosittion(position: Int): Int {
if (position == 0) {
return 0
}
if (position >= itemCount) {
throw IndexOutOfBoundsException("There's less items in RecyclerView than given position. Position is $position")
}
var elementsCount = 1
var round = 0
var coeff = 1
do {
round++
elementsCount += 6 * coeff
coeff++
} while (position > elementsCount)
return round
}
private fun getCurrentRound(): Int {
var childCount = childCount
if (childCount <= 1) {
return 0
} else if (childCount <= 7) {
return 1
}
childCount --
var round = 1
var coeff = 1
while (childCount > 0) {
childCount -= 6 * coeff
coeff++
round++
}
return round
}
private fun findHighestChild(): View? {
val childCount = childCount
if (childCount > 0) {
var highestView = getChildAt(0)
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val top = getDecoratedTop(view)
val highestViewTop = getDecoratedTop(highestView!!)
if (top < highestViewTop) {
highestView = view
}
}
}
return highestView
}
return null
}
private fun findLowestChild(): View? {
val childCount = childCount
if (childCount > 0) {
var lowestView = getChildAt(0)
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val bottom = getDecoratedBottom(view)
val lowestViewBottom = getDecoratedBottom(lowestView!!)
if (bottom > lowestViewBottom) {
lowestView = view
}
}
}
return lowestView
}
return null
}
private fun findMostLeftChild(): View? {
val childCount = childCount
if (childCount > 0) {
var mostLeftView = getChildAt(0)
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val left = getDecoratedLeft(view)
val mostLeftViewLeft = getDecoratedLeft(mostLeftView!!)
if (left < mostLeftViewLeft) {
mostLeftView = view
}
}
}
return mostLeftView
}
return null
}
private fun findMostRightChild(): View? {
val childCount = childCount
if (childCount > 0) {
var mostRightView = getChildAt(0)
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val right = getDecoratedRight(view)
val mostRightViewRight = getDecoratedRight(mostRightView!!)
if (right > mostRightViewRight) {
mostRightView = view
}
}
}
return mostRightView
}
return null
}
private fun findAchorView(): View? {
val childCount = childCount
val centerX = width / 2
val centerY = height / 2
var anchorView: View? = null
var minDistance = Int.MAX_VALUE
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val distance = distanceBetweenCenters(view, centerX, centerY)
if (distance < minDistance) {
minDistance = distance
anchorView = view
}
}
}
return anchorView
}
private fun distanceBetweenCenters(view: View, centerX: Int, centerY: Int): Int {
val viewCenterX = getDecoratedLeft(view) + getDecoratedMeasuredWidth(view) / 2
val viewCenterY = getDecoratedTop(view) + getDecoratedMeasuredHeight(view) / 2
return sqrt((centerX - viewCenterX) * (centerX - viewCenterX) * 1.0 + (centerY - viewCenterY) * (centerY - viewCenterY)).toInt()
}
}
您需要创建自己的自定义 ViewGroup 子类,它将处理所有项目的大小调整和滚动。
不需要回收视图,因为您的视图数量相对较少。
终于,我有了一个解决方案。在元素布局之前,我只是定义了一个特殊模型的集合,其中包含有关其在屏幕上的位置的信息。然后,在滚动期间我修改集合,布置现在在屏幕上的元素并回收不在屏幕上的项目。但是有一个缺点:我必须将项目大小传递给经理的构造函数以提供正确的子项填充。 itemSize
应与项目的 XML 中定义的相同。也许解决方案并不完美,但对我来说效果很好。这是 LayoutManager
.
class BubbleLayoutManager(private val itemSize: Int) : RecyclerView.LayoutManager() {
private val children = mutableListOf<Child>()
override fun generateDefaultLayoutParams() = RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT)
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
fillChildren()
detachAndScrapAttachedViews(recycler)
fillView(recycler)
}
override fun canScrollVertically() = true
override fun canScrollHorizontally() = true
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
val delta = scrollVerticallyInternal(dy)
offsetChildren(yOffset = -delta)
offsetChildrenVertical(-delta)
fillAndRecycle(recycler)
return dy
}
override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
val delta = scrollHorizontallyInternal(dx)
offsetChildren(xOffset = -delta)
offsetChildrenHorizontal(-delta)
fillAndRecycle(recycler)
return dx
}
private fun fillAndRecycle(recycler: RecyclerView.Recycler) {
val itemCount = itemCount
for (i in 0 until itemCount) {
if (i < children.size) {
val child = children[i]
val childRect = childRect(child)
val alreadyDrawn = alreadyDrawn(child)
if (!alreadyDrawn && fitOnScreen(childRect)) {
val view = recycler.getViewForPosition(i)
addView(view)
measureChildWithMargins(view, 0, 0)
layoutDecorated(view, childRect.left, childRect.top, childRect.right, childRect.bottom)
}
}
}
recycleViews(recycler)
updateScales()
}
private fun recycleViews(recycler: RecyclerView.Recycler) {
val childCount = childCount
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null && !fitOnScreen(view)) {
detachView(view)
recycler.recycleView(view)
}
}
}
private fun fillView(recycler: RecyclerView.Recycler) {
val itemCount = itemCount
for (i in 0 until itemCount) {
if (i < children.size) {
val childRect = childRect(children[i])
if (fitOnScreen(childRect)) {
val view = recycler.getViewForPosition(i)
addView(view)
measureChildWithMargins(view, 0, 0)
layoutDecorated(view, childRect.left, childRect.top, childRect.right, childRect.bottom)
}
}
}
updateScales()
}
private fun scrollVerticallyInternal(dy: Int): Int {
if (childCount == 0) {
return 0
}
val highestChild = children.minBy { it.y }
val lowestChild = children.maxBy { it.y }
if (highestChild != null && lowestChild != null) {
if (lowestChild.y + itemSize / 2 <= height && highestChild.y - itemSize / 2 >= 0) {
return 0
}
} else {
return 0
}
var delta = 0
if (dy < 0) {
delta = if (highestChild.y - itemSize / 2 < 0) {
max(highestChild.y - itemSize / 2, dy)
} else 0
} else if (dy > 0) {
delta = if (lowestChild.y + itemSize / 2 > height) {
min(lowestChild.y + itemSize / 2 - height, dy)
} else 0
}
return delta
}
private fun scrollHorizontallyInternal(dx: Int): Int {
if (childCount == 0) {
return 0
}
val mostLeftChild = children.minBy { it.x }
val mostRightChild = children.maxBy { it.x }
if (mostLeftChild != null && mostRightChild != null) {
if (mostLeftChild.x - itemSize / 2 >= 0 && mostRightChild.x + itemSize / 2 <= width) {
return 0
}
} else {
return 0
}
var delta = 0
if (dx < 0) {
delta = if (mostLeftChild.x - itemSize / 2 < 0) {
max(mostLeftChild.x - itemSize / 2, dx)
} else 0
} else if (dx > 0) {
delta = if (mostRightChild.x + itemSize / 2 > width) {
min(mostRightChild.x + itemSize / 2 - width, dx)
} else 0
}
return delta
}
private fun offsetChildren(xOffset: Int = 0, yOffset: Int = 0) {
children.forEach { it.offset(xOffset, yOffset) }
}
private fun updateScales() {
val centerX = width / 2
val centerY = height / 2
val distanceMap = sortedMapOf<Int, MutableList<Int>>()
val childCount = childCount
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val distance = distance(centerX, centerY, view.x.toInt() + view.width / 2, view.y.toInt() + view.height / 2)
val positions = distanceMap.getOrPut(distance) { mutableListOf() }
positions.add(i)
}
}
var scale = 1f
distanceMap.keys.forEach { key ->
val positions = distanceMap[key]
if (positions != null) {
for (position in positions) {
val view = getChildAt(position)
if (view != null) {
view.scaleX = scale
view.scaleY = scale
}
}
}
scale *= 0.95f
}
}
private fun distance(x1: Int, y1: Int, x2: Int, y2: Int) = sqrt(((x2 - x1) * (x2 - x1)).toFloat() + ((y2 - y1) * (y2 - y1)).toFloat()).toInt()
private fun childRect(child: Child): Rect {
val left = child.x - itemSize / 2
val top = child.y - itemSize / 2
val right = left + itemSize
val bottom = top + itemSize
return Rect(left, top, right, bottom)
}
private fun fillChildren() {
children.clear()
val centerX = width / 2
val centerY = height / 2
val itemCount = itemCount
if (itemCount > 0) {
children.add(Child(centerX, centerY))
if (itemCount > 1) {
for (i in 1 until itemCount) {
fillChildrenRelative(children[i - 1], itemCount)
}
}
}
}
private fun fillChildrenRelative(anchorChild: Child, itemCount: Int) {
var i = 0
var direction = Direction.initial()
while (i < 4 && children.size < itemCount) {
val childX = anchorChild.x + (itemSize / 2) * direction.widthMultiplier
val childY = anchorChild.y + itemSize * direction.heightMultiplier
if (!hasChild(childX, childY)) {
children.add(Child(childX, childY))
}
direction = direction.next()
i++
}
}
private fun hasChild(x: Int, y: Int) = children.any { it.x == x && it.y == y }
private fun fitOnScreen(view: View) = fitOnScreen(getViewRect(view))
private fun getViewRect(view: View) = Rect(
getDecoratedLeft(view),
getDecoratedTop(view),
getDecoratedRight(view),
getDecoratedBottom(view)
)
private fun fitOnScreen(rect: Rect): Boolean = rect.intersects(0, 0, width, height)
private fun alreadyDrawn(child: Child): Boolean {
val rect = childRect(child)
val childCount = childCount
for (i in 0 until childCount) {
val view = getChildAt(i)
if (view != null) {
val viewRect = getViewRect(view)
if (viewRect.intersects(rect.left, rect.top, rect.right, rect.bottom)) {
return true
}
}
}
return false
}
private data class Child(
var x: Int,
var y: Int
) {
fun offset(xOffset: Int = 0, yOffset: Int = 0) {
x += xOffset
y += yOffset
}
}
}
// Direction.kt
internal sealed class Direction(
val widthMultiplier: Int, val heightMultiplier: Int
) {
companion object {
internal fun initial(): Direction = LeftTop
}
}
internal object LeftTop : Direction(-1, -1)
internal object RightTop : Direction(1, -1)
internal object LeftBottom : Direction(-1, 1)
internal object RightBottom : Direction(1, 1)
internal fun Direction.next() = when (this) {
is LeftTop -> RightTop
is RightTop -> LeftBottom
is LeftBottom -> RightBottom
is RightBottom -> LeftTop
}