一、需求
Android 多屏物聯的時代,必然會出現多屏連接的問題。本篇主要解決副屏可插拔之後Dialog組建展示問題。存在副屏時,讓Dialog展示在副屏上,如果不存在,就需要讓它自動展示在主屏上。
二、方案
【1】自行實現Presentation,由於早期的TYPE_PRESENTATION存在指紋信息“被借用”而造成損失的風險,以前利用 (TYPE_PRESENTATION=TYPE_APPLICATION_OVERLAY-1)可以實現屏幕外彈框,在之後的版本做了修復,同時對TYPE_PRESENTATION展示必須有Token等校驗,因此自行實現可以參考Presentation中的代碼,當然難點是WindowManagerImpl類獲取,因爲它是@hide標註的。
解決方式一:早期我們可以利用 compileOnly layoutlib.jar的方式倒入WindowManagerImpl,但是新版本中layoutlib.jar中的類已經幾乎被刪,另外如果要使用layoutlib.jar,那麼你的項目中的kotlin版本就會和layoutlib.jar產生衝突,雖然可以刪除相關的類,但是這種維護方式非常繁瑣。
解決方式二:反射,利用反射本身就是一種方式,當然android 9開始,很多@hide反射不被允許,但是辦法也是很多的,比如freeflection開源項目,因此推薦這種方式。
此外還有一個需要注意的是Presentation繼承的是Dialog構造方法是無法被包外的子類使用,但是影響不大。
【2】借殼Dialog,這種事只是套用Dialog一層,異動態代理方式實現,本篇推薦此方案。
三、代碼實現
3.1 方案【1】代碼實現
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;
}
}
};
}
3.2 方案【2】代碼實現
public class ComplexPresentation extends Dialog implements View.OnAttachStateChangeListener {
private View mDecorView;
private Dialog dialog = null;
private boolean isCreate = false;
private boolean isStart = false;
private final String TAG = "ComplexPresentation";
public ComplexPresentation(Context context, Display display, int themeResId) {
super(context);
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);
}
mDecorView = dialog.getWindow().getDecorView();
mDecorView.addOnAttachStateChangeListener(this);
}
@Override
public boolean isShowing() {
return dialog.isShowing();
}
@Override
public Window getWindow() {
return dialog.getWindow();
}
@Override
public View getCurrentFocus() {
return dialog.getCurrentFocus();
}
@Override
public ActionBar getActionBar() {
return dialog.getActionBar();
}
@Override
public LayoutInflater getLayoutInflater() {
return dialog.getLayoutInflater();
}
@Override
public void setOnShowListener(OnShowListener listener) {
dialog.setOnShowListener(listener);
}
@Override
public void setOnDismissListener(OnDismissListener listener) {
dialog.setOnDismissListener(listener);
}
@Override
public void setOnKeyListener(OnKeyListener onKeyListener) {
dialog.setOnKeyListener(onKeyListener);
}
@Override
public void setOnCancelListener(OnCancelListener listener) {
dialog.setOnCancelListener(listener);
}
@Override
public void setDismissMessage(Message msg) {
dialog.setDismissMessage(msg);
}
@Override
public void setContentView(int layoutResID) {
dialog.setContentView(layoutResID);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
dialog.setContentView(view, params);
}
@Override
public void setContentView(View view) {
dialog.setContentView(view);
}
@Override
public void setCancelMessage(Message msg) {
dialog.setCancelMessage(msg);
}
@Override
public void show() {
if (!isCreate) {
onCreate(null);
isCreate = true;
}
dialog.show();
if (!isStart) {
onStart();
isStart = true;
}
}
@Override
public void hide() {
dialog.hide();
}
@Override
public void dismiss() {
dialog.dismiss();
if (isStart) {
onStop();
isStart = false;
}
}
@Override
public void cancel() {
dialog.cancel();
}
@Override
public void setCancelable(boolean flag) {
dialog.setCancelable(flag);
}
@Override
public void setCanceledOnTouchOutside(boolean cancel) {
dialog.setCanceledOnTouchOutside(cancel);
}
@Override
public void setTitle(CharSequence title) {
dialog.setTitle(title);
}
@Override
public void setTitle(int titleId) {
dialog.setTitle(titleId);
}
@Override
public void onViewAttachedToWindow(View v) {
onAttachedToWindow();
MLog.d(TAG, "onAttachedToWindow");
}
@Override
public void onViewDetachedFromWindow(View v) {
onDetachedFromWindow();
MLog.d(TAG, "onDetachedFromWindow");
}
}