背景
为了不影响到用户玩游戏的体验,渠道菜单通常以悬浮小球的方式显示在游戏界面中,支持拖动和隐藏。点击之后弹出对应的菜单然后跳转至功能页面。
实现方案
通过设置onTouchListener来监听手指的滑动情况,在ACTION_MOVE中实时获取小球在屏幕中的位置,减去点击事件相对小球的位置,通过更新layoutParams来动态改变悬浮球的位置
具体代码如下:
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
/*
* 手指按下时记录必要数据,纵坐标的值都需要减去状态栏高度
* 全屏状态考虑状态栏高度
*/
xInView = event.getX();
yInView = event.getY();
xInScreen = event.getRawX();
yInScreen = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
yInScreen = event.getRawY();
xInScreen = event.getRawX();
// 手指移动的时候更新小悬浮窗的位置
updateViewPosition(false);
break;
case MotionEvent.ACTION_UP:
...
break;
default:
break;
}
return true;
}
// 更新悬浮球的位置
public void updateViewPosition(boolean isUp) {
floatViewParams.x = (int) (xInScreen - xInView);
floatViewParams.y = (int) (yInScreen - yInView);
...
}
用上面这种方法来实现在刘海屏后面发生了一个异常,具体表现为:在刘海屏幕手机上面,移动小球时候,小球的位置会相对正确的位置有一个向下的偏移。
经过分析,发现通过event.getRawY()获取的位置是相对屏幕的绝对位置,也就是屏幕的上边缘;但是在绘制的时候却避开了刘海,导致view的真实绘制位置向下移动了一个刘海的高度。
这里转载一张图说明一下:
解决思路
如果能判断出设备是否有刘海屏,然后知道其高度,通过event.getRawY()获取到高度之后减去刘海的高度就能解决问题。
1、判断设备是否有刘海
刘海屏是在Android P里面推出的新功能,但是很多Android O的手机也实现了刘海,那么获取刘海信息就分9.0之前和9.0之后。
9.0之前:各家设备厂商实现方案不一样,判断方式也不一样,以下代码是主流手机厂商判断方法:
/**
* 判断是否是刘海屏
* @return
*/
public static boolean hasNotchScreen(Activity activity){
if (getInt("ro.miui.notch",activity) == 1 || hasNotchAtHuawei(activity) || hasNotchAtOPPO(activity)
|| hasNotchAtVivo(activity)){
return true;
}
return false;
}
/**
* 小米刘海屏判断.
* @return 0 if it is not notch ; return 1 means notch
* @throws IllegalArgumentException if the key exceeds 32 characters
*/
public static int getInt(String key,Activity activity) {
int result = 0;
if (isXiaomi()){
try {
ClassLoader classLoader = activity.getClassLoader();
@SuppressWarnings("rawtypes")
Class SystemProperties = classLoader.loadClass("android.os.SystemProperties");
//参数类型
@SuppressWarnings("rawtypes")
Class[] paramTypes = new Class[2];
paramTypes[0] = String.class;
paramTypes[1] = int.class;
Method getInt = SystemProperties.getMethod("getInt", paramTypes);
//参数
Object[] params = new Object[2];
params[0] = new String(key);
params[1] = new Integer(0);
result = (Integer) getInt.invoke(SystemProperties, params);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 华为刘海屏判断
* @return
*/
public static boolean hasNotchAtHuawei(Context context) {
boolean ret = false;
try {
ClassLoader classLoader = context.getClassLoader();
Class HwNotchSizeUtil = classLoader.loadClass("com.huawei.android.util.HwNotchSizeUtil");
Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
ret = (boolean) get.invoke(HwNotchSizeUtil);
} catch (ClassNotFoundException e) {
Log.e("","hasNotchAtHuawei ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("","hasNotchAtHuawei NoSuchMethodException");
} catch (Exception e) {
Log.e("","hasNotchAtHuawei Exception");
} finally {
return ret;
}
}
public static final int VIVO_NOTCH = 0x00000020;
public static final int VIVO_FILLET = 0x00000008;
/**
* VIVO刘海屏判断
* @return
*/
public static boolean hasNotchAtVivo(Context context) {
boolean ret = false;
try {
ClassLoader classLoader = context.getClassLoader();
Class FtFeature = classLoader.loadClass("android.util.FtFeature");
Method method = FtFeature.getMethod("isFeatureSupport", int.class);
ret = (boolean) method.invoke(FtFeature, VIVO_NOTCH);
} catch (ClassNotFoundException e) {
Log.e("","hasNotchAtVivo ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("","hasNotchAtVivo NoSuchMethodException");
} catch (Exception e) {
Log.e("","hasNotchAtVivo Exception");
} finally {
return ret;
}
}
/**
* OPPO刘海屏判断
* @return
*/
public static boolean hasNotchAtOPPO(Context context) {
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
// 是否是小米手机
public static boolean isXiaomi() {
return "Xiaomi".equals(Build.MANUFACTURER);
}
9.0之后:
通过获取DisplayCutout来判断刘海的位置和大小。
/**
* Android P 刘海屏判断
* @param activity
* @return
*/
public static DisplayCutout isAndroidP(Activity activity){
View decorView = activity.getWindow().getDecorView();
if (decorView != null && android.os.Build.VERSION.SDK_INT >= 28){
WindowInsets windowInsets = decorView.getRootWindowInsets();
if (windowInsets != null) {
return windowInsets.getDisplayCutout();
}
}
return null;
}
2、计算刘海的高度
大部分刘海屏的高度和状态栏的高度一致,所以读取状态栏的高度就等同于刘海的高度,但是也有一些不一致。这样就存在误差。
获取状态栏高度
int resourceId = mContext.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
cutStatusHeight = mContext.getResources().getDimensionPixelSize(resourceId);
}
如果是9.0,就直接通过DisplayCutout来获取刘海的高度即可。
3、横竖屏处理
在判断出设备是否有刘海屏并得到其高度之后,在计算悬浮球位置的时候减去其高度,就能得到正确的坐标。
经过自测成功解决问题,美滋滋。
经过一段时间,测试找上门来,发现悬浮球还能斜着偏移,惊呆了。
一看游戏是横屏的,XY坐标竖屏状态相反,本来应该在X方向上面减结果在Y方向上面减,导致其位置往斜方向偏移。在计算的时候判断一下界面的横竖屏状态即可解决问题。