Android-游戏小窗模式

通过获取当前第一个Activity的DecorView去根据规则找到目标View,如:SurfaceView/GlSurfaceView/WebView/指定类名View等,最终将其从父容器remove添加到悬浮窗内

1. 悬浮窗View

WindowManager设置全局悬浮窗

ini 复制代码
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

处理初始化

获取屏幕尺寸信息以及游戏的横竖屏来初始化小窗口的最小和最大尺寸

scss 复制代码
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
	}
}

触摸处理事件

ini 复制代码
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
	}
}

缩放事件处理

kotlin 复制代码
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
	}
}

配置更改处理

当手机横竖屏切换或不同的横竖屏游戏小窗时需要灵活的横竖屏和竖屏的状态

kotlin 复制代码
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. 目标视图抽取与设置

获取目标查看

kotlin 复制代码
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
}

钩子逻辑

对于GlSurfaceView需要使用Pine等hook框架处理相关方法,配合使用:Android-替换Instrumentation拦截Activity生命周期进行处理,针对特殊游戏需要使用jadx等工具分析后下发拦截逻辑实现。

scala 复制代码
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);
        }
    }
}
相关推荐
老白讲技术15 小时前
2026年国内APP开发与软件定制服务商能力观察:从AI应用到行业系统开发
大数据·人工智能·ai·app·软件需求·app开发
私人珍藏库17 小时前
[Android] 一木百宝箱-万能百宝箱+抖音去水印等几百种功能
android·人工智能·app·软件·多功能
_瑞2 天前
深入理解灵动岛
前端·ios·app
私人珍藏库3 天前
[Android] BBLL 开源第三方B哩电视TV端
android·app·生活·工具·多功能
私人珍藏库3 天前
[Android] zip解压缩管理-全格式压缩包一键解压+打包
android·app·生活·工具·多功能
Fansi7 天前
iOS 实时活动(Live Activity)开发指南
app
duanze7 天前
从零开始Android商业项目Vibe coding完全指南(八)
app·vibecoding
Bigger12 天前
Tauri (26)——托盘图标总对不上系统主题?一行 Template Image 搞定
前端·rust·app
duanze17 天前
从零开始Android商业项目Vibe coding完全指南(七)
app·vibecoding
方白羽22 天前
一份 AGENTS.md,让 Android AI 代码规范率飙升
android·app·客户端