Android?雙屏異顯自適應(yīng)Dialog的實現(xiàn)
一、前言
Android 多屏互聯(lián)的時代,必然會出現(xiàn)多屏連接的問題,通常意義上的多屏連接包括HDMI/USB、WifiDisplay,除此之外Android 還有OverlayDisplay和VirtualDisplay,其中VirtualDisplay相比不少人錄屏的時候都會用到,在Android中他們都是Display,除了物理屏幕,你在OverlayDisplay和VirtualDisplay同樣也可以展示彈窗或者展示Activity,所有的Display的差異化通過DisplayManagerService 進(jìn)行了兼容,同樣任意一種Display都擁有自己的密度和大小以及display Id,對于測試雙屏應(yīng)用,一般也可以通過VirtualDisplay進(jìn)行模擬操作。
需求
本篇主要解決副屏Dialog 組建展示問題。存在任意類型的副屏?xí)r,讓 Dialog 展示在副屏上,如果不存在,就需要讓它自動展示在主屏上。
為什么會有這種需求呢?默認(rèn)情況下,實現(xiàn)雙屏異顯的時候, 通常不是使用Presentation就是Activity,然而,Dialog只能展示在主屏上,而Presentation只能展示的副屏上。想象一下這種雙屏場景,在切換視頻的時候,Loading展示應(yīng)該是在主屏還是副屏呢 ?毫無疑問,答案當(dāng)然是副屏。
問題
我們要解決的問題當(dāng)然是隨著場景的切換,Dialog展示在不同的屏幕上。同樣,內(nèi)容也可以這樣展示,當(dāng)存在副屏的時候在副屏上展示內(nèi)容,當(dāng)只有主屏的時候在主屏上展示內(nèi)容。
二、方案
我們這里梳理一下兩種方案。
方案:自定義Presentation
作為Presentation的核心點有兩個,其中一個是displayId,另一個是WindowType,第一個是通常意義上指定Display Id,第二個是窗口類型。如果是副屏,那么displayId是必須的參數(shù),且不能和DefaultDisplay的id一樣,除此之外WindowType是一個需要重點關(guān)注的東西。
早期的 TYPE_PRESENTATION 存在指紋信息 “被借用” 而造成用戶資產(chǎn)損失的風(fēng)險,即便外部無法獲取,但是早期的Android 8.0版本利用 (TYPE_PRESENTATION=TYPE_APPLICATION_OVERLAY-1)可以實現(xiàn)屏幕外彈框,在之后的版本做了修復(fù),同時對 TYPE_PRESENTATION 展示必須有 Token 等校驗,但是在這種過程中,Presentation的WindowType 變了又變,因此,我們?nèi)绾潍@取到兼容每個版本的WindowType呢?
原理
Display Id的問題我們不需要重點處理,從display 獲取即可。WindowType才是重點,方法當(dāng)然是有的,我們不繼承Presentation,而是繼承Dialog因此自行實現(xiàn)可以參考 Presentation 中的代碼,當(dāng)然難點是 WindowManagerImpl 和WindowType類獲取,前者 @hide 標(biāo)注的,而后者不固定。
早期我們可以利用 compileOnly layoutlib.jar 的方式導(dǎo)入 WindowManagerImpl,但是新版本中 layoutlib.jar 中的類已經(jīng)幾乎被刪,另外如果要使用 layoutlib.jar,那么你的項目中的 kotlin 版本就會和 layoutlib.jar 產(chǎn)生沖突,雖然可以刪除相關(guān)的類,但是這種維護(hù)方式非常繁瑣,因此我們這里借助反射實現(xiàn)。當(dāng)然除了反射也可以利用Dexmaker或者xposed Hook方式,只是復(fù)雜性會很多。
WindowType問題解決
我們知道,創(chuàng)建Presentation的時候,framework源碼是設(shè)置了WindowType的,我們完全在我們自己的Dialog創(chuàng)建Presentation對象,讀取出來設(shè)置上到我們自己的Dialog上即可。
不過,我們先要對Display進(jìn)行隔離,避免主屏走這段邏輯
WindowManager wm = (WindowManager) outerContext.getSystemService(WINDOW_SERVICE);
if(display==null || wm.getDefaultDisplay().getDisplayId()==display.getDisplayId()){
return;
}//注意,這里需要借助Presentation的一些屬性,否則無法正常彈出彈框,要么有權(quán)限問題、要么有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,事實上每個版本是不一樣的,因此這里動態(tài)獲取 w.setGravity(Gravity.FILL); w.setType(standardAttributes.type);
WindowManagerImpl 問題
其實我們知道,Presentation的WindowManagerImpl并不是給自己用的,而是給Dialog上的其他組件(如Menu、PopWindow等),將其他組件加到Dialog的 Window上,因為在Android系統(tǒng)中,WindowManager都是parent Window所具備的能力,所以創(chuàng)建這個不是為了把Dialog加進(jìn)去,而是為了把基于Dialog的Window組件加到Dialog上,這和Activity是一樣的。那么,其實如果我們沒有Menu、PopWindow,這里實際上是可以不處理的,但是作為一個完整的類,我們這里使用反射處理一下。
怎么處理呢?
我們知道,異顯屏的Context是通過createDisplayContext創(chuàng)建的,但是我們這里并不是Hook這個方法,只是在創(chuàng)建這個Display Context之后,再通過ContextThemeWrapper,設(shè)置進(jìn)去即可。
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的一些屬性,否則無法正常彈出彈框,要么有權(quán)限問題、要么有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,事實上每個版本是不一樣的,因此這里動態(tài)獲取
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 開源項目,不過對于開發(fā)者,能減少對@hide的使用也是為了后續(xù)的維護(hù)。此外還有一個需要注意的是 Presentation 繼承的是 Dialog 構(gòu)造方法是無法被包外的子類使用,但是影響不大,我們在和Presentation的包名下創(chuàng)建我們的自己的Dialog依然可以解決。不過,對于反射天然厭惡的人來說,可以使用代理。
這種方式借殼 Dialog,套用 Dialog 一層,以代理方式實現(xiàn),不過相比前一種方案來說,這種方案也有很多缺陷,比如他的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那種同步,因此只適合在單一線程中使用。
總結(jié)
本篇總結(jié)了2種異顯屏彈窗,總體上來說,都有一定的瑕疵,但是第一種方案顯然要好的多,主要是View更新上和可擴(kuò)展上,當(dāng)然第二種對于非多線程且不關(guān)注嚴(yán)格回調(diào)的需求,也是足以應(yīng)付,在實際情況中,合適的才是最重要的。
到此這篇關(guān)于Android 雙屏異顯自適應(yīng)Dialog的實現(xiàn)的文章就介紹到這了,更多相關(guān)Android 雙屏異顯自適應(yīng)Dialog內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android組件化工具ARouter使用方法詳細(xì)分析
這篇文章主要介紹了Android組件化工具ARouter使用方法,組件化項目存在各個模塊之間耦合,通信麻煩的問題,為了解決這個問題,阿里巴巴的開發(fā)者就搞出了Arouter這個框架,以解決上述問題2022-10-10
Android中通過Notification&NotificationManager實現(xiàn)消息通知
關(guān)于通知Notification相信大家都不陌生了,平時上QQ的時候有消息來了或者有收到了短信,手機(jī)頂部就會顯示有新消息什么的,就類似這種。今天就稍微記錄下幾種Notification的用法。3.0以前的通知和3.0以后的通知是有些區(qū)別的。2015-10-10
Android開發(fā)InputManagerService創(chuàng)建與啟動流程
這篇文章主要為大家介紹了Android開發(fā)InputManagerService創(chuàng)建與啟動流程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
Android開發(fā)使用Handler實現(xiàn)圖片輪播功能示例
這篇文章主要介紹了Android開發(fā)使用Handler實現(xiàn)圖片輪播功能,涉及Android基于Handler操作圖片的相關(guān)實現(xiàn)技巧與操作注意事項,需要的朋友可以參考下2017-09-09
Android studio 3.0 查看手機(jī)文件系統(tǒng)的方法(超簡單)
本文給大家分享Android studio更新到3.0版本之后,查看手機(jī)文件系統(tǒng)的方法,需要的朋友參考下吧2017-11-11
Android讀取用戶號碼,手機(jī)串號,SIM卡序列號的實現(xiàn)代碼
以下是對Android讀取用戶號碼,手機(jī)串號,SIM卡序列號的實現(xiàn)代碼進(jìn)行了介紹,需要的朋友可以過來參考下2013-07-07
Kotlin協(xié)程Job生命周期結(jié)構(gòu)化并發(fā)詳解
這篇文章主要為大家介紹了Kotlin協(xié)程Job生命周期結(jié)構(gòu)化并發(fā)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

