feat: 添加键盘状态同步功能,优化 H5ShellPage 的用户体验

This commit is contained in:
Booker
2026-05-29 11:47:26 +07:00
parent f1f08947d2
commit 6b35e80d47

View File

@@ -19,6 +19,7 @@ const _shellBrandingChannel =
MethodChannel('io.openim.flutter.im_webview_app/shell_branding');
const _androidFilePickerChannel =
MethodChannel('io.openim.flutter.openim/file_picker');
const _keyboardAnimationDuration = Duration(milliseconds: 250);
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -104,6 +105,9 @@ class _H5ShellPageState extends State<H5ShellPage> {
bool _shellBrandingLoaded = false;
int _currentLineIndex = 0;
double _lastKeyboardInset = 0;
bool _lastKeyboardVisible = false;
int _keyboardSyncToken = 0;
late ShellBranding _shellBranding;
H5Line get _currentLine => _h5Lines[_currentLineIndex];
@@ -244,6 +248,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
await _loadShellBrandingIfNeeded();
await _syncShellBranding(lineIndex);
await _installRouteObserver(lineIndex);
await _syncKeyboardState(lineIndex);
final slot = _lineSlots[lineIndex];
slot.progress = 100;
if (mounted &&
@@ -265,6 +270,8 @@ class _H5ShellPageState extends State<H5ShellPage> {
_hideShellCover(lineIndex);
} else if (decoded is Map && decoded['type'] == 'route-changed') {
_updateSlotUrl(lineIndex, decoded['url']?.toString());
} else if (decoded is Map && decoded['type'] == 'keyboard-bridge-ready') {
unawaited(_syncKeyboardState(lineIndex));
}
} catch (_) {
if (message.message == 'first-screen-ready') {
@@ -296,6 +303,8 @@ class _H5ShellPageState extends State<H5ShellPage> {
);
case 'openAppSettings':
unawaited(openAppSettings());
case 'keyboard-bridge-ready':
unawaited(_syncKeyboardState(lineIndex));
}
} catch (_) {
// Ignore malformed shell messages from web content.
@@ -366,6 +375,53 @@ class _H5ShellPageState extends State<H5ShellPage> {
return _runJavaScriptSafely(lineIndex, script);
}
void _scheduleKeyboardStateSync(double bottomInset) {
final nextInset = bottomInset < 1 ? 0.0 : bottomInset;
final nextVisible = nextInset > 0;
if ((nextInset - _lastKeyboardInset).abs() < 1 &&
nextVisible == _lastKeyboardVisible) {
return;
}
_lastKeyboardInset = nextInset;
_lastKeyboardVisible = nextVisible;
final token = ++_keyboardSyncToken;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || token != _keyboardSyncToken) {
return;
}
unawaited(_syncKeyboardState(_currentLineIndex));
});
}
Future<void> _syncKeyboardState(int lineIndex) {
if (lineIndex < 0 || lineIndex >= _lineSlots.length) {
return Future<void>.value();
}
final payload = jsonEncode({
'height': _lastKeyboardInset.round(),
'visible': _lastKeyboardVisible,
'duration': _keyboardAnimationDuration.inMilliseconds,
});
final script = '''
(() => {
try {
const keyboard = $payload;
if (typeof window.__OPENIM_KEYBOARD_UPDATE__ === 'function') {
window.__OPENIM_KEYBOARD_UPDATE__(keyboard);
return;
}
if (typeof window.openIMKeyboardUpdate === 'function') {
window.openIMKeyboardUpdate(keyboard);
}
} catch (_) {}
})();
''';
return _runJavaScriptSafely(lineIndex, script);
}
Future<void> _installRouteObserver(int lineIndex) {
const script = '''
(() => {
@@ -551,6 +607,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
}
await _ensureLineLoaded(safeIndex);
await _syncKeyboardState(safeIndex);
}
Widget _buildWebViewWidget(int index) {
@@ -635,6 +692,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
final currentSlot = _currentSlot;
final topInset = MediaQuery.paddingOf(context).top;
final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
_scheduleKeyboardStateSync(bottomInset);
final showLineSwitch = !currentSlot.showShellCover &&
currentSlot.loadError == null &&
currentSlot.isLoginPage &&