Add tap gestures support in lib, rework settings popup

This commit is contained in:
Oleksandr Balan
2022-07-24 16:50:44 +02:00
parent 840093c2e7
commit faa198f7b0
9 changed files with 280 additions and 126 deletions

2
.gitignore vendored
View File

@@ -8,6 +8,8 @@
/.idea/workspace.xml /.idea/workspace.xml
/.idea/navEditor.xml /.idea/navEditor.xml
/.idea/assetWizardSettings.xml /.idea/assetWizardSettings.xml
/.idea/misc.xml
/.idea/deploymentTargetDropDown.xml
.DS_Store .DS_Store
/build /build
/captures /captures

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -22,12 +23,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.center
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.toOffset
import eu.wewox.pagecurl.components.SettingsAction
import eu.wewox.pagecurl.components.SettingsPopup import eu.wewox.pagecurl.components.SettingsPopup
import eu.wewox.pagecurl.components.overlayControls import eu.wewox.pagecurl.config.InteractionConfig
import eu.wewox.pagecurl.config.PageCurlConfig import eu.wewox.pagecurl.config.PageCurlConfig
import eu.wewox.pagecurl.config.copy
import eu.wewox.pagecurl.page.PageCurl import eu.wewox.pagecurl.page.PageCurl
import eu.wewox.pagecurl.page.PageCurlState
import eu.wewox.pagecurl.page.rememberPageCurlState import eu.wewox.pagecurl.page.rememberPageCurlState
import eu.wewox.pagecurl.ui.theme.PageCurlTheme import eu.wewox.pagecurl.ui.theme.PageCurlTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -38,28 +44,28 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
PageCurlTheme { PageCurlTheme {
Box { Box {
val scope = rememberCoroutineScope()
val state = rememberPageCurlState(max = 6) val state = rememberPageCurlState(max = 6)
var showPopup by remember { mutableStateOf(false) } var showPopup by remember { mutableStateOf(false) }
var interaction by remember {
mutableStateOf(
InteractionConfig(
tap = InteractionConfig.Tap(
custom = InteractionConfig.Tap.CustomInteraction(true) { size, position ->
if ((position - size.center.toOffset()).getDistance() < 64.dp.toPx()) {
showPopup = true
true
} else {
false
}
}
)
)
)
}
PageCurl( PageCurl(
state = state, state = state,
config = PageCurlConfig(), config = PageCurlConfig(interaction = interaction)
modifier = Modifier.overlayControls(
next = {
scope.launch {
state.next()
}
},
prev = {
scope.launch {
state.prev()
}
},
center = {
showPopup = true
}
)
) { index -> ) { index ->
Box( Box(
modifier = Modifier modifier = Modifier
@@ -96,17 +102,10 @@ class MainActivity : ComponentActivity() {
if (showPopup) { if (showPopup) {
SettingsPopup( SettingsPopup(
onSnapToFirst = { state = state,
scope.launch { interaction = interaction,
state.snapTo(0) onConfigChange = {
showPopup = false interaction = it
}
},
onSnapToLast = {
scope.launch {
state.snapTo(state.max - 1)
showPopup = false
}
}, },
onDismiss = { onDismiss = {
showPopup = false showPopup = false
@@ -119,6 +118,46 @@ class MainActivity : ComponentActivity() {
} }
} }
@Composable
private fun SettingsPopup(
state: PageCurlState,
interaction: InteractionConfig,
onConfigChange: (InteractionConfig) -> Unit,
onDismiss: () -> Unit,
) {
val scope = rememberCoroutineScope()
SettingsPopup(
interactionConfig = interaction,
onAction = { action ->
when (action) {
SettingsAction.GoToFirst -> {
scope.launch {
state.snapTo(0)
}
}
SettingsAction.GoToLast -> {
scope.launch {
state.snapTo(state.max - 1)
}
}
is SettingsAction.ForwardDragEnabled -> {
onConfigChange(interaction.copy(dragForwardEnabled = action.value))
}
is SettingsAction.BackwardDragEnabled -> {
onConfigChange(interaction.copy(dragBackwardEnabled = action.value))
}
is SettingsAction.ForwardTapEnabled -> {
onConfigChange(interaction.copy(tapForwardEnabled = action.value))
}
is SettingsAction.BackwardTapEnabled -> {
onConfigChange(interaction.copy(tapBackwardEnabled = action.value))
}
}
},
onDismiss = onDismiss
)
}
private object Data { private object Data {
val Lorem1 = val Lorem1 =
"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam erat volutpat. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. Et harum quidem rerum facilis est et expedita distinctio. In sem justo, commodo ut, suscipit at, pharetra vitae, orci. Vestibulum fermentum tortor id mi. Sed elit dui, pellentesque a, faucibus vel, interdum nec, diam. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. Nunc tincidunt ante vitae massa. Maecenas fermentum, sem in pharetra pellentesque, velit turpis volutpat ante, in pharetra metus odio a lectus. Proin in tellus sit amet nibh dignissim sagittis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Etiam posuere lacus quis dolor. Class aptent taciti sociosqu ad litora." "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam erat volutpat. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. Et harum quidem rerum facilis est et expedita distinctio. In sem justo, commodo ut, suscipit at, pharetra vitae, orci. Vestibulum fermentum tortor id mi. Sed elit dui, pellentesque a, faucibus vel, interdum nec, diam. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. Nunc tincidunt ante vitae massa. Maecenas fermentum, sem in pharetra pellentesque, velit turpis volutpat ante, in pharetra metus odio a lectus. Proin in tellus sit amet nibh dignissim sagittis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Etiam posuere lacus quis dolor. Class aptent taciti sociosqu ad litora."

View File

@@ -1,37 +0,0 @@
package eu.wewox.pagecurl.components
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.center
import androidx.compose.ui.unit.toOffset
internal fun Modifier.overlayControls(
next: () -> Unit,
prev: () -> Unit,
center: () -> Unit,
): Modifier = pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
val down = awaitFirstDown().also { it.consume() }
val up = waitForUpOrCancellation() ?: return@awaitPointerEventScope
up.consume()
if ((down.position - up.position).getDistance() > viewConfiguration.touchSlop) {
return@awaitPointerEventScope
}
if ((up.position - size.center.toOffset()).getDistance() < 100f) {
center()
return@awaitPointerEventScope
}
if (up.position.x < size.width / 2) {
prev()
} else {
next()
}
}
}
}

View File

@@ -1,11 +1,17 @@
@file:OptIn(ExperimentalPageCurlApi::class)
package eu.wewox.pagecurl.components package eu.wewox.pagecurl.components
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme import androidx.compose.material.Switch
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextButton import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -13,43 +19,95 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import eu.wewox.pagecurl.ExperimentalPageCurlApi
import eu.wewox.pagecurl.config.InteractionConfig
internal sealed interface SettingsAction {
object GoToFirst : SettingsAction
object GoToLast : SettingsAction
class ForwardDragEnabled(val value: Boolean) : SettingsAction
class BackwardDragEnabled(val value: Boolean) : SettingsAction
class ForwardTapEnabled(val value: Boolean) : SettingsAction
class BackwardTapEnabled(val value: Boolean) : SettingsAction
}
@Composable @Composable
internal fun SettingsPopup( internal fun SettingsPopup(
onSnapToFirst: () -> Unit, interactionConfig: InteractionConfig,
onSnapToLast: () -> Unit, onAction: (SettingsAction) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
Popup( Popup(
alignment = Alignment.Center, alignment = Alignment.Center,
properties = PopupProperties(focusable = true),
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
) { ) {
Card( Card(
shape = RoundedCornerShape(24.dp), shape = RoundedCornerShape(24.dp),
backgroundColor = MaterialTheme.colors.primary,
elevation = 16.dp, elevation = 16.dp,
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(8.dp) modifier = Modifier
.width(IntrinsicSize.Max)
.padding(8.dp)
) { ) {
TextButton( TextButton(onClick = { onAction(SettingsAction.GoToFirst) }) {
onClick = onSnapToFirst Text("Go to first")
) {
Text(
text = "Go to first",
color = MaterialTheme.colors.onPrimary
)
} }
TextButton( TextButton(onClick = { onAction(SettingsAction.GoToLast) }) {
onClick = onSnapToLast Text("Go to last")
) { }
Text(
text = "Go to last", val switchRowModifier = Modifier
color = MaterialTheme.colors.onPrimary .fillMaxWidth()
.padding(horizontal = 10.dp)
SwitchRow(
text = "Forward drag enabled",
enabled = interactionConfig.drag.forward.enabled,
onChanged = { onAction(SettingsAction.ForwardDragEnabled(it)) },
modifier = switchRowModifier
)
SwitchRow(
text = "Backward drag enabled",
enabled = interactionConfig.drag.backward.enabled,
onChanged = { onAction(SettingsAction.BackwardDragEnabled(it)) },
modifier = switchRowModifier
)
SwitchRow(
text = "Forward tap enabled",
enabled = interactionConfig.tap.forward.enabled,
onChanged = { onAction(SettingsAction.ForwardTapEnabled(it)) },
modifier = switchRowModifier
)
SwitchRow(
text = "Backward tap enabled",
enabled = interactionConfig.tap.backward.enabled,
onChanged = { onAction(SettingsAction.BackwardTapEnabled(it)) },
modifier = switchRowModifier
) )
} }
} }
} }
} }
@Composable
private fun SwitchRow(
text: String,
enabled: Boolean,
onChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
Text(text = text)
Switch(
checked = enabled,
onCheckedChange = onChanged
)
}
} }

View File

@@ -3,28 +3,17 @@ package eu.wewox.pagecurl.config
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.wewox.pagecurl.ExperimentalPageCurlApi import eu.wewox.pagecurl.ExperimentalPageCurlApi
@ExperimentalPageCurlApi @ExperimentalPageCurlApi
public data class PageCurlConfig( public data class PageCurlConfig(
val curl: CurlConfig = CurlConfig(), val curl: CurlConfig = CurlConfig(),
val direction: PageCurlDirection = PageCurlDirection.StartToEnd, val interaction: InteractionConfig = InteractionConfig(),
val interaction: InteractionConfig = InteractionConfig(forward = direction.forward()),
)
@ExperimentalPageCurlApi
public data class InteractionConfig(
val forward: DragDirection,
val backward: DragDirection = DragDirection(forward.end, forward.start),
)
@ExperimentalPageCurlApi
public data class DragDirection(
val start: Rect,
val end: Rect,
) )
@ExperimentalPageCurlApi @ExperimentalPageCurlApi
@@ -48,18 +37,60 @@ public data class ShadowConfig(
) )
@ExperimentalPageCurlApi @ExperimentalPageCurlApi
public enum class PageCurlDirection { public data class InteractionConfig(
StartToEnd, val drag: Drag = Drag(),
// TODO (Alex) Add support for reversed end-to-start direction val tap: Tap = Tap(),
// EndToStart, ) {
@ExperimentalPageCurlApi
public data class Drag(
val forward: Interaction = Interaction(true, rightHalf(), leftHalf()),
val backward: Interaction = Interaction(true, forward.end, forward.start),
) {
@ExperimentalPageCurlApi
public data class Interaction(
val enabled: Boolean,
val start: Rect = Rect.Zero,
val end: Rect = Rect.Zero,
)
} }
private fun left(): Rect = Rect(Offset(0.0f, 0.0f), Offset(0.5f, 1.0f))
private fun right(): Rect = Rect(Offset(0.5f, 0.0f), Offset(1.0f, 1.0f))
@ExperimentalPageCurlApi @ExperimentalPageCurlApi
private fun PageCurlDirection.forward(): DragDirection = public data class Tap(
when (this) { val forward: Interaction = Interaction(true, rightHalf()),
PageCurlDirection.StartToEnd -> DragDirection(right(), left()) val backward: Interaction = Interaction(true, leftHalf()),
val custom: CustomInteraction = CustomInteraction(false)
) {
@ExperimentalPageCurlApi
public data class Interaction(
val enabled: Boolean,
val target: Rect = Rect.Zero,
)
@ExperimentalPageCurlApi
public data class CustomInteraction(
val enabled: Boolean,
val onTap: Density.(IntSize, Offset) -> Boolean = { _, _ -> false },
)
} }
}
@ExperimentalPageCurlApi
public fun InteractionConfig.copy(
dragForwardEnabled: Boolean = drag.forward.enabled,
dragBackwardEnabled: Boolean = drag.backward.enabled,
tapForwardEnabled: Boolean = tap.forward.enabled,
tapBackwardEnabled: Boolean = tap.backward.enabled,
): InteractionConfig = copy(
drag = drag.copy(
forward = drag.forward.copy(enabled = dragForwardEnabled),
backward = drag.backward.copy(enabled = dragBackwardEnabled)
),
tap = tap.copy(
forward = tap.forward.copy(enabled = tapForwardEnabled),
backward = tap.backward.copy(enabled = tapBackwardEnabled)
)
)
private fun leftHalf(): Rect = Rect(0.0f, 0.0f, 0.5f, 1.0f)
private fun rightHalf(): Rect = Rect(0.5f, 0.0f, 1.0f, 1.0f)

View File

@@ -10,12 +10,11 @@ import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.unit.IntSize
import eu.wewox.pagecurl.ExperimentalPageCurlApi import eu.wewox.pagecurl.ExperimentalPageCurlApi
import eu.wewox.pagecurl.config.DragDirection import eu.wewox.pagecurl.config.InteractionConfig
import eu.wewox.pagecurl.utils.multiply
import eu.wewox.pagecurl.utils.rotate import eu.wewox.pagecurl.utils.rotate
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -26,7 +25,7 @@ internal fun Modifier.curlGesture(
state: PageCurlState.InternalState, state: PageCurlState.InternalState,
enabled: Boolean, enabled: Boolean,
scope: CoroutineScope, scope: CoroutineScope,
direction: DragDirection, direction: InteractionConfig.Drag.Interaction,
start: Edge, start: Edge,
end: Edge, end: Edge,
edge: Animatable<Edge, AnimationVector4D>, edge: Animatable<Edge, AnimationVector4D>,
@@ -68,7 +67,7 @@ internal fun Modifier.curlGesture(
internal fun Modifier.curlGesture( internal fun Modifier.curlGesture(
key: Any?, key: Any?,
enabled: Boolean, enabled: Boolean,
direction: DragDirection, direction: InteractionConfig.Drag.Interaction,
onStart: () -> Unit, onStart: () -> Unit,
onCurl: (Offset, Offset) -> Unit, onCurl: (Offset, Offset) -> Unit,
onEnd: () -> Unit, onEnd: () -> Unit,
@@ -127,9 +126,3 @@ internal fun Modifier.curlGesture(
} }
} }
} }
private fun Rect.multiply(size: IntSize): Rect =
Rect(
topLeft = Offset(size.width * left, size.height * top),
bottomRight = Offset(size.width * right, size.height * bottom),
)

View File

@@ -30,9 +30,9 @@ public fun PageCurl(
Modifier Modifier
.curlGesture( .curlGesture(
state = internalState, state = internalState,
enabled = updatedCurrent < state.max - 1, enabled = config.interaction.drag.forward.enabled && updatedCurrent < state.max - 1,
scope = scope, scope = scope,
direction = config.interaction.forward, direction = config.interaction.drag.forward,
start = internalState.rightEdge, start = internalState.rightEdge,
end = internalState.leftEdge, end = internalState.leftEdge,
edge = internalState.forward, edge = internalState.forward,
@@ -40,14 +40,21 @@ public fun PageCurl(
) )
.curlGesture( .curlGesture(
state = internalState, state = internalState,
enabled = updatedCurrent > 0, enabled = config.interaction.drag.backward.enabled && updatedCurrent > 0,
scope = scope, scope = scope,
direction = config.interaction.backward, direction = config.interaction.drag.backward,
start = internalState.leftEdge, start = internalState.leftEdge,
end = internalState.rightEdge, end = internalState.rightEdge,
edge = internalState.backward, edge = internalState.backward,
onChange = { state.current = updatedCurrent - 1 } onChange = { state.current = updatedCurrent - 1 }
) )
.tapGesture(
state = internalState,
scope = scope,
interaction = config.interaction.tap,
onTapForward = state::next,
onTapBackward = state::prev,
)
) { ) {
if (updatedCurrent + 1 < state.max) { if (updatedCurrent + 1 < state.max) {
content(updatedCurrent + 1) content(updatedCurrent + 1)

View File

@@ -0,0 +1,50 @@
package eu.wewox.pagecurl.page
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import eu.wewox.pagecurl.ExperimentalPageCurlApi
import eu.wewox.pagecurl.config.InteractionConfig
import eu.wewox.pagecurl.utils.multiply
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ExperimentalPageCurlApi
internal fun Modifier.tapGesture(
state: PageCurlState.InternalState,
scope: CoroutineScope,
interaction: InteractionConfig.Tap,
onTapForward: suspend () -> Unit,
onTapBackward: suspend () -> Unit,
): Modifier = pointerInput(interaction, state) {
forEachGesture {
awaitPointerEventScope {
val down = awaitFirstDown().also { it.consume() }
val up = waitForUpOrCancellation() ?: return@awaitPointerEventScope
if ((down.position - up.position).getDistance() > viewConfiguration.touchSlop) {
return@awaitPointerEventScope
}
if (interaction.custom.enabled && interaction.custom.onTap(this, size, up.position)) {
return@awaitPointerEventScope
}
if (interaction.forward.enabled && interaction.forward.target.multiply(size).contains(up.position)) {
scope.launch {
onTapForward()
}
return@awaitPointerEventScope
}
if (interaction.backward.enabled && interaction.backward.target.multiply(size).contains(up.position)) {
scope.launch {
onTapBackward()
}
return@awaitPointerEventScope
}
}
}
}

View File

@@ -0,0 +1,11 @@
package eu.wewox.pagecurl.utils
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.unit.IntSize
internal fun Rect.multiply(size: IntSize): Rect =
Rect(
topLeft = Offset(size.width * left, size.height * top),
bottomRight = Offset(size.width * right, size.height * bottom),
)