Skip to content

Commit

Permalink
Merge pull request #2 from erolaksoy/FixVisibilityCalculation
Browse files Browse the repository at this point in the history
Fix visibility calculation and update compose versions
  • Loading branch information
erolaksoy authored Mar 6, 2024
2 parents 0bf2a13 + c7a65d5 commit d3e7770
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 29 deletions.
2 changes: 1 addition & 1 deletion buildSrc/src/main/java/AppVersions.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const val PUBLISHING_GROUP = "com.erolaksoy"
const val PUBLISHING_ARTIFACT_ID = "compose-impression"
const val VERSION = "0.1"
const val VERSION = "0.1.1"
29 changes: 14 additions & 15 deletions gradle/libraries.versions.toml
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
[versions]
agp = "8.0.2"
androidx_activity_compose = "1.7.2"
agp = "8.2.2"
androidx_activity_compose = "1.8.2"
androidx_test = "1.5.0"
androidx_test_ext = "1.1.5"
appcompat = "1.6.1"
compile_sdk_version = "33"
compose = "1.4.3"
compose_compilerextension = "1.4.5"
constraint_layout = "2.1.4"
compile_sdk_version = "34"
compose = "1.6.2"
compose_compilerextension = "1.5.1"
core_ktx = "1.10.1"
detekt = "1.23.0"
dokka = "1.8.10"
espresso_core = "3.5.1"
ksp = "1.8.20-1.0.11"
junit = "4.13.2"
kotlin = "1.8.20"
kotlin = "1.9.0"
ktlint = "0.45.2"
ktlint_gradle = "10.2.1"
min_sdk_version = "21"
target_sdk_version = "33"
min_sdk_version = "23"
target_sdk_version = "34"

[libraries]
junit = { module = "junit:junit", version.ref = "junit" }
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "androidx_activity_compose" }
androidx_appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx_core_ktx = { module = "androidx.core:core-ktx", version.ref = "core.ktx" }
androidx_test_rules = { module = "androidx.test:rules", version.ref = "androidx.test" }
androidx_test_runner = { module = "androidx.test:runner", version.ref = "androidx.test" }
androidx_test_ext_junit = { module = "androidx.test.ext:junit", version.ref = "androidx.test.ext" }
androidx_test_ext_junit_ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx.test.ext" }
androidx_test_rules = { module = "androidx.test:rules", version.ref = "androidx_test" }
androidx_test_runner = { module = "androidx.test:runner", version.ref = "androidx_test" }
androidx_test_ext_junit = { module = "androidx.test.ext:junit", version.ref = "androidx_test_ext" }
androidx_test_ext_junit_ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx_test_ext" }
compose_material = { module = "androidx.compose.material:material", version.ref = "compose" }
compose_foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
compose_ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
compose_ui_tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose_ui_test_junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }
compose_ui_test_manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" }
detekt_formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
espressoCore = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso.core" }
espressoCore = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso_core" }
agp = { module = "com.android.tools.build:gradle", version.ref = "agp" }
kgp = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
dokkaCore = { module = "org.jetbrains.dokka:dokka-core", version.ref = "dokka" }
Expand All @@ -46,6 +45,6 @@ kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-pl
[plugins]
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
dokka = { id = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint.gradle" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint_gradle" }
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Tue Jun 06 20:46:26 TRT 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
51 changes: 41 additions & 10 deletions impression/src/main/java/com/erolaksoy/impression/Impression.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package com.erolaksoy.impression

import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onGloballyPositioned
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
Expand All @@ -25,15 +25,34 @@ fun <T : Any> Modifier.impression(

val coroutineScope = rememberCoroutineScope()

LaunchedEffect(key) {
impressionState.seenEvent.collect {
if (key == it) {
onImpressionHappened(key)
DisposableEffect(key) {
val job = coroutineScope.launch {
impressionState.seenEvent.collect {
if (key == it) {
onImpressionHappened(key)
}
}
}
onDispose { job.cancel() }
}

onPlaced {
onGloballyPositioned {
coroutineScope.launch {
if (it.isAttached.not()) return@launch
impressionState.onItemPlaced(key = key)
}
}
}

@Stable
fun <T : Any> Modifier.impression(
key: T,
impressionState: ImpressionState,
) = composed {

val coroutineScope = rememberCoroutineScope()

onGloballyPositioned {
coroutineScope.launch {
if (it.isAttached.not()) return@launch
impressionState.onItemPlaced(key = key)
Expand All @@ -44,13 +63,17 @@ fun <T : Any> Modifier.impression(
@Composable
fun rememberImpressionState(
lazyListState: LazyListState,
block: ImpressionState.() -> Unit = {},
block: ImpressionState.() -> Unit = defaultImpressionStateBlock,
): ImpressionState {
return remember {
ImpressionStateImpl(lazyListState).apply(block)
}
}

internal val defaultImpressionStateBlock: ImpressionState.() -> Unit = {
addValidator(VisibilityPercentImpressionValidator())
}

class ImpressionStateImpl(private val state: LazyListState) : ImpressionState {
private val _seenEvent = MutableSharedFlow<Any>(extraBufferCapacity = 1)
override val seenEvent: SharedFlow<Any> = _seenEvent.asSharedFlow()
Expand All @@ -63,8 +86,12 @@ class ImpressionStateImpl(private val state: LazyListState) : ImpressionState {
override suspend fun onItemPlaced(key: Any) = withContext(Dispatchers.Default) {
if (_recordedItems.contains(key)) return@withContext

val filteredList = state.layoutInfo.visibleItemsInfo.filter {
_validators.any { it.isValid(state, key) }
val visibleItemsInfo = state.layoutInfo.visibleItemsInfo

val filteredList = if (_validators.isEmpty()) {
visibleItemsInfo
} else {
visibleItemsInfo.filter { _validators.any { it.isValid(state, key) } }
}

val isItemInList = filteredList.any { it.key == key }
Expand All @@ -79,6 +106,10 @@ class ImpressionStateImpl(private val state: LazyListState) : ImpressionState {
_validators.add(validator)
}

override fun clearValidators() {
_validators.clear()
}

override fun clearAll() {
_recordedItems.clear()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ interface ImpressionState {
*/
fun addValidator(validator: ImpressionValidator)

/**
* Removes all validators from the impression state.
*/
fun clearValidators()

/**
* Clears all state from the impression state.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState

class VisibilityPercentImpressionValidator(
@FloatRange(0.0, 1.0) private val visibilityPercentThreshold: Float,
@FloatRange(0.0, 1.0) private val visibilityPercentThreshold: Float = VISIBILITY_THRESHOLD,
) : ImpressionValidator {

override suspend fun isValid(state: LazyListState, key: Any): Boolean {
Expand All @@ -23,6 +23,11 @@ class VisibilityPercentImpressionValidator(
private fun LazyListState.visibilityPercent(itemInfo: LazyListItemInfo): Float {
val start = (layoutInfo.viewportStartOffset - itemInfo.offset).coerceAtLeast(0)
val end = (itemInfo.offset + itemInfo.size - layoutInfo.viewportEndOffset).coerceAtLeast(0)
return (1f - (start + end).toFloat() / itemInfo.size).coerceAtLeast(0f)
val visibleArea = maxOf(0f, (start + end).toFloat())
return 1f - (visibleArea / itemInfo.size.toFloat())
}

companion object {
private const val VISIBILITY_THRESHOLD = 0.5F
}
}

0 comments on commit d3e7770

Please sign in to comment.