Android?雙屏異顯自適應(yīng)Dialog的實(shí)現(xiàn)
一、前言
Android 多屏互聯(lián)的時(shí)代,必然會(huì)出現(xiàn)多屏連接的問(wèn)題,通常意義上的多屏連接包括HDMI/USB、WifiDisplay,除此之外Android 還有OverlayDisplay和VirtualDisplay,其中VirtualDisplay相比不少人錄屏的時(shí)候都會(huì)用到,在Android中他們都是Display,除了物理屏幕,你在OverlayDisplay和VirtualDisplay同樣也可以展示彈窗或者展示Activity,所有的Display的差異化通過(guò)DisplayManagerService 進(jìn)行了兼容,同樣任意一種Display都擁有自己的密度和大小以及display Id,對(duì)于測(cè)試雙屏應(yīng)用,一般也可以通過(guò)VirtualDisplay進(jìn)行模擬操作。
需求
本篇主要解決副屏Dialog 組建展示問(wèn)題。存在任意類(lèi)型的副屏?xí)r,讓 Dialog 展示在副屏上,如果不存在,就需要讓它自動(dòng)展示在主屏上。
為什么會(huì)有這種需求呢?默認(rèn)情況下,實(shí)現(xiàn)雙屏異顯的時(shí)候, 通常不是使用Presentation就是Activity,然而,Dialog只能展示在主屏上,而Presentation只能展示的副屏上。想象一下這種雙屏場(chǎng)景,在切換視頻的時(shí)候,Loading展示應(yīng)該是在主屏還是副屏呢 ?毫無(wú)疑問(wèn),答案當(dāng)然是副屏。
問(wèn)題
我們要解決的問(wèn)題當(dāng)然是隨著場(chǎng)景的切換,Dialog展示在不同的屏幕上。同樣,內(nèi)容也可以這樣展示,當(dāng)存在副屏的時(shí)候在副屏上展示內(nèi)容,當(dāng)只有主屏的時(shí)候在主屏上展示內(nèi)容。
二、方案
我們這里梳理一下兩種方案。
方案:自定義Presentation
作為Presentation的核心點(diǎn)有兩個(gè),其中一個(gè)是displayId,另一個(gè)是WindowType,第一個(gè)是通常意義上指定Display Id,第二個(gè)是窗口類(lèi)型。如果是副屏,那么displayId是必須的參數(shù),且不能和DefaultDisplay的id一樣,除此之外WindowType是一個(gè)需要重點(diǎn)關(guān)注的東西。
早期的 TYPE_PRESENTATION 存在指紋信息 “被借用” 而造成用戶資產(chǎn)損失的風(fēng)險(xiǎn),即便外部無(wú)法獲取,但是早期的Android 8.0版本利用 (TYPE_PRESENTATION=TYPE_APPLICATION_OVERLAY-1)可以實(shí)現(xiàn)屏幕外彈框,在之后的版本做了修復(fù),同時(shí)對(duì) TYPE_PRESENTATION 展示必須有 Token 等校驗(yàn),但是在這種過(guò)程中,Presentation的WindowType 變了又變,因此,我們?nèi)绾潍@取到兼容每個(gè)版本的WindowType呢?
原理
Display Id的問(wèn)題我們不需要重點(diǎn)處理,從display 獲取即可。WindowType才是重點(diǎn),方法當(dāng)然是有的,我們不繼承Presentation,而是繼承Dialog因此自行實(shí)現(xiàn)可以參考 Presentation 中的代碼,當(dāng)然難點(diǎn)是 WindowManagerImpl 和WindowType類(lèi)獲取,前者 @hide 標(biāo)注的,而后者不固定。
早期我們可以利用 compileOnly layoutlib.jar 的方式導(dǎo)入 WindowManagerImpl,但是新版本中 layoutlib.jar 中的類(lèi)已經(jīng)幾乎被刪,另外如果要使用 layoutlib.jar,那么你的項(xiàng)目中的 kotlin 版本就會(huì)和 layoutlib.jar 產(chǎn)生沖突,雖然可以刪除相關(guān)的類(lèi),但是這種維護(hù)方式非常繁瑣,因此我們這里借助反射實(shí)現(xiàn)。當(dāng)然除了反射也可以利用Dexmaker或者xposed Hook方式,只是復(fù)雜性會(huì)很多。
WindowType問(wèn)題解決
我們知道,創(chuàng)建Presentation的時(shí)候,framework源碼是設(shè)置了WindowType的,我們完全在我們自己的Dialog創(chuàng)建Presentation對(duì)象,讀取出來(lái)設(shè)置上到我們自己的Dialog上即可。
不過(guò),我們先要對(duì)Display進(jìn)行隔離,避免主屏走這段邏輯
WindowManager wm = (WindowManager) outerContext.getSystemService(WINDOW_SERVICE); if(display==null || wm.getDefaultDisplay().getDisplayId()==display.getDisplayId()){ return; }
//注意,這里需要借助Presentation的一些屬性,否則無(wú)法正常彈出彈框,要么有權(quán)限問(wèn)題、要么有token問(wèn)題
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,事實(shí)上每個(gè)版本是不一樣的,因此這里動(dòng)態(tài)獲取 w.setGravity(Gravity.FILL); w.setType(standardAttributes.type);
WindowManagerImpl 問(wèn)題
其實(shí)我們知道,Presentation的WindowManagerImpl并不是給自己用的,而是給Dialog上的其他組件(如Menu、PopWindow等),將其他組件加到Dialog的 Window上,因?yàn)樵贏ndroid系統(tǒng)中,WindowManager都是parent Window所具備的能力,所以創(chuàng)建這個(gè)不是為了把Dialog加進(jìn)去,而是為了把基于Dialog的Window組件加到Dialog上,這和Activity是一樣的。那么,其實(shí)如果我們沒(méi)有Menu、PopWindow,這里實(shí)際上是可以不處理的,但是作為一個(gè)完整的類(lèi),我們這里使用反射處理一下。
怎么處理呢?
我們知道,異顯屏的Context是通過(guò)createDisplayContext創(chuàng)建的,但是我們這里并不是Hook這個(gè)方法,只是在創(chuàng)建這個(gè)Display Context之后,再通過(guò)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的一些屬性,否則無(wú)法正常彈出彈框,要么有權(quán)限問(wèn)題、要么有token問(wèn)題 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,事實(shí)上每個(gè)版本是不一樣的,因此這里動(dòng)態(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 開(kāi)始,很多 @hide 反射不被允許,但是辦法也是很多的,比如 freeflection 開(kāi)源項(xiàng)目,不過(guò)對(duì)于開(kāi)發(fā)者,能減少對(duì)@hide的使用也是為了后續(xù)的維護(hù)。此外還有一個(gè)需要注意的是 Presentation 繼承的是 Dialog 構(gòu)造方法是無(wú)法被包外的子類(lèi)使用,但是影響不大,我們?cè)诤蚉resentation的包名下創(chuàng)建我們的自己的Dialog依然可以解決。不過(guò),對(duì)于反射天然厭惡的人來(lái)說(shuō),可以使用代理。
這種方式借殼 Dialog,套用 Dialog 一層,以代理方式實(shí)現(xiàn),不過(guò)相比前一種方案來(lái)說(shuō),這種方案也有很多缺陷,比如他的onCreate\onShow\onStop\onAttachToWindow\onDetatchFromWindow等方法并沒(méi)有完全和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問(wèn)題 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; } }
從兼容代碼上來(lái)看,顯然沒(méi)有做到Dialog那種同步,因此只適合在單一線程中使用。
總結(jié)
本篇總結(jié)了2種異顯屏彈窗,總體上來(lái)說(shuō),都有一定的瑕疵,但是第一種方案顯然要好的多,主要是View更新上和可擴(kuò)展上,當(dāng)然第二種對(duì)于非多線程且不關(guān)注嚴(yán)格回調(diào)的需求,也是足以應(yīng)付,在實(shí)際情況中,合適的才是最重要的。
到此這篇關(guān)于Android 雙屏異顯自適應(yīng)Dialog的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Android 雙屏異顯自適應(yīng)Dialog內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Android實(shí)現(xiàn)可滾動(dòng)的環(huán)形菜單效果
這篇文章主要為大家詳細(xì)介紹了Android如何使用kotlin實(shí)現(xiàn)可滾動(dòng)的環(huán)形菜單,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03Android完美實(shí)現(xiàn)平滑過(guò)渡的ViewPager廣告條
這篇文章主要為大家詳細(xì)介紹了Android完美實(shí)現(xiàn)平滑過(guò)渡的ViewPager廣告條,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Android開(kāi)發(fā)環(huán)境安裝和配置圖文教程
輕松搞定Android開(kāi)發(fā)環(huán)境部署,這篇文章主要為大家詳細(xì)介紹了Android開(kāi)發(fā)環(huán)境安裝和配置圖文教程,感興趣的小伙伴們可以參考一下2016-06-06RecyclerView實(shí)現(xiàn)橫向滾動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了RecyclerView實(shí)現(xiàn)橫向滾動(dòng)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-01-01Android開(kāi)發(fā)中類(lèi)加載器DexClassLoader的簡(jiǎn)單使用講解
這篇文章主要介紹了Android開(kāi)發(fā)中類(lèi)加載器DexClassLoader的簡(jiǎn)單使用講解,DexClassLoader可以看作是一個(gè)特殊的Java中的ClassLoader,需要的朋友可以參考下2016-04-04Android自定義TimeButton實(shí)現(xiàn)倒計(jì)時(shí)按鈕
這篇文章主要為大家詳細(xì)介紹了Android自定義TimeButton實(shí)現(xiàn)倒計(jì)時(shí)按鈕,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12詳解Android沉浸式實(shí)現(xiàn)兼容解決辦法
本篇文章主要介紹了詳解Android沉浸式實(shí)現(xiàn)兼容解決辦法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11Android使用AutoCompleteTextView實(shí)現(xiàn)自動(dòng)填充功能的案例
今天小編就為大家分享一篇關(guān)于Android使用AutoCompleteTextView實(shí)現(xiàn)自動(dòng)填充功能的案例,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03