Skip to content

Instantly share code, notes, and snippets.

@ElianFabian
Last active May 12, 2024 18:37
Show Gist options
  • Save ElianFabian/e7147b3c4c115a428a0fbb2dd1c224fd to your computer and use it in GitHub Desktop.
Save ElianFabian/e7147b3c4c115a428a0fbb2dd1c224fd to your computer and use it in GitHub Desktop.
Several View extension functions.
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
fun <K : Any> RecyclerView.itemVisibilityFlow(
itemKey: K,
getItemKey: (position: Int) -> K?,
): Flow<Boolean> {
return callbackFlow {
val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val layoutManager = recyclerView.layoutManager
check(layoutManager is LinearLayoutManager) {
"To use RecyclerView.itemVisibilityFlow RecyclerView must have a LinearLayoutManager."
}
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
var isVisible = false
for (position in firstVisiblePosition..lastVisiblePosition) {
val currentItemKey = getItemKey(position) ?: continue
isVisible = currentItemKey == itemKey
if (isVisible) {
break
}
}
trySend(isVisible)
}
}
addOnScrollListener(scrollListener)
awaitClose {
removeOnScrollListener(scrollListener)
}
}.distinctUntilChanged()
}
/**
* Source: https://proandroiddev.com/horizontal-recyclerview-within-viewpager2-1f49f366d54e
*/
fun RecyclerView.avoidConflictsWithHorizontalViewPager() {
addOnItemTouchListener(
object : RecyclerView.OnItemTouchListener {
private var startX = 0f
override fun onInterceptTouchEvent(
recyclerView: RecyclerView,
event: MotionEvent,
): Boolean =
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startX = event.x
}
MotionEvent.ACTION_MOVE -> {
val isScrollingRight = event.x < startX
val scrollItemsToRight = isScrollingRight && recyclerView.canScrollHorizontally(1)
val scrollItemsToLeft = !isScrollingRight && recyclerView.canScrollHorizontally(-1)
val disallowIntercept = scrollItemsToRight || scrollItemsToLeft
recyclerView.parent.requestDisallowInterceptTouchEvent(disallowIntercept)
}
MotionEvent.ACTION_UP -> {
startX = 0f
}
else -> Unit
}.let { false }
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) = Unit
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) = Unit
}
)
}
suspend fun RecyclerView.awaitScrollState(
scrollState: Int,
) {
// If a smooth scroll has just been started, it won't actually start until the next
// animation frame, so we'll await that first
awaitAnimationFrame()
// Now we can check if we're actually idle. If so, return now
if (this.scrollState == scrollState) return
callbackFlow<Unit> {
val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == scrollState) {
// Make sure we remove the listener so we don't keep leak the
// coroutine continuation
recyclerView.removeOnScrollListener(this)
// Finally, resume the coroutine
trySend(Unit)
}
}
}
addOnScrollListener(scrollListener)
awaitClose {
removeOnScrollListener(scrollListener)
}
}
}
suspend inline fun RecyclerView.awaitScrollEnd() {
awaitScrollState(RecyclerView.SCROLL_STATE_IDLE)
}
inline val LinearLayoutManager.visibleItemCount: Int
get() {
val firstVisiblePosition = findFirstVisibleItemPosition()
val lastVisiblePosition = findLastVisibleItemPosition()
return lastVisiblePosition - firstVisiblePosition + 1
}
inline val RecyclerView.visibleItemCount: Int
get() {
val layoutManager = layoutManager
require(layoutManager is LinearLayoutManager) {
"To use RecyclerView.visibleItemCount RecyclerView must have a LinearLayoutManager."
}
return layoutManager.visibleItemCount
}
import android.view.View
import kotlinx.coroutines.suspendCancellableCoroutine
// Source: https://github.com/chrisbanes/tivi/blob/ee7c5f9870cb4c5ce0b5c2d2ba18e538cefc1254/common-ui-view/src/main/java/app/tivi/extensions/ViewExtensions.kt
suspend fun View.awaitAnimationFrame() {
suspendCancellableCoroutine<Unit> { cont ->
val runnable = Runnable {
cont.resumeWith(Result.success(Unit))
}
// If the coroutine is cancelled, remove the callback
cont.invokeOnCancellation { removeCallbacks(runnable) }
// And finally post the runnable
postOnAnimation(runnable)
}
}
suspend fun View.awaitPost() {
suspendCancellableCoroutine<Unit> { cont ->
val runnable = Runnable {
cont.resumeWith(Result.success(Unit))
}
cont.invokeOnCancellation { removeCallbacks(runnable) }
post(runnable)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment