自Android P发布以来,陆陆续续的有用户向我反映Android P下输入法存在导航栏变黑的问题,情况如下所示。 用户提供的效果图

于是我抽时间研究了一下这个问题。 经过一番搜索,我在Simple Keyboard下找到了解决方案,其代码大致如下:

  private int mOriginalNavBarColor = 0;
  private int mOriginalNavBarFlags = 0;
  ......

 private void setNavigationBarColor() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
             ......
            final Window window = getWindow().getWindow();
            if (window == null) {
                return;
            }
            mOriginalNavBarColor = window.getNavigationBarColor();
            window.setNavigationBarColor(keyboardColor);

            final View view = window.getDecorView();

            mOriginalNavBarFlags = view.getSystemUiVisibility();
            if (ResourceUtils.isBrightColor(keyboardColor)) {
                view.setSystemUiVisibility(mOriginalNavBarFlags | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
            } else {
                view.setSystemUiVisibility(mOriginalNavBarFlags & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
            }
        }
    }

    private void clearNavigationBarColor() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            final Window window = getWindow().getWindow();
            if (window == null) {
                return;
            }
            window.setNavigationBarColor(mOriginalNavBarColor);
            final View view = window.getDecorView();
            view.setSystemUiVisibility(mOriginalNavBarFlags);
        }
    }

具体的原理,我也不班门弄斧了,想要深入了解的同学请自行百度(代码中的ResourceUtils.isBrightColor实现,我在后面的C#代码中给出)。而因为我使用Xamarin开发的岁寒输入法,所以不能简单地Ctrl+V、Ctrl+C,需要将其转写成C#代码,说白了,就是抄。但这一抄,抄出了大问题。

Xamarin.Android会对Android的原生API进行二次封装,一般而言,get/set方法会转换成属性,flag值会封装成枚举型。 在这个场景下,具体就是:

View.getSystemUiVisibility()/View.setSystemUiVisibility(int) =>StatusBarVisibility View.SystemUiVisibility {get;set;}

其中StatusBarVisibility就是setSystemUiVisibility所能接受的flag值的封装。然而这里出现了错误! setSystemUiVisibility方法所能接受flag值如下图:

而StatusBarVisibility的内容是这样的:

显然,这两者是对不上的。 经过研究,我发现有另一个枚举类型——SystemUiFlags,他的内容是这样的:

显而易见,SystemUiFlags正是对setSystemUiVisibility方法所能接受的flag值的正确封装。可是为什么 View.SystemUiVisibility的类型会是StatusBarVisibility呢?

我在Xamarin.Android的GitHub项目的下发现了一些相关的issue,才确认,这确实是Xamarin.Android的的封装错误,而且年代久远,因为无法修复。这里有bugzilla上的bug反馈记录。 从bug记录中的官方回复可知,如果对这个问题进行更正,会破坏Xamarin.Android的二进制接口(ABI)。

官方如此解释这种破坏的原因:

所以这个封装错误将不会得到修正,只能将错就错。

如此一来,难道这个接口就不能用了吗?倒也不是。 官方有云: The fact that the type is an enum is *irrelevant*. The fact that values of the type can be cast to other types is irrelevant. What matters are the actual types, as contained in the assembly: System.StringComparison != System.TypeCode. (Note in particular that both StringComparison and TypeCode are enumeration types withintas the underlying enum type.) 简单而言就是,枚举型本质上还是int型,对其进行强制类型转换不会改变其本质。所以,解决方案就是在适当的时候使用强制类型转换来解决问题,虽然代码会因此看上去有点丑。

        int originalNavBarColor;
        int originalNavBarFlags;
        private void setNavigationBarColor() {
            if (Build.VERSION.SdkInt >= BuildVersionCodes.P) {
                Window window = this.Window.Window;
                if (window == null) return;
                originalNavBarColor = window.NavigationBarColor;
                
                window.SetNavigationBarColor(newColor);
                var view = window.DecorView;
                originalNavBarFlags = (int)view.SystemUiVisibility;
                if (isBrightColor(color)) {
                    view.SystemUiVisibility = (StatusBarVisibility)(originalNavBarFlags | (int)SystemUiFlags.LightNavigationBar);
                } else {
                    view.SystemUiVisibility = (StatusBarVisibility)(originalNavBarFlags & ~(int)SystemUiFlags.LightNavigationBar);
                }
            }
        }

        private void clearNavigationBarColor() {
            if (Build.VERSION.SdkInt >= BuildVersionCodes.P) {
                Window window = this.Window.Window;
                if (window == null) return;
                window.SetNavigationBarColor(new Color(originalNavBarColor));
                window.DecorView.SystemUiVisibility = (StatusBarVisibility)originalNavBarFlags;
            }
        }

        static bool isBrightColor(int color) {
            if (Android.Resource.Color.Transparent == color) {
                return true;
            }
            // See http://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx
            bool bright = false;
            int[] rgb = { Color.GetRedComponent(color), Color.GetGreenComponent(color), Color.GetBlueComponent(color) };
            int brightness = (int)Math.Sqrt(rgb[0] * rgb[0] * .241 + rgb[1] * rgb[1] * .691 + rgb[2] * rgb[2] * .068);
            if (brightness >= 210) {
                bright = true;
            }
            return bright;
        }

以上。

希望本文能对你有所帮助,感谢阅读。