Android 單雙屏自適應Presentation

一、需求

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");
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章