本文最后更新于 91 天前,其中的信息可能已经有所发展或是发生改变。
通过获取当前顶层Activity的DecorView去根据规则查找目标View,如:SurfaceView/GlSurfaceView/WebView/指定类名View等,最终将其从父容器remove添加到悬浮窗内
1. 悬浮窗View
WindowManager设置全局悬浮窗
open var params: WindowManager.LayoutParams = WindowManager.LayoutParams().apply {
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) TYPE_APPLICATION_OVERLAY
else TYPE_PHONE
format = PixelFormat.RGBA_8888
gravity = Gravity.START or Gravity.TOP
// 设置浮窗以外的触摸事件可以传递给后面的窗口、不自动获取焦点
flags =
FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE or FLAG_LAYOUT_NO_LIMITS or FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_FULLSCREEN or
WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
width = WRAP_CONTENT
height = WRAP_CONTENT
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_FULLSCREEN
}
open var windowManager: WindowManager =
GmLifecycleUtils.application.getSystemService(Service.WINDOW_SERVICE) as WindowManager
初始化处理
获取屏幕尺寸信息以及游戏的横竖屏来初始化小窗的最小和最大尺寸
minW = if (orientation == 1) { DisplayUtils.getScreenWidth(context) / 4F + sideGameIconWidth } else { DisplayUtils.getScreenHeight(context) / 1.8F + sideGameIconWidth } minH = if (orientation == 1) { (minW - sideGameIconWidth) * 2f } else { (minW - sideGameIconWidth) / 2.3F } private fun initMaxWH() { maxW = if (orientation == 1) { DisplayUtils.getScreenWidth(context) / 2f + sideGameIconWidth } else { DisplayUtils.getScreenHeight(context) * 1.0F + sideGameIconWidth } maxH = if (orientation == 1) { (maxW - sideGameIconWidth) * 2f } else { (maxW - sideGameIconWidth) / 2.3F } }
Touch事件处理
mContentView.touchListener = object : GmOnFloatTouchListener {
override fun onTouch(event: MotionEvent): Boolean {
if (event.pointerCount == 2) {
if (handlerType == 0 || handlerType == 2) {
return false
}
if (scaleGestureDetector == null) {
scaleGestureDetector = ScaleGestureDetector(context, ScaleListener())
}
isScaling = true
return scaleGestureDetector?.onTouchEvent(event) ?: false
} else if (event.pointerCount > 2) {
} else {
if (isScaling) {
if (event.action == 1) {
isScaling = false
handlerUpEvent()
}
} else {
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
isNeedHandleUnMoveEventAndDone = false
lastX = event.rawX
lastY = event.rawY
}
MotionEvent.ACTION_MOVE -> {
val rawX: Float = event.rawX
val rawY: Float = event.rawY
val dx: Float = rawX - lastX
val dy: Float = rawY - lastY
val (w, h) = getRealWH()
if (dx * dx + dy * dy < 81) return false
isNeedHandleUnMoveEventAndDone = true
var x = params.x + dx.toInt()
var y = params.y + dy.toInt()
// 边界修正
val mViewW = mContentView.measuredWidth
val mViewH = mContentView.measuredHeight
if (x < -mViewW) {
x = -mViewW
}
if (x > w + mViewW) {
x = w + mViewW
}
if (y <= 0) {
y = 0
}
val bottomBorder = h - mViewH
if (y >= bottomBorder) {
y = bottomBorder
}
params.x = x
params.y = y
windowManager.updateViewLayout(mContentView, params)
// 更新上次触摸点的数据
lastX = event.rawX
lastY = event.rawY
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
handlerUpEvent()
}
}
}
}
return false
}
}
缩放事件处理
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
scaleFactor *= detector.scaleFactor
scaleFactor = scaleFactor.coerceIn(0.8f, 1.2f)
val currentWidth = mContentView.width
val currentHeight = mContentView.height
// 计算新的宽高,并确保它们在最小和最大值之间
val newWidth = (currentWidth * scaleFactor).coerceIn(minW, maxW).toInt()
val newHeight = if (orientation == 1) {
((newWidth - sideGameIconWidth) * 2f).coerceIn(minH, maxH).toInt()
} else {
((newWidth - sideGameIconWidth) / 2.3F).coerceIn(minH, maxH).toInt()
}
// 计算宽高变化量
val dx = (newWidth - currentWidth) / 2
val dy = (newHeight - currentHeight) / 2
// 更新 LayoutParams 的宽高
params.width = newWidth
params.height = newHeight
// // 调整 x 和 y 以保持中心位置
params.x -= dx
params.y -= dy
// 更新视图布局
windowManager.updateViewLayout(mContentView, params)
return true
}
}
ConfigurationChanged处理
当手机横竖屏切换或不同的横竖屏游戏 小窗需要自适应跳转横屏和竖屏的状态
override fun onConfigurationChanged(newConfiguration: Configuration) { var w = DisplayUtils.getScreenWidth(context) var h = DisplayUtils.getScreenHeight(context) // 垂直1 水平2 GameHelperInnerLog.d("iichen", ">>>>>>>>>>>>>>>>>onConfigurationChanged 回调:$w $h getOrientation: ${getOrientation()} orientation: $orientation lastOrientation:$lastOrientation") if (getOrientation() != lastOrientation) { getRealWH().apply { w = first h = second } lastOrientation = getOrientation() val viewW = mContentView.measuredWidth val viewH = mContentView.measuredHeight // 计算mContent 宽度和高度相对于之前的比例 // 横屏变竖屏 // 如果是折叠的 if (handlerType == 0) { params.x = 0 handlerType = -1 hideAdhesion() } else if (handlerType == 2) { handlerType = -1 handlerUpEvent() } // 宽高不是之前的了 需要取反 去计算之前的相对比例 val rationX = params.x * 1.0 / h val rationY = params.y * 1.0 / w // 新的x params.x = ceil((w - viewW) * rationX).toInt() // 处理上端距离 params.y // 新的x params.y = ceil((h - viewH) * rationY).toInt() GameHelperInnerLog.d("iichen", ">>>>>>>>>>>>>>>>>onConfigurationChanged handler:$w $h rationX:$rationX rationY:$rationY params.x: ${params.x} params.y: ${params.x}") windowManager.updateViewLayout(mContentView, params) } } private fun getOrientation() : Int { return when (windowManager.defaultDisplay.rotation) { Surface.ROTATION_0 -> 1 // 自然方向 Surface.ROTATION_90 -> 2 // 顺时针 90 度 Surface.ROTATION_180 -> 1 // 顺时针 180 度 Surface.ROTATION_270 -> 2 // 顺时针 270 度 else -> lastOrientation } }
除此之外,额外左右侧边界贴边处理等,详见github
2. 目标View抽取与设置
获取目标View
private fun findMatchingView(root: View): View? {
val queue: Queue<View> = LinkedList()
queue.add(root)
var surfaceViewTag: SurfaceView? = null
val exactlyViewName = GmSpacePipManager.getInstance().exactlyViewName
val excludeViewParentList = GmSpacePipManager.getInstance().excludeViewParentList.apply {
addAll(GmApiManager.initConfigExcludeViewParentList)
}
while (queue.isNotEmpty()) {
val current = queue.poll()
if (exactlyViewName.isNotBlank()) {
if (current.javaClass.name == exactlyViewName) {
return current
}
} else {
if (current is GLSurfaceView || (current != null && (current is WebView || isTxWebView(current)))) {
if (current.parent is ViewGroup) {
val className = (current.parent as ViewGroup).javaClass.name
// 单次只作用一次
if (excludeViewParentList.contains(className)) {
excludeViewParentList.remove(className)
} else {
return current
}
} else {
return current
}
} else if (current is SurfaceView) {
surfaceViewTag = current
}
}
if (current is ViewGroup) {
for (i in 0 until current.childCount) {
queue.add(current.getChildAt(i))
}
}
}
return surfaceViewTag
}
Hook逻辑
对于GlSurfaceView需要使用Pine等hook框架处理相关方法,配合使用:Android-替换Instrumentation拦截Activity生命周期进行处理,针对特殊游戏需要使用jadx等工具分析后下发拦截逻辑实现。
public static void hookGlSurfaceView() {
try {
Pine.hook(Class.forName("android.opengl.GLSurfaceView").getDeclaredMethod("onAttachedToWindow", new Class[0]), new GLSurfaceViewInvokeOnAttachedToWindow());
Pine.hook(Class.forName("android.opengl.GLSurfaceView").getDeclaredMethod("onDetachedFromWindow", new Class[0]), new GLSurfaceViewInvokeOnDetachedFromWindow());
} catch (Exception e) {
Log.d(TAG, "hookGlSurfaceView: ", e);
}
}
class GLSurfaceViewInvokeOnDetachedFromWindow extends MethodHook {
Object asm = null;
GLSurfaceViewInvokeOnDetachedFromWindow() {
}
@SuppressLint("SoonBlockedPrivateApi")
@Override
public void afterCall(Pine.CallFrame callFrame) throws Throwable {
super.afterCall(callFrame);
if (this.asm != null) {
Field field = GLSurfaceView.class.getDeclaredField("mGLThread");
field.setAccessible(true);
field.set(callFrame.thisObject, this.asm);
Field detachedFiled = GLSurfaceView.class.getDeclaredField("mDetached");
detachedFiled.setAccessible(true);
detachedFiled.set(callFrame.thisObject, false);
}
}
@SuppressLint("SoonBlockedPrivateApi")
@Override
public void beforeCall(Pine.CallFrame callFrame) throws Throwable {
super.beforeCall(callFrame);
try {
Field field = GLSurfaceView.class.getDeclaredField("mGLThread");
field.setAccessible(true);
this.asm = field.get(callFrame.thisObject);
field.set(callFrame.thisObject, null);
} catch (Exception e) {
Log.d("iichen", "onDetachedFromWindow: " + e.getMessage());
}
}
}
class GLSurfaceViewInvokeOnAttachedToWindow extends MethodHook {
GLSurfaceViewInvokeOnAttachedToWindow() {
}
@Override
public void afterCall(Pine.CallFrame callFrame) throws Throwable {
super.afterCall(callFrame);
}
@SuppressLint("SoonBlockedPrivateApi")
@Override
public void beforeCall(Pine.CallFrame callFrame) throws Throwable {
super.beforeCall(callFrame);
try {
Field detachedFiled = GLSurfaceView.class.getDeclaredField("mDetached");
detachedFiled.setAccessible(true);
detachedFiled.set(callFrame.thisObject, false);
} catch (Exception e) {
Log.d("iichen", "onAttachedToWindow: ", e);
}
}
}