background

Problem description

When using SwipeBackLayout or SlidingMenu to close the Activity framework in a project, there are two problems when you press the Home button to return to the App because the windowIsTranslucent property is set to True.

  • The upper-level Activity is displayed first, and then the Activity of the current interaction. (Feels a flash)
  • The entire page of the current Activity is transparent. The screen shows the Activity from the previous interface, but the current Activity is not destroyed and can interact

This is a serious user experience problem, especially in Xiaomi phones will be particularly obvious.

process

Problem conjecture

Previously, there had been a situation where the home page showed the desktop transparently because of the problem caused by the “Theme” windowIsTranslucent = true. By modifying the “Theme” attribute, the home page transparency problem was completely solved.

The implementation of

Option A: Change all Activity Theme windowIsTranslucent = true properties

Same recipe same flavor Replace all All Activity Theme change window to opaque, background color to transparent

    <style name="AppBaseTheme" parent="@style/Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowIsTranslucent">false</item>
        <item name="android:windowBackground">@color/transparent</item>
    </style>
Copy the code

Effect picture after operation:

Option B: Dynamically set the Activity Theme

Replace the Activity with an opaque theme when the current App is in the background, and replace it with a transparent theme when the Activity returns to the foreground and is clicked; How do I dynamically modify the 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(a) {
        Intent intent = getIntent();
        overridePendingTransition(0.0);
        intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
        finish();
        overridePendingTransition(0.0);
        startActivity(intent);
    }
Copy the code

In practice, the theme must be set before any view is created, so it is not possible to change the theme after the activity’s onCreate. If we must, we would just call setTheme() and then call Concrete () to recreate an activity. And destroys the previous activity; So this solution is not feasible and the entire interface has to be destroyed and rebuilt. Known Android theme modification methods

  • AndroidManifest sets the Activity Theme
  • Call setTheme before the Activity setContentView executes

Can I modify Activity windowIsTranslucent properties in other ways?

Option B+ : Reflect dynamic setting Activity windowIsTranslucent

Check the Activity source code to see how it becomes transparent

     /**
     * 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(a) {
        try {
            mTranslucentCallback = null;
            if (ActivityManager.getService().convertFromTranslucent(mToken)) {
                WindowManagerGlobal.getInstance().changeCanvasOpacity(mToken, true); }}catch (RemoteException e) {
            // pass}}Copy the code

Can see the two Api is transparent and opaque through the Activity can be converted to ActivityManager. GetService () and WindowManagerGlobal. GetInstance (). ChangeCanvasOpacity transparency () to modify the Window;

  • Converttoalways // Sets the current Activity Window to transparent
  • Convertfromalways // Sets the current Activity Window to opaque

Because it is a system Api with @hide annotation, it cannot be called normally, can be called by reflection; The reflection call is as follows:

	/** * Convert a themed 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 converttomethod 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 converttomethod 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) {
		}
	}

Copy the code
Thinking about performance problems

Does this reflection cost performance? The time test is done at the time of the call and you can see in the log print that performance is not affected at all;

To further optimize and reduce reflection calls, change the Activity transparency property only when the user triggers the sideslip and the sideslip is fully closed

	public void setWindowToTranslucent(boolean translucent) {
		if(isTranslucentWindow == translucent || ! isSlidingEnabled()){return;
		}
		isTranslucentWindow = translucent;
		if (isTranslucentWindow) {
			convertActivityToTranslucent(((Activity) getContext()));
		} else{ convertActivityFromTranslucent(((Activity) getContext())); }}Copy the code
Thinking about stability problems

Because the system Api will be slightly different in different versions, make version differentiation. In addition, try/catch protection is made for reflection Api. In the case of abnormal reflection Api call, App functions will not be affected. The original Activity windowIsTranslucent property remains unchanged

conclusion

  1. If you set windowIsTranslucent =true, the Activity in the upper layer will be drawn again when you leave the background and open the App again

  2. There are two ways that an Activity can replace a theme

  • AndroidManifest sets the Activity Theme
  • Call setTheme before the Activity setContentView executes
  1. Activity source code analysis
  • Converttoalways // Sets the current Activity Window to transparent
  • Convertfromalways // Sets the current Activity Window to opaque
  1. Reflection calls

thinking

1. The @hide Api cannot be called by reflection after 9.0, followed by solution 2. Don’t you have any other solutions besides windowIsTranslucent? 3. How to think and solve problems from the root