📖 故事开始:小车车的大烦恼
有一天,一辆名叫"小智"的智能汽车正在高速公路上飞驰。它搭载的导航App叫"导导侠",是个经验丰富的老司机。但今天,小智遇到了一个问题:
副驾驶的小明想看音乐App,而驾驶员需要导航。这可怎么办?
原来,Android车载系统支持分屏模式——就像把屏幕切成两块,左边导航,右边放音乐!
但我们的导导侠还没学会分屏技能,一进分屏模式就乱套了:地图变形、按钮错位、导航中断...
让我们帮导导侠学会分屏技能吧!
🎯 第一关:防止Activity重建(避免地图重新加载)
故事场景:
每次分屏,导导侠就像被施了"失忆魔法"——忘记当前路线,需要重新规划。
代码解决:
// AndroidManifest.xml中配置Activity
<activity
android:name=".NavigationActivity"
android:resizeableActivity="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:screenOrientation="unspecified">
</activity>
原理讲解:
public class NavigationActivity extends AppCompatActivity {
private boolean isRecreating = false;
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 分屏状态改变,但不重建Activity
Log.d("导导侠", "配置改变啦,但我不用重建!");
// 获取当前分屏状态
boolean isInMultiWindow = isInMultiWindowMode();
if (isInMultiWindow) {
Log.d("导导侠", "我进入分屏模式啦!");
adaptToSplitScreen();
} else {
Log.d("导导侠", "我回到全屏模式啦!");
adaptToFullScreen();
}
}
}
🎯 第二关:两套布局的魔法衣橱
故事场景:
全屏时,导导侠穿着华丽的礼服(全屏布局);分屏时,需要换上轻便的运动装(分屏布局)。
代码实现:
// 布局文件结构
res/
├── layout/
│ └── activity_navigation.xml # 全屏布局
└── layout-w600dp/ # 最小宽度600dp时使用(分屏)
└── activity_navigation.xml
// 或者动态切换布局
public class NavigationActivity extends AppCompatActivity {
private FrameLayout mapContainer;
private View fullScreenLayout;
private View splitScreenLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 方法1:使用尺寸限定符自动选择布局
setContentView(R.layout.activity_navigation);
// 方法2:动态添加/移除视图
initLayouts();
updateLayoutForCurrentMode();
}
private void initLayouts() {
// 加载两套布局
fullScreenLayout = LayoutInflater.from(this)
.inflate(R.layout.layout_fullscreen, null);
splitScreenLayout = LayoutInflater.from(this)
.inflate(R.layout.layout_splitscreen, null);
mapContainer = findViewById(R.id.map_container);
// 全屏时显示完整布局,分屏时显示简化布局
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
fullScreenLayout.setLayoutParams(params);
splitScreenLayout.setLayoutParams(params);
}
private void updateLayoutForCurrentMode() {
mapContainer.removeAllViews();
if (isInMultiWindowMode()) {
mapContainer.addView(splitScreenLayout);
// 分屏模式:隐藏不重要的UI,保留核心导航
hideNonCriticalViews();
} else {
mapContainer.addView(fullScreenLayout);
// 全屏模式:显示所有功能
showAllViews();
}
}
}
🎯 第三关:屏幕尺寸的魔法尺子
故事场景:
分屏后,导导侠需要知道"我现在的房子有多大",才能正确摆放家具(UI元素)。
代码实现:
public class ScreenUtils {
// 正确的获取屏幕宽度方法(考虑导航栏等系统UI)
public static int getRealScreenWidth(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metrics = new DisplayMetrics();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+ 推荐方式
WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
Rect bounds = windowMetrics.getBounds();
return bounds.width();
} else {
// 兼容旧版本
wm.getDefaultDisplay().getRealMetrics(metrics);
return metrics.widthPixels;
}
}
// 获取应用可用区域(排除系统UI)
public static Rect getAppUsableScreenArea(Activity activity) {
Rect rect = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
return rect;
}
// 动态计算地图尺寸
public static void calculateMapSize(NavigationActivity activity) {
DisplayMetrics displayMetrics = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int screenWidth = displayMetrics.widthPixels;
int screenHeight = displayMetrics.heightPixels;
// 考虑状态栏和导航栏高度
int statusBarHeight = getStatusBarHeight(activity);
int navigationBarHeight = getNavigationBarHeight(activity);
// 计算实际可用高度
int usableHeight = screenHeight - statusBarHeight - navigationBarHeight;
// 如果是分屏模式,宽度可能只有一半
if (activity.isInMultiWindowMode()) {
// 获取分屏中的实际尺寸
Rect bounds = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(bounds);
int mapWidth = bounds.width();
int mapHeight = bounds.height();
Log.d("导导侠", String.format("分屏地图尺寸:%d x %d", mapWidth, mapHeight));
// 设置地图视图尺寸
MapView mapView = activity.getMapView();
ViewGroup.LayoutParams params = mapView.getLayoutParams();
params.width = mapWidth;
params.height = mapHeight;
mapView.setLayoutParams(params);
}
}
private static int getStatusBarHeight(Context context) {
int resourceId = context.getResources()
.getIdentifier("status_bar_height", "dimen", "android");
return resourceId > 0 ?
context.getResources().getDimensionPixelSize(resourceId) : 0;
}
private static int getNavigationBarHeight(Context context) {
int resourceId = context.getResources()
.getIdentifier("navigation_bar_height", "dimen", "android");
return resourceId > 0 ?
context.getResources().getDimensionPixelSize(resourceId) : 0;
}
}
🎯 第四关:监听分屏状态的顺风耳
故事场景:
导导侠需要时刻竖起耳朵,听系统说"现在要分屏啦"或"现在要全屏啦"。
代码实现:
public class NavigationActivity extends AppCompatActivity {
private boolean isInSplitScreen = false;
private MultiWindowModeCallback multiWindowModeCallback;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 方法1:使用OnMultiWindowModeChangedListener(API 24+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
multiWindowModeCallback = new MultiWindowModeCallback();
addOnMultiWindowModeChangedListener(multiWindowModeCallback);
}
// 方法2:使用View的OnAttachStateChangeListener
getWindow().getDecorView().addOnAttachStateChangeListener(
new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
updateWindowState();
}
@Override
public void onViewDetachedFromWindow(View v) {
// 清理资源
}
}
);
// 方法3:注册配置变化监听
ConfigurationChangedReceiver receiver = new ConfigurationChangedReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
registerReceiver(receiver, filter);
}
@RequiresApi(api = Build.VERSION_CODES.N)
private class MultiWindowModeCallback implements
ComponentCallbacks2.OnMultiWindowModeChangedListener {
@Override
public void onMultiWindowModeChanged(boolean isInMultiWindowMode,
Configuration newConfig) {
Log.d("导导侠", "分屏状态改变: " + isInMultiWindowMode);
isInSplitScreen = isInMultiWindowMode;
// 延迟处理,确保UI已更新
getWindow().getDecorView().post(() -> {
if (isInMultiWindowMode) {
onEnterSplitScreenMode();
} else {
onExitSplitScreenMode();
}
});
}
}
private void onEnterSplitScreenMode() {
Log.d("导导侠", "进入分屏模式,调整布局...");
// 1. 调整地图尺寸
ScreenUtils.calculateMapSize(this);
// 2. 简化UI
simplifyUIForSplitScreen();
// 3. 调整导航信息显示方式
adjustNavigationDisplay();
// 4. 通知地图SDK尺寸变化
notifyMapSizeChanged();
}
private void onExitSplitScreenMode() {
Log.d("导导侠", "退出分屏模式,恢复布局...");
// 恢复完整UI
restoreFullUI();
// 重新计算地图尺寸
ScreenUtils.calculateMapSize(this);
}
// 检查当前是否在分屏模式
public boolean isInSplitScreenMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return isInMultiWindowMode();
}
return false;
}
}
🎯 第五关:地图SDK的特别照顾
故事场景:
地图SDK是个倔强的家伙,需要特别的方式告诉它"你的房子变小了"。
代码实现:
public class MapManager {
private MapView mapView;
private boolean isMapInitialized = false;
public void handleSplitScreenChange(boolean isSplitScreen,
int newWidth, int newHeight) {
if (mapView == null) return;
// 方法1:重新设置地图尺寸
ViewGroup.LayoutParams params = mapView.getLayoutParams();
params.width = newWidth;
params.height = newHeight;
mapView.setLayoutParams(params);
// 方法2:通知地图视图尺寸变化(不同地图SDK方法不同)
mapView.onSizeChanged(newWidth, newHeight,
params.width, params.height);
// 方法3:延迟重绘地图
mapView.postDelayed(() -> {
// 高德地图
// mapView.onResume();
// 百度地图
// mapView.onResume();
// Google Maps
// mapView.onResume();
Log.d("导导侠", "地图已重新调整尺寸");
}, 100);
}
// 处理地图生命周期
public void onPauseInSplitScreen(boolean isSplitScreenVisible) {
if (mapView == null) return;
if (isSplitScreenVisible) {
// 分屏中仍然可见,只暂停动画,不停止地图
pauseMapAnimations();
} else {
// 完全不可见,停止地图
mapView.onPause();
}
}
public void onResumeInSplitScreen(boolean isSplitScreenVisible) {
if (mapView == null) return;
if (isSplitScreenVisible && isMapInitialized) {
// 恢复地图
mapView.onResume();
resumeMapAnimations();
}
}
private void pauseMapAnimations() {
// 暂停地图上的动画效果,但保持地图渲染
// 例如:暂停路线动画、车辆图标动画等
}
}
🎯 第六关:系统UI的捉迷藏游戏
故事场景:
导航栏和状态栏像调皮的精灵,有时出现有时隐藏,导导侠需要和它们玩好捉迷藏。
代码实现:
public class SystemUIHelper {
// 沉浸式模式处理
public static void setupImmersiveMode(Activity activity,
boolean isSplitScreen) {
if (isSplitScreen) {
// 分屏模式下,通常不需要全沉浸式
setupSplitScreenSystemUI(activity);
} else {
// 全屏模式下,使用沉浸式
setupFullScreenSystemUI(activity);
}
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static void setupFullScreenSystemUI(Activity activity) {
View decorView = activity.getWindow().getDecorView();
int systemUiVisibility = decorView.getSystemUiVisibility();
// 隐藏状态栏
systemUiVisibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
// 隐藏导航栏(如果不需要)
systemUiVisibility |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
// 沉浸式粘性模式
systemUiVisibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
decorView.setSystemUiVisibility(systemUiVisibility);
}
private static void setupSplitScreenSystemUI(Activity activity) {
// 分屏时通常显示系统UI
View decorView = activity.getWindow().getDecorView();
int systemUiVisibility = decorView.getSystemUiVisibility();
// 显示状态栏
systemUiVisibility &= ~View.SYSTEM_UI_FLAG_FULLSCREEN;
// 显示导航栏
systemUiVisibility &= ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
decorView.setSystemUiVisibility(systemUiVisibility);
}
// 监听系统UI变化
public static void addSystemUIChangeListener(Activity activity,
SystemUIChangeListener listener) {
View decorView = activity.getWindow().getDecorView();
decorView.setOnApplyWindowInsetsListener((v, insets) -> {
// 处理WindowInsets,获取系统UI区域
int systemBars = WindowInsetsCompat.Type.systemBars();
boolean isSystemUIVisible = insets.isVisible(systemBars);
listener.onSystemUIChanged(isSystemUIVisible, insets);
return insets;
});
}
public interface SystemUIChangeListener {
void onSystemUIChanged(boolean isVisible, WindowInsets insets);
}
}
🎯 终极关卡:完整的导航Activity实现
完整示例代码:
public class NavigationActivity extends AppCompatActivity {
// 核心组件
private MapView mapView;
private MapManager mapManager;
private FrameLayout mainContainer;
// 状态标志
private boolean isInSplitScreen = false;
private boolean isActivityVisible = true;
private boolean isMapReady = false;
// 布局视图
private View fullScreenView;
private View splitScreenView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 1. 防止重建
if (savedInstanceState != null) {
restoreState(savedInstanceState);
}
// 2. 初始化基础布局
setContentView(R.layout.activity_base);
mainContainer = findViewById(R.id.main_container);
// 3. 初始化地图
initMap();
// 4. 加载两套布局
loadBothLayouts();
// 5. 设置系统UI监听
setupSystemUIListeners();
// 6. 初始适配
adaptToCurrentMode();
}
@Override
protected void onStart() {
super.onStart();
isActivityVisible = true;
}
@Override
protected void onStop() {
super.onStop();
isActivityVisible = false;
}
@Override
protected void onResume() {
super.onResume();
// 正确处理地图恢复
if (mapManager != null) {
boolean isVisibleInSplitScreen = isInSplitScreen && isActivityVisible;
mapManager.onResumeInSplitScreen(isVisibleInSplitScreen);
}
}
@Override
protected void onPause() {
super.onPause();
// 关键!正确处理分屏下的暂停
if (mapManager != null) {
// 判断是否在分屏中且仍然可见
boolean isVisibleInSplitScreen = isInSplitScreen &&
(isInPictureInPictureMode() || isInMultiWindowMode());
mapManager.onPauseInSplitScreen(isVisibleInSplitScreen);
}
}
@Override
public void onMultiWindowModeChanged(boolean isInMultiWindowMode,
Configuration newConfig) {
super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig);
Log.d("Navigation", "分屏状态变化: " + isInMultiWindowMode);
this.isInSplitScreen = isInMultiWindowMode;
// 延迟执行,确保UI已完成更新
mainContainer.post(() -> {
adaptToCurrentMode();
updateMapSize();
});
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
Configuration newConfig) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
if (isInPictureInPictureMode) {
Log.d("Navigation", "进入画中画模式");
adaptToPipMode();
}
}
private void adaptToCurrentMode() {
// 移除所有子视图
mainContainer.removeAllViews();
if (isInSplitScreen) {
// 使用分屏布局
mainContainer.addView(splitScreenView);
setupSplitScreenUI();
} else {
// 使用全屏布局
mainContainer.addView(fullScreenView);
setupFullScreenUI();
}
// 更新系统UI
SystemUIHelper.setupImmersiveMode(this, isInSplitScreen);
}
private void updateMapSize() {
if (mapView == null || !isMapReady) return;
// 获取当前可用尺寸
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
// 考虑系统UI遮挡
Rect usableArea = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(usableArea);
int usableWidth = usableArea.width();
int usableHeight = usableArea.height();
Log.d("Navigation",
String.format("可用区域: %dx%d", usableWidth, usableHeight));
// 更新地图尺寸
mapManager.handleSplitScreenChange(isInSplitScreen,
usableWidth, usableHeight);
}
private void setupSplitScreenUI() {
// 简化UI元素
hideNonCriticalElements();
// 调整字体大小
adjustTextSizeForSplitScreen();
// 调整按钮大小和间距
adjustButtonLayout();
// 可能需要的其他调整
if (isLandscape()) {
setupLandscapeSplitScreen();
} else {
setupPortraitSplitScreen();
}
}
private void setupFullScreenUI() {
// 恢复完整UI
showAllElements();
// 恢复字体大小
restoreTextSize();
// 恢复按钮布局
restoreButtonLayout();
}
private void adjustTextSizeForSplitScreen() {
// 根据屏幕尺寸调整字体
float scaleFactor = calculateScaleFactor();
TextView[] textViews = findViewById(R.id.text_views_container)
.getTouchables().toArray(new TextView[0]);
for (TextView textView : textViews) {
float originalSize = textView.getTextSize();
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
originalSize * scaleFactor);
}
}
private float calculateScaleFactor() {
// 根据屏幕宽度计算缩放比例
DisplayMetrics metrics = getResources().getDisplayMetrics();
int screenWidth = metrics.widthPixels;
// 基准宽度(例如:全屏时的宽度)
int baseWidth = 1920; // 根据实际设备调整
if (isInSplitScreen) {
// 分屏时宽度约为一半
return Math.min(1.0f, (float) screenWidth / baseWidth * 0.8f);
}
return Math.min(1.0f, (float) screenWidth / baseWidth);
}
// 保存状态
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
// 保存导航状态
outState.putBoolean("isInSplitScreen", isInSplitScreen);
outState.putBoolean("isMapReady", isMapReady);
// 保存当前路线信息等
saveNavigationState(outState);
}
private void restoreState(Bundle savedInstanceState) {
isInSplitScreen = savedInstanceState.getBoolean("isInSplitScreen", false);
isMapReady = savedInstanceState.getBoolean("isMapReady", false);
restoreNavigationState(savedInstanceState);
}
// 辅助方法
private boolean isLandscape() {
return getResources().getConfiguration().orientation ==
Configuration.ORIENTATION_LANDSCAPE;
}
private void hideNonCriticalElements() {
// 隐藏分屏时不重要的UI元素
int[] nonCriticalViews = {
R.id.ad_banner,
R.id.promo_section,
R.id.extra_info_panel
};
for (int viewId : nonCriticalViews) {
View view = findViewById(viewId);
if (view != null) {
view.setVisibility(View.GONE);
}
}
}
}
📋 分屏适配要点总结(小抄)
🚨 必须记住的要点:
-
防止重建:
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" -
使用sw限定符:
// 创建不同尺寸的布局 res/layout-sw600dp/activity_main.xml // 最小宽度600dp res/layout-sw720dp/activity_main.xml // 最小宽度720dp -
两套布局策略:
- 全屏布局:显示所有功能
- 分屏布局:简化UI,保留核心功能
-
正确监听状态:
// API 24+ onMultiWindowModeChanged() // 兼容方案 onConfigurationChanged() -
正确获取屏幕尺寸:
// 不要用这个方法(不包括系统UI): getWindowManager().getDefaultDisplay().getMetrics(metrics); // 要用这个方法(真实尺寸): getWindowManager().getDefaultDisplay().getRealMetrics(metrics); -
合理设置地图宽高:
// 在onGlobalLayout回调中设置 view.getViewTreeObserver().addOnGlobalLayoutListener(() -> { // 此时可以获取准确尺寸 }); -
注意onPause中的动画:
// 分屏中且仍然可见时,只暂停动画 if (isInMultiWindowMode() && isVisible()) { pauseAnimationsOnly(); } else { pauseEverything(); } -
处理系统UI:
// 监听WindowInsets变化 ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> { // 处理导航栏、状态栏 return insets; });
🎮 测试 Checklist:
- 全屏 ↔ 分屏切换,地图是否正常?
- 分屏调整大小时,UI是否自适应?
- 分屏中切换到其他App,导航是否继续?
- 系统UI(导航栏)显示/隐藏时,布局是否调整?
- 横屏/竖屏分屏是否都正常?
- 内存是否泄漏?生命周期是否正确?
🎉 故事结局
经过这一系列改造,导导侠终于掌握了分屏技能!现在它可以:
- 优雅分屏:左边导航,右边音乐,两不误
- 智能适配:自动调整UI布局和字体大小
- 持续导航:分屏切换不中断导航
- 资源友好:分屏时节省资源,全屏时发挥全力
小智非常满意,现在它既可以安全导航,又可以享受音乐,旅途更加愉快了!
💡 最后的小贴士
记住,车载导航的分屏适配不只是技术问题,更是用户体验问题:
- 安全第一:驾驶时核心信息必须清晰可见
- 简单明了:分屏时简化UI,避免分散注意力
- 响应迅速:分屏切换要流畅,不能卡顿
- 稳定可靠:导航不能中断,路线不能丢失
现在,你也像导导侠一样,成为分屏适配的高手了!出发吧,去征服更多复杂的车载场景!🚗💨