安卓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.png
序号 时间节点 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
辅助屏_地图_内存.png
序号 时间节点 内存占用峰值 内存占用均值
1 辅助屏地图页显示之前 86MB 85.4MB
2 辅助屏开始显示地图页10s取样 100.6MB 60MB
3 辅助屏显示地图页后 73MB 60MB

可以看到内存占用情况与页面负责度相关,当辅助屏为普通页面的情况下,内存占用只增加了4MB左右,内存平稳。当辅助屏为基于SurfaceView的地图页面的情况下,内存拉升20MB左右。

示例源码

文章对Presentation功能展示的Demo已上传GitHub,感兴趣的朋友可以clone下来共同探讨一下。
GitHub源码链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335