cashapp / redwood

Multiplatform reactive UI for Android, iOS, and web using Kotlin and Jetpack Compose
https://cashapp.github.io/redwood/0.x/docs/
Apache License 2.0
1.65k stars 73 forks source link

Calculate size for individual items. This commented code currently hangs the UI ... #1254

Open github-actions[bot] opened 1 year ago

github-actions[bot] commented 1 year ago

val item = items.itemForGlobalIndex(sizeForItemAtIndexPath.item.toInt()) collectionView.frame().useContents { item.sizeThatFits(size.readValue()).useContents { return CGSizeMake(width, height) } }

https://github.com/cashapp/redwood/blob/a4bbf1c2c8e718becede7d49ab06acc9813b8f80/redwood-lazylayout-uiview/src/commonMain/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyList.kt#L170


import app.cash.redwood.lazylayout.widget.LazyList
import app.cash.redwood.lazylayout.widget.RefreshableLazyList
import app.cash.redwood.ui.Margin
import app.cash.redwood.widget.ChangeListener
import app.cash.redwood.widget.MutableListChildren
import app.cash.redwood.widget.Widget
import kotlin.math.max
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ObjCClass
import kotlinx.cinterop.readValue
import kotlinx.cinterop.useContents
import platform.CoreGraphics.CGRect
import platform.CoreGraphics.CGRectZero
import platform.CoreGraphics.CGSize
import platform.CoreGraphics.CGSizeMake
import platform.Foundation.NSIndexPath
import platform.Foundation.classForCoder
import platform.UIKit.UICollectionView
import platform.UIKit.UICollectionViewCell
import platform.UIKit.UICollectionViewDataSourceProtocol
import platform.UIKit.UICollectionViewDelegateFlowLayoutProtocol
import platform.UIKit.UICollectionViewFlowLayout
import platform.UIKit.UICollectionViewLayout
import platform.UIKit.UICollectionViewScrollDirection
import platform.UIKit.UIControlEventValueChanged
import platform.UIKit.UIEdgeInsetsMake
import platform.UIKit.UIRefreshControl
import platform.UIKit.UIScrollView
import platform.UIKit.UIView
import platform.UIKit.item
import platform.darwin.NSInteger
import platform.darwin.NSObject

internal interface ViewPortItems : Widget.Children<UIView> {
  var placeholders: MutableList<Widget<UIView>>
  var itemsBefore: Int
  var itemsAfter: Int
  fun itemForGlobalIndex(index: Int): UIView
  fun itemCount(): Int
}

internal open class UIViewLazyList() : LazyList<UIView>, ChangeListener {

  override val items: ViewPortItems = object : ViewPortItems {
    override var placeholders = mutableListOf<Widget<UIView>>()

    override var itemsBefore: Int = 0
    override var itemsAfter: Int = 0

    private val viewPortList = mutableListOf<Widget<UIView>>()

    override fun insert(index: Int, widget: Widget<UIView>) {
      viewPortList.add(index, widget)
    }

    override fun move(fromIndex: Int, toIndex: Int, count: Int) {
      viewPortList.move(fromIndex, toIndex, count)
    }

    override fun remove(index: Int, count: Int) {
      viewPortList.remove(index, count = count)
    }

    override fun onModifierUpdated() {}

    // Fetch the item from the viewPortList relative to the entire collection view
    override fun itemForGlobalIndex(index: Int): UIView {
      val viewPortIndex: Int = max(index - itemsBefore, 0)

      viewPortList.getOrNull(viewPortIndex)?.let {
        return it.value
      }

      // If we don't have a value, fallback to our pools of placeholders
      val placeholderIndex = index % placeholders.size
      return placeholders.get(placeholderIndex).value
    }

    override fun itemCount(): Int {
      // TODO: replace with this with "ItemCounts"
      return max(itemsBefore - 1, 0) + viewPortList.count() + itemsAfter
    }
  }

  override val placeholder: Widget.Children<UIView> = MutableListChildren(list = items.placeholders)

  private val viewPortListCoordinator = object {
    var minIndex: Int = 0
    var maxIndex: Int = 0

    fun notifyViewportChanged() {
      onViewportChanged(minIndex, maxIndex)
    }

    fun updateViewport(minIndex: Int, maxIndex: Int) {
      if (minIndex != this.minIndex || maxIndex != this.maxIndex) {
        this.minIndex = minIndex
        this.maxIndex = maxIndex

        notifyViewportChanged()
      }
    }
  }

  // A callback to tell LazyList that we have an update to the window of items available
  private lateinit var onViewportChanged: (firstVisibleItemIndex: Int, lastVisibleItemIndex: Int) -> Unit

  // UICollectionView + Protocols
  private var collectionViewFlowLayout: UICollectionViewFlowLayout =
    object : UICollectionViewFlowLayout() {}

  private val collectionViewDataSource: UICollectionViewDataSourceProtocol =
    object : NSObject(), UICollectionViewDataSourceProtocol {

      override fun collectionView(
        collectionView: UICollectionView,
        numberOfItemsInSection: NSInteger,
      ): NSInteger {
        return items.itemCount().toLong()
      }

      override fun collectionView(
        collectionView: UICollectionView,
        cellForItemAtIndexPath: NSIndexPath,
      ): UICollectionViewCell {
        val cell = collectionView.dequeueReusableCellWithReuseIdentifier(
          identifier = reuseIdentifier,
          forIndexPath = cellForItemAtIndexPath,
        ) as LazyListContainerCell

        val view = items.itemForGlobalIndex(cellForItemAtIndexPath.item.toInt())
        cell.set(view)

        return cell
      }
    }

  private val collectionViewDelegate: UICollectionViewDelegateFlowLayoutProtocol =
    object : NSObject(), UICollectionViewDelegateFlowLayoutProtocol {
      override fun collectionView(
        collectionView: UICollectionView,
        layout: UICollectionViewLayout,
        sizeForItemAtIndexPath: NSIndexPath,
      ): CValue<CGSize> {
        // TODO: Calculate size for individual items. This commented code currently hangs the UI Thread
//        val item = items.itemForGlobalIndex(sizeForItemAtIndexPath.item.toInt())
//        collectionView.frame().useContents {
//          item.sizeThatFits(size.readValue()).useContents {
//            return CGSizeMake(width, height)
//          }
//        }

        // TODO: Handle size for horizontal scrollDirection
        return CGSizeMake(collectionView.frame().useContents { size.width }, 64.0)
      }

      override fun scrollViewDidScroll(scrollView: UIScrollView) {
        val visibleIndexPaths = collectionView.indexPathsForVisibleItems()

        if (visibleIndexPaths.isNotEmpty()) {
          // TODO: Optimize this for less operations
          viewPortListCoordinator.updateViewport(
            visibleIndexPaths.minOf { (it as NSIndexPath).item.toInt() },
            visibleIndexPaths.maxOf { (it as NSIndexPath).item.toInt() },
          )
        }
      }
    }

  internal val collectionView = UICollectionView(
    frame = CGRectZero.readValue(),
    collectionViewLayout = this.collectionViewFlowLayout,
  ).apply {
    dataSource = collectionViewDataSource
    delegate = collectionViewDelegate
    prefetchingEnabled = true

    registerClass(
      LazyListContainerCell(CGRectZero.readValue()).classForCoder() as ObjCClass?,
      reuseIdentifier,
    )
  }

  // LazyList
  override fun isVertical(isVertical: Boolean) {
    if (!isVertical) {
      collectionViewFlowLayout.scrollDirection = if (isVertical) {
        UICollectionViewScrollDirection.UICollectionViewScrollDirectionVertical
      } else {
        UICollectionViewScrollDirection.UICollectionViewScrollDirectionHorizontal
      }
    }
  }
squarejesse commented 2 days ago

We should implement this by supporting ResizableWidget children in LazyColumn.