Merge pull request #104 from yschimke/simpler_scroll
Simpler implementation of Wear scrolling
This commit is contained in:
commit
93aac4c617
4 changed files with 38 additions and 69 deletions
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
Loading…
Reference in a new issue