美文网首页Android进阶之路Android开发Android技术知识
Android知识笔记:Android 仿iOS 侧滑关闭Act

Android知识笔记:Android 仿iOS 侧滑关闭Act

作者: Z_萧晓 | 来源:发表于2020-04-13 22:17 被阅读0次

    背景

    问题描述

    在项目中使用 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 改为不透明,背景颜色改为透明

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

    运行后的效果图:

    闪烁透底的问题是解决了,但是侧滑框架出现了侧滑后看不到底部内容,方案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透明属性;

    • 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 属性不变

    总结

    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.如何从根源思考、解决问题

    最后我想说:对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

    这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司19年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

    相信它会给大家带来很多收获:

    上述【高清技术脑图】以及【配套的架构技术PDF】可以 关注我 【主页简介】 或者【简信】免费获取

    Android学习PDF+架构视频+面试文档+源码笔记

    当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

    相关文章

      网友评论

        本文标题:Android知识笔记:Android 仿iOS 侧滑关闭Act

        本文链接:https://www.haomeiwen.com/subject/vnmomhtx.html