android 双屏异显自适应dialog的实现-kb88凯时官网登录

来自:网络
时间:2024-06-09
阅读:

一、前言

android 多屏互联的时代,必然会出现多屏连接的问题,通常意义上的多屏连接包括hdmi/usb、wifidisplay,除此之外android 还有overlaydisplay和virtualdisplay,其中virtualdisplay相比不少人录屏的时候都会用到,在android中他们都是display,除了物理屏幕,你在overlaydisplay和virtualdisplay同样也可以展示弹窗或者展示activity,所有的display的差异化通过displaymanagerservice 进行了兼容,同样任意一种display都拥有自己的密度和大小以及display id,对于测试双屏应用,一般也可以通过virtualdisplay进行模拟操作。

需求

本篇主要解决副屏dialog 组建展示问题。存在任意类型的副屏时,让 dialog 展示在副屏上,如果不存在,就需要让它自动展示在主屏上。
为什么会有这种需求呢?默认情况下,实现双屏异显的时候, 通常不是使用presentation就是activity,然而,dialog只能展示在主屏上,而presentation只能展示的副屏上。想象一下这种双屏场景,在切换视频的时候,loading展示应该是在主屏还是副屏呢 ?毫无疑问,答案当然是副屏。

问题

我们要解决的问题当然是随着场景的切换,dialog展示在不同的屏幕上。同样,内容也可以这样展示,当存在副屏的时候在副屏上展示内容,当只有主屏的时候在主屏上展示内容。

二、方案

我们这里梳理一下两种方案。

方案:自定义presentation

作为presentation的核心点有两个,其中一个是displayid,另一个是windowtype,第一个是通常意义上指定display id,第二个是窗口类型。如果是副屏,那么displayid是必须的参数,且不能和defaultdisplay的id一样,除此之外windowtype是一个需要重点关注的东西。

早期的 type_presentation 存在指纹信息 “被借用” 而造成用户资产损失的风险,即便外部无法获取,但是早期的android 8.0版本利用 (type_presentation=type_application_overlay-1)可以实现屏幕外弹框,在之后的版本做了修复,同时对 type_presentation 展示必须有 token 等校验,但是在这种过程中,presentation的windowtype 变了又变,因此,我们如何获取到兼容每个版本的windowtype呢?

原理

display id的问题我们不需要重点处理,从display 获取即可。windowtype才是重点,方法当然是有的,我们不继承presentation,而是继承dialog因此自行实现可以参考 presentation 中的代码,当然难点是 windowmanagerimpl 和windowtype类获取,前者 @hide 标注的,而后者不固定。
早期我们可以利用 compileonly layoutlib.jar 的方式导入 windowmanagerimpl,但是新版本中 layoutlib.jar 中的类已经几乎被删,另外如果要使用 layoutlib.jar,那么你的项目中的 kotlin 版本就会和 layoutlib.jar 产生冲突,虽然可以删除相关的类,但是这种维护方式非常繁琐,因此我们这里借助反射实现。当然除了反射也可以利用dexmaker或者xposed hook方式,只是复杂性会很多。

windowtype问题解决

我们知道,创建presentation的时候,framework源码是设置了windowtype的,我们完全在我们自己的dialog创建presentation对象,读取出来设置上到我们自己的dialog上即可。
不过,我们先要对display进行隔离,避免主屏走这段逻辑

windowmanager wm = (windowmanager) outercontext.getsystemservice(window_service); 
if(display==null || wm.getdefaultdisplay().getdisplayid()==display.getdisplayid()){  
return; 
}

//注意,这里需要借助presentation的一些属性,否则无法正常弹出弹框,要么有权限问题、要么有token问题

presentation presentation = new presentation(outercontext, display, theme);  
windowmanager.layoutparams standardattributes =presentation.getwindow().getattributes();  
final window w = getwindow(); 
final windowmanager.layoutparams attr = w.getattributes(); 
attr.token = standardattributes.token; w.setattributes(attr);
//type 源码中是type_presentation,事实上每个版本是不一样的,因此这里动态获取 w.setgravity(gravity.fill);
w.settype(standardattributes.type); 

windowmanagerimpl 问题

其实我们知道,presentation的windowmanagerimpl并不是给自己用的,而是给dialog上的其他组件(如menu、popwindow等),将其他组件加到dialog的 window上,因为在android系统中,windowmanager都是parent window所具备的能力,所以创建这个不是为了把dialog加进去,而是为了把基于dialog的window组件加到dialog上,这和activity是一样的。那么,其实如果我们没有menu、popwindow,这里实际上是可以不处理的,但是作为一个完整的类,我们这里使用反射处理一下。

怎么处理呢?

我们知道,异显屏的context是通过createdisplaycontext创建的,但是我们这里并不是hook这个方法,只是在创建这个display context之后,再通过contextthemewrapper,设置进去即可。

private static context createpresentationcontext(
      context outercontext, display display, int theme) {
   if (outercontext == null) {
      throw new illegalargumentexception("outercontext must not be null");
   }
   windowmanager outerwindowmanager = (windowmanager) outercontext.getsystemservice(window_service);
   if (display == null || display.getdisplayid()==outerwindowmanager.getdefaultdisplay().getdisplayid()) {
      return outercontext;
   }
   context displaycontext = outercontext.createdisplaycontext(display);
   if (theme == 0) {
      typedvalue outvalue = new typedvalue();
      displaycontext.gettheme().resolveattribute(
            android.r.attr.presentationtheme, outvalue, true);
      theme = outvalue.resourceid;
   }
   // derive the display's window manager from the outer window manager.
   // we do this because the outer window manager have some extra information
   // such as the parent window, which is important if the presentation uses
   // an application window type.
   //  final windowmanager outerwindowmanager =
   //        (windowmanager) outercontext.getsystemservice(window_service);
   //   final windowmanagerimpl displaywindowmanager =
   //         outerwindowmanager.createpresentationwindowmanager(displaycontext);
   windowmanager displaywindowmanager = null;
   try {
      classloader classloader = complexpresentationv1.class.getclassloader();
      class loadclass = classloader.loadclass("android.view.windowmanagerimpl");
      method createpresentationwindowmanager = loadclass.getdeclaredmethod("createpresentationwindowmanager", context.class);
      displaywindowmanager = (windowmanager) loadclass.cast(createpresentationwindowmanager.invoke(outerwindowmanager,displaycontext));
   } catch (classnotfoundexception | nosuchmethodexception e) {
      e.printstacktrace();
   } catch (illegalaccessexception e) {
      e.printstacktrace();
   } catch (invocationtargetexception e) {
      e.printstacktrace();
   }
   final windowmanager windowmanager = displaywindowmanager;
   return new contextthemewrapper(displaycontext, theme) {
      @override
      public object getsystemservice(string name) {
         if (window_service.equals(name)) {
            return windowmanager;
         }
         return super.getsystemservice(name);
      }
   };
}

全部源码

public class complexpresentationv1 extends dialog  {
    private static final string tag = "complexpresentationv1";
    private static final int msg_cancel = 1;
    private  display mpresentationdisplay;
    private  displaymanager mdisplaymanager;
    /**
     * creates a new presentation that is attached to the specified display
     * using the default theme.
     *
     * @param outercontext the context of the application that is showing the presentation.
     * the presentation will create its own context (see {@link #getcontext()}) based
     * on this context and information about the associated display.
     * @param display the display to which the presentation should be attached.
     */
    public complexpresentationv1(context outercontext, display display) {
        this(outercontext, display, 0);
    }
    /**
     * creates a new presentation that is attached to the specified display
     * using the optionally specified theme.
     *
     * @param outercontext the context of the application that is showing the presentation.
     * the presentation will create its own context (see {@link #getcontext()}) based
     * on this context and information about the associated display.
     * @param display the display to which the presentation should be attached.
     * @param theme a style resource describing the theme to use for the window.
     * see 
     * style and theme resources for more information about defining and using
     * styles.  this theme is applied on top of the current theme in
     * outercontext.  if 0, the default presentation theme will be used.
     */
    public complexpresentationv1(context outercontext, display display, int theme) {
        super(createpresentationcontext(outercontext, display, theme), theme);
        windowmanager wm = (windowmanager) outercontext.getsystemservice(window_service);
        if(display==null || wm.getdefaultdisplay().getdisplayid()==display.getdisplayid()){
            return;
        }
        mpresentationdisplay = display;
        mdisplaymanager = (displaymanager)getcontext().getsystemservice(display_service);
        //注意,这里需要借助presentation的一些属性,否则无法正常弹出弹框,要么有权限问题、要么有token问题
        presentation presentation = new presentation(outercontext, display, theme);
        windowmanager.layoutparams standardattributes = presentation.getwindow().getattributes();
        final window w = getwindow();
        final windowmanager.layoutparams attr = w.getattributes();
        attr.token = standardattributes.token;
        w.setattributes(attr);
        w.settype(standardattributes.type); 
//type 源码中是type_presentation,事实上每个版本是不一样的,因此这里动态获取
        w.setgravity(gravity.fill);
        setcanceledontouchoutside(false);
    }
    /**
     * gets the {@link display} that this presentation appears on.
     *
     * @return the display.
     */
    public display getdisplay() {
        return mpresentationdisplay;
    }
    /**
     * gets the {@link resources} that should be used to inflate the layout of this presentation.
     * this resources object has been configured according to the metrics of the
     * display that the presentation appears on.
     *
     * @return the presentation resources object.
     */
    public resources getresources() {
        return getcontext().getresources();
    }
    @override
    protected void onstart() {
        super.onstart();
        if(mpresentationdisplay ==null){
            return;
        }
        mdisplaymanager.registerdisplaylistener(mdisplaylistener, mhandler);
        // since we were not watching for display changes until just now, there is a
        // chance that the display metrics have changed.  if so, we will need to
        // dismiss the presentation immediately.  this case is expected
        // to be rare but surprising, so we'll write a log message about it.
        if (!isconfigurationstillvalid()) {
            log.i(tag, "presentation is being dismissed because the "
                      "display metrics have changed since it was created.");
            mhandler.sendemptymessage(msg_cancel);
        }
    }
    @override
    protected void onstop() {
        if(mpresentationdisplay ==null){
            return;
        }
        mdisplaymanager.unregisterdisplaylistener(mdisplaylistener);
        super.onstop();
    }
    /**
     * inherited from {@link dialog#show}. will throw
     * {@link android.view.windowmanager.invaliddisplayexception} if the specified secondary
     * {@link display} can't be found.
     */
    @override
    public void show() {
        super.show();
    }
    /**
     * called by the system when the {@link display} to which the presentation
     * is attached has been removed.
     *
     * the system automatically calls {@link #cancel} to dismiss the presentation
     * after sending this event.
     *
     * @see #getdisplay
     */
    public void ondisplayremoved() {
    }
    /**
     * called by the system when the properties of the {@link display} to which
     * the presentation is attached have changed.
     *
     * if the display metrics have changed (for example, if the display has been
     * resized or rotated), then the system automatically calls
     * {@link #cancel} to dismiss the presentation.
     *
     * @see #getdisplay
     */
    public void ondisplaychanged() {
    }
    private void handledisplayremoved() {
        ondisplayremoved();
        cancel();
    }
    private void handledisplaychanged() {
        ondisplaychanged();
        // we currently do not support configuration changes for presentations
        // (although we could add that feature with a bit more work).
        // if the display metrics have changed in any way then the current configuration
        // is invalid and the application must recreate the presentation to get
        // a new context.
        if (!isconfigurationstillvalid()) {
            log.i(tag, "presentation is being dismissed because the "
                      "display metrics have changed since it was created.");
            cancel();
        }
    }
    private boolean isconfigurationstillvalid() {
        if(mpresentationdisplay ==null){
            return true;
        }
        displaymetrics dm = new displaymetrics();
        mpresentationdisplay.getmetrics(dm);
        try {
            method equalsphysical = displaymetrics.class.getdeclaredmethod("equalsphysical", displaymetrics.class);
            return (boolean) equalsphysical.invoke(dm,getresources().getdisplaymetrics());
        } catch (nosuchmethodexception e) {
            e.printstacktrace();
        } catch (illegalaccessexception e) {
            e.printstacktrace();
        } catch (invocationtargetexception e) {
            e.printstacktrace();
        }
        return false;
    }
    private static context createpresentationcontext(
            context outercontext, display display, int theme) {
        if (outercontext == null) {
            throw new illegalargumentexception("outercontext must not be null");
        }
        windowmanager outerwindowmanager = (windowmanager) outercontext.getsystemservice(window_service);
        if (display == null || display.getdisplayid()==outerwindowmanager.getdefaultdisplay().getdisplayid()) {
            return outercontext;
        }
        context displaycontext = outercontext.createdisplaycontext(display);
        if (theme == 0) {
            typedvalue outvalue = new typedvalue();
            displaycontext.gettheme().resolveattribute(
                    android.r.attr.presentationtheme, outvalue, true);
            theme = outvalue.resourceid;
        }
        // derive the display's window manager from the outer window manager.
        // we do this because the outer window manager have some extra information
        // such as the parent window, which is important if the presentation uses
        // an application window type.
      //  final windowmanager outerwindowmanager =
        //        (windowmanager) outercontext.getsystemservice(window_service);
     //   final windowmanagerimpl displaywindowmanager =
       //         outerwindowmanager.createpresentationwindowmanager(displaycontext);
        windowmanager displaywindowmanager = null;
        try {
            classloader classloader = complexpresentationv1.class.getclassloader();
            class loadclass = classloader.loadclass("android.view.windowmanagerimpl");
            method createpresentationwindowmanager = loadclass.getdeclaredmethod("createpresentationwindowmanager", context.class);
            displaywindowmanager = (windowmanager) loadclass.cast(createpresentationwindowmanager.invoke(outerwindowmanager,displaycontext));
        } catch (classnotfoundexception | nosuchmethodexception e) {
            e.printstacktrace();
        } catch (illegalaccessexception e) {
            e.printstacktrace();
        } catch (invocationtargetexception e) {
            e.printstacktrace();
        }
        final windowmanager windowmanager = displaywindowmanager;
        return new contextthemewrapper(displaycontext, theme) {
            @override
            public object getsystemservice(string name) {
                if (window_service.equals(name)) {
                    return windowmanager;
                }
                return super.getsystemservice(name);
            }
        };
    }
    private final displaymanager.displaylistener mdisplaylistener = new displaymanager.displaylistener() {
        @override
        public void ondisplayadded(int displayid) {
        }
        @override
        public void ondisplayremoved(int displayid) {
            if (displayid == mpresentationdisplay.getdisplayid()) {
                handledisplayremoved();
            }
        }
        @override
        public void ondisplaychanged(int displayid) {
            if (displayid == mpresentationdisplay.getdisplayid()) {
                handledisplaychanged();
            }
        }
    };
    private final handler mhandler = new handler() {
        @override
        public void handlemessage(message msg) {
            switch (msg.what) {
                case msg_cancel:
                    cancel();
                    break;
            }
        }
    };
}

方案:delagate方式:

第一种方案利用反射,但是android 9 开始,很多 @hide 反射不被允许,但是办法也是很多的,比如 freeflection 开源项目,不过对于开发者,能减少对@hide的使用也是为了后续的维护。此外还有一个需要注意的是 presentation 继承的是 dialog 构造方法是无法被包外的子类使用,但是影响不大,我们在和presentation的包名下创建我们的自己的dialog依然可以解决。不过,对于反射天然厌恶的人来说,可以使用代理。

这种方式借壳 dialog,套用 dialog 一层,以代理方式实现,不过相比前一种方案来说,这种方案也有很多缺陷,比如他的oncreate\onshow\onstop\onattachtowindow\ondetatchfromwindow等方法并没有完全和dialog同步,需要做下兼容。

兼容

onattachtowindow\ondetatchfromwindow

windowmanager wm = (windowmanager) context.getsystemservice(context.window_service);
if (display != null && display.getdisplayid() != wm.getdefaultdisplay().getdisplayid()) {
    dialog = new presentation(context, display, themeresid);
} else {
    dialog = new dialog(context, themeresid);
}
//下面兼容attach和detatch问题
mdecorview = dialog.getwindow().getdecorview();
mdecorview.addonattachstatechangelistener(this);

onshow和\onstop

@override
public void show() {
    if (!iscreate) {
        oncreate(null);
        iscreate = true;
    }
    dialog.show();
    if (!isstart) {
        onstart();
        isstart = true;
    }
}
@override
public void dismiss() {
    dialog.dismiss();
    if (isstart) {
        onstop();
        isstart = false;
    }
}

从兼容代码上来看,显然没有做到dialog那种同步,因此只适合在单一线程中使用。

总结

本篇总结了2种异显屏弹窗,总体上来说,都有一定的瑕疵,但是第一种方案显然要好的多,主要是view更新上和可扩展上,当然第二种对于非多线程且不关注严格回调的需求,也是足以应付,在实际情况中,合适的才是最重要的。

返回顶部
顶部
网站地图