Skip to content

Instantly share code, notes, and snippets.

@laggedHero
Created January 25, 2021 13:23
Show Gist options
  • Save laggedHero/cc7580ceb69bf65d918f6a5576e08d04 to your computer and use it in GitHub Desktop.
Save laggedHero/cc7580ceb69bf65d918f6a5576e08d04 to your computer and use it in GitHub Desktop.
Some crude "pin view" using compose.
import androidx.compose.animation.core.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.unit.dp
/**
* This is heavily based (copied) from compose-ui's cursor modifier
*/
internal class PinInputTextFieldState {
var textLayoutResult: TextLayoutResult? = null
var text: String? = null
var hasFocus = false
}
internal fun Modifier.pinCursor(
state: PinInputTextFieldState,
cursorColor: Color
) = composed {
val animationClocks = AmbientAnimationClock.current
val cursorAlpha = remember(animationClocks) { AnimatedFloatModel(0f, animationClocks) }
if (state.hasFocus && cursorColor != Color.Unspecified) {
onCommit(cursorColor, state.text) {
cursorAlpha.animateTo(0f, anim = cursorAnimationSpec)
onDispose {
cursorAlpha.snapTo(0f)
}
}
drawWithContent {
this.drawContent()
val cursorAlphaValue = cursorAlpha.value.coerceIn(0f, 1f)
if (cursorAlphaValue != 0f) {
val transformedOffset = state.text?.length ?: 0
val cursorRect = state.textLayoutResult?.getCursorRect(transformedOffset)
?: Rect(0f, 0f, 0f, 0f)
val cursorWidth = DefaultCursorThickness.toPx()
val cursorX = (cursorRect.left + cursorWidth / 2)
.coerceAtMost(size.width - cursorWidth / 2)
drawLine(
cursorColor,
Offset(cursorX, cursorRect.top),
Offset(cursorX, cursorRect.bottom),
alpha = cursorAlphaValue,
strokeWidth = cursorWidth
)
}
}
} else {
Modifier
}
}
private class AnimatedFloatModel(
initialValue: Float,
clock: AnimationClockObservable,
visibilityThreshold: Float = Spring.DefaultDisplacementThreshold
) : AnimatedFloat(clock, visibilityThreshold) {
override var value: Float by mutableStateOf(initialValue, structuralEqualityPolicy())
}
private val cursorAnimationSpec: AnimationSpec<Float>
get() = infiniteRepeatable(
animation = keyframes {
durationMillis = 1000
1f at 0
1f at 499
0f at 500
0f at 999
}
)
private val DefaultCursorThickness = 2.dp
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.gesture.tapGestureFilter
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.AmbientFontLoader
import androidx.compose.ui.platform.AmbientTextInputService
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.*
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun PinInput() {
val focusRequester = FocusRequester()
// Ambient
val textInputService = AmbientTextInputService.current
val density = AmbientDensity.current
val resourceLoader = AmbientFontLoader.current
// Could become a single "state"
val pinCode = remember { mutableStateOf(Array(5) { "" }) }
val (isFocused, setIsFocused) = remember { mutableStateOf(false) }
val (inputSessionToken, setInputSessionToken) = remember { mutableStateOf(-1) }
val paragraphIntrinsics = ParagraphIntrinsics(
text = "0",
style = MaterialTheme.typography.h5,
density = density,
resourceLoader = resourceLoader
)
val textWidthRaw = paragraphIntrinsics.maxIntrinsicWidth
val textWidth = with(density) { textWidthRaw.toDp() + 4.dp }
val textStyle = MaterialTheme.typography.h5
.copy(textAlign = TextAlign.Center)
val validKeyboardEntries = listOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "")
val onEditCommandWrapper: (List<EditCommand>) -> Unit = { commands ->
commands.forEach {
when (it) {
is CommitTextCommand -> {
if (!validKeyboardEntries.contains(it.text)) return@forEach
val newPinCode = pinCode.value.clone()
val index = newPinCode.indexOf("")
if (index > -1 && index < 5) {
newPinCode[index] = it.text
pinCode.value = newPinCode
}
}
is BackspaceCommand -> {
val newPinCode = pinCode.value.clone()
val lookUp = newPinCode.indexOf("")
val index = if (lookUp == -1) 4 // last pos
else lookUp - 1
if (index > -1 && index < 5) {
newPinCode[index] = ""
pinCode.value = newPinCode
}
}
}
}
}
val onImeActionPerformedWrapper: (ImeAction) -> Unit = {
//
}
val onFocusChanged = Modifier.onFocusChanged {
if (isFocused == it.isFocused) return@onFocusChanged
setIsFocused(it.isFocused)
if (it.isFocused) {
val token = textInputService?.startInput(
value = TextFieldValue(),
imeOptions = ImeOptions.Default.copy(keyboardType = KeyboardType.Number),
onEditCommand = onEditCommandWrapper,
onImeActionPerformed = onImeActionPerformedWrapper
) ?: INVALID_SESSION
setInputSessionToken(token)
textInputService?.showSoftwareKeyboard(token)
} else {
textInputService?.stopInput(inputSessionToken)
textInputService?.hideSoftwareKeyboard(inputSessionToken)
}
}
val focusRequestTapModifier = Modifier.tapGestureFilter {
if (!isFocused) {
focusRequester.requestFocus()
} else {
textInputService?.showSoftwareKeyboard(inputSessionToken)
}
}
Box(
modifier = Modifier
.wrapContentWidth()
.focusRequester(focusRequester)
.then(onFocusChanged)
.then(focusRequestTapModifier)
.focusable(),
) {
Row(
modifier = Modifier
.align(Alignment.Center)
.padding(start = 56.dp, top = 8.dp, end = 56.dp, bottom = 8.dp)
) {
PinInputTextField(
textWidth,
textStyle,
pinCode.value[0],
isFocused && pinCode.value[0].isEmpty()
)
Spacer(modifier = Modifier.width(8.dp))
PinInputTextField(
textWidth,
textStyle,
pinCode.value[1],
isFocused && pinCode.value[0].isNotEmpty() && pinCode.value[1].isEmpty()
)
Spacer(modifier = Modifier.width(8.dp))
PinInputTextField(
textWidth,
textStyle,
pinCode.value[2],
isFocused && pinCode.value[1].isNotEmpty() && pinCode.value[2].isEmpty()
)
Spacer(modifier = Modifier.width(8.dp))
PinInputTextField(
textWidth,
textStyle,
pinCode.value[3],
isFocused && pinCode.value[2].isNotEmpty() && pinCode.value[3].isEmpty()
)
Spacer(modifier = Modifier.width(8.dp))
PinInputTextField(
textWidth,
textStyle,
pinCode.value[4],
isFocused && pinCode.value[3].isNotEmpty()
)
}
}
}
@Composable
private fun PinInputTextField(
textWidth: Dp,
textStyle: TextStyle,
pinCodePart: String,
hasFocus: Boolean
) {
val state = remember { PinInputTextFieldState() }
state.text = pinCodePart
state.hasFocus = hasFocus
Column(
modifier = Modifier
.width(textWidth)
.pinCursor(state, Color.Black),
horizontalAlignment = Alignment.CenterHorizontally
) {
BasicText(
modifier = Modifier
.fillMaxWidth(),
style = textStyle,
text = pinCodePart,
onTextLayout = { textLayoutResult ->
state.textLayoutResult = textLayoutResult
}
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(2.dp)
.background(Color.LightGray)
)
}
}
@Preview
@Composable
fun PinInputPreview() {
PinInput()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment