注册

Android 双屏异显自适应Dialog

一、前言


Android 多屏互联的时代,必然会出现多屏连接的问题,通常意义上的多屏连接包括HDMI/USB、WifiDisplay,除此之外Android 还有OverlayDisplay和VirtualDisplay,其中VirtualDisplay相比不少人录屏的时候都会用到,在Android中他们都是Display,除了物理屏幕,你在OverlayDisplay和VirtualDisplay同样也可以展示弹窗或者展示Activity,所有的Dislplay的差异化通过DisplayManagerService进行了兼容,同样自己的密度和大小以及displayId。


企业微信20231224-132106@2x.png


需求


本篇主要解决副屏可插拔之后 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呢?


自定义


方法当然是有的,我们不继承Presentation,而是继承Dialog因此自行实现可以参考 Presentation 中的代码,当然难点是 WindowManagerImpl 和WindowType类获取,前者 @hide 标注的,而后者不固定。


解决方式一:


早期我们可以利用 compileOnly layoutlib.jar 的方式倒入 WindowManagerImpl,但是新版本中 layoutlib.jar 中的类已经几乎被删,另外如果要使用 layoutlib.jar,那么你的项目中的 kotlin 版本就会和 layoutlib.jar 产生冲突,虽然可以删除相关的类,但是这种维护方式非常繁琐。


WindowType问题解决

我们知道,创建Presentation的时候,framework源码是设置了WindowType的,我们完全在我们自己的Dialog创建Presentation对象,读取出来设置上即可。


不过,我们先要对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上,当然你也可以通过另类方式实现Dialog,抛开通用性不谈的话。那么,其实如果我们没有Menu或者PopWindow,这里实际上是可以不处理的,但是作为一个完整的类,我们这里使用反射处理一下。


怎么处理呢?


我们知道,异显屏的Context是通过createDisplayContext创建的,但是我们这里并不是Hook这个方法,知识在创建这个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 开源项目。


此外还有一个需要注意的是 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更新上和可扩展上,当然第二种对于非多线程且不关注严格回调的需求,也是足以应付,在实际情况中,合适的才是最重要的。


作者:时光少年
来源:juejin.cn/post/7315846805920972809

0 个评论

要回复文章请先登录注册