背景
问题描述
在项目中使用 SwipeBackLayout 或 SlidingMenu 侧滑关闭Activity框架时,由于windowIsTranslucent这个属性设置为了true,导致按home键退到桌面在返回App时会出现两个问题。
先显示上层的Activity,再显示当前交互的Activity。(感觉闪一下)
概率出现当前Activity整个页面为透明,屏幕显示的是上一个界面的Activity,但是当前Activity并没有销毁,并且可以交互
这个是比较严重的用户体验问题,特别在小米手机上会特别明显。
过程
问题猜想
之前就出现过首页透底显示桌面的情况,是因为Theme中windowIsTranslucent = true导致这个问题,通过修改windowIsTranslucent = false属性,彻底解决了首页透底问题。
实施
方案A: 修改所有Activity Theme windowIsTranslucent = true 属性
同样的配方同样的味道
替换所有所有Activity Theme
将window 改为不透明,背景颜色改为透明
false
@color/transparent
复制代码
运行后的效果图:
闪烁透底的问题是解决了,但是侧滑框架出现了侧滑后看不到底部内容,方案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()}.
*
* 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.
*
* 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 true
if Window was opaque and will become translucent or
* false
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.
*
* 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.
*
* 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透明属性;
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) {
}
}
复制代码性能问题的思考
这样的反射是否对性能有损耗呢?在调用时做了耗时测试
在日志打印中可以看到性能完全不会受影响;
为了进一步优化并减少反射调用,仅在用户触发侧滑、侧滑完全闭合时修改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 属性不变
总结
设置windowIsTranslucent =true 后,退后台再打开App时上层的Activity 会被再次绘制
Activity 替换主题的两种方式
AndroidManifest 设置Activity Theme
在Activity setContentView执行前 调用setTheme
Activity 源码分析
convertToTranslucent //将当前Activity Window 设置为透明
convertFromTranslucent //将当前 Activity Window 设置为非透明
反射调用
思考
1.在9.0后 @hide Api 通过反射是无法调用,后续是解决方案
2.除了修改windowIsTranslucent 还没有有其他的解决方案?
3.如何从根源思考、解决问题