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);
        }
    }
}
相关推荐
私人珍藏库1 天前
【Android】豆图助手-永久HY-模拟微X~zfb各种截图
android·app·工具·软件·多功能
ZZH_AI项目交付2 天前
我把 AI 最容易改坏真实 App 的地方,整理成了 skills
人工智能·ios·app
YF02112 天前
Android 异形屏与横屏全屏沉浸式适配技术方案
android·app
曦月合一4 天前
语音识别网页版转化成APP版
app·语音识别·谷歌浏览器
私人珍藏库5 天前
【Android】Solid文件管理器3.5.2 安卓文件管理器
android·人工智能·app·工具·软件·多功能
YF02116 天前
深入剖析 Kotlin 的高效之道与核心实战
android·kotlin·app
私人珍藏库7 天前
【Android】小小最新AI--千变万化扮演任何角色--沉浸式互动
android·app·工具·软件·多功能
YF02117 天前
Android App 高效升级指南:OkDownload 多线程断点续传与全版本安装适配
android·okhttp·app
木风未来9 天前
四川 APP 开发企业排名
小程序·app·软件开发·app开发
洲洲不是州州10 天前
单片机onenet云平台的万能APP
单片机·onenet·app·嵌入式·云平台