欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

Android 双屏异显自适应Dialog的实现,

来源: 开发者 投稿于  被查看 38640 次 评论:61

Android 双屏异显自适应Dialog的实现,


目录
  • 一、前言
    • 需求
    • 问题
  • 二、方案
    • 方案:自定义Presentation
    • 原理
      • WindowType问题解决
      • WindowManagerImpl 问题
    • 方案:Delagate方式:
      • 兼容
  • 总结

    一、前言

    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 <a href="{@docRoot}guide/topics/resources/available-resources.html#stylesandthemes">
         * Style and Theme Resources</a> for more information about defining and using
         * styles.  This theme is applied on top of the current theme in
         * <var>outerContext</var>.  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更新上和可扩展上,当然第二种对于非多线程且不关注严格回调的需求,也是足以应付,在实际情况中,合适的才是最重要的。

    到此这篇关于Android 双屏异显自适应Dialog的实现的文章就介绍到这了,更多相关Android 双屏异显自适应Dialog内容请搜索3672js教程以前的文章或继续浏览下面的相关文章希望大家以后多多支持3672js教程!

    您可能感兴趣的文章:
    • Android DialogFragment使用之底部弹窗封装示例
    • Android自定义Dialog的方法实例
    • Android自定义Dialog的2种常见方法
    • Android自定义弹框Dialog效果
    • 浅谈Android Dialog窗口机制
    • Android自定义样式圆角dialog对话框
    • Android中常用的三个Dialog弹窗总结解析

    用户评论