Last active
May 12, 2024 18:37
-
-
Save ElianFabian/e7147b3c4c115a428a0fbb2dd1c224fd to your computer and use it in GitHub Desktop.
Several View extension functions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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