mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Implement VirtualViewContainerStateExperimental (interval tree, updateModes optimization) (#54213)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/54213 ## Changelog: [Internal] Modifies `VirtualViewContainerStateExperimental` to use an interval tree to store the container's VirtualViews. Interval tree's query allows us to lower the frequently-used updateModes() operation from `O(n)` to `O(m + log n)`, where `m` is the number of VirtualViews in the hysteresis range. General layout changes incur a penalty due to the requirement of maintaining a balanced BST. Performance Changes: * updateModes(): `O(n) -> O(m + log n)` (new algo) * updateMode(): `O(1) -> O(1)` * add VV: `O(1) -> O(log n)` * update VV: `O(1) -> O(log n)` * delete VV: `O(1) -> O(log n)` We expect scrolling to be much more frequent than a total layout change/updating many of the elements in the container at once. However, the initial load of a page will take `O(n log n)` time rather than `O(n)` time. We also include some bookkeeping sets for "old" hysteresis, prerender, and visible VVs. This will increase the memory footprint of `VirtualViewContainerState` by `O(m)`. More details are in https://docs.google.com/document/d/1Cmamx6fNfruoHLApX8tedqNVZsCEUyoBvQWpMvBsfcY/edit?tab=t.gqla6vu3231k. ### Note This diff only covers the Android version. iOS may follow after experimentation. Reviewed By: lunaleaps Differential Revision: D83783201 fbshipit-source-id: 93a7a93784d668e09d8ddacc7e30d289ccd8b7a2
This commit is contained in:
committed by
meta-codesync[bot]
parent
793f99d949
commit
328d07a30d
+39
@@ -111,6 +111,45 @@ internal abstract class VirtualViewContainerState {
|
||||
updateModes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the coordinates for the Rects this class cares about (visibleRect, prerenderRect,
|
||||
* hysteresisRect)
|
||||
*/
|
||||
protected fun updateRects() {
|
||||
scrollView.getDrawingRect(visibleRect)
|
||||
|
||||
// This happens because ScrollView content isn't ready yet. The danger here is if ScrollView
|
||||
// intentionally goes but curently ScrollView and v1 Fling use this check to determine if
|
||||
// "content ready"
|
||||
if (visibleRect.isEmpty()) {
|
||||
debugLog("updateRects", { "scrollView visibleRect is empty" })
|
||||
// should set the other rects here in case scrollview is suddenly empty after the other rects
|
||||
// are non-empty
|
||||
prerenderRect.set(visibleRect)
|
||||
hysteresisRect.set(prerenderRect)
|
||||
return
|
||||
}
|
||||
|
||||
prerenderRect.set(visibleRect)
|
||||
prerenderRect.inset(
|
||||
(-prerenderRect.width() * prerenderRatio).toInt(),
|
||||
(-prerenderRect.height() * prerenderRatio).toInt(),
|
||||
)
|
||||
|
||||
hysteresisRect.set(prerenderRect)
|
||||
hysteresisRect.inset(
|
||||
(-visibleRect.width() * hysteresisRatio).toInt(),
|
||||
(-visibleRect.height() * hysteresisRatio).toInt(),
|
||||
)
|
||||
|
||||
debugLog(
|
||||
"updateRects",
|
||||
{
|
||||
"visibleRect ${visibleRect.toString()} prerenderRect ${prerenderRect.toString()} hysteresisRect ${hysteresisRect.toString()}"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
protected abstract fun updateModes(virtualView: VirtualView? = null)
|
||||
}
|
||||
|
||||
|
||||
+482
-59
@@ -7,84 +7,507 @@
|
||||
|
||||
package com.facebook.react.views.scroll
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.ViewGroup
|
||||
import com.facebook.common.logging.FLog
|
||||
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
|
||||
import com.facebook.react.views.virtual.VirtualViewMode
|
||||
|
||||
/**
|
||||
* A class that implements VirtualViewContainerState with a more efficient updateModes() algorithm.
|
||||
* This has been filled out with the Classic implementation for now to support the new Factory
|
||||
* creation method.
|
||||
*/
|
||||
internal class VirtualViewContainerStateExperimental(scrollView: ViewGroup) :
|
||||
VirtualViewContainerState(scrollView) {
|
||||
|
||||
// Provide the concrete implementation for the abstract virtualViews property
|
||||
override val virtualViews: MutableCollection<VirtualView> = mutableSetOf()
|
||||
private val horizontal: Boolean =
|
||||
when (scrollView) {
|
||||
is ReactScrollView -> false
|
||||
is ReactHorizontalScrollView -> true
|
||||
else -> false // default
|
||||
}
|
||||
override val virtualViews = IntervalTree(horizontal)
|
||||
|
||||
// set of all VirtualViews that are in hysteresis, prerender, or visible range
|
||||
var HPV: MutableSet<String> = mutableSetOf()
|
||||
// set of all VirtualViews that are in prerender (not in viewport)
|
||||
var P: MutableSet<String> = mutableSetOf()
|
||||
// set of all VirtualViews that are in viewport
|
||||
var V: MutableSet<String> = mutableSetOf()
|
||||
|
||||
override fun onChange(virtualView: VirtualView) {
|
||||
if (virtualViews.add(virtualView)) {
|
||||
debugLog("add", { "virtualViewID=${virtualView.virtualViewID}" })
|
||||
} else {
|
||||
debugLog("update", { "virtualViewID=${virtualView.virtualViewID}" })
|
||||
}
|
||||
updateModes(virtualView)
|
||||
}
|
||||
|
||||
// Implement the abstract updateModes method
|
||||
override fun updateModes(virtualView: VirtualView?) {
|
||||
scrollView.getDrawingRect(visibleRect)
|
||||
updateRects()
|
||||
if (virtualView != null) {
|
||||
updateMode(virtualView)
|
||||
} else {
|
||||
updateModesAll()
|
||||
}
|
||||
}
|
||||
|
||||
if (visibleRect.isEmpty()) {
|
||||
debugLog("updateModes", { "scrollView visibleRect is empty" })
|
||||
/**
|
||||
* Perform mode update check on a single VirtualView. Does not check other VirtualViews in the
|
||||
* collection. Use carefully
|
||||
*/
|
||||
private fun updateMode(virtualView: VirtualView) {
|
||||
val rect = virtualView.containerRelativeRect
|
||||
|
||||
var mode: VirtualViewMode? = VirtualViewMode.Hidden
|
||||
var thresholdRect = emptyRect
|
||||
when {
|
||||
rectsOverlap(rect, visibleRect) -> {
|
||||
thresholdRect = visibleRect
|
||||
if (onWindowFocusChangeListener != null) {
|
||||
if (scrollView.hasWindowFocus()) {
|
||||
mode = VirtualViewMode.Visible
|
||||
} else {
|
||||
mode = VirtualViewMode.Prerender
|
||||
}
|
||||
} else {
|
||||
mode = VirtualViewMode.Visible
|
||||
}
|
||||
}
|
||||
rectsOverlap(rect, prerenderRect) -> {
|
||||
mode = VirtualViewMode.Prerender
|
||||
thresholdRect = prerenderRect
|
||||
}
|
||||
(hysteresisRatio > 0.0 && rectsOverlap(rect, hysteresisRect)) -> {
|
||||
mode = null
|
||||
}
|
||||
}
|
||||
|
||||
if (mode != null) {
|
||||
virtualView.onModeChange(mode, thresholdRect)
|
||||
}
|
||||
|
||||
// move the virtualView into the correct set(s)
|
||||
when {
|
||||
mode == VirtualViewMode.Visible -> {
|
||||
HPV.add(virtualView.virtualViewID)
|
||||
P.remove(virtualView.virtualViewID)
|
||||
V.add(virtualView.virtualViewID)
|
||||
}
|
||||
|
||||
mode == VirtualViewMode.Prerender -> {
|
||||
HPV.add(virtualView.virtualViewID)
|
||||
P.add(virtualView.virtualViewID)
|
||||
V.remove(virtualView.virtualViewID)
|
||||
}
|
||||
|
||||
mode == VirtualViewMode.Hidden -> {
|
||||
// remove from all sets
|
||||
HPV.remove(virtualView.virtualViewID)
|
||||
P.remove(virtualView.virtualViewID)
|
||||
V.remove(virtualView.virtualViewID)
|
||||
}
|
||||
|
||||
else -> { // hysteresis
|
||||
HPV.add(virtualView.virtualViewID)
|
||||
P.remove(virtualView.virtualViewID)
|
||||
V.remove(virtualView.virtualViewID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently check and update modes for all VirtualViews in the collection. Runs in O(m + log
|
||||
* n), where m = size of the prerender window in terms of index
|
||||
*/
|
||||
private fun updateModesAll() {
|
||||
// collect V', PV', HPV' sets
|
||||
val VPrime = virtualViews.query(visibleRect)
|
||||
val PVPrime = virtualViews.query(prerenderRect)
|
||||
val HPVPrime = virtualViews.query(hysteresisRect)
|
||||
|
||||
debugLog("updateModes", { "V: ${V}, P: ${P}, HPV: ${HPV}" })
|
||||
|
||||
/** Perform utility set differences: */
|
||||
// P'=PV'-V'
|
||||
val PPrime = PVPrime.minus(VPrime)
|
||||
|
||||
debugLog("updateModes", { "V': ${VPrime}, P': ${PPrime}, HPV': ${HPVPrime}" })
|
||||
|
||||
/** Get useful set differences */
|
||||
// V'-V - update to visible
|
||||
val toVisible = VPrime.minus(V)
|
||||
// P'-P - update to prerender
|
||||
val toPrerender = PPrime.minus(P)
|
||||
// ignore H' - we don't care about VVs in hysteresis
|
||||
// HPV-HPV' - update to hidden
|
||||
val toHidden = HPV.minus(HPVPrime)
|
||||
|
||||
debugLog("updateModes", { "toV: ${toVisible}, toP: ${toPrerender}, toH: ${toHidden}" })
|
||||
|
||||
/** Perform updates with the calculated sets */
|
||||
for (vvID in toVisible) {
|
||||
virtualViews.getVirtualView(vvID)?.onModeChange(VirtualViewMode.Visible, visibleRect)
|
||||
}
|
||||
for (vvID in toPrerender) {
|
||||
virtualViews.getVirtualView(vvID)?.onModeChange(VirtualViewMode.Prerender, prerenderRect)
|
||||
}
|
||||
for (vvID in toHidden) {
|
||||
virtualViews.getVirtualView(vvID)?.onModeChange(VirtualViewMode.Hidden, emptyRect)
|
||||
}
|
||||
|
||||
/** update old sets - V, P, and HPV */
|
||||
V = VPrime
|
||||
P = PPrime.toMutableSet()
|
||||
HPV = HPVPrime.toMutableSet()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* since we use 1D intervals in this implementation, we reimplement intersection (exclusive) this
|
||||
* interval implementation needs to use ID as an extra differentiator because we may have
|
||||
* overlapping VirtualViews
|
||||
*/
|
||||
private data class Interval(val start: Int, val end: Int, val id: String) {
|
||||
public fun intersects(other: Interval): Boolean {
|
||||
debugLog(
|
||||
"Interval: intersect",
|
||||
{ "${id}:(${start}, ${end}) vs ${other.id}:(${other.start}, ${other.end})" },
|
||||
)
|
||||
return this.start < other.end && other.start < this.end
|
||||
}
|
||||
}
|
||||
|
||||
private class IntervalNode(
|
||||
var interval: Interval,
|
||||
var virtualView: VirtualView,
|
||||
var max: Int = interval.end,
|
||||
var height: Int = 1,
|
||||
var left: IntervalNode? = null,
|
||||
var right: IntervalNode? = null,
|
||||
)
|
||||
|
||||
internal class IntervalTree(private val horizontal: Boolean) : MutableCollection<VirtualView> {
|
||||
private var root: IntervalNode? = null
|
||||
private val idToIntervalNode = mutableMapOf<String, IntervalNode>()
|
||||
|
||||
private fun rectToInterval(rect: Rect, id: String? = ""): Interval {
|
||||
return if (horizontal) {
|
||||
Interval(rect.left, rect.right, id.orEmpty())
|
||||
} else {
|
||||
Interval(rect.top, rect.bottom, id.orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
/** AVL Tree Operations */
|
||||
private fun height(node: IntervalNode?): Int = node?.height ?: 0
|
||||
|
||||
private fun updateHeight(node: IntervalNode) {
|
||||
node.height = 1 + maxOf(height(node.left), height(node.right))
|
||||
}
|
||||
|
||||
private fun updateMax(node: IntervalNode) {
|
||||
node.max =
|
||||
maxOf(
|
||||
node.interval.end,
|
||||
node.left?.max ?: Int.MIN_VALUE,
|
||||
node.right?.max ?: Int.MIN_VALUE,
|
||||
)
|
||||
}
|
||||
|
||||
private fun balanceFactor(node: IntervalNode?): Int {
|
||||
return if (node == null) 0 else height(node.left) - height(node.right)
|
||||
}
|
||||
|
||||
private fun rotateRight(parent: IntervalNode): IntervalNode {
|
||||
val newParent =
|
||||
requireNotNull(parent.left) {
|
||||
"[IntervalTree] AVL node's left must not be null when rotating right."
|
||||
}
|
||||
val replacingGrandchild = newParent.right
|
||||
|
||||
newParent.right = parent
|
||||
parent.left = replacingGrandchild
|
||||
|
||||
updateHeight(parent)
|
||||
updateMax(parent)
|
||||
updateHeight(newParent)
|
||||
updateMax(newParent)
|
||||
|
||||
return newParent
|
||||
}
|
||||
|
||||
private fun rotateLeft(parent: IntervalNode): IntervalNode {
|
||||
val newParent =
|
||||
requireNotNull(parent.right) {
|
||||
"[IntervalTree] AVL node's right must not be null when rotating left."
|
||||
}
|
||||
val replacingGrandchild = newParent.left
|
||||
|
||||
newParent.left = parent
|
||||
parent.right = replacingGrandchild
|
||||
|
||||
updateHeight(parent)
|
||||
updateMax(parent)
|
||||
updateHeight(newParent)
|
||||
updateMax(newParent)
|
||||
|
||||
return newParent
|
||||
}
|
||||
|
||||
private fun balance(node: IntervalNode): IntervalNode {
|
||||
updateHeight(node)
|
||||
updateMax(node)
|
||||
|
||||
val bf = balanceFactor(node)
|
||||
|
||||
// Left heavy
|
||||
if (bf > 1) {
|
||||
if (balanceFactor(node.left) < 0) {
|
||||
node.left =
|
||||
rotateLeft(
|
||||
requireNotNull(node.left) {
|
||||
"[IntervalTree] node.left must not be null when performing left rotation around it"
|
||||
}
|
||||
)
|
||||
}
|
||||
return rotateRight(node)
|
||||
}
|
||||
|
||||
// Right heavy
|
||||
if (bf < -1) {
|
||||
if (balanceFactor(node.right) > 0) {
|
||||
node.right =
|
||||
rotateLeft(
|
||||
requireNotNull(node.right) {
|
||||
"[IntervalTree] node.right must not be null when performing right rotation around it"
|
||||
}
|
||||
)
|
||||
}
|
||||
return rotateLeft(node)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
private fun compareIntervals(i1: Interval, i2: Interval): Int {
|
||||
return when {
|
||||
i1.start != i2.start -> i1.start.compareTo(i2.start)
|
||||
i1.end != i2.end -> i1.end.compareTo(i2.end)
|
||||
else -> i1.id.compareTo(i2.id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insert(
|
||||
node: IntervalNode?,
|
||||
intervalNode: IntervalNode,
|
||||
): IntervalNode {
|
||||
if (node == null) {
|
||||
return intervalNode
|
||||
}
|
||||
|
||||
when {
|
||||
compareIntervals(intervalNode.interval, node.interval) < 0 -> {
|
||||
node.left = insert(node.left, intervalNode)
|
||||
}
|
||||
else -> {
|
||||
node.right = insert(node.right, intervalNode)
|
||||
}
|
||||
}
|
||||
|
||||
return balance(node)
|
||||
}
|
||||
|
||||
private fun findMin(node: IntervalNode): IntervalNode {
|
||||
return node.left?.let { findMin(it) } ?: node
|
||||
}
|
||||
|
||||
private fun delete(node: IntervalNode?, target: IntervalNode): IntervalNode? {
|
||||
if (node == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
when {
|
||||
compareIntervals(target.interval, node.interval) < 0 -> {
|
||||
node.left = delete(node.left, target)
|
||||
}
|
||||
compareIntervals(target.interval, node.interval) > 0 -> {
|
||||
node.right = delete(node.right, target)
|
||||
}
|
||||
else -> {
|
||||
// Node to delete found
|
||||
return when {
|
||||
node.left == null -> node.right
|
||||
node.right == null -> node.left
|
||||
else -> {
|
||||
val successor =
|
||||
findMin(
|
||||
requireNotNull(node.right) {
|
||||
"[IntervalTree] node.right must not be null when finding node's successor"
|
||||
}
|
||||
)
|
||||
node.virtualView = successor.virtualView
|
||||
node.interval = successor.interval
|
||||
node.right = delete(node.right, successor)
|
||||
node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return balance(node)
|
||||
}
|
||||
|
||||
private fun queryHelper(
|
||||
node: IntervalNode?,
|
||||
interval: Interval,
|
||||
results: MutableSet<String>,
|
||||
) {
|
||||
debugLog(
|
||||
"queryHelper",
|
||||
{
|
||||
"Check node (${node?.virtualView?.virtualViewID}, ${node?.virtualView?.containerRelativeRect}) against interval(${interval.start}, ${interval.end})"
|
||||
},
|
||||
)
|
||||
if (node == null || node.max <= interval.start) {
|
||||
return
|
||||
}
|
||||
|
||||
prerenderRect.set(visibleRect)
|
||||
prerenderRect.inset(
|
||||
(-prerenderRect.width() * prerenderRatio).toInt(),
|
||||
(-prerenderRect.height() * prerenderRatio).toInt(),
|
||||
queryHelper(node.left, interval, results)
|
||||
|
||||
if (node.interval.intersects(interval)) {
|
||||
results.add(node.virtualView.virtualViewID)
|
||||
}
|
||||
|
||||
if (node.interval.start < interval.end) {
|
||||
queryHelper(node.right, interval, results)
|
||||
}
|
||||
}
|
||||
|
||||
private fun inorderTraversal(node: IntervalNode?, results: MutableList<VirtualView>) {
|
||||
if (node == null) {
|
||||
return
|
||||
}
|
||||
inorderTraversal(node.left, results)
|
||||
results.add(node.virtualView)
|
||||
inorderTraversal(node.right, results)
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries all intervals that overlap with the given rectangle in the set direction. Because this
|
||||
* is a 1D implementation, this will ignore all cross-axis intersection
|
||||
*/
|
||||
fun query(queryRect: Rect): MutableSet<String> {
|
||||
val queryInterval = rectToInterval(queryRect)
|
||||
val results = HashSet<String>()
|
||||
debugLog("query", { "Querying tree for rect ${queryRect}" })
|
||||
queryHelper(root, queryInterval, results)
|
||||
debugLog("query", { "Query results: ${results}" })
|
||||
return results
|
||||
}
|
||||
|
||||
fun getVirtualView(virtualViewID: String): VirtualView? {
|
||||
return idToIntervalNode[virtualViewID]?.virtualView
|
||||
}
|
||||
|
||||
fun traverse(): MutableList<VirtualView> {
|
||||
val results = mutableListOf<VirtualView>()
|
||||
inorderTraversal(root, results)
|
||||
return results
|
||||
}
|
||||
|
||||
/** MutableCollection public API */
|
||||
|
||||
/**
|
||||
* Adds or updates an interval. If an interval with the given ID exists, it updates that interval.
|
||||
* Otherwise, adds a new interval.
|
||||
*
|
||||
* Returns true if we are adding a new element, false otherwise (update). The update case will
|
||||
* still override the old node.
|
||||
*/
|
||||
override fun add(element: VirtualView): Boolean {
|
||||
val id = element.virtualViewID
|
||||
var newElement = true
|
||||
|
||||
// If interval node with this ID exists, remove it first
|
||||
val intervalNode = idToIntervalNode[id]
|
||||
if (intervalNode != null) {
|
||||
root = delete(root, intervalNode)
|
||||
newElement = false
|
||||
}
|
||||
|
||||
val newInterval = rectToInterval(element.containerRelativeRect, id)
|
||||
val newIntervalNode = IntervalNode(newInterval, element)
|
||||
|
||||
root = insert(root, newIntervalNode)
|
||||
idToIntervalNode[id] = newIntervalNode
|
||||
|
||||
debugLog(
|
||||
"IntervalTree: add",
|
||||
{
|
||||
"New VirtualView: (${element.virtualViewID}, ${element.containerRelativeRect}). Node interval ${newIntervalNode.interval.id}"
|
||||
},
|
||||
)
|
||||
|
||||
if (hysteresisRatio > 0.0) {
|
||||
hysteresisRect.set(prerenderRect)
|
||||
hysteresisRect.inset(
|
||||
(-visibleRect.width() * hysteresisRatio).toInt(),
|
||||
(-visibleRect.height() * hysteresisRatio).toInt(),
|
||||
)
|
||||
}
|
||||
return newElement
|
||||
}
|
||||
|
||||
val virtualViewsIt =
|
||||
if (virtualView != null) listOf(virtualView) else virtualViews.toMutableSet()
|
||||
virtualViewsIt.forEach { vv ->
|
||||
val rect = vv.containerRelativeRect
|
||||
|
||||
var mode: VirtualViewMode? = VirtualViewMode.Hidden
|
||||
var thresholdRect = emptyRect
|
||||
when {
|
||||
rectsOverlap(rect, visibleRect) -> {
|
||||
thresholdRect = visibleRect
|
||||
if (onWindowFocusChangeListener != null) {
|
||||
if (scrollView.hasWindowFocus()) {
|
||||
mode = VirtualViewMode.Visible
|
||||
} else {
|
||||
mode = VirtualViewMode.Prerender
|
||||
}
|
||||
} else {
|
||||
mode = VirtualViewMode.Visible
|
||||
}
|
||||
}
|
||||
rectsOverlap(rect, prerenderRect) -> {
|
||||
mode = VirtualViewMode.Prerender
|
||||
thresholdRect = prerenderRect
|
||||
}
|
||||
(hysteresisRatio > 0.0 && rectsOverlap(rect, hysteresisRect)) -> {
|
||||
mode = null
|
||||
}
|
||||
}
|
||||
|
||||
if (mode != null) {
|
||||
vv.onModeChange(mode, thresholdRect)
|
||||
debugLog(
|
||||
"updateModes",
|
||||
{
|
||||
"virtualView=${vv.virtualViewID} mode=$mode rect=$rect thresholdRect=$thresholdRect"
|
||||
},
|
||||
)
|
||||
override fun addAll(elements: Collection<VirtualView>): Boolean {
|
||||
var changed = false
|
||||
elements.forEach { element ->
|
||||
if (add(element)) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
root = null
|
||||
idToIntervalNode.clear()
|
||||
}
|
||||
|
||||
override fun iterator(): MutableIterator<VirtualView> {
|
||||
val list = mutableListOf<VirtualView>()
|
||||
inorderTraversal(root, list)
|
||||
return list.iterator()
|
||||
}
|
||||
|
||||
override fun remove(element: VirtualView): Boolean {
|
||||
val intervalNode = idToIntervalNode[element.virtualViewID]
|
||||
if (intervalNode != null) {
|
||||
root = delete(root, intervalNode)
|
||||
idToIntervalNode.remove(element.virtualViewID)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeAll(elements: Collection<VirtualView>): Boolean {
|
||||
var removedAny = false
|
||||
elements.forEach {
|
||||
if (remove(it)) {
|
||||
removedAny = true
|
||||
}
|
||||
}
|
||||
return removedAny
|
||||
}
|
||||
|
||||
override fun retainAll(elements: Collection<VirtualView>): Boolean {
|
||||
throw Error("IntervalTree does not support retainAll yet")
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = idToIntervalNode.size
|
||||
|
||||
override fun contains(element: VirtualView): Boolean {
|
||||
return idToIntervalNode.contains(element.virtualViewID)
|
||||
}
|
||||
|
||||
override fun containsAll(elements: Collection<VirtualView>): Boolean {
|
||||
return elements.all { contains(it) }
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return (size == 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user