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)
setContent {
val rotaryEventDispatcher = RotaryEventDispatcher()
CompositionLocalProvider(
LocalImageLoader provides imageLoader,
LocalRotaryEventDispatcher provides rotaryEventDispatcher,
) {
val navController = rememberSwipeDismissableNavController()
RotaryEventHandlerSetup(rotaryEventDispatcher)
SwipeDismissableNavHost(
navController = navController,
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.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -50,7 +51,6 @@ fun PersonDetailsScreen(personName: String) {
}
val scrollState = rememberScrollState()
RotaryEventState(scrollState)
PersonDetailsScreen(person, scrollState)
}
@ -60,6 +60,9 @@ private fun PersonDetailsScreen(
person: Assignment?,
scrollState: ScrollState = rememberScrollState(),
) {
// Activate scrolling
LocalView.current.requestFocus()
MaterialTheme {
Scaffold(
vignette = {
@ -71,6 +74,7 @@ private fun PersonDetailsScreen(
) {
Column(
modifier = Modifier
.scrollHandler(scrollState)
.fillMaxSize()
.padding(horizontal = if (LocalConfiguration.current.isScreenRound) 18.dp else 8.dp)
.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.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.contentDescription
@ -55,9 +56,7 @@ fun PersonListScreen(
peopleInSpaceViewModel: PeopleInSpaceViewModel = getViewModel()
) {
val peopleState by peopleInSpaceViewModel.peopleInSpace.collectAsState()
val scrollState = rememberScalingLazyListState()
RotaryEventState(scrollState)
PersonListScreen(peopleState, personSelected, issMapClick, scrollState)
PersonListScreen(peopleState, personSelected, issMapClick)
}
@OptIn(ExperimentalAnimationApi::class)
@ -66,7 +65,6 @@ fun PersonListScreen(
people: List<Assignment>?,
personSelected: (person: Assignment) -> Unit,
issMapClick: () -> Unit,
scrollState: ScalingLazyListState = rememberScalingLazyListState(),
) {
MaterialTheme {
AnimatedVisibility(
@ -75,7 +73,7 @@ fun PersonListScreen(
) {
if (people != null) {
if (people.isNotEmpty()) {
PersonList(people, personSelected, issMapClick, scrollState)
PersonList(people, personSelected, issMapClick)
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Card(
@ -98,15 +96,22 @@ fun PersonList(
issMapClick: () -> Unit,
scrollState: ScalingLazyListState = rememberScalingLazyListState(),
) {
// Activate scrolling
LocalView.current.requestFocus()
ScalingLazyColumn(
modifier = Modifier
.testTag(PersonListTag)
.scrollHandler(scrollState),
contentPadding = PaddingValues(8.dp),
modifier = Modifier.testTag(PersonListTag),
state = scrollState,
) {
item {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Button(
modifier = Modifier.size(ButtonDefaults.SmallButtonSize).wrapContentSize(),
modifier = Modifier
.size(ButtonDefaults.SmallButtonSize)
.wrapContentSize(),
onClick = issMapClick
) {
// https://www.svgrepo.com/svg/170716/international-space-station

View file

@ -4,72 +4,37 @@ import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
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.LocalView
import androidx.core.view.InputDeviceCompat
import androidx.core.view.MotionEventCompat
import androidx.core.view.ViewConfigurationCompat
import kotlinx.coroutines.launch
val LocalRotaryEventDispatcher = staticCompositionLocalOf<RotaryEventDispatcher> {
noLocalProvidedFor("LocalRotaryEventDispatcher")
}
/**
* 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
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.scrollHandler(scrollState: ScrollableState): Modifier = composed {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val scaledVerticalScrollFactor =
remember { ViewConfiguration.get(context).getScaledVerticalScrollFactor() }
view.requestFocus()
view.setOnGenericMotionListener { _, event ->
if (event?.action != MotionEvent.ACTION_SCROLL ||
this.pointerInteropFilter(RequestDisallowInterceptTouchEvent()) { event ->
if (event.action != MotionEvent.ACTION_SCROLL ||
!event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)
) {
return@setOnGenericMotionListener false
}
false
} else {
val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) *
ViewConfigurationCompat.getScaledVerticalScrollFactor(
ViewConfiguration.get(context), context
)
scaledVerticalScrollFactor
scope.launch {
rotaryEventDispatcher.onRotate(delta)
scrollState.scrollBy(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")
}