Simpler scrolling

This commit is contained in:
Yuri Schimke 2021-12-19 12:16:16 +00:00
parent d35f8e64fa
commit ee6408a5b0
4 changed files with 43 additions and 67 deletions

View file

@ -34,16 +34,11 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
val rotaryEventDispatcher = RotaryEventDispatcher()
CompositionLocalProvider( CompositionLocalProvider(
LocalImageLoader provides imageLoader, LocalImageLoader provides imageLoader,
LocalRotaryEventDispatcher provides rotaryEventDispatcher,
) { ) {
val navController = rememberSwipeDismissableNavController() val navController = rememberSwipeDismissableNavController()
RotaryEventHandlerSetup(rotaryEventDispatcher)
SwipeDismissableNavHost( SwipeDismissableNavHost(
navController = navController, navController = navController,
startDestination = Screen.PersonList.route startDestination = Screen.PersonList.route

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -50,7 +51,6 @@ fun PersonDetailsScreen(personName: String) {
} }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
RotaryEventState(scrollState)
PersonDetailsScreen(person, scrollState) PersonDetailsScreen(person, scrollState)
} }
@ -60,6 +60,9 @@ private fun PersonDetailsScreen(
person: Assignment?, person: Assignment?,
scrollState: ScrollState = rememberScrollState(), scrollState: ScrollState = rememberScrollState(),
) { ) {
// Activate scrolling
LocalView.current.requestFocus()
MaterialTheme { MaterialTheme {
Scaffold( Scaffold(
vignette = { vignette = {
@ -71,6 +74,7 @@ private fun PersonDetailsScreen(
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.scrollHandler(scrollState)
.fillMaxSize() .fillMaxSize()
.padding(horizontal = if (LocalConfiguration.current.isScreenRound) 18.dp else 8.dp) .padding(horizontal = if (LocalConfiguration.current.isScreenRound) 18.dp else 8.dp)
.verticalScroll(scrollState), .verticalScroll(scrollState),

View file

@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -23,8 +24,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
@ -55,9 +56,7 @@ fun PersonListScreen(
peopleInSpaceViewModel: PeopleInSpaceViewModel = getViewModel() peopleInSpaceViewModel: PeopleInSpaceViewModel = getViewModel()
) { ) {
val peopleState by peopleInSpaceViewModel.peopleInSpace.collectAsState() val peopleState by peopleInSpaceViewModel.peopleInSpace.collectAsState()
val scrollState = rememberScalingLazyListState() PersonListScreen(peopleState, personSelected, issMapClick)
RotaryEventState(scrollState)
PersonListScreen(peopleState, personSelected, issMapClick, scrollState)
} }
@OptIn(ExperimentalAnimationApi::class) @OptIn(ExperimentalAnimationApi::class)
@ -66,7 +65,6 @@ fun PersonListScreen(
people: List<Assignment>?, people: List<Assignment>?,
personSelected: (person: Assignment) -> Unit, personSelected: (person: Assignment) -> Unit,
issMapClick: () -> Unit, issMapClick: () -> Unit,
scrollState: ScalingLazyListState = rememberScalingLazyListState(),
) { ) {
MaterialTheme { MaterialTheme {
AnimatedVisibility( AnimatedVisibility(
@ -75,7 +73,7 @@ fun PersonListScreen(
) { ) {
if (people != null) { if (people != null) {
if (people.isNotEmpty()) { if (people.isNotEmpty()) {
PersonList(people, personSelected, issMapClick, scrollState) PersonList(people, personSelected, issMapClick)
} else { } else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Card( Card(
@ -98,15 +96,22 @@ fun PersonList(
issMapClick: () -> Unit, issMapClick: () -> Unit,
scrollState: ScalingLazyListState = rememberScalingLazyListState(), scrollState: ScalingLazyListState = rememberScalingLazyListState(),
) { ) {
// Activate scrolling
LocalView.current.requestFocus()
ScalingLazyColumn( ScalingLazyColumn(
modifier = Modifier
.testTag(PersonListTag)
.scrollHandler(scrollState),
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(8.dp),
modifier = Modifier.testTag(PersonListTag),
state = scrollState, state = scrollState,
) { ) {
item { item {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Button( Button(
modifier = Modifier.size(ButtonDefaults.SmallButtonSize).wrapContentSize(), modifier = Modifier
.size(ButtonDefaults.SmallButtonSize)
.wrapContentSize(),
onClick = issMapClick onClick = issMapClick
) { ) {
// https://www.svgrepo.com/svg/170716/international-space-station // https://www.svgrepo.com/svg/170716/international-space-station

View file

@ -1,51 +1,28 @@
package com.surrus.peopleinspace package com.surrus.peopleinspace
import android.content.Context
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewConfiguration import android.view.ViewConfiguration
import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.input.pointer.RequestDisallowInterceptTouchEvent
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.InputDeviceCompat import androidx.core.view.InputDeviceCompat
import androidx.core.view.MotionEventCompat import androidx.core.view.MotionEventCompat
import androidx.core.view.ViewConfigurationCompat import androidx.core.view.ViewConfigurationCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
val LocalRotaryEventDispatcher = staticCompositionLocalOf<RotaryEventDispatcher> { fun processEvent(context: Context, scope: CoroutineScope, event: MotionEvent, scrollState: ScrollableState): Boolean {
noLocalProvidedFor("LocalRotaryEventDispatcher") if (event.action != MotionEvent.ACTION_SCROLL ||
}
/**
* Dispatcher to link rotary event to [ScrollableState].
* The instance should be set up by calling [RotaryEventHandlerSetup] function.
*/
class RotaryEventDispatcher(
var scrollState: ScrollableState? = null
) {
suspend fun onRotate(delta: Float): Float? =
scrollState?.scrollBy(delta)
}
/**
* Custom rotary event setup (Currently, Column / LazyColumn doesn't handle rotary event.)
* Refer to https://developer.android.com/training/wearables/user-input/rotary-input
*/
@Composable
fun RotaryEventHandlerSetup(rotaryEventDispatcher: RotaryEventDispatcher) {
val view = LocalView.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
view.requestFocus()
view.setOnGenericMotionListener { _, event ->
if (event?.action != MotionEvent.ACTION_SCROLL ||
!event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER) !event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)
) { ) {
return@setOnGenericMotionListener false return false
} }
val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) * val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) *
@ -53,23 +30,18 @@ fun RotaryEventHandlerSetup(rotaryEventDispatcher: RotaryEventDispatcher) {
ViewConfiguration.get(context), context ViewConfiguration.get(context), context
) )
scope.launch { scope.launch {
rotaryEventDispatcher.onRotate(delta) scrollState.scrollBy(delta)
}
true
} }
return true
} }
/** @OptIn(ExperimentalComposeUiApi::class)
* Register a [ScrollableState] to [LocalRotaryEventDispatcher] fun Modifier.scrollHandler(scrollState: ScrollableState): Modifier = composed {
*/ val context = LocalContext.current
@Composable val scope = rememberCoroutineScope()
fun RotaryEventState(scrollState: ScrollableState?) {
val dispatcher = LocalRotaryEventDispatcher.current
SideEffect {
dispatcher.scrollState = scrollState
}
}
private fun noLocalProvidedFor(name: String): Nothing { this.pointerInteropFilter(RequestDisallowInterceptTouchEvent()) { event ->
error("CompositionLocal $name not present") println(event)
processEvent(context, scope, event, scrollState)
}
} }