车载分屏适配技术浅谈

89 阅读9分钟

📖 故事开始:小车车的大烦恼

有一天,一辆名叫"小智"的智能汽车正在高速公路上飞驰。它搭载的导航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);
            }
        }
    }
}

📋 分屏适配要点总结(小抄)

🚨 必须记住的要点:

  1. 防止重建

    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
    
  2. 使用sw限定符

    // 创建不同尺寸的布局
    res/layout-sw600dp/activity_main.xml  // 最小宽度600dp
    res/layout-sw720dp/activity_main.xml  // 最小宽度720dp
    
  3. 两套布局策略

    • 全屏布局:显示所有功能
    • 分屏布局:简化UI,保留核心功能
  4. 正确监听状态

    // API 24+
    onMultiWindowModeChanged()
    // 兼容方案
    onConfigurationChanged()
    
  5. 正确获取屏幕尺寸

    // 不要用这个方法(不包括系统UI):
    getWindowManager().getDefaultDisplay().getMetrics(metrics);
    
    // 要用这个方法(真实尺寸):
    getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
    
  6. 合理设置地图宽高

    // 在onGlobalLayout回调中设置
    view.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
        // 此时可以获取准确尺寸
    });
    
  7. 注意onPause中的动画

    // 分屏中且仍然可见时,只暂停动画
    if (isInMultiWindowMode() && isVisible()) {
        pauseAnimationsOnly();
    } else {
        pauseEverything();
    }
    
  8. 处理系统UI

    // 监听WindowInsets变化
    ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
        // 处理导航栏、状态栏
        return insets;
    });
    

🎮 测试 Checklist:

  • 全屏 ↔ 分屏切换,地图是否正常?
  • 分屏调整大小时,UI是否自适应?
  • 分屏中切换到其他App,导航是否继续?
  • 系统UI(导航栏)显示/隐藏时,布局是否调整?
  • 横屏/竖屏分屏是否都正常?
  • 内存是否泄漏?生命周期是否正确?

🎉 故事结局

经过这一系列改造,导导侠终于掌握了分屏技能!现在它可以:

  1. 优雅分屏:左边导航,右边音乐,两不误
  2. 智能适配:自动调整UI布局和字体大小
  3. 持续导航:分屏切换不中断导航
  4. 资源友好:分屏时节省资源,全屏时发挥全力

小智非常满意,现在它既可以安全导航,又可以享受音乐,旅途更加愉快了!

💡 最后的小贴士

记住,车载导航的分屏适配不只是技术问题,更是用户体验问题

  1. 安全第一:驾驶时核心信息必须清晰可见
  2. 简单明了:分屏时简化UI,避免分散注意力
  3. 响应迅速:分屏切换要流畅,不能卡顿
  4. 稳定可靠:导航不能中断,路线不能丢失

现在,你也像导导侠一样,成为分屏适配的高手了!出发吧,去征服更多复杂的车载场景!🚗💨