android使用系统隐藏接口ActivityView实现主界面分窗显示三方应用Activity

前言

车载系统开发中大屏需要在主界面增加一些特色画面,例如在主界面划分多块区域显示,其中一个显示地图应用或者其他应用。android9.0之后的版本提供一个车载开发相关的Car模块,包含了车辆控制交互以及UI界面。其中CarLauncher为车载设计的一款主界面,配合CarSystemUI实现车载专属的UI效果。
在这里插入图片描述

预实现效果

在这里插入图片描述
在这里插入图片描述

CarLauncher中使用了android系统隐藏API–android.app.ActivityView来进行多Activity窗口显示。
在原生CarLauncher的布局中,ActivityView嵌套在activity的布局中,用法如下:

   <androidx.cardview.widget.CardView
        style="@style/CardViewStyle"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="@dimen/main_screen_widget_margin"
        app:layout_constraintBottom_toTopOf="@+id/divider_horizontal"
        app:layout_constraintRight_toLeftOf="@+id/end_edge"
        app:layout_constraintLeft_toRightOf="@+id/start_edge"
        android:layoutDirection="locale"
        app:layout_constraintTop_toBottomOf="@+id/top_edge">
        <android.car.app.CarActivityView
            android:id="@+id/maps"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </androidx.cardview.widget.CardView>

CarActivityView继承ActivityView,CarActivityView主要处理一些业务逻辑。
在主Activity中,继承了FragmentActivity以达到多个碎片Fragment的加载,使布局更加灵活。

public class CarLauncher extends FragmentActivity {
    ...代码省略
}

CarActivityView需要设置生命周期的监听。

     mActivityView = findViewById(R.id.maps);
        if (mActivityView != null) {
            mActivityView.setCallback(mActivityViewCallback);
        }

当收到onActivityViewReady创建成功的回调时再去启动其他进程的应用。

    private final ActivityView.StateCallback mActivityViewCallback =
            new ActivityView.StateCallback() {
                @Override
                public void onActivityViewReady(ActivityView view) {
                    if (DEBUG) Log.d(TAG, "onActivityViewReady(" + getUserId() + ")");
                    mActivityViewReady = true;
                    startMapsInActivityView();
                    maybeLogReady();
                }

                @Override
                public void onActivityViewDestroyed(ActivityView view) {
                    if (DEBUG) Log.d(TAG, "onActivityViewDestroyed(" + getUserId() + ")");
                    mActivityViewReady = false;
                }

                @Override
                public void onTaskMovedToFront(int taskId) {
                    if (DEBUG) {
                        Log.d(TAG, "onTaskMovedToFront(" + getUserId() + "): started="
                                + mIsStarted);
                    }
                    try {
                        if (mIsStarted) {
                            ActivityManager am =
                                    (ActivityManager) getSystemService(ACTIVITY_SERVICE);
                            am.moveTaskToFront(CarLauncher.this.getTaskId(), /* flags= */ 0);
                        }
                    } catch (RuntimeException e) {
                        Log.w(TAG, "Failed to move CarLauncher to front.");
                    }
                }
            };

启动其他进程的应用使用如下:

   private void startMapsInActivityView() {
        if (mActivityView == null || !mActivityViewReady) {
            return;
        }
        // If we happen to be be resurfaced into a multi display mode we skip launching content
        // in the activity view as we will get recreated anyway.
        if (isInMultiWindowMode() || isInPictureInPictureMode()) {
            return;
        }
        // Don't start Maps when the display is off for ActivityVisibilityTests.
        if (getDisplay().getState() != Display.STATE_ON) {
            return;
        }
        try {
            mActivityView.startActivity(getMapsIntent());
        } catch (ActivityNotFoundException e) {
            Log.w(TAG, "Maps activity not found", e);
        }
    }
        
    private Intent getMapsIntent() {
        return Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_MAPS);
    }

通过ActivityView.startActivity去启动其他进程的Activity,Intent.makeMainSelectorActivity创建意图。
CarLauncher源码地址:

/packages/apps/Car/Launcher/src/com/android/car/carlauncher/

原理

ActivityView创建时会初始化一个VirtualDisplay

    private void initVirtualDisplay() {
        if (this.mVirtualDisplay != null) {
            throw new IllegalStateException("Trying to initialize for the second time.");
        } else {
            int width = this.mSurfaceView.getWidth();
            int height = this.mSurfaceView.getHeight();
            DisplayManager displayManager = (DisplayManager)this.mContext.getSystemService(DisplayManager.class);
            this.mVirtualDisplay = displayManager.createVirtualDisplay("ActivityViewVirtualDisplay@" + System.identityHashCode(this), width, height, this.getBaseDisplayDensity(), this.mSurface, 9);
            if (this.mVirtualDisplay == null) {
                Log.e("ActivityView", "Failed to initialize ActivityView");
            } else {
                int displayId = this.mVirtualDisplay.getDisplay().getDisplayId();
                IWindowManager wm = WindowManagerGlobal.getWindowManagerService();

                try {
                    wm.dontOverrideDisplayInfo(displayId);
                } catch (RemoteException var8) {
                    var8.rethrowAsRuntimeException();
                }

                this.mInputForwarder = InputManager.getInstance().createInputForwarder(displayId);
                this.mTaskStackListener = new ActivityView.TaskStackListenerImpl();

                try {
                    this.mActivityManager.registerTaskStackListener(this.mTaskStackListener);
                } catch (RemoteException var7) {
                    Log.e("ActivityView", "Failed to register task stack listener", var7);
                }

            }
        }
    }

MediaProjection内部通过VirtualDisplay来实现屏幕录制,ActivityView本质上是一个虚拟屏幕VirtualDisplay
接着主要调用ActivityView.startActivity来启动Activity。

    public void startActivity(Intent intent) {
        ActivityOptions options = this.prepareActivityOptions();
        this.getContext().startActivity(intent, options.toBundle());
    }

ActivityOptions为Activity的配置项,包含了虚拟屏幕的displayId。
ActivityOptions.java源码地址:

/frameworks/base/core/java/android/app/ActivityOptions.java
通过setLaunchDisplayId设置对应虚拟屏幕。

  private ActivityOptions prepareActivityOptions() {
        if (this.mVirtualDisplay == null) {
            throw new IllegalStateException("Trying to start activity before ActivityView is ready.");
        } else {
            ActivityOptions options = ActivityOptions.makeBasic();
            options.setLaunchDisplayId(this.mVirtualDisplay.getDisplay().getDisplayId());
            return options;
        }
    }

setLaunchDisplayId源码入下:

      /**
       * Sets the id of the display where activity should be launched.
       * An app can launch activities on public displays or private displays that are owned by the app
       * or where an app already has activities. Otherwise, trying to launch on a private display
       * or providing an invalid display id will result in an exception.
       * <p>
       * Setting launch display id will be ignored on devices that don't have
       * {@link android.content.pm.PackageManager#FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS}.
       * @param launchDisplayId The id of the display where the activity should be launched.
       * @return {@code this} {@link ActivityOptions} instance.
       */
      public ActivityOptions setLaunchDisplayId(int launchDisplayId) {
          mLaunchDisplayId = launchDisplayId;
          return this;
      }

ActivityOptionsKEY_LAUNCH_DISPLAY_ID最为key值传入displayId。

```java
private static final String KEY_LAUNCH_DISPLAY_ID = "android.activity.launchDisplayId";

 public Bundle toBundle() {
      ...
         if (mLaunchDisplayId != INVALID_DISPLAY) {
              b.putInt(KEY_LAUNCH_DISPLAY_ID, mLaunchDisplayId);
         }
      ...
 }

ActivityOptions最终SafeActivityOptions 在框架中应用。

      public static SafeActivityOptions fromBundle(Bundle bOptions) {
          return bOptions != null
                  ? new SafeActivityOptions(ActivityOptions.fromBundle(bOptions))
                  : null;
      }

Context.startActivity的流程中通过RootActivityContainer进行处理。
RootActivityContainer源码地址:

frameworks/base/services/core/java/com/android/server/wm/RootActivityContainer.java

RootActivityContainer创建ActivityDisplay,每个ActivityDisplay对应一个VirtualDisplay
ActivityDisplay源码地址:

/frameworks/base/services/core/java/com/android/server/wm/ActivityDisplay.java

ActivityDisplay与WMS交互来控制窗口显示。

  • 8
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 24
    评论
FragmentTabHost和FragmentActivityAndroid开发中常用的两个类。FragmentTabHost用于实现Tab切换功能,FragmentActivity用于管理Fragment。下面是一个简单的示例,演示如何使用FragmentTabHost和FragmentActivity实现应用界面: 1. 创建Activity,继承自FragmentActivity,并在布局文件中添加FragmentTabHost组件。 2. 在Activity的onCreate方法中,初始化FragmentTabHost,设置Tab的选项卡和内容。 3. 创建多个Fragment用于不同的Tab选项卡,每个Tab选项卡对应一个Fragment。 4. 在Fragment中实现对应Tab的内容。 下面是一个示例代码: MainActivity.java ``` public class MainActivity extends FragmentActivity { private FragmentTabHost mTabHost; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTabHost = (FragmentTabHost) findViewById(android.R.id.tabhost); mTabHost.setup(this, getSupportFragmentManager(), android.R.id.tabcontent); mTabHost.addTab(mTabHost.newTabSpec("home").setIndicator("Home"), HomeFragment.class, null); mTabHost.addTab(mTabHost.newTabSpec("search").setIndicator("Search"), SearchFragment.class, null); mTabHost.addTab(mTabHost.newTabSpec("settings").setIndicator("Settings"), SettingsFragment.class, null); } } ``` activity_main.xml ``` <android.support.v4.app.FragmentTabHost android:id="@android:id/tabhost" android:layout_width="match_parent" android:layout_height="match_parent"> <FrameLayout android:id="@android:id/tabcontent" android:layout_width="match_parent" android:layout_height="match_parent" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TabWidget android:id="@android:id/tabs" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> </android.support.v4.app.FragmentTabHost> ``` HomeFragment.java ``` public class HomeFragment extends Fragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_home, container, false); // TODO: add your home fragment content here return view; } } ``` fragment_home.xml ``` <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Home Fragment" /> </RelativeLayout> ``` SearchFragment.java和SettingsFragment.java类似于HomeFragment,分别对应Search和Settings选项卡的内容。 这样,我们就可以通过FragmentTabHost和FragmentActivity实现应用界面的Tab切换功能了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

言并肃

感谢大哥支持!您的鼓励是我动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值