-
Notifications
You must be signed in to change notification settings - Fork 317
Prevent MessageList item jump during IME animation
#6539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
0689c18
324424b
f94852d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /* | ||
| * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. | ||
| * | ||
| * Licensed under the Stream License; | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package io.getstream.chat.android.compose.util | ||
|
|
||
| import androidx.compose.foundation.layout.ExperimentalLayoutApi | ||
| import androidx.compose.foundation.layout.WindowInsets | ||
| import androidx.compose.foundation.layout.imeAnimationSource | ||
| import androidx.compose.foundation.layout.imeAnimationTarget | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.ui.platform.LocalDensity | ||
|
|
||
| /** | ||
| * Returns `true` while an IME animation is in progress; `false` when it's fully settled | ||
| * (either open or closed). | ||
| * | ||
| * `imeAnimationSource` and `imeAnimationTarget` diverge for the whole duration of the | ||
| * animation and converge again once it settles. Reading them synchronously in composition | ||
| * means the value flips on the same frame the animation starts, avoiding a one-frame lag | ||
| * that a coroutine-based debounce approach would incur at the transition edges. | ||
| */ | ||
| @Composable | ||
| @OptIn(ExperimentalLayoutApi::class) | ||
| internal fun isImeAnimating(): Boolean { | ||
| val density = LocalDensity.current | ||
| val source = WindowInsets.imeAnimationSource.getBottom(density) | ||
| val target = WindowInsets.imeAnimationTarget.getBottom(density) | ||
| return source != target | ||
| } | ||
|
Comment on lines
+35
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: set -euo pipefail
# Locate relevant files and usages
git ls-files | rg 'stream-chat-android-compose/.*/IsImeAnimating\.kt|stream-chat-android-compose/.*/Messages|stream-chat-android-compose/.*/ChatComponentFactory|stream-chat-android-compose/.*/MessageList|stream-chat-android-compose/.*/ChannelList'
printf '\n--- isImeAnimating usages ---\n'
rg -n "isImeAnimating\(" stream-chat-android-compose || true
printf '\n--- imeAnimationSource/Target usages ---\n'
rg -n "imeAnimation(Source|Target)" stream-chat-android-compose || true
printf '\n--- target file ---\n'
nl -ba stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt | sed -n '1,200p'Repository: GetStream/stream-chat-android Length of output: 4619 🏁 Script executed: set -euo pipefail
# Map the target file and likely caller sites
git ls-files | rg 'stream-chat-android-compose/.*/IsImeAnimating\.kt|stream-chat-android-compose/.*/Messages|stream-chat-android-compose/.*/ChatComponentFactory|stream-chat-android-compose/.*/MessageList|stream-chat-android-compose/.*/ChannelList'
printf '\n--- isImeAnimating usages ---\n'
rg -n "isImeAnimating\(" stream-chat-android-compose || true
printf '\n--- imeAnimationSource/Target usages ---\n'
rg -n "imeAnimation(Source|Target)" stream-chat-android-compose || true
printf '\n--- target file ---\n'
nl -ba stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt | sed -n '1,200p'Repository: GetStream/stream-chat-android Length of output: 4619 🏁 Script executed: set -euo pipefail
git ls-files | rg 'stream-chat-android-compose/.*/IsImeAnimating\.kt|stream-chat-android-compose/.*/Messages|stream-chat-android-compose/.*/ChatComponentFactory|stream-chat-android-compose/.*/MessageList|stream-chat-android-compose/.*/ChannelList'
printf '\n--- isImeAnimating usages ---\n'
rg -n "isImeAnimating\(" stream-chat-android-compose || true
printf '\n--- imeAnimationSource/Target usages ---\n'
rg -n "imeAnimation(Source|Target)" stream-chat-android-compose || true
printf '\n--- target file ---\n'
nl -ba stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt | sed -n '1,200p'Repository: GetStream/stream-chat-android Length of output: 4619 🌐 Web query:
💡 Result: In Jetpack Compose, WindowInsets values should not be read directly during the composition phase [1][2]. Doing so causes the composition to re-execute whenever the insets change, which—during an animation—results in triggering composition for every single frame [1][2]. This can lead to significant performance degradation and potential UI lag [1][2][3]. To avoid this, you should use built-in Compose modifiers and APIs designed to handle insets efficiently without triggering unnecessary recomposition [1][2]. Recommended practices include: Use specialized modifiers: Utilize modifiers such as Modifier.windowInsetsPadding, Modifier.systemBarsPadding, Modifier.imePadding, or Modifier.windowInsetsTopHeight [1][2]. These modifiers are designed to apply inset changes during the layout phase rather than the composition phase, ensuring performance efficiency even during animations [1][2]. Avoid reading values in composition: Do not access properties like WindowInsets.getTop(density) or WindowInsets.getBottom(density) within your Composable functions if those values change frequently [1][2]. If you have complex requirements that necessitate manual handling, look into APIs like onConsumedWindowInsetsChanged or MutableWindowInsets, though these should be used with care to maintain proper performance patterns [4][5]. Generally, sticking to the provided layout modifiers is the most performant and recommended approach [1][2]. Citations:
🏁 Script executed: set -uo pipefail
printf '%s\n' '--- files ---'
git ls-files | rg 'stream-chat-android-compose/.*/IsImeAnimating\.kt|stream-chat-android-compose/.*/Messages|stream-chat-android-compose/.*/ChatComponentFactory|stream-chat-android-compose/.*/MessageList|stream-chat-android-compose/.*/ChannelList' || true
printf '\n%s\n' '--- isImeAnimating usages ---'
rg -n "isImeAnimating\(" stream-chat-android-compose || true
printf '\n%s\n' '--- imeAnimationSource/Target usages ---'
rg -n "imeAnimation(Source|Target)" stream-chat-android-compose || true
printf '\n%s\n' '--- target file ---'
nl -ba stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt | sed -n '1,200p'Repository: GetStream/stream-chat-android Length of output: 4633 🏁 Script executed: set -euo pipefail
python3 - <<'PY'
from pathlib import Path
paths = [
Path("stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt"),
Path("stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt"),
]
for path in paths:
print(f"\n--- {path} ---")
text = path.read_text()
for i, line in enumerate(text.splitlines(), 1):
if path.name == "IsImeAnimating.kt" or (860 <= i <= 885):
print(f"{i:4d}: {line}")
PYRepository: GetStream/stream-chat-android Length of output: 3631 🏁 Script executed: set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path("stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt")
text = path.read_text().splitlines()
for start in [840, 860, 868, 880]:
print(f"\n--- lines {start}-{start+25} ---")
for i in range(start, min(start+25, len(text))+1):
print(f"{i:4d}: {text[i-1]}")
PYRepository: GetStream/stream-chat-android Length of output: 5014 🏁 Script executed: set -euo pipefail
rg -n "messageListItemModifier|animateItem\(placementSpec = null\)|isImeAnimating\(\)" stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/Messages.kt stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.ktRepository: GetStream/stream-chat-android Length of output: 759 🏁 Script executed: set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path("stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt")
for i, line in enumerate(path.read_text().splitlines(), 1):
print(f"{i:4d}: {line}")
PYRepository: GetStream/stream-chat-android Length of output: 2196 🏁 Script executed: set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path("stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt")
lines = path.read_text().splitlines()
for i in range(850, 881):
print(f"{i:4d}: {lines[i-1]}")
PYRepository: GetStream/stream-chat-android Length of output: 1549 🏁 Script executed: set -euo pipefail
python3 - <<'PY'
from pathlib import Path
files = [
"stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt",
"stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt",
]
for file in files:
path = Path(file)
print(f"\n=== {file} ===")
lines = path.read_text().splitlines()
if file.endswith("ChatComponentFactory.kt"):
for start, end in [(820, 890), (1, 120)]:
print(f"\n--- lines {start}-{end} ---")
for i in range(start, min(end, len(lines)) + 1):
print(f"{i:4d}: {lines[i-1]}")
else:
print("\n--- full file ---")
for i, line in enumerate(lines, 1):
print(f"{i:4d}: {line}")
PYRepository: GetStream/stream-chat-android Length of output: 14306 Hoist IME animation state out of
🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The neighbouring
isKeyboardVisibleAsState()avoidsWindowInsets.imebecause it can report wrong values on some OEM ROMs (Xiaomi/MIUI), see its KDoc. This helper readsimeAnimationSource/imeAnimationTargetfrom the same IME inset channel, so on those ROMs (or when the host window isn't set up for edge-to-edge inset dispatch) it may never reportsource != targetand the fix just won't apply. It degrades gracefully, so not blocking. Worth a one-line note in the KDoc to document the dependency?