Merge pull request #104 from yschimke/simpler_scroll

Simpler implementation of Wear scrolling
This commit is contained in:
John O'Reilly 2021-12-19 13:02:21 +00:00 committed by GitHub
commit 93aac4c617
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 38 additions and 69 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

@ -4,72 +4,37 @@ 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.remember
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 kotlinx.coroutines.launch import kotlinx.coroutines.launch
val LocalRotaryEventDispatcher = staticCompositionLocalOf<RotaryEventDispatcher> { @OptIn(ExperimentalComposeUiApi::class)
noLocalProvidedFor("LocalRotaryEventDispatcher") fun Modifier.scrollHandler(scrollState: ScrollableState): Modifier = composed {
}
/**
* 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 context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val scaledVerticalScrollFactor =
remember { ViewConfiguration.get(context).getScaledVerticalScrollFactor() }
view.requestFocus() this.pointerInteropFilter(RequestDisallowInterceptTouchEvent()) { event ->
view.setOnGenericMotionListener { _, event -> if (event.action != MotionEvent.ACTION_SCROLL ||
if (event?.action != MotionEvent.ACTION_SCROLL ||
!event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER) !event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)
) { ) {
return@setOnGenericMotionListener false false
} else {
val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) *
scaledVerticalScrollFactor
scope.launch {
scrollState.scrollBy(delta)
}
true
} }
val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) *
ViewConfigurationCompat.getScaledVerticalScrollFactor(
ViewConfiguration.get(context), context
)
scope.launch {
rotaryEventDispatcher.onRotate(delta)
}
true
} }
} }
/**
* Register a [ScrollableState] to [LocalRotaryEventDispatcher]
*/
@Composable
fun RotaryEventState(scrollState: ScrollableState?) {
val dispatcher = LocalRotaryEventDispatcher.current
SideEffect {
dispatcher.scrollState = scrollState
}
}
private fun noLocalProvidedFor(name: String): Nothing {
error("CompositionLocal $name not present")
}