Checkpoint android fixes

This commit is contained in:
Eric Bailey
2026-04-01 11:20:13 -05:00
parent 7c9139ffd5
commit 1c8e4c5f05
3 changed files with 47 additions and 10 deletions
+46 -9
View File
@@ -48,7 +48,7 @@ import {
} from '#/components/Autocomplete'
import {useOnKeyboard} from '#/components/hooks/useOnKeyboard'
import {Span, Text} from '#/components/Typography'
import {IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
import {IS_ANDROID, IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
/*
* ─── Types ────────────────────────────────────────────────────────────────────
@@ -216,9 +216,17 @@ export function Composer({
const xh = maxNumberOfLines
? lineHeight * maxNumberOfLines + verticalSpace
: 999
/*
* On web, we set an initial height and auto-resize via DOM measurement.
* On Android, we drive height explicitly via onContentSizeChange to avoid
* sub-pixel oscillation that causes layout jumpiness.
* On iOS, minHeight/maxHeight works fine natively.
*/
const tas = IS_WEB
? {height: lineHeight + verticalSpace}
: {minHeight: mh, maxHeight: xh}
: IS_ANDROID
? {height: mh}
: {minHeight: mh, maxHeight: xh}
if (IS_IOS) {
delete ts.lineHeight
@@ -227,6 +235,14 @@ export function Composer({
return {textStyle: ts, textAreaStyle: tas, minHeight: mh, maxHeight: xh}
}, [t, fonts, padding, rawTextStyle, initialNumberOfLines, maxNumberOfLines])
/*
* On Android, multiline TextInput oscillates between slightly different
* contentSize values on consecutive layout passes (sub-pixel rounding).
* This causes visible jumpiness when using minHeight/maxHeight. Instead,
* we drive the height explicitly and ceil the value to stabilize it.
*/
const [androidInputHeight, setAndroidInputHeight] = useState(minHeight)
// ─── Height auto-resize + sift positioning ────────────────────────────
const updateAutocompletePosition = useCallback(() => {
@@ -253,14 +269,26 @@ export function Composer({
return
}
textInputRef.current?.measure((_x, _y, _w, h) => {
if (h !== prevHeight.current) {
prevHeight.current = h
updateAutocompletePosition()
}
})
if (IS_IOS) {
textInputRef.current?.measure((_x, _y, _w, h) => {
if (h !== prevHeight.current) {
prevHeight.current = h
updateAutocompletePosition()
}
})
}
}, [tapper.state.text, minHeight, maxHeight, updateAutocompletePosition])
/*
* On Android, height is driven by onContentSizeChange (see the TextInput
* below), so we update the autocomplete position when that height changes.
*/
useEffect(() => {
if (IS_ANDROID) {
updateAutocompletePosition()
}
}, [androidInputHeight, updateAutocompletePosition])
// ─── Scroll sync ──────────────────────────────────────────────────────
const previewScrollStyle = useAnimatedStyle(() => ({
@@ -356,7 +384,7 @@ export function Composer({
textAlignVertical: 'top',
includeFontPadding: false,
},
textAreaStyle,
IS_ANDROID ? {height: androidInputHeight} : textAreaStyle,
web({
resize: 'none',
outline: 'none',
@@ -388,6 +416,15 @@ export function Composer({
inputScrollSharedValue.value = e.nativeEvent.contentOffset.y
}
}}
onContentSizeChange={
IS_ANDROID
? e => {
const h = Math.ceil(e.nativeEvent.contentSize.height)
const clamped = Math.min(Math.max(h, minHeight), maxHeight)
setAndroidInputHeight(clamped)
}
: undefined
}
// @ts-ignore web only
onCompositionStart={() => {
isComposing.current = true
@@ -139,7 +139,7 @@ export function MessageComposer({
padding={{
paddingLeft: IS_WEB ? 30 + 8 : 16,
paddingTop: 12,
paddingBottom: IS_ANDROID ? 4 : 12,
paddingBottom: 12,
paddingRight: 35 + a.p_sm.padding,
}}
textStyle={[a.text_md, a.leading_snug]}