Android-游戏小窗模式
本文最后更新于 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);
        }
    }
}
本文链接:https://iichen.cn/?p=824
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇