Android 仿iOS 側滑關閉Activity框架透底問題解決

背景

問題描述

在項目中使用 SwipeBackLayout 或 SlidingMenu 側滑關閉Activity框架時,因爲windowIsTranslucent這個屬性設置爲了true,致使按home鍵退到桌面在返回App時會出現兩個問題。java

  • 先顯示上層的Activity,再顯示當前交互的Activity。(感受閃一下)
  • 機率出現當前Activity整個頁面爲透明,屏幕顯示的是上一個界面的Activity,可是當前Activity並無銷燬,而且能夠交互

這個是比較嚴重的用戶體驗問題,特別在小米手機上會特別明顯。android

過程

問題猜測

以前就出現過首頁透底顯示桌面的狀況,是由於Theme中windowIsTranslucent = true致使這個問題,經過修改windowIsTranslucent = false屬性,完全解決了首頁透底問題。app

實施

方案A: 修改全部Activity Theme windowIsTranslucent = true 屬性

一樣的配方一樣的味道 替換全部全部Activity Theme 將window 改成不透明,背景顏色改成透明框架

<style name="AppBaseTheme" parent="@style/Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowIsTranslucent">false</item>
        <item name="android:windowBackground">@color/transparent</item>
    </style>
複製代碼

運行後的效果圖:ide

閃爍透底的問題是解決了,可是側滑框架出現了側滑後看不到底部內容,方案A失敗;

方案B:動態設置Activity Theme

在當前App退到後臺時替換Activity爲非透明主題,在Activity恢復到前臺被點擊時替換爲透明主題; 如何動態修改Activity Theme?源碼分析

@Override
    protected void onCreate(Bundle savedInstanceState) {
        if (current_theme!= -1){
            this.setTheme(current_theme);
        }
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.bt_theme).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                changeTheme(GREEN_THEME);
            }
        });
    }


    public void changeTheme(int index) {
        switch (index) {
            case DEFAULT_THEME:
                current_theme = R.style.DefaultTheme;
                break;
            case GREEN_THEME:
                current_theme = R.style.GreenTheme;
                break;
            case ORANGE_THEME:
                current_theme = R.style.OrangeTheme;
                break;
            default:
                break;
        }
    }

    protected void reload() {
        Intent intent = getIntent();
        overridePendingTransition(0, 0);
        intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
        finish();
        overridePendingTransition(0, 0);
        startActivity(intent);
    }
複製代碼

其實設置主題必須在任何view建立以前,因此咱們不可能在activity的onCreate以後來更改主題,若是必定要作,就只能調用setTheme(),而後調用recreate(),從新建立一個activity,而且銷燬上一個activity; 因此這個方案並不可行,整個界面必須銷燬重建。 已知的Android theme修改方式性能

  • AndroidManifest 設置Activity Theme
  • 在Activity setContentView執行前 調用setTheme

能夠經過其餘方式修改Activity windowIsTranslucent 屬性嗎?測試

方案B+:反射動態設置Activity windowIsTranslucent

查閱Activity源碼,看一下他是如何變成透明的優化

/** * Convert a translucent themed Activity {@link android.R.attr#windowIsTranslucent} back from * opaque to translucent following a call to {@link #convertFromTranslucent()}. * <p> * Calling this allows the Activity behind this one to be seen again. Once all such Activities * have been redrawn {@link TranslucentConversionListener#onTranslucentConversionComplete} will * be called indicating that it is safe to make this activity translucent again. Until * {@link TranslucentConversionListener#onTranslucentConversionComplete} is called the image * behind the frontmost Activity will be indeterminate. * <p> * This call has no effect on non-translucent activities or on activities with the * {@link android.R.attr#windowIsFloating} attribute. * * @param callback the method to call when all visible Activities behind this one have been * drawn and it is safe to make this Activity translucent again. * @param options activity options delivered to the activity below this one. The options * are retrieved using {@link #getActivityOptions}. * @return <code>true</code> if Window was opaque and will become translucent or * <code>false</code> if window was translucent and no change needed to be made. * * @see #convertFromTranslucent() * @see TranslucentConversionListener * * @hide */
    @SystemApi
    public boolean convertToTranslucent(TranslucentConversionListener callback, ActivityOptions options) {
        boolean drawComplete;
        try {
            mTranslucentCallback = callback;
            mChangeCanvasToTranslucent = ActivityManager.getService().convertToTranslucent(
                    mToken, options == null ? null : options.toBundle());
            WindowManagerGlobal.getInstance().changeCanvasOpacity(mToken, false);
            drawComplete = true;
        } catch (RemoteException e) {
            // Make callback return as though it timed out.
            mChangeCanvasToTranslucent = false;
            drawComplete = false;
        }
        if (!mChangeCanvasToTranslucent && mTranslucentCallback != null) {
            // Window is already translucent.
            mTranslucentCallback.onTranslucentConversionComplete(drawComplete);
        }
        return mChangeCanvasToTranslucent;
    }
 
 /** * Convert a translucent themed Activity {@link android.R.attr#windowIsTranslucent} to a * fullscreen opaque Activity. * <p> * Call this whenever the background of a translucent Activity has changed to become opaque. * Doing so will allow the {@link android.view.Surface} of the Activity behind to be released. * <p> * This call has no effect on non-translucent activities or on activities with the * {@link android.R.attr#windowIsFloating} attribute. * * @see #convertToTranslucent(android.app.Activity.TranslucentConversionListener, * ActivityOptions) * @see TranslucentConversionListener * * @hide */
    @SystemApi
    public void convertFromTranslucent() {
        try {
            mTranslucentCallback = null;
            if (ActivityManager.getService().convertFromTranslucent(mToken)) {
                WindowManagerGlobal.getInstance().changeCanvasOpacity(mToken, true);
            }
        } catch (RemoteException e) {
            // pass
        }
    }
複製代碼

能夠看到這個兩個Api就是將Activity轉化爲投透明和非透明經過 ActivityManager.getService() 和 WindowManagerGlobal.getInstance().changeCanvasOpacity()修改Window透明屬性;ui

  • convertToTranslucent //將當前Activity Window 設置爲透明
  • convertFromTranslucent //將當前 Activity Window 設置爲非透明

因爲是系統Api 並有 @hide 標註 正常是沒法調用的,能夠經過反射來調用; 反射調用以下:

/** * Convert a translucent themed Activity * 將Activity 改成透明 */
	public static void convertActivityToTranslucent(Activity activity) {
		long timeMillis = SystemClock.currentThreadTimeMillis();
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
			convertActivityToTranslucentAfterL(activity);
		} else {
			convertActivityToTranslucentBeforeL(activity);
		}
		FxLog.d("convertActivity : convertActivityToTranslucent time = " + (SystemClock.currentThreadTimeMillis() - timeMillis));
	}

	/** * Calling the convertToTranslucent method on platforms before Android 5.0 */
	public static void convertActivityToTranslucentBeforeL(Activity activity) {
		try {
			Class<?>[] classes = Activity.class.getDeclaredClasses();
			Class<?> translucentConversionListenerClazz = null;
			for (Class clazz : classes) {
				if (clazz.getSimpleName().contains("TranslucentConversionListener")) {
					translucentConversionListenerClazz = clazz;
				}
			}
			Method method = Activity.class.getDeclaredMethod("convertToTranslucent",
					translucentConversionListenerClazz);
			method.setAccessible(true);
			method.invoke(activity, new Object[] {
					null
			});
		} catch (Throwable t) {
		}
	}

	/** * Calling the convertToTranslucent method on platforms after Android 5.0 */
	private static void convertActivityToTranslucentAfterL(Activity activity) {
		try {
			Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions");
			getActivityOptions.setAccessible(true);
			Object options = getActivityOptions.invoke(activity);

			Class<?>[] classes = Activity.class.getDeclaredClasses();
			Class<?> translucentConversionListenerClazz = null;
			for (Class clazz : classes) {
				if (clazz.getSimpleName().contains("TranslucentConversionListener")) {
					translucentConversionListenerClazz = clazz;
				}
			}
			Method convertToTranslucent = Activity.class.getDeclaredMethod("convertToTranslucent",
					translucentConversionListenerClazz, ActivityOptions.class);
			convertToTranslucent.setAccessible(true);
			convertToTranslucent.invoke(activity, null, options);
		} catch (Throwable t) {
		}
	}

複製代碼
性能問題的思考

這樣的反射是否對性能有損耗呢?在調用時作了耗時測試 在日誌打印中能夠看到性能徹底不會受影響;

04a845d39eebf493d7683025e6a1aeeb.png

爲了進一步優化並減小反射調用,僅在用戶觸發側滑、側滑徹底閉合時修改Activity透明屬性

public void setWindowToTranslucent(boolean translucent) {
		if (isTranslucentWindow == translucent || !isSlidingEnabled()){
			return;
		}
		isTranslucentWindow = translucent;
		if (isTranslucentWindow) {
			convertActivityToTranslucent(((Activity) getContext()));
		} else {
			convertActivityFromTranslucent(((Activity) getContext()));
		}
	}
複製代碼
穩定性問題的思考

因爲是系統Api 在不一樣版本會略有不一樣,作了版本區分。並對反射Api作了try/catch保護,在反射Api調用異常的狀況下,不會對App功能有影響。原Activity windowIsTranslucent 屬性不變

總結

  1. 設置windowIsTranslucent =true 後,退後臺再打開App時上層的Activity 會被再次繪製

  2. Activity 替換主題的兩種方式

  • AndroidManifest 設置Activity Theme
  • 在Activity setContentView執行前 調用setTheme
  1. Activity 源碼分析
  • convertToTranslucent //將當前Activity Window 設置爲透明
  • convertFromTranslucent //將當前 Activity Window 設置爲非透明
  1. 反射調用

思考

1.在9.0後 @hide Api 經過反射是沒法調用,後續是解決方案 2.除了修改windowIsTranslucent 尚未有其餘的解決方案? 3.如何從根源思考、解決問題

相關文章
相關標籤/搜索