add visible bounds caching by UIElementPath
@@ -18,6 +18,10 @@
|
||||
4391F7C32A77DB9B006D8818 /* DivViewController+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4391F7C22A77DB9B006D8818 /* DivViewController+SwiftUI.swift */; };
|
||||
52184FD8296C68E400ECC0E9 /* DivKitExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 52184FD7296C68E400ECC0E9 /* DivKitExtensions */; };
|
||||
5219194D297B3974007DD5F4 /* RunLoopCardUpdateAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5219194C297B3974007DD5F4 /* RunLoopCardUpdateAggregatorTests.swift */; };
|
||||
52907F662A71199B00518F50 /* DivLastVisibleBoundsCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52907F652A71199B00518F50 /* DivLastVisibleBoundsCacheTests.swift */; };
|
||||
52CD80672A7122AA00B828B0 /* DivBaseExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CD80662A7122AA00B828B0 /* DivBaseExtensionsTests.swift */; };
|
||||
52CD80692A7159C200B828B0 /* DivStateExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CD80682A7159C200B828B0 /* DivStateExtensionsTests.swift */; };
|
||||
52CD806B2A7172B400B828B0 /* XCTestCase+ViewVisibilityCallCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CD806A2A7172B400B828B0 /* XCTestCase+ViewVisibilityCallCount.swift */; };
|
||||
52DED04F29F7349000D64519 /* DivKit in Frameworks */ = {isa = PBXBuildFile; productRef = 52DED04E29F7349000D64519 /* DivKit */; };
|
||||
52DED05129F7349000D64519 /* DivKitExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 52DED05029F7349000D64519 /* DivKitExtensions */; };
|
||||
52DED05329F734B900D64519 /* ShimmerStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52DED03C29F6E3D500D64519 /* ShimmerStyleTests.swift */; };
|
||||
@@ -260,6 +264,10 @@
|
||||
437B19D329C8A73300A4C467 /* MaskValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskValidatorTests.swift; sourceTree = "<group>"; };
|
||||
4391F7C22A77DB9B006D8818 /* DivViewController+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DivViewController+SwiftUI.swift"; sourceTree = "<group>"; };
|
||||
5219194C297B3974007DD5F4 /* RunLoopCardUpdateAggregatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RunLoopCardUpdateAggregatorTests.swift; sourceTree = "<group>"; };
|
||||
52907F652A71199B00518F50 /* DivLastVisibleBoundsCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DivLastVisibleBoundsCacheTests.swift; sourceTree = "<group>"; };
|
||||
52CD80662A7122AA00B828B0 /* DivBaseExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DivBaseExtensionsTests.swift; sourceTree = "<group>"; };
|
||||
52CD80682A7159C200B828B0 /* DivStateExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DivStateExtensionsTests.swift; sourceTree = "<group>"; };
|
||||
52CD806A2A7172B400B828B0 /* XCTestCase+ViewVisibilityCallCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+ViewVisibilityCallCount.swift"; sourceTree = "<group>"; };
|
||||
52DED03C29F6E3D500D64519 /* ShimmerStyleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShimmerStyleTests.swift; sourceTree = "<group>"; };
|
||||
52DED04729F7346900D64519 /* DivKitExtensionsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DivKitExtensionsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
52EEC66D293E64C90030FB11 /* DivBlockModelingContextErrorsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DivBlockModelingContextErrorsTests.swift; sourceTree = "<group>"; };
|
||||
@@ -843,6 +851,7 @@
|
||||
8CB960E528883B8E00D16E47 /* Platform.swift */,
|
||||
5219194C297B3974007DD5F4 /* RunLoopCardUpdateAggregatorTests.swift */,
|
||||
8C4DC3E72892946F000C0963 /* RuntimeTestCase.swift */,
|
||||
52907F652A71199B00518F50 /* DivLastVisibleBoundsCacheTests.swift */,
|
||||
);
|
||||
path = DivKitTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -872,6 +881,9 @@
|
||||
8C7B1B252865C01C0036EF4C /* DivTabsExtensionsTests.swift */,
|
||||
8C7B1B282865C01C0036EF4C /* DivTextExtensionsTests.swift */,
|
||||
8C1CF625286B561F0016D0A1 /* Expected.swift */,
|
||||
52CD80662A7122AA00B828B0 /* DivBaseExtensionsTests.swift */,
|
||||
52CD80682A7159C200B828B0 /* DivStateExtensionsTests.swift */,
|
||||
52CD806A2A7172B400B828B0 /* XCTestCase+ViewVisibilityCallCount.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -1505,6 +1517,7 @@
|
||||
8C23F41529DDB3810069F3F7 /* EntityWithArrayOfEnumsTemplate.swift in Sources */,
|
||||
8C23F43229DDB3810069F3F7 /* EntityWithStringArrayPropertyTemplate.swift in Sources */,
|
||||
8C23F44829DDB3810069F3F7 /* StringEnumPropertyTests.swift in Sources */,
|
||||
52CD806B2A7172B400B828B0 /* XCTestCase+ViewVisibilityCallCount.swift in Sources */,
|
||||
8C23F43329DDB3810069F3F7 /* EntityWithStringEnumProperty.swift in Sources */,
|
||||
8C23F44729DDB3810069F3F7 /* StrictArrayTests.swift in Sources */,
|
||||
8C23F43B29DDB3810069F3F7 /* ArrayTests.swift in Sources */,
|
||||
@@ -1513,6 +1526,7 @@
|
||||
8C1CF626286B561F0016D0A1 /* Expected.swift in Sources */,
|
||||
7035529E28D4881300E237CF /* DivDeserializationErrorsTests.swift in Sources */,
|
||||
E945FC46293E1CA80066C3EA /* Utils.swift in Sources */,
|
||||
52907F662A71199B00518F50 /* DivLastVisibleBoundsCacheTests.swift in Sources */,
|
||||
8C23F41729DDB3810069F3F7 /* EntityWithArrayOfExpressionsTemplate.swift in Sources */,
|
||||
8C23F41D29DDB3810069F3F7 /* EntityWithComplexProperty.swift in Sources */,
|
||||
8C7B1CA62865C01D0036EF4C /* TemplatesTests.swift in Sources */,
|
||||
@@ -1529,6 +1543,7 @@
|
||||
8C7B1BF32865C01C0036EF4C /* DivTabsExtensionsTests.swift in Sources */,
|
||||
8C23F44429DDB3810069F3F7 /* OptionalStringEnumPropertyTests.swift in Sources */,
|
||||
891672372A5F6F9300121131 /* DivPersistentValuesStorageTests.swift in Sources */,
|
||||
52CD80672A7122AA00B828B0 /* DivBaseExtensionsTests.swift in Sources */,
|
||||
8C23F43F29DDB3810069F3F7 /* EntityProtocol.swift in Sources */,
|
||||
52EEC66E293E64C90030FB11 /* DivBlockModelingContextErrorsTests.swift in Sources */,
|
||||
8C23F42729DDB3810069F3F7 /* EntityWithOptionalStringEnumProperty.swift in Sources */,
|
||||
@@ -1559,6 +1574,7 @@
|
||||
8C4DC3E82892946F000C0963 /* RuntimeTestCase.swift in Sources */,
|
||||
8C23F42D29DDB3810069F3F7 /* EntityWithSimpleProperties.swift in Sources */,
|
||||
8C23F42B29DDB3810069F3F7 /* EntityWithRequiredProperty.swift in Sources */,
|
||||
52CD80692A7159C200B828B0 /* DivStateExtensionsTests.swift in Sources */,
|
||||
8C23F44329DDB3810069F3F7 /* OptionalPropertyTests.swift in Sources */,
|
||||
8C1DEBCE2A2F8277009237F1 /* DivStateManagementTests.swift in Sources */,
|
||||
8C7B1BFA2865C01C0036EF4C /* DivDataExtensionsTests.swift in Sources */,
|
||||
|
||||
@@ -20,6 +20,7 @@ public struct DivBlockModelingContext {
|
||||
public var stateManager: DivStateManager
|
||||
public let blockStateStorage: DivBlockStateStorage
|
||||
public let visibilityCounter: DivVisibilityCounting
|
||||
public let lastVisibleBoundsCache: DivLastVisibleBoundsCache
|
||||
public let imageHolderFactory: ImageHolderFactory
|
||||
public let highPriorityImageHolderFactory: ImageHolderFactory?
|
||||
public let divCustomBlockFactory: DivCustomBlockFactory
|
||||
@@ -30,6 +31,7 @@ public struct DivBlockModelingContext {
|
||||
private let variables: DivVariables
|
||||
public let layoutDirection: UserInterfaceLayoutDirection
|
||||
public let debugParams: DebugParams
|
||||
public let scheduler: Scheduling
|
||||
public let playerFactory: PlayerFactory?
|
||||
public var childrenA11yDescription: String?
|
||||
public weak var parentScrollView: ScrollView?
|
||||
@@ -61,6 +63,7 @@ public struct DivBlockModelingContext {
|
||||
stateManager: DivStateManager,
|
||||
blockStateStorage: DivBlockStateStorage = DivBlockStateStorage(),
|
||||
visibilityCounter: DivVisibilityCounting = DivVisibilityCounter(),
|
||||
lastVisibleBoundsCache: DivLastVisibleBoundsCache = DivLastVisibleBoundsCache(),
|
||||
imageHolderFactory: ImageHolderFactory,
|
||||
highPriorityImageHolderFactory: ImageHolderFactory? = nil,
|
||||
divCustomBlockFactory: DivCustomBlockFactory = EmptyDivCustomBlockFactory(),
|
||||
@@ -71,6 +74,7 @@ public struct DivBlockModelingContext {
|
||||
variables: DivVariables = [:],
|
||||
playerFactory: PlayerFactory? = nil,
|
||||
debugParams: DebugParams = DebugParams(),
|
||||
scheduler: Scheduling? = nil,
|
||||
childrenA11yDescription: String? = nil,
|
||||
parentScrollView: ScrollView? = nil,
|
||||
errorsStorage: DivErrorsStorage = DivErrorsStorage(errors: []),
|
||||
@@ -85,6 +89,7 @@ public struct DivBlockModelingContext {
|
||||
self.stateManager = stateManager
|
||||
self.blockStateStorage = blockStateStorage
|
||||
self.visibilityCounter = visibilityCounter
|
||||
self.lastVisibleBoundsCache = lastVisibleBoundsCache
|
||||
self.imageHolderFactory = imageHolderFactory
|
||||
self.highPriorityImageHolderFactory = highPriorityImageHolderFactory
|
||||
self.divCustomBlockFactory = divCustomBlockFactory
|
||||
@@ -93,6 +98,7 @@ public struct DivBlockModelingContext {
|
||||
self.variables = variables
|
||||
self.playerFactory = playerFactory
|
||||
self.debugParams = debugParams
|
||||
self.scheduler = scheduler ?? TimerScheduler()
|
||||
self.childrenA11yDescription = childrenA11yDescription
|
||||
self.parentScrollView = parentScrollView
|
||||
self.errorsStorage = errorsStorage
|
||||
|
||||
@@ -33,6 +33,7 @@ public final class DivKitComponents {
|
||||
public let urlHandler: DivUrlHandler
|
||||
public let variablesStorage: DivVariablesStorage
|
||||
public let visibilityCounter = DivVisibilityCounter()
|
||||
public let lastVisibleBoundsCache = DivLastVisibleBoundsCache()
|
||||
public var updateCardSignal: Signal<[DivActionURLHandler.UpdateReason]> {
|
||||
updateCardPipe.signal
|
||||
}
|
||||
@@ -227,6 +228,7 @@ public final class DivKitComponents {
|
||||
stateManager: stateManagement.getStateManagerForCard(cardId: cardId),
|
||||
blockStateStorage: blockStateStorage,
|
||||
visibilityCounter: visibilityCounter,
|
||||
lastVisibleBoundsCache: lastVisibleBoundsCache,
|
||||
imageHolderFactory: imageHolderFactory
|
||||
.withInMemoryCache(cachedImageHolders: cachedImageHolders),
|
||||
divCustomBlockFactory: divCustomBlockFactory,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import CoreGraphics
|
||||
import LayoutKit
|
||||
import CommonCorePublic
|
||||
|
||||
public final class DivLastVisibleBoundsCache {
|
||||
private let rwLock = RWLock()
|
||||
|
||||
private var storage: [UIElementPath: CGRect] = [:]
|
||||
|
||||
public init() {}
|
||||
|
||||
public func lastVisibleBounds(for path: UIElementPath) -> CGRect {
|
||||
rwLock.read {
|
||||
storage[path] ?? .zero
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLastVisibleBounds(for path: UIElementPath, bounds: CGRect) {
|
||||
rwLock.write {
|
||||
storage[path] = bounds
|
||||
}
|
||||
}
|
||||
|
||||
public func dropVisibleBounds(forMatchingPrefix prefix: UIElementPath) {
|
||||
let prefix = prefix.description + "/"
|
||||
rwLock.write {
|
||||
storage.forEach {
|
||||
if ($0.key.description + "/").starts(with: prefix) {
|
||||
storage[$0.key] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
rwLock.write {
|
||||
storage.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ extension DivBase {
|
||||
|
||||
let visibility = resolveVisibility(expressionResolver)
|
||||
if visibility == .gone {
|
||||
context.lastVisibleBoundsCache.dropVisibleBounds(forMatchingPrefix: context.parentPath)
|
||||
context.stateManager.setBlockVisibility(statePath: statePath, div: self, isVisible: false)
|
||||
return EmptyBlock.zeroSized
|
||||
}
|
||||
@@ -39,6 +40,7 @@ extension DivBase {
|
||||
|
||||
let externalInsets = margins.makeEdgeInsets(context: context)
|
||||
if visibility == .invisible {
|
||||
context.lastVisibleBoundsCache.dropVisibleBounds(forMatchingPrefix: context.parentPath)
|
||||
context.stateManager.setBlockVisibility(statePath: statePath, div: self, isVisible: false)
|
||||
block = applyExtensionHandlersAfterBaseProperties(
|
||||
to: block.addingEdgeInsets(externalInsets),
|
||||
@@ -72,6 +74,16 @@ extension DivBase {
|
||||
border: border.makeBlockBorder(with: expressionResolver),
|
||||
shadow: border.makeBlockShadow(with: expressionResolver),
|
||||
visibilityActions: visibilityActions.isEmpty ? nil : visibilityActions,
|
||||
lastVisibleBounds: visibilityActions.isEmpty ? nil : Property<CGRect>(
|
||||
getter: { context.lastVisibleBoundsCache.lastVisibleBounds(for: context.parentPath) },
|
||||
setter: {
|
||||
context.lastVisibleBoundsCache.updateLastVisibleBounds(
|
||||
for: context.parentPath,
|
||||
bounds: $0
|
||||
)
|
||||
}
|
||||
),
|
||||
scheduler: context.scheduler,
|
||||
tooltips: try makeTooltips(context: context)
|
||||
)
|
||||
.addingTransform(
|
||||
|
||||
@@ -11,6 +11,10 @@ extension DivData: DivBlockModeling {
|
||||
throw DivBlockModelingError("DivData has no states", path: context.parentPath)
|
||||
}
|
||||
|
||||
if let previousRootState = getPreviousRootState(stateManager: stateManager) {
|
||||
context.lastVisibleBoundsCache.dropVisibleBounds(forMatchingPrefix: context.parentPath + previousRootState.rawValue)
|
||||
}
|
||||
|
||||
let stateId = String(state.stateId)
|
||||
let statePath = DivStatePath(rawValue: UIElementPath(stateId))
|
||||
let div = state.div
|
||||
@@ -31,7 +35,6 @@ extension DivData: DivBlockModeling {
|
||||
)
|
||||
return block
|
||||
.addingStateBlock(
|
||||
stateId: stateId,
|
||||
ids: stateManager.getVisibleIds(statePath: statePath)
|
||||
)
|
||||
.addingDebugInfo(context: divContext)
|
||||
@@ -53,6 +56,22 @@ extension DivData: DivBlockModeling {
|
||||
context.addError(level: .error, message: "DivData.State not found: \(stateId)")
|
||||
return states.first
|
||||
}
|
||||
|
||||
private func getPreviousRootState(stateManager: DivStateManager) -> DivStateID? {
|
||||
guard let item = stateManager.get(stateBlockPath: DivData.rootPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch item.previousState {
|
||||
case .empty, .initial:
|
||||
return nil
|
||||
case let .withID(id):
|
||||
if item.currentStateID != id {
|
||||
return id
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DivData {
|
||||
|
||||
@@ -46,6 +46,8 @@ extension DivState: DivBlockModeling {
|
||||
let previousState = getPreviousState(stateManagerItem: stateManagerItem),
|
||||
previousState.stateId != activeStateId,
|
||||
let previousDiv = previousState.div {
|
||||
// state changed -> drop visibility cache for all children
|
||||
context.lastVisibleBoundsCache.dropVisibleBounds(forMatchingPrefix: context.parentPath)
|
||||
previousBlock = try previousDiv.value.makeBlock(
|
||||
context: context.makeContextForState(
|
||||
id: id,
|
||||
@@ -99,7 +101,6 @@ extension DivState: DivBlockModeling {
|
||||
),
|
||||
]
|
||||
).addingStateBlock(
|
||||
stateId: activeStateId,
|
||||
ids: stateManager.getVisibleIds(statePath: activeStatePath)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import XCTest
|
||||
|
||||
@testable import DivKit
|
||||
import CommonCorePublic
|
||||
@testable import DivKit
|
||||
import LayoutKit
|
||||
import NetworkingPublic
|
||||
import Serialization
|
||||
@@ -48,7 +48,8 @@ extension DivBlockModelingContext {
|
||||
static let `default` = DivBlockModelingContext()
|
||||
|
||||
init(
|
||||
blockStateStorage: DivBlockStateStorage = DivBlockStateStorage()
|
||||
blockStateStorage: DivBlockStateStorage = DivBlockStateStorage(),
|
||||
scheduler: Scheduling? = nil
|
||||
) {
|
||||
self.init(
|
||||
cardId: DivKitTests.cardId,
|
||||
@@ -56,6 +57,7 @@ extension DivBlockModelingContext {
|
||||
stateManager: DivStateManager(),
|
||||
blockStateStorage: blockStateStorage,
|
||||
imageHolderFactory: ImageHolderFactory(make: { _, _ in FakeImageHolder() }),
|
||||
scheduler: scheduler,
|
||||
persistentValuesStorage: DivPersistentValuesStorage()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import DivKit
|
||||
import LayoutKit
|
||||
import XCTest
|
||||
|
||||
final class DivLastVisibleBoundsCacheTests: XCTestCase {
|
||||
let cache = DivLastVisibleBoundsCache()
|
||||
|
||||
func test_WhenCacheIsEmpty_RetrievesZeroFromCache() {
|
||||
XCTAssertEqual(cache.lastVisibleBounds(for: "path"), .zero)
|
||||
}
|
||||
|
||||
func test_WhenUpdateLastVisibleBounds_RetrievesUpdatedValueFromCache() {
|
||||
let rect = CGRect(origin: CGPoint(x: 10, y: 10), size: CGSize(squareDimension: 10))
|
||||
|
||||
cache.updateLastVisibleBounds(for: "path", bounds: rect)
|
||||
|
||||
XCTAssertEqual(cache.lastVisibleBounds(for: "path"), rect)
|
||||
}
|
||||
|
||||
func test_WhenReset_RetrievesZeroFromCache() {
|
||||
let rect = CGRect(origin: CGPoint(x: 10, y: 10), size: CGSize(squareDimension: 10))
|
||||
|
||||
cache.updateLastVisibleBounds(for: "path", bounds: rect)
|
||||
cache.reset()
|
||||
|
||||
XCTAssertEqual(cache.lastVisibleBounds(for: "path"), .zero)
|
||||
}
|
||||
|
||||
func test_WhenDropVisibleBoundsWithMatchingPrefix_ClearsAllChildPaths() {
|
||||
let rect1 = CGRect(origin: CGPoint(x: 10, y: 10), size: CGSize(squareDimension: 10))
|
||||
let rect2 = CGRect(origin: .zero, size: CGSize(squareDimension: 10))
|
||||
let rect3 = CGRect(origin: .zero, size: CGSize(squareDimension: 20))
|
||||
let parentPath: UIElementPath = "path"
|
||||
let parentPath2: UIElementPath = "path2"
|
||||
let childPath: UIElementPath = parentPath + "child"
|
||||
let nestedChildPath: UIElementPath = childPath + "nestedChild"
|
||||
|
||||
cache.updateLastVisibleBounds(for: parentPath, bounds: rect1)
|
||||
cache.updateLastVisibleBounds(for: parentPath2, bounds: rect1)
|
||||
cache.updateLastVisibleBounds(for: childPath, bounds: rect2)
|
||||
cache.updateLastVisibleBounds(for: nestedChildPath, bounds: rect3)
|
||||
|
||||
cache.dropVisibleBounds(forMatchingPrefix: parentPath)
|
||||
|
||||
XCTAssertEqual(cache.lastVisibleBounds(for: childPath), .zero)
|
||||
XCTAssertEqual(cache.lastVisibleBounds(for: nestedChildPath), .zero)
|
||||
XCTAssertEqual(cache.lastVisibleBounds(for: parentPath2), rect1)
|
||||
XCTAssertEqual(cache.lastVisibleBounds(for: parentPath), .zero)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
@testable import LayoutKit
|
||||
|
||||
import XCTest
|
||||
|
||||
import CommonCorePublic
|
||||
import DivKit
|
||||
import NetworkingPublic
|
||||
|
||||
final class DivBaseExtensionsTests: XCTestCase {
|
||||
private let timer = TestTimerScheduler()
|
||||
|
||||
func test_WhenCreatesBlockAfterItBeingGone_ReportsVisibility() throws {
|
||||
try expectVisibilityActionsToRun(
|
||||
forVisibleBlockFile: "div-text-visibility-actions-visible",
|
||||
invisibleBlockFile: "div-text-visibility-actions-gone"
|
||||
)
|
||||
}
|
||||
|
||||
func test_WhenCreatesBlockAfterItBeingInvisible_ReportsVisibility() throws {
|
||||
try expectVisibilityActionsToRun(
|
||||
forVisibleBlockFile: "div-text-visibility-actions-visible",
|
||||
invisibleBlockFile: "div-text-visibility-actions-invisible"
|
||||
)
|
||||
}
|
||||
|
||||
func test_WhenReusesBlockWithAnotherDivCardID_ExpectToCancelPreviousBlockTimers() throws {
|
||||
let context = DivBlockModelingContext(scheduler: timer)
|
||||
|
||||
let firstBlock = try makeBlock(fromFile: "div-text-visibility-actions-visible", context: context)
|
||||
|
||||
// trigger visibility actions for first time
|
||||
let rect = CGRect(origin: .zero, size: CGSize(squareDimension: 20))
|
||||
let view = firstBlock.makeBlockView()
|
||||
view.frame = rect
|
||||
view.layoutIfNeeded()
|
||||
view.onVisibleBoundsChanged(from: rect, to: rect)
|
||||
|
||||
let oldTimers = timer.timers
|
||||
|
||||
let secondBlock = try makeBlock(
|
||||
fromFile: "div-text-visibility-actions-visible",
|
||||
context: DivBlockModelingContext(
|
||||
cardId: "another_cardId",
|
||||
stateManager: DivStateManager(),
|
||||
imageHolderFactory: ImageHolderFactory(make: { _, _ in FakeImageHolder() })
|
||||
)
|
||||
)
|
||||
let view2 = secondBlock.reuse(view, observer: nil, overscrollDelegate: nil, renderingDelegate: nil, superview: nil)
|
||||
view2.frame = rect
|
||||
view2.layoutIfNeeded()
|
||||
view.onVisibleBoundsChanged(from: rect, to: rect)
|
||||
|
||||
XCTAssertEqual(oldTimers.allSatisfy { !$0.isValid }, true)
|
||||
}
|
||||
|
||||
private func expectVisibilityActionsToRun(
|
||||
forVisibleBlockFile file: String,
|
||||
invisibleBlockFile: String
|
||||
) throws {
|
||||
let context = DivBlockModelingContext(scheduler: timer)
|
||||
|
||||
let blockVisibleFirst = try makeBlock(fromFile: file, context: context)
|
||||
|
||||
// trigger visibility actions for first time
|
||||
let rect = CGRect(origin: .zero, size: CGSize(squareDimension: 20))
|
||||
let view = blockVisibleFirst.makeBlockView()
|
||||
XCTAssertEqual(getViewVisibilityCallCount(view: view, rect: rect, timerScheduler: timer), 1)
|
||||
|
||||
// expect to drop lastVisibleBounds
|
||||
let _ = try makeBlock(fromFile: invisibleBlockFile, context: context)
|
||||
let view2 = blockVisibleFirst.makeBlockView()
|
||||
|
||||
XCTAssertEqual(getViewVisibilityCallCount(view: view2, rect: rect, timerScheduler: timer), 1)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeBlock(
|
||||
fromFile filename: String,
|
||||
context: DivBlockModelingContext = .default
|
||||
) throws -> Block {
|
||||
try DivTextTemplate.make(
|
||||
fromFile: filename,
|
||||
subdirectory: "div-base",
|
||||
context: context
|
||||
)
|
||||
}
|
||||
@@ -60,6 +60,26 @@ final class DivDataExtensionsTests: XCTestCase {
|
||||
let expectedPath = UIElementPath.root + "0" + DivGallery.type
|
||||
XCTAssertEqual(galleryBlock?.model.path, expectedPath)
|
||||
}
|
||||
|
||||
func test_WhenStateChanges_ReportsVisibilityForNewState() throws {
|
||||
let timerScheduler = TestTimerScheduler()
|
||||
let context = DivBlockModelingContext(scheduler: timerScheduler)
|
||||
|
||||
for rootStateId in [nil, "1", "0", "1"] {
|
||||
if let rootStateId {
|
||||
context.stateManager.setStateWithHistory(path: DivData.rootPath, stateID: DivStateID(rawValue: rootStateId))
|
||||
}
|
||||
|
||||
let block = try makeBlock(fromFile: "root_states_visibility", context: context)
|
||||
|
||||
let rect = CGRect(
|
||||
origin: .zero,
|
||||
size: CGSize(width: 100, height: block.intrinsicContentHeight(forWidth: 100))
|
||||
)
|
||||
let view = block.makeBlockView()
|
||||
XCTAssertEqual(getViewVisibilityCallCount(view: view, rect: rect, timerScheduler: timerScheduler), 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let data = makeDivData(
|
||||
@@ -103,3 +123,15 @@ extension Block {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
private func makeBlock(
|
||||
fromFile filename: String,
|
||||
context: DivBlockModelingContext = .default
|
||||
) throws -> Block {
|
||||
try DivDataTemplate.make(
|
||||
fromFile: filename,
|
||||
subdirectory: "div-data",
|
||||
context: context
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
@testable import LayoutKit
|
||||
|
||||
import XCTest
|
||||
|
||||
import CommonCorePublic
|
||||
import DivKit
|
||||
|
||||
final class DivStateExtensionsTests: XCTestCase {
|
||||
private let timerScheduler = TestTimerScheduler()
|
||||
func test_WhenVisibilityWasTriggeredAndStateChanges_ReportsVisibilityAgain() throws {
|
||||
let context = DivBlockModelingContext(scheduler: timerScheduler)
|
||||
|
||||
let block = try makeBlock(fromFile: "states_visibility", context: context)
|
||||
let rect = CGRect(
|
||||
origin: .zero,
|
||||
size: CGSize(width: 100, height: block.intrinsicContentHeight(forWidth: 100))
|
||||
)
|
||||
let view = block.makeBlockView()
|
||||
XCTAssertEqual(getViewVisibilityCallCount(view: view, rect: rect, timerScheduler: timerScheduler), 1)
|
||||
|
||||
context.stateManager.setState(stateBlockPath: "mystate", stateID: "second")
|
||||
|
||||
let block2 = try makeBlock(fromFile: "states_visibility", context: context)
|
||||
let view2 = block2.reuse(
|
||||
view,
|
||||
observer: nil,
|
||||
overscrollDelegate: nil,
|
||||
renderingDelegate: nil,
|
||||
superview: nil
|
||||
)
|
||||
XCTAssertEqual(getViewVisibilityCallCount(view: view2, rect: rect, timerScheduler: timerScheduler), 1)
|
||||
}
|
||||
|
||||
func test_WhenVisibilityWasTriggeredAndStateDoesNotChange_DoesNotReportVisibilityAgain() throws {
|
||||
let context = DivBlockModelingContext(scheduler: timerScheduler)
|
||||
|
||||
let block = try makeBlock(fromFile: "states_visibility", context: context)
|
||||
let rect = CGRect(
|
||||
origin: .zero,
|
||||
size: CGSize(width: 100, height: block.intrinsicContentHeight(forWidth: 100))
|
||||
)
|
||||
let view = block.makeBlockView()
|
||||
XCTAssertEqual(getViewVisibilityCallCount(view: view, rect: rect, timerScheduler: timerScheduler), 1)
|
||||
|
||||
let block2 = try makeBlock(fromFile: "states_visibility", context: context)
|
||||
let view2 = block2.reuse(
|
||||
view,
|
||||
observer: nil,
|
||||
overscrollDelegate: nil,
|
||||
renderingDelegate: nil,
|
||||
superview: nil
|
||||
)
|
||||
XCTAssertEqual(getViewVisibilityCallCount(view: view2, rect: rect, timerScheduler: timerScheduler), 0)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeBlock(
|
||||
fromFile filename: String,
|
||||
context: DivBlockModelingContext = .default
|
||||
) throws -> Block {
|
||||
try DivStateTemplate.make(
|
||||
fromFile: filename,
|
||||
subdirectory: "div-state",
|
||||
context: context
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import LayoutKit
|
||||
import XCTest
|
||||
|
||||
extension XCTestCase {
|
||||
func getViewVisibilityCallCount(
|
||||
view: BlockView,
|
||||
rect: CGRect,
|
||||
timerScheduler: TestTimerScheduler
|
||||
) -> Int {
|
||||
let performer = UIActionEventPerformerMock()
|
||||
|
||||
performer.addSubview(view)
|
||||
view.frame = rect
|
||||
view.layoutIfNeeded()
|
||||
view.onVisibleBoundsChanged(from: rect, to: rect)
|
||||
timerScheduler.timers.forEach { guard $0.isValid else { return }; $0.fire() }
|
||||
|
||||
return performer.callCount
|
||||
}
|
||||
}
|
||||
|
||||
private class UIActionEventPerformerMock: UIView, UIActionEventPerforming {
|
||||
var callCount: Int = 0
|
||||
|
||||
func perform(uiActionEvent _: UIActionEvent, from _: AnyObject) {
|
||||
callCount += 1
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ extension Block {
|
||||
longTapActions: LongTapActions? = nil,
|
||||
analyticsURL: URL? = nil,
|
||||
visibilityActions: [VisibilityAction]? = nil,
|
||||
scheduler: Scheduling? = nil,
|
||||
lastVisibleBounds: Property<CGRect>? = nil,
|
||||
tooltips: [BlockTooltip]? = nil,
|
||||
forceWrapping: Bool,
|
||||
accessibilityElement: AccessibilityElement? = nil
|
||||
@@ -92,6 +94,8 @@ extension Block {
|
||||
childAlpha: alpha.map { $0 * block.childAlpha },
|
||||
blurEffect: blurEffect ?? block.blurEffect,
|
||||
visibilityActions: visibilityActions ?? block.visibilityActions,
|
||||
scheduler: scheduler ?? block.scheduler,
|
||||
lastVisibleBounds: lastVisibleBounds ?? block.lastVisibleBounds,
|
||||
tooltips: [tooltips, block.tooltips].compactMap { $0 }.flatMap { $0 },
|
||||
accessibilityElement: accessibilityElement
|
||||
)
|
||||
@@ -116,6 +120,8 @@ extension Block {
|
||||
childAlpha: alpha ?? DecoratingBlock.defaultChildAlpha,
|
||||
blurEffect: blurEffect,
|
||||
visibilityActions: visibilityActions ?? [],
|
||||
scheduler: scheduler,
|
||||
lastVisibleBounds: lastVisibleBounds,
|
||||
tooltips: tooltips ?? [],
|
||||
accessibilityElement: accessibilityElement
|
||||
)
|
||||
@@ -143,6 +149,8 @@ extension Block {
|
||||
analyticsURL: URL? = nil,
|
||||
shadow: BlockShadow? = nil,
|
||||
visibilityActions: [VisibilityAction]? = nil,
|
||||
lastVisibleBounds: Property<CGRect>? = nil,
|
||||
scheduler: Scheduling? = nil,
|
||||
tooltips: [BlockTooltip]? = nil,
|
||||
forceWrapping: Bool = false,
|
||||
accessibilityElement: AccessibilityElement? = nil
|
||||
@@ -160,6 +168,8 @@ extension Block {
|
||||
longTapActions: longTapActions,
|
||||
analyticsURL: analyticsURL,
|
||||
visibilityActions: visibilityActions,
|
||||
scheduler: scheduler,
|
||||
lastVisibleBounds: lastVisibleBounds,
|
||||
tooltips: tooltips,
|
||||
forceWrapping: forceWrapping,
|
||||
accessibilityElement: accessibilityElement
|
||||
|
||||
@@ -25,6 +25,8 @@ final class DecoratingBlock: WrapperBlock {
|
||||
let blurEffect: BlurEffect?
|
||||
let paddings: EdgeInsets
|
||||
let visibilityActions: [VisibilityAction]
|
||||
let scheduler: Scheduling?
|
||||
let lastVisibleBounds: Property<CGRect>?
|
||||
let tooltips: [BlockTooltip]
|
||||
let accessibilityElement: AccessibilityElement?
|
||||
|
||||
@@ -43,6 +45,8 @@ final class DecoratingBlock: WrapperBlock {
|
||||
blurEffect: BlurEffect? = nil,
|
||||
paddings: EdgeInsets = .zero,
|
||||
visibilityActions: [VisibilityAction] = [],
|
||||
scheduler: Scheduling? = nil,
|
||||
lastVisibleBounds: Property<CGRect>? = nil,
|
||||
tooltips: [BlockTooltip] = [],
|
||||
accessibilityElement: AccessibilityElement? = nil
|
||||
) {
|
||||
@@ -60,6 +64,8 @@ final class DecoratingBlock: WrapperBlock {
|
||||
self.blurEffect = blurEffect
|
||||
self.paddings = paddings
|
||||
self.visibilityActions = visibilityActions
|
||||
self.scheduler = scheduler
|
||||
self.lastVisibleBounds = lastVisibleBounds
|
||||
self.tooltips = tooltips
|
||||
self.accessibilityElement = accessibilityElement
|
||||
}
|
||||
@@ -144,6 +150,8 @@ extension DecoratingBlock {
|
||||
blurEffect: BlurEffect? = nil,
|
||||
paddings: EdgeInsets? = nil,
|
||||
visibilityActions: [VisibilityAction]? = nil,
|
||||
scheduler: Scheduling? = nil,
|
||||
lastVisibleBounds: Property<CGRect>? = nil,
|
||||
tooltips: [BlockTooltip]? = nil,
|
||||
accessibilityElement: AccessibilityElement? = nil
|
||||
) -> DecoratingBlock {
|
||||
@@ -162,6 +170,8 @@ extension DecoratingBlock {
|
||||
blurEffect: blurEffect ?? self.blurEffect,
|
||||
paddings: paddings ?? self.paddings,
|
||||
visibilityActions: visibilityActions ?? self.visibilityActions,
|
||||
scheduler: scheduler ?? self.scheduler,
|
||||
lastVisibleBounds: lastVisibleBounds ?? self.lastVisibleBounds,
|
||||
tooltips: tooltips ?? self.tooltips,
|
||||
accessibilityElement: accessibilityElement ?? self.accessibilityElement
|
||||
)
|
||||
|
||||
@@ -2,16 +2,13 @@ import CommonCorePublic
|
||||
|
||||
public final class StateBlock: WrapperBlock, LayoutCachingDefaultImpl {
|
||||
public let child: Block
|
||||
public let stateId: String
|
||||
public let ids: Set<String>
|
||||
|
||||
public init(
|
||||
child: Block,
|
||||
stateId: String,
|
||||
ids: Set<String>
|
||||
) {
|
||||
self.child = child
|
||||
self.stateId = stateId
|
||||
self.ids = ids
|
||||
}
|
||||
|
||||
@@ -23,14 +20,13 @@ public final class StateBlock: WrapperBlock, LayoutCachingDefaultImpl {
|
||||
}
|
||||
|
||||
public func makeCopy(wrapping child: Block) -> StateBlock {
|
||||
StateBlock(child: child, stateId: stateId, ids: ids)
|
||||
StateBlock(child: child, ids: ids)
|
||||
}
|
||||
}
|
||||
|
||||
extension StateBlock: Equatable {
|
||||
public static func ==(lhs: StateBlock, rhs: StateBlock) -> Bool {
|
||||
lhs.child == rhs.child
|
||||
&& lhs.stateId == rhs.stateId
|
||||
&& lhs.ids == rhs.ids
|
||||
}
|
||||
}
|
||||
@@ -41,9 +37,8 @@ extension StateBlock: CustomDebugStringConvertible {
|
||||
|
||||
extension Block {
|
||||
public func addingStateBlock(
|
||||
stateId: String,
|
||||
ids: Set<String>
|
||||
) -> Block {
|
||||
StateBlock(child: self, stateId: stateId, ids: ids)
|
||||
StateBlock(child: self, ids: ids)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ extension DecoratingBlock {
|
||||
blurEffect: blurEffect,
|
||||
paddings: paddings,
|
||||
source: Variable { [weak self] in self },
|
||||
scheduler: scheduler,
|
||||
visibilityActions: visibilityActions,
|
||||
lastVisibleBounds: lastVisibleBounds,
|
||||
tooltips: tooltips,
|
||||
accessibility: accessibilityElement
|
||||
)
|
||||
@@ -101,7 +103,9 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT
|
||||
let blurEffect: BlurEffect?
|
||||
let paddings: EdgeInsets
|
||||
let source: Variable<AnyObject?>
|
||||
let scheduler: Scheduling?
|
||||
let visibilityActions: [VisibilityAction]
|
||||
let lastVisibleBounds: Property<CGRect>?
|
||||
let tooltips: [BlockTooltip]
|
||||
let accessibility: AccessibilityElement?
|
||||
|
||||
@@ -289,6 +293,7 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT
|
||||
guard model != self.model || observer !== self.observer else {
|
||||
return
|
||||
}
|
||||
let oldModel = self.model
|
||||
|
||||
if self.model?.tooltips.isEmpty ?? true, !model.tooltips.isEmpty {
|
||||
renderingDelegate?.tooltipAnchorViewAdded(anchorView: self)
|
||||
@@ -320,24 +325,28 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT
|
||||
model.actions?
|
||||
.forEach { applyAccessibility($0.accessibilityElement) }
|
||||
|
||||
visibilityActionPerformers = model.visibilityActions.isEmpty
|
||||
? nil
|
||||
: VisibilityActionPerformers(
|
||||
visibilityCheckParams: model.visibilityActions.map { visibilityAction in
|
||||
VisibilityCheckParam(
|
||||
requiredDuration: visibilityAction.requiredDuration,
|
||||
targetPercentage: visibilityAction.targetPercentage,
|
||||
limiter: visibilityAction.limiter,
|
||||
action: { [unowned self] in
|
||||
UIActionEvent(
|
||||
uiAction: visibilityAction.uiAction,
|
||||
originalSender: self
|
||||
).sendFrom(self)
|
||||
},
|
||||
type: visibilityAction.actionType
|
||||
)
|
||||
}
|
||||
)
|
||||
if oldModel?.visibilityActions != model.visibilityActions {
|
||||
visibilityActionPerformers = model.visibilityActions.isEmpty
|
||||
? nil
|
||||
: VisibilityActionPerformers(
|
||||
visibilityCheckParams: model.visibilityActions.map { visibilityAction in
|
||||
VisibilityCheckParam(
|
||||
requiredDuration: visibilityAction.requiredDuration,
|
||||
targetPercentage: visibilityAction.targetPercentage,
|
||||
limiter: visibilityAction.limiter,
|
||||
action: { [unowned self] in
|
||||
UIActionEvent(
|
||||
uiAction: visibilityAction.uiAction,
|
||||
originalSender: self
|
||||
).sendFrom(self)
|
||||
},
|
||||
type: visibilityAction.actionType
|
||||
)
|
||||
},
|
||||
lastVisibleBounds: model.lastVisibleBounds ?? Property(initialValue: .zero),
|
||||
scheduling: model.scheduler ?? TimerScheduler()
|
||||
)
|
||||
}
|
||||
|
||||
tapRecognizer.isEnabled = model.shouldHandleTap
|
||||
doubleTapRecognizer.isEnabled = model.shouldHandleDoubleTap
|
||||
|
||||
@@ -19,7 +19,6 @@ extension StateBlock {
|
||||
) {
|
||||
(view as! StateBlockView).configure(
|
||||
child: child,
|
||||
stateId: stateId,
|
||||
ids: Set(ids.map { BlockViewID(rawValue: $0) }),
|
||||
observer: observer,
|
||||
overscrollDelegate: overscrollDelegate,
|
||||
@@ -109,7 +108,6 @@ private final class StateBlockView: BlockView {
|
||||
private var subviewStorage = SubviewStorage(wrappedRenderingDelegate: nil, ids: [])
|
||||
private var childView: BlockView?
|
||||
private var stateId: String?
|
||||
private var isStateChanged = false
|
||||
|
||||
var effectiveBackgroundColor: UIColor? { childView?.effectiveBackgroundColor }
|
||||
|
||||
@@ -124,17 +122,11 @@ private final class StateBlockView: BlockView {
|
||||
|
||||
func configure(
|
||||
child: Block,
|
||||
stateId: String,
|
||||
ids: Set<BlockViewID>,
|
||||
observer: ElementStateObserver?,
|
||||
overscrollDelegate: ScrollDelegate?,
|
||||
renderingDelegate: RenderingDelegate?
|
||||
) {
|
||||
if (self.stateId != stateId) {
|
||||
isStateChanged = true
|
||||
self.stateId = stateId
|
||||
}
|
||||
|
||||
// remove views with unfinished animations
|
||||
subviews.forEach {
|
||||
if $0 !== childView {
|
||||
@@ -188,15 +180,6 @@ private final class StateBlockView: BlockView {
|
||||
}
|
||||
|
||||
extension StateBlockView: VisibleBoundsTrackingContainer {
|
||||
func onVisibleBoundsChanged(from: CGRect, to: CGRect) {
|
||||
if isStateChanged {
|
||||
isStateChanged = false
|
||||
passVisibleBoundsChanged(from: .zero, to: to)
|
||||
} else {
|
||||
passVisibleBoundsChanged(from: from, to: to)
|
||||
}
|
||||
}
|
||||
|
||||
var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] {
|
||||
childView.asArray()
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ final class VisibilityActionPerformer {
|
||||
limiter: ActionLimiter,
|
||||
action: @escaping Action,
|
||||
type: VisibilityActionType,
|
||||
timerScheduler: Scheduling = TimerScheduler()
|
||||
timerScheduler: Scheduling
|
||||
) {
|
||||
self.requiredDuration = requiredDuration
|
||||
self.targetPercentage = targetVisibilityPercentage
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
import BasePublic
|
||||
|
||||
public final class VisibilityActionPerformers {
|
||||
private let actionPerformers: [VisibilityActionPerformer]
|
||||
private let lastVisibleBounds: Property<CGRect>
|
||||
|
||||
init(visibilityCheckParams: [VisibilityCheckParam]) {
|
||||
actionPerformers = visibilityCheckParams.map(VisibilityActionPerformer.init)
|
||||
init(
|
||||
visibilityCheckParams: [VisibilityCheckParam],
|
||||
lastVisibleBounds: Property<CGRect>,
|
||||
scheduling: Scheduling
|
||||
) {
|
||||
self.lastVisibleBounds = lastVisibleBounds
|
||||
actionPerformers = visibilityCheckParams.map {
|
||||
VisibilityActionPerformer(visibilityCheckParam: $0, scheduling: scheduling)
|
||||
}
|
||||
}
|
||||
|
||||
func onVisibleBoundsChanged(from: CGRect, to: CGRect, bounds: CGRect) {
|
||||
let visibleAreaPercentageBefore = bounds.isEmpty ? 0 : Int(from.area * 100 / bounds.area)
|
||||
func onVisibleBoundsChanged(from _: CGRect, to: CGRect, bounds: CGRect) {
|
||||
let beforeVisibleBounds = lastVisibleBounds.value
|
||||
lastVisibleBounds.value = to
|
||||
|
||||
let visibleAreaPercentageBefore = bounds
|
||||
.isEmpty ? 0 : Int(beforeVisibleBounds.area * 100 / bounds.area)
|
||||
let visibleAreaPercentageAfter = bounds.isEmpty ? 0 : Int(to.area * 100 / bounds.area)
|
||||
|
||||
for performer in actionPerformers {
|
||||
@@ -22,13 +36,14 @@ public final class VisibilityActionPerformers {
|
||||
}
|
||||
|
||||
extension VisibilityActionPerformer {
|
||||
fileprivate convenience init(visibilityCheckParam: VisibilityCheckParam) {
|
||||
fileprivate convenience init(visibilityCheckParam: VisibilityCheckParam, scheduling: Scheduling) {
|
||||
self.init(
|
||||
requiredDuration: visibilityCheckParam.requiredDuration,
|
||||
targetVisibilityPercentage: visibilityCheckParam.targetPercentage,
|
||||
limiter: visibilityCheckParam.limiter,
|
||||
action: visibilityCheckParam.action,
|
||||
type: visibilityCheckParam.type
|
||||
type: visibilityCheckParam.type,
|
||||
timerScheduler: scheduling
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "text",
|
||||
"text": "text",
|
||||
"visibility": "gone",
|
||||
"visibility_actions": [
|
||||
{
|
||||
"log_id": "some",
|
||||
"log_limit": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "text",
|
||||
"text": "text",
|
||||
"visibility": "invisible",
|
||||
"visibility_actions": [
|
||||
{
|
||||
"log_id": "some",
|
||||
"log_limit": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "text",
|
||||
"text": "text",
|
||||
"visibility": "visibile",
|
||||
"visibility_actions": [
|
||||
{
|
||||
"log_id": "some",
|
||||
"log_limit": 0,
|
||||
"visibility_duration": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"log_id": "sample_card",
|
||||
"states": [
|
||||
{
|
||||
"state_id": 0,
|
||||
"div": {
|
||||
"type": "text",
|
||||
"text": "state zero",
|
||||
"visibility_action": {
|
||||
"log_id": "zero state visible",
|
||||
"log_limit": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"state_id": 1,
|
||||
"div": {
|
||||
"type": "text",
|
||||
"text": "state first",
|
||||
"visibility_action": {
|
||||
"log_id": "first state visible",
|
||||
"log_limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"type": "state",
|
||||
"id": "mystate",
|
||||
"states": [
|
||||
{
|
||||
"state_id": "first",
|
||||
"div": {
|
||||
"type": "text",
|
||||
"text": "state first",
|
||||
"visibility_action": {
|
||||
"log_id": "first state visible",
|
||||
"log_limit": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"state_id": "second",
|
||||
"div": {
|
||||
"type": "text",
|
||||
"text": "seconds first",
|
||||
"visibility_action": {
|
||||
"log_id": "second state visible",
|
||||
"log_limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||