悬浮窗正常情况下,是不能移动到刘海屏所在的区域,那么你看到结果就是:类似这样
如上图所示,刚好是隔了一个刘海屏宽度的距离
要解决悬浮窗可以靠边显示到刘海屏所在区域,只要设置一下Flag那个参数为LayoutParams.FLAG_LAYOUT_NO_LIMITS
即可,主要如下:
mParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_LAYOUT_NO_LIMITS;
这个参数指明悬浮窗的位置不受系统屏幕边界的限制,也就是可以移出屏幕,所以在悬浮窗的onTouch事件中要注意控制悬浮窗不要被移出了屏幕:
case MotionEvent.ACTION_MOVE: {
mParams.x += (int) event.getRawX() - x;
mParams.y += (int) event.getRawY() - y;
//限制不要移出屏幕
mParams.x = mParams.x < 0 ? 0 : mParams.x;
mParams.x = mParams.x > mSreenWidth - mParams.width ? mSreenWidth - mParams.width : mParams.x;
mParams.y = mParams.y < 0 ? 0 : mParams.y;
mParams.y = mParams.y > mScreenHeight - mParams.height ? mScreenHeight - mParams.height : mParams.y;
mWindowManager.updateViewLayout(mContentView, mParams);
x = (int) event.getRawX();
y = (int) event.getRawY();
break;
}
其中,用到的屏幕宽高,最好是用当前窗口可见区域的大小来表示,避免刘海屏影响悬浮窗的位置
//获取当前窗口可见区域宽高
Rect rect = new Rect();
act.getWindow().getDecorView().getGlobalVisibleRect(rect);
mSreenWidth = rect.right - rect.left;
mScreenHeight = rect.bottom - rect.top;
PopupWindow按正常流程设置如下:
mPopupWindow = new CustomPopupWindow(contentView, width, height, false);
mPopupWindow.setOnBackPressListener(new CustomPopupWindow.OnBackPressListener() {
@Override
public void onBack() {
wv.loadUrl("javascript:android_back()");
}
});
if (isLandscape) {//横屏
mPopupWindow.setAnimationStyle(ResourceUtil.getStyleId(mActivityRef.get(), "com_sswl_left_anim"));
} else {
mPopupWindow.setAnimationStyle(ResourceUtil.getStyleId(mActivityRef.get(), "com_sswl_bottom_anim"));
}
//在指定位置显示出来
mPopupWindow.showAtLocation(mActivityRef.get().getWindow().getDecorView(), Gravity.TOP | Gravity.LEFT, x, y);
然而,即使x =0,y=0,PopupWindow也并没有靠边显示,明显是隔了一个刘海的区域
这时候需要设置一下PopupWindow内容不受屏幕限制修剪,即设置:
//设置这个,popuwindow才能显示到刘海屏区域
mPopupWindow.setClippingEnabled(false);
这样设置之后,假如你的contentview宽高是具体尺寸是没有什么问题了,但是假如是MATCH_PARENT的话,里面的内容就会显示得很大,超出可视区域,所以,要是contentview宽高是MATCH_PARENT,还是需要设置具体的尺寸给它
// setClippingEnabled(false)之后,一定要设置里面的宽高,要不里面内容会变得很大
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(width, height);
llWv.setLayoutParams(lp);
完成上述步骤,就完美解决了PopupWindow不能靠边显示问题了
1、上面提到PopupWindow怎么适配刘海屏,但是可能很多小伙伴还不知道怎么设置自己的Activity,才使得Activity的内容延伸到刘海屏区域,设置其实也很简单,从Android 9.0开始,Google提供了官方Api可以设置,直接在你的Activity onCreate方法中setContentView之前调用即可
//解决android 9.0水滴屏/刘海屏有黑边的问题
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(lp);
}
其中,lp.layoutInDisplayCutoutMode可以有三种取值,不同取值的效果说明如下:
模式 | 模式说明 |
---|---|
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT | 只有当DisplayCutout完全包含在系统栏中时,才允许窗口延伸到DisplayCutout区域。 否则,窗口布局不与DisplayCutout区域重叠。 |
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER | 该窗口决不允许与DisplayCutout区域重叠。 |
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES | 该窗口始终允许延伸到屏幕短边上的DisplayCutout区域。 |
经过上述设置之后Activity的内容是显示到了刘海区域,但是弹出的Dialog的时候,刘海区域的黑边又出来了,所以也需要在Dialog的onCreate
方法中设置上述代码,因为上述代码是对每个window单独设置生效的,弹出的dialog属于新的window,故需要重新设置
2、经过上述设置,activity的内容是延伸到了刘海屏,但是这时候又引入了一个新的问题,那就是你的activity内容延伸到刘海区域显示了,这就存在被刘海遮挡的问题,所以就需要我们计算出刘海的位置与宽高,在Android 9.0之后Google官方提供了Api可以获取:
@TargetApi(28)
public void getNotchParams() {
final View decorView = getWindow().getDecorView();
decorView.post(new Runnable() {
@Override
public void run() {
DisplayCutout displayCutout = decorView.getRootWindowInsets().getDisplayCutout();
Log.e("TAG", "安全区域距离屏幕左边的距离 SafeInsetLeft:" + displayCutout.getSafeInsetLeft());
Log.e("TAG", "安全区域距离屏幕右部的距离 SafeInsetRight:" + displayCutout.getSafeInsetRight());
Log.e("TAG", "安全区域距离屏幕顶部的距离 SafeInsetTop:" + displayCutout.getSafeInsetTop());
Log.e("TAG", "安全区域距离屏幕底部的距离 SafeInsetBottom:" + displayCutout.getSafeInsetBottom());
List<Rect> rects = displayCutout.getBoundingRects();
if (rects == null || rects.size() == 0) {
Log.e("TAG", "不是刘海屏");
} else {
Log.e("TAG", "刘海屏数量:" + rects.size());
for (Rect rect : rects) {
Log.e("TAG", "刘海屏区域:" + rect);
}
}
}
});
}
输出结果,看一下:
E/TAG: 安全区域距离屏幕左边的距离 SafeInsetLeft:81
E/TAG: 安全区域距离屏幕右部的距离 SafeInsetRight:0
E/TAG: 安全区域距离屏幕顶部的距离 SafeInsetTop:0
E/TAG: 安全区域距离屏幕底部的距离 SafeInsetBottom:0
E/TAG: 刘海屏数量:1
E/TAG: 刘海屏区域:Rect(0, 428 - 81, 653)
这个是横屏Activity,所以你可以看到日志,假如你Activity内容距离左边为81px的话,是不会被刘海遮挡的,而且刘海占据的区域是横向x坐标:0到81,竖向y坐标:428到653
3、上面提到的解决方案都是Android 9.0之后的,但是在Android 9.0之前就有很多国产机子出现了刘海屏/水滴屏,那么在没有官方提供Api,我们就需要单独对每个牌子的手机进行适配,这些可以在对应牌子的官网中找
1)先通过每个牌子提供的Api判断是否是有刘海屏的机子:
public class DisplayCutoutUtils {
/**
* 是否是刘海屏手机
* @param activity
* @return
*/
public static boolean isCutoutScreen(Activity activity) {
try {
if (activity == null) {
return false;
}
if (Build.VERSION.SDK_INT >= 28) {
return activity.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout() != null;
}
String manufacturer = Build.MANUFACTURER;
if (!TextUtils.isEmpty(manufacturer)) {
if (manufacturer.equalsIgnoreCase("HUAWEI")) {
return hwCutout(activity);
}
if (manufacturer.equalsIgnoreCase("xiaomi")) {
return xiaomiCutout(activity);
}
if (manufacturer.equalsIgnoreCase("oppo")) {
return oppoCutout(activity);
}
if (manufacturer.equalsIgnoreCase("vivo")) {
return vivoCutout(activity);
}
}
} catch (Throwable e) {
e.printStackTrace();
Logger.e(e.getMessage());
}
return false;
}
/**
* 获取状态栏高度
* @param activity
* @return
*/
public static int getStatusBarHeight(Activity activity) {
int identifier;
if (activity != null && (identifier = activity.getResources().getIdentifier("status_bar_height", "dimen", "android")) > 0) {
return activity.getResources().getDimensionPixelSize(identifier);
}
return 0;
}
/**
* 华为刘海屏判断
* @param activity
* @return
*/
public static boolean hwCutout(Activity activity) {
try {
Class<?> loadClass = activity.getClassLoader().loadClass("com.huawei.android.util.HwNotchSizeUtil");
return ((Boolean) loadClass.getMethod("hasNotchInScreen", new Class[0]).invoke(loadClass, new Object[0])).booleanValue();
} catch (Exception unused) {
return false;
}
}
/**
* oppo刘海屏判断
* @param activity
* @return
*/
public static boolean oppoCutout(Activity activity) {
return activity.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
/**
* vivo刘海屏判断
* @param activity
* @return
*/
public static boolean vivoCutout(Activity activity) {
try {
Class<?> cls = Class.forName("android.util.FtFeature");
return ((Boolean) cls.getMethod("isFeatureSupport", new Class[]{Integer.TYPE}).invoke(cls, new Object[]{32})).booleanValue();
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 小米刘海屏判断
* @param activity
* @return
*/
public static boolean xiaomiCutout(Activity activity) {
try {
Class<?> cls = Class.forName("android.os.SystemProperties");
return ((Integer) cls.getMethod("getInt", new Class[]{String.class, Integer.TYPE}).invoke(cls, new Object[]{"ro.miui.notch", 0})).intValue() == 1;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
2)再获取刘海区域的尺寸,这个基本是获取状态栏的高度就差不多了,刘海屏一般是在状态栏高度内或者略大于状态栏高度而已
public static int getStatusBarHeight(Context context) {
int statusBarHeight = 0;
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
}
return statusBarHeight;
}