31 KiB
📱 تاثیر در UI:
- iOS: Cupertino-style transitions
- Android: Material ripples
- Safe area handling مختلف
- Font rendering
📦 ۶. JSON کامل Initial Config
این JSON باید یکبار هنگام load WebView ارسال شود:
{
"version": "1.0.0",
"timestamp": 1234567890,
"layout": {
"breakpoint": "mobile",
"screenWidth": 375,
"screenHeight": 812,
"isMobile": true,
"isTablet": false,
"isDesktop": false
},
"safeArea": {
"top": 44,
"bottom": 34,
"left": 0,
"right": 0
},
"viewInsets": {
"top": 0,
"bottom": 0,
"left": 0,
"right": 0
},
"designSize": {
"mobile": { "width": 375, "height": 814 },
"tablet": { "width": 834, "height": 1194 }
},
"platform": {
"os": "ios",
"version": "17.2",
"model": "iPhone 14 Pro",
"isPhysicalDevice": true,
"hasNotch": true,
"hasDynamicIsland": true
},
"locale": {
"languageCode": "en",
"countryCode": "US",
"isRtl": false
},
"theme": {
"mode": "light",
"primaryColor": "#FFB236"
}
}
🔄 ۷. Events که باید به WebView ارسال شوند
۱. Layout Changed
{
"type": "LAYOUT_CHANGED",
"payload": {
"breakpoint": "tablet",
"screenWidth": 820,
"isMobile": false
}
}
۲. Keyboard Changed
{
"type": "KEYBOARD_CHANGED",
"payload": {
"visible": true,
"height": 336,
"animationDuration": 250
}
}
۳. Safe Area Changed
{
"type": "SAFE_AREA_CHANGED",
"payload": {
"top": 0,
"bottom": 0,
"left": 0,
"right": 0
}
}
۴. Orientation Changed
{
"type": "ORIENTATION_CHANGED",
"payload": {
"orientation": "landscape",
"screenWidth": 812,
"screenHeight": 375
}
}
💻 کد Flutter برای ارسال اطلاعات به WebView
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class TalkWebViewBridge {
final WebViewController controller;
BuildContext context;
TalkWebViewBridge(this.controller, this.context) {
_sendInitialConfig();
_listenToChanges();
}
// ارسال اولیه config
void _sendInitialConfig() {
final config = jsonEncode({
'version': '1.0.0',
'timestamp': DateTime.now().millisecondsSinceEpoch,
'layout': {
'breakpoint': _getBreakpoint(),
'screenWidth': MediaQuery.of(context).size.width,
'screenHeight': MediaQuery.of(context).size.height,
'isMobile': context.isMobile,
'isTablet': context.isTablet,
'isDesktop': context.isDesktop,
},
'safeArea': {
'top': MediaQuery.of(context).padding.top,
'bottom': MediaQuery.of(context).padding.bottom,
'left': MediaQuery.of(context).padding.left,
'right': MediaQuery.of(context).padding.right,
},
'viewInsets': {
'top': MediaQuery.of(context).viewInsets.top,
'bottom': MediaQuery.of(context).viewInsets.bottom,
'left': MediaQuery.of(context).viewInsets.left,
'right': MediaQuery.of(context).viewInsets.right,
},
'designSize': {
'mobile': {'width': 375, 'height': 814},
'tablet': {'width': 834, 'height': 1194},
},
'platform': {
'os': Platform.operatingSystem,
'version': Platform.operatingSystemVersion,
},
'locale': {
'languageCode': context.languageCode,
'isRtl': context.isRtl,
},
});
controller.runJavaScript('''
window.dispatchEvent(new CustomEvent('flutterConfig', {
detail: $config
}));
''');
}
// لیسن به تغییرات keyboard
void _listenToChanges() {
// در هر frame چک میکنیم که آیا viewInsets تغییر کرده
WidgetsBinding.instance.addPostFrameCallback((_) {
final currentInsets = MediaQuery.of(context).viewInsets;
if (_lastInsets?.bottom != currentInsets.bottom) {
_sendKeyboardChanged(currentInsets.bottom);
_lastInsets = currentInsets;
}
_listenToChanges(); // تکرار
});
}
EdgeInsets? _lastInsets;
void _sendKeyboardChanged(double height) {
final event = jsonEncode({
'type': 'KEYBOARD_CHANGED',
'payload': {
'visible': height > 0,
'height': height,
'animationDuration': 250,
},
});
controller.runJavaScript('''
window.dispatchEvent(new CustomEvent('flutterEvent', {
detail: $event
}));
''');
}
String _getBreakpoint() {
if (context.isMobile) return 'mobile';
if (context.isTablet) return 'tablet';
return 'desktop';
}
}
📱 توضیح کامل با مثالهای واقعی
🎯 Initial Config - ارسال یکبار هنگام Load
این JSON فقط یکبار زمانی که WebView لود میشود ارسال میشود تا React بداند در چه محیطی اجرا میشود.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1️⃣ layout - اطلاعات صفحه
"layout": { "breakpoint": "mobile", // نوع دستگاه "screenWidth": 375, // عرض صفحه (پیکسل) "screenHeight": 812, // ارتفاع صفحه (پیکسل) "isMobile": true, "isTablet": false, "isDesktop": false }
📱 مثال عملی:
// در React استفاده میشود برای:
function ChatPage({ layout }) { return ( <div style={{ padding: layout.isMobile ? '16px' : '64px' // موبایل = 16، تبلت = 64 }}> {/* محتوای چت */} ); }
// نتیجه: // iPhone (mobile): padding = 16px // iPad (tablet): padding = 64px
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2️⃣ safeArea - فضای امن صفحه
"safeArea": { "top": 44, // ارتفاع status bar / notch "bottom": 34, // ارتفاع home indicator (خط سفید iPhone X) "left": 0, "right": 0 }
📱 چرا مهم است؟
مثال عملی:
// ❌ بدون safe area <button style={{ position: 'fixed', bottom: '16px' }}> Send Message // نتیجه: دکمه زیر home indicator مخفی میشود!
// ✅ با safe area
<button style={{
position: 'fixed',
bottom: ${16 + safeArea.bottom}px // 16 + 34 = 50px
}}>
Send Message
// نتیجه: دکمه بالای home indicator نمایش داده میشود ✅
مثال واقعی از کد فلاتر:
// فلاتر: bottom: 16 + context.padding.bottom, // 16 + 34 = 50px
// باید در React شود: bottom: 16 + safeArea.bottom // 16 + 34 = 50px
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3️⃣ viewInsets - فضای اشغال شده توسط سیستم
"viewInsets": { "top": 0, "bottom": 0, // این عدد تغییر میکند! "left": 0, "right": 0 }
📱 کاربرد اصلی: Keyboard Height
┌─────────────────┐ │ Chat Messages │ ← محتوا │ │ │ │ ├─────────────────┤ │ TextField │ ← ورودی متن ├─────────────────┤ │ │ │ Keyboard │ ← viewInsets.bottom = 336px │ ⌨️ │ └─────────────────┘
مثال عملی:
// هنگام load اولیه: viewInsets.bottom = 0 // کیبورد بسته است
// کاربر روی TextField کلیک میکند: // Flutter event میفرستد: { "type": "KEYBOARD_CHANGED", "payload": { "bottom": 336 } }
// React باید TextField را بالا ببرد:
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
4️⃣ designSize - اندازه طراحی Figma
"designSize": { "mobile": { "width": 375, "height": 814 }, "tablet": { "width": 834, "height": 1194 } }
📱 کاربرد: Responsive Scaling
// اگر طراح در Figma با عرض 375 طراحی کرده: const figmaWidth = 375; const figmaFontSize = 16;
// دستگاه واقعی: const deviceWidth = 414; // iPhone 14 Pro Max
// محاسبه font size: const scaleFactor = deviceWidth / figmaWidth; // 414 / 375 = 1.104 const actualFontSize = figmaFontSize * scaleFactor; // 16 * 1.104 = 17.7px
// نتیجه: فونت در گوشی بزرگتر به نسبت اندازه صفحه بزرگتر میشود
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
5️⃣ platform - اطلاعات دستگاه
"platform": { "os": "ios", // ios | android "version": "17.2", "model": "iPhone 14 Pro", "isPhysicalDevice": true, // true = گوشی واقعی، false = emulator "hasNotch": true, // دارای notch "hasDynamicIsland": true // دارای Dynamic Island }
📱 کاربرد:
// مثال 1: تنظیم safe area مختلف if (platform.hasNotch) { safeAreaTop = 44; } else { safeAreaTop = 20; }
// مثال 2: استایل مختلف برای iOS/Android <button className={platform.os === 'ios' ? 'ios-style' : 'material-style'}> Send
// مثال 3: تشخیص Dynamic Island {platform.hasDynamicIsland && ( <div style={{ paddingTop: '59px' }}> {/* فضا برای Dynamic Island */} {/* محتوا */}
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
6️⃣ locale - تنظیمات زبان
"locale": { "languageCode": "en", // en | fa | ar "countryCode": "US", "isRtl": false // راست به چپ؟ }
📱 کاربرد:
// مثال 1: تنظیم direction
// مثال 2: متن مناسب زبان const messages = { en: "Hello", fa: "سلام", ar: "مرحبا" };
{messages[locale.languageCode]}
// مثال 3: فرمت تاریخ const date = new Date(); date.toLocaleDateString(locale.languageCode); // en-US | fa-IR
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
7️⃣ theme - تنظیمات تم
"theme": { "mode": "light", // light | dark "primaryColor": "#FFB236" }
📱 کاربرد:
// تنظیم رنگها const colors = { light: { bg: '#F5F5F5', text: '#202227' }, dark: { bg: '#1A1A1A', text: '#FFFFFF' } };
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
🔄 Events - ارسال Real-time
این eventها هر بار که چیزی تغییر میکند ارسال میشوند.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1️⃣ SCREEN_SIZE_CHANGED - تغییر اندازه صفحه
📱 چه موقع رخ میدهد؟
┌─────────────┐ ┌──────────────────────┐ │ Portrait │ │ Landscape │ │ │ │ │ │ iPhone │ چرخش گوشی │ iPhone │ │ │ ──────────> │ │ │ 📱 │ │ 📱 │ │ │ │ │ └─────────────┘ └──────────────────────┘ 375 × 812 812 × 375
Event ارسالی:
{ "type": "SCREEN_SIZE_CHANGED", "payload": { "width": 812, "height": 375, "orientation": "landscape", "breakpoint": "tablet" // ممکن است تغییر کند! } }
React چه کار باید بکند:
useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'SCREEN_SIZE_CHANGED') { const { width, orientation, breakpoint } = event.data.payload;
// 1. تغییر layout
if (breakpoint === 'tablet') {
setPadding(64); // در landscape = tablet
} else {
setPadding(16); // در portrait = mobile
}
// 2. تنظیم مجدد video player
if (orientation === 'landscape') {
setVideoPlayerFullWidth(true);
}
}
});
}, []);
مثال واقعی:
کاربر گوشی را میچرخاند (portrait → landscape) → عرض صفحه: 375 → 812 → breakpoint تغییر میکند: mobile → tablet → padding چت باید تغییر کند: 16px → 64px
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2️⃣ KEYBOARD_CHANGED - باز/بسته شدن کیبورد
📱 چه موقع رخ میدهد؟
┌─────────────────┐ ┌─────────────────┐ │ Chat │ │ Chat │ │ Messages │ │ Messages │ │ │ │ (کمتر فضا) │ │ │ ├─────────────────┤ │ │ کلیک → │ TextField │ ├─────────────────┤ ├─────────────────┤ │ TextField │ │ Keyboard │ └─────────────────┘ │ ⌨️ │ └─────────────────┘
Event ارسالی:
// کیبورد باز شد: { "type": "KEYBOARD_CHANGED", "payload": { "visible": true, "height": 336, // ارتفاع کیبورد "animationDuration": 250 // مدت زمان انیمیشن (ms) } }
// کیبورد بسته شد: { "type": "KEYBOARD_CHANGED", "payload": { "visible": false, "height": 0, "animationDuration": 250 } }
React چه کار باید بکند:
const [keyboardHeight, setKeyboardHeight] = useState(0);
useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'KEYBOARD_CHANGED') { const { visible, height, animationDuration } = event.data.payload;
// با انیمیشن smooth
setKeyboardHeight(visible ? height : 0);
// Scroll به آخرین پیام
if (visible) {
setTimeout(() => {
chatListRef.current?.scrollToBottom();
}, animationDuration);
}
}
});
}, []);
// در UI:
مثال واقعی:
کاربر روی TextField کلیک میکند → کیبورد باز میشود (336px) → لیست چت باید 336px بالا برود → با انیمیشن smooth (250ms)
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
3️⃣ SAFE_AREA_CHANGED - تغییر فضای امن
📱 چه موقع رخ میدهد؟
┌─────────────────┐ ┌─────────────────┐ │ Status Bar 44px │ │ (مخفی شده) │ ├─────────────────┤ ├─────────────────┤ │ │ │ │ │ Full Screen │ تمام صفحه│ Video/Game │ │ Video │ ──────→ │ Full Screen │ │ │ │ │ ├─────────────────┤ ├─────────────────┤ │ Home Bar 34px │ │ (مخفی شده) │ └─────────────────┘ └─────────────────┘
Event ارسالی:
// حالت عادی: { "type": "SAFE_AREA_CHANGED", "payload": { "top": 44, "bottom": 34, "left": 0, "right": 0 } }
// حالت full screen (video player): { "type": "SAFE_AREA_CHANGED", "payload": { "top": 0, // status bar مخفی شد "bottom": 0, // home indicator مخفی شد "left": 0, "right": 0 } }
React چه کار باید بکند:
useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'SAFE_AREA_CHANGED') { const { top, bottom } = event.data.payload;
// اگر safe area = 0 → یعنی full screen شده
if (top === 0 && bottom === 0) {
setIsFullScreen(true);
hideHeader();
hideBottomNav();
} else {
setIsFullScreen(false);
showHeader();
showBottomNav();
}
}
});
}, []);
مثال واقعی:
کاربر video call را full screen میکند → status bar و home indicator مخفی میشوند → safe area: top=44 → 0, bottom=34 → 0 → ویدیو تمام صفحه را میپوشاند
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
4️⃣ ORIENTATION_CHANGED - تغییر orientation
📱 چه موقع رخ میدهد؟
Portrait → Landscape → Portrait 📱 → 📱 → 📱
Event ارسالی:
{ "type": "ORIENTATION_CHANGED", "payload": { "orientation": "landscape", // portrait | landscape "screenWidth": 812, "screenHeight": 375 } }
React چه کار باید بکند:
useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'ORIENTATION_CHANGED') { const { orientation } = event.data.payload;
if (orientation === 'landscape') {
// ویدیو پلیر را بزرگتر کن
setVideoPlayerSize('large');
// دکمهها را در کنار قرار بده (row)
setButtonsLayout('horizontal');
} else {
// ویدیو پلیر را عادی کن
setVideoPlayerSize('normal');
// دکمهها را زیر هم قرار بده (column)
setButtonsLayout('vertical');
}
}
});
}, []);
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
📊 مثال کامل: سناریوی واقعی
سناریو: کاربر در حال چت است و گوشی را میچرخاند.
// 1. Initial Load { "layout": { "screenWidth": 375, "isMobile": true }, "safeArea": { "top": 44, "bottom": 34 } } → Chat با padding 16px نمایش داده میشود
// 2. کاربر روی TextField کلیک میکند { "type": "KEYBOARD_CHANGED", "payload": { "visible": true, "height": 336 } } → Chat list 336px بالا میرود
// 3. کاربر گوشی را میچرخاند (landscape) { "type": "ORIENTATION_CHANGED", "payload": { "orientation": "landscape", "screenWidth": 812 } } → Layout تغییر میکند
{ "type": "SCREEN_SIZE_CHANGED", "payload": { "width": 812, "breakpoint": "tablet" } } → Padding از 16px به 64px تغییر میکند
{ "type": "KEYBOARD_CHANGED", "payload": { "visible": true, "height": 220 } // کیبورد کوچکتر در landscape } → Keyboard height جدید
// 4. کاربر کیبورد را میبندد { "type": "KEYBOARD_CHANGED", "payload": { "visible": false, "height": 0 } } → Chat list به حالت عادی برمیگردد
✅ خلاصه
┌─────────────────────┬───────────────────┬────────────────────────────────────────────────┐ │ Event │ چه موقع؟ │ چرا مهم است؟ │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ Initial Config │ یکبار هنگام load │ تنظیمات اولیه (breakpoint, safeArea, platform) │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ SCREEN_SIZE_CHANGED │ چرخش گوشی │ تغییر padding, layout │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ KEYBOARD_CHANGED │ باز/بسته کیبورد │ جا دادن TextField، scroll به پیام │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ SAFE_AREA_CHANGED │ full screen video │ مخفی/نمایش header/footer │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ ORIENTATION_CHANGED │ چرخش گوشی │ تغییر UI (horizontal/vertical) │ └─────────────────────┴───────────────────┴────────────────────────────────────────────────┘