为Android实施Apple Watch Board UI

时间:2018-10-14 22:02:16

标签: android android-layout apple-watch android-viewgroup

我需要实现自定义ViewGroup,它看起来像带气泡的Apple Watch主屏幕(下面是屏幕截图)

enter image description here

ViewGroup必须在两个方向上都可以滚动,并且其子项必须根据它们与中心的接近程度来更改其比例。 我尝试使用带有自定义RecyclerView的{​​{1}}来实现这一点,第一个元素位于中心,其他元素在中间。但是,当我尝试在滚动过程中动态添加/删除项目时,我坚持使用了它。 所以,我需要任何帮助。也许有人知道现有的解决方案或有一些线索。我将很高兴为您提供任何帮助!我还附加了自定义LayoutManager

的来源
LayoutManager

2 个答案:

答案 0 :(得分:1)

最后,我有了解决方案。在元素布局之前,我只定义了一个特殊模型的集合,其中包含有关其在屏幕上位置的信息。然后,在滚动过程中,我修改了集合,布局了现在在屏幕上的元素,并回收了不在屏幕上的项目。但是有一个缺点:我必须将项目大小传递给manager的构造函数,以正确填充子代。 itemSize应该与item 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
}

答案 1 :(得分:0)

您需要创建自己的自定义ViewGroup子类,该子类将处理所有项目的大小调整和滚动。

由于视图数量较少,因此无需回收视图。