安卓多屏互动Presentation
前言
随着时代的发展,单在一块屏幕上操作应用已远远不能满足与日俱增的用户需求,安卓系统多屏互动也随即诞生。起初提到多屏幕的交互,开发者们更多的是想到使用RTP实现的视频流传输,然而传输视频流存在着性能损耗,视频压缩,刷新帧率等多方面的问题,让不少想尝试多屏互动开发的开发者望而却步。其实实现多屏互动也不止是只有视频流传输一条路,今天就让我们来实现一个使用安卓系统组件Presentation实现的多屏互动应用。
Presentation
A presentation is a special kind of dialog whose purpose is to
present content on a secondary display.
----------Google
从本质上来说Presentation只是一个特殊的Dialog而已,常规的Dialog是show在了操作屏上,而Presentation是show在了指定的操作屏上,这个指定屏可以是系统的第二块甚至是第三块屏幕,同时也可以是主应用操作屏。如果Presentation显示到了主应用屏上,显示效果与常规Dialog无异。
根据不同的显示需求,Presentation有两种显示方式,一种是随应用Activity显示隐藏同步的辅助屏显示,另一种是常驻辅助屏的保持显示模式。如果需要使用常驻显示模式,那么Presentation的显示需要放到Service中。
双屏开发准备
常规的开发设备是不俱备辅助屏幕的,开发调试双屏显示,我们可以借助开发者模式中的模拟辅助屏幕功能。进入手机---设置---开发者模式---模拟辅助显示设备---选择一款需要调试分辨率的屏幕。
模拟辅助屏幕.png
这里需要注意一件事情,开发设备的系统版本要保证在安卓4.2以上,因为Presentation支持的版本是minSdkVersion >= 17。开启辅助显示设备的时候默认是采用透屏模式,即辅助屏显示主屏的投影,当对辅助屏有定制开发需求显示指定界面的时候,就需要用到Presentation了。
辅助屏.png
双屏异显实例
创建辅助屏幕Presentation
辅助屏中有地图与搜索两个页面,并可以自由的在两个页面间进行切换。Presentation构造函数中的Context参数可以是Activity,也可以是ApplicationContext或者是Service,但如果是ApplicationContext或是Service类型的Context,Presentation的Type类型必须为SYSTEM_ALERT。
/**
* @author : YangHaoYi on 2019/1/2.
* Email : yang.haoyi@qq.com
* Description :双屏异显辅助屏幕
* Change : YangHaoYi on 2019/1/2.
* Version : V 1.0
*/
public class SecondScreenPresentation extends Presentation implements MapFrameLayout.MapCallBackListener,
SearchFrameLayout.SearchCallBack {
/** TAG **/
private static final String TAG = "Presentation";
/** 根布局 **/
private FrameLayout fmContent;
/** 地图页 **/
private MapFrameLayout mapFrameLayout;
/** 搜索页 **/
private SearchFrameLayout searchFrameLayout;
/** Context 可以为Activity Application Service **/
public SecondScreenPresentation(Context outerContext, Display display) {
super(outerContext, display);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.presentation_search);
init();
}
private void init(){
initView();
initEvent();
}
// ··· 省略页面初始化与页面事件代码
}
搭建Presentation显示环境
因为我们所要实现的功能是在应用退居后台也能够在辅助屏进行显示,所以采用Service进行Presentation的显示,前文提到过Presentation与Dialog的区别在于,Presentation可以自行选择显示的屏幕,那么他是怎么控制显示在指定屏幕上的呢?这里我们需要用到安卓系统的DisplayManager,通过他的getDisplays方法我们可以获取到当前系统所挂载的所有屏幕,然后对指定的屏幕进行Presentation显示操作。以一块主屏和一块辅屏为例,在获取到displays数组后,displays[0]即为我们系统的主屏幕,displays[1]即为系统的辅助屏幕。因为设置了显示类型为SYSTEM_ALERT,所以这里需要给我们的应用添加相应的权限,6.0以上系统记得申请动态权限。
/** 弹窗权限 **/
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
/** 屏幕管理器 **/
private DisplayManager mDisplayManager;
/** 屏幕数组 **/
private Display[] displays;
/** 初始化第二块屏幕 **/
private void initPresentation(){
if(null==presentation){
mDisplayManager = (DisplayManager) this.getSystemService(Context.DISPLAY_SERVICE);
displays = mDisplayManager.getDisplays();
if(displays.length > 1){
// displays[1]是副屏
presentation = new SecondScreenPresentation(this, displays[1]);
presentation.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
}
}
}
绑定Presentation服务
主屏通过绑定服务开启Presentation的显示,在服务的onServiceConnected方法中进行Presentation的show操作。
/**
* @author : YangHaoYi on 2019/4/30.
* Email : yang.haoyi@qq.com
* Description :离屏逻辑控制中心
* Change : YangHaoYi on 2019/4/30.
* Version : V 1.0
*/
public class PresentationPresenter {
private MultiScreenService multiScreenService;
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
multiScreenService = ((MultiScreenService.MultiScreenBinder) service).getService();
//显示第二块屏幕
multiScreenService.showSearchPresentation();
}
@Override
public void onServiceDisconnected(ComponentName name) {
//恢复置空
multiScreenService = null;
}
};
public void openSearchPresentation(Activity activity){
Intent intent = new Intent(activity,MultiScreenService.class);
activity.bindService(intent,serviceConnection, Context.BIND_AUTO_CREATE);
}
}
业务逻辑
为了方便对双屏异现实现功能的理解,这里通过一个简单业务逻辑的示例来展示。主屏幕上显示地图应用,地图具有放大、缩小与跳转搜索页面的功能,搜索页具有搜索与返回地图页的功能。辅助屏Presentation上具有和主屏应用相同的两个页面地图与搜索页,同时具有相同的功能事件。
业务逻辑.png
主屏与辅助屏是完全相同的两个页面,笔者做示例Demo采用的是MVP的模式,即View层保证主屏与辅助屏是两个相同对象,页面封装在MapFrameLayout与SearchFrameLayout中。
/** 初始化页面 **/
private void initPage(){
mapFrameLayout = new MapFrameLayout(getContext());
mapFrameLayout.setMapCallBackListener(this);
fmContent.addView(mapFrameLayout);
searchFrameLayout = new SearchFrameLayout(getContext());
}
/** 地图回调 **/
@Override
public void mapCallBackContent(MapFrameLayout.MapEvent event, Object data) {
switch (event){
case Search:
if(null == searchFrameLayout){
searchFrameLayout = new SearchFrameLayout(getContext());
searchFrameLayout.setSearchBackListener(this);
fmContent.addView(searchFrameLayout);
}else {
fmContent.addView(searchFrameLayout);
}
break;
default:
break;
}
}
Model层与Presenter层完全复用。
/**
* @author : YangHaoYi on 2019/4/3017:23.
* Email : yang.haoyi@qq.com
* Description :搜索数据中心
* Change : YangHaoYi on 2019/4/3017:23.
* Version : V 1.0
*/
public class SearchModel {
/**
* 转换搜索数据
* @return 搜索结果
* **/
public SearchResultData doNearbySearch(){
SearchResultData resultData = new SearchResultData();
resultData.setCode(0);
resultData.setDescription("请求成功");
SearchResultData.PayloadData payloadData = new SearchResultData.PayloadData();
payloadData.setTitle("市图书馆");
payloadData.setAddress("辽宁省沈阳市沈河区青年大街205号");
payloadData.setNotice("市图书馆");
payloadData.setLat("41.765923");
payloadData.setLng("123.442674");
return resultData;
}
}
/**
* @author : YangHaoYi on 2019/4/3010:54.
* Email : yang.haoyi@qq.com
* Description :搜索逻辑控制中心
* Change : YangHaoYi on 2019/4/3010:54.
* Version : V 1.0
*/
public class SearchPresenter {
/** 搜索数据Model **/
private SearchModel searchModel;
/** 搜索View接口 **/
private ISearchView searchView;
public SearchPresenter(ISearchView searchView) {
this.searchView = searchView;
searchModel = new SearchModel();
}
/** 执行搜索 **/
public void nearbySearch(){
searchView.showSearchResult(searchModel.doNearbySearch());
}
}
主屏与辅助屏事件支持同屏同显与同屏异显两种模式,同屏同显即主屏操作事件的时候同步给辅助屏,可以通过事件总线EventBus或者RxBus实现。页面数据的刷新与变更可以使用总线机制实现,也可以使用 Jetpack的LiveData实现,基于谷歌 Jetpack的LiveData实现可以参考笔者关于LiveData的文章《基于DataBinding与LiveData的MVVM实践(Kotlin)》。
同屏同显.png 同屏异显.png多屏显示性能分析
CPU占用
我们知道,在一个典型的显示系统中,一般包括CPU、GPU、Display三个部分。CPU 计算屏幕数据、GPU 进一步处理和缓存、最后 Display 再将缓存中(buffer)的屏幕数据显示出来。而CPU计算屏幕数据指的就是View树的绘制过程,也就是窗体对应的视图树从根布局 DecorView 开始层层遍历每个 View,分别执行测量、布局、绘制三个操作的过程,对于多屏显示来说,因为新增了一个辅助屏幕的页面展示,势必会增加CPU的占用。
让我们通过Android Studio的Android Profiler来实际看一下CPU的使用情况:
辅助屏_显示设置页_CPU.png
序号 | 时间节点 | CPU峰值 | CPU均值 |
---|---|---|---|
1 | 辅助屏设置页显示之前 | 11.9% | 9.7% |
2 | 辅助屏开始显示设置页10s取样 | 28.5% | 26.3% |
3 | 辅助屏显示设置页后 | 21.2% | 16.1% |
序号 | 时间节点 | CPU峰值 | CPU均值 |
---|---|---|---|
1 | 辅助屏地图页显示之前 | 13.3% | 12.5% |
2 | 辅助屏开始显示地图页10s取样 | 60.5% | 43.2% |
3 | 辅助屏显示地图页后 | 41.9% | 37% |
可以看到CPU的占用情况与辅助屏页的绘制相关,普通页面CPU占用约提升0.7,显示双屏UI后CPU占用均值16.1%。基于SurfaceView的地图页面CPU占用提升1.5,显示双屏UI后CPU占用均值37%。
内存占用
多屏显示增加CPU占用的同时也将引发更多的内存分配,内存的占用与页面的复杂度息息相关,页面使用的对象越丰富,特效越复杂,带来的内存消耗也就越多,通常来说有些功能本身就会很耗内存,例如视频直播,音乐播放,地图显示等。
同样的,让我们继续通过Android Studio的Android Profiler分析一下多屏显示的内存占用情况:
辅助屏_设置页_内存.png
序号 | 时间节点 | 内存占用峰值 | 内存占用均值 |
---|---|---|---|
1 | 辅助屏设置页显示之前 | 82.5MB | 82.1MB |
2 | 辅助屏开始显示设置页10s取样 | 85.4MB | 85MB |
3 | 辅助屏显示设置页后 | 86.1MB | 86MB |
序号 | 时间节点 | 内存占用峰值 | 内存占用均值 |
---|---|---|---|
1 | 辅助屏地图页显示之前 | 86MB | 85.4MB |
2 | 辅助屏开始显示地图页10s取样 | 100.6MB | 60MB |
3 | 辅助屏显示地图页后 | 73MB | 60MB |
可以看到内存占用情况与页面负责度相关,当辅助屏为普通页面的情况下,内存占用只增加了4MB左右,内存平稳。当辅助屏为基于SurfaceView的地图页面的情况下,内存拉升20MB左右。
示例源码
文章对Presentation功能展示的Demo已上传GitHub,感兴趣的朋友可以clone下来共同探讨一下。
GitHub源码链接