【问题标题】:Implementing of Apple watch board UI for Android为 Android 实现 Apple Watch Board UI
【发布时间】:2018-10-14 22:02:16
【问题描述】:

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

ViewGroup 必须在两个方向上都可以滚动,并且它的子元素必须根据它们与中心的距离来改变它们的比例。 我尝试使用RecyclerView 和自定义LayoutManager 来实现这一点,其中第一个元素位于中心,其他元素在周围。但是当我尝试在滚动期间动态添加/删除项目时,我坚持了下来。 所以,我需要任何帮助。也许有人知道现有的解决方案或有一些线索。我很乐意为您提供任何帮助!我还附上了我的自定义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()
    }
}

【问题讨论】:

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


    【解决方案1】:

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

    【讨论】:

      【解决方案2】:

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

      无需回收视图,因为您的视图数量相对较少。

      【讨论】:

      • 就我而言,我必须提供至少 1000 个项目的流畅工作(我需要在此表示中显示用户列表)。所以我认为我需要回收视图的机制,这就是我选择RecyclerView的原因
      猜你喜欢
      • 2019-12-11
      • 2020-07-19
      • 1970-01-01
      • 2015-11-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多