前言
首次尝试unity项目的安卓移植,核心需求就是在其他应用之上以浮窗(小窗口?)显示,同时保证只显示宠物,背景透明(就像桌宠理所当然的样子😐)。开发过程中尝试参考了众多CSDN和Unity Discussions的内容,最终及大家之所长找到了解决方案(详见参考链接)。
目录
模拟UnityPlayer的嵌套结构在自定义Layout上的行为
Unity设置
让背景透明唯一要做的就是将Camera中的 Camera➡️Clear Flags设置为Solid Color, Camera➡️Background 中的RGBA全部拉到0。结束!
Unity设置-网上说的其他有用操作
1.使用有8位颜色值的老版本Unity:未尝试(移植项目,不敢换版本)✖
2.将Project Settings➡️Player➡️安卓tab➡️Resolution and Presentation➡️preserve Alpha Channel 启用:笔者使用的Unity2022.3.0版本没找到这个选项 ✖
3. 将Project Settings➡️Player➡️安卓tab➡️Resolution and Presentation➡️Render Over Native UI启用:点不点这个都没区别 ✖
结论:都没有用(或不能用)
导出Android项目
如果大家还会在Unity中对项目进行修改的话,建议使用一些版本控制工具,因为每次重新导出项目Unity都会会覆盖AndroidMainfest配置文件,而我们需要修改这个文件来实现效果。
Android Studio项目配置及修改
需要修改UnityLibrary中的AndroidMainfest,res➡️layout中添加新的layout,此layout需要包含一个子layout,修改gradle文件等。(详见参考1)
将UnityPlayerActivity中控制mUnityPlayer的语句注释掉,新建一个Service类控制其生命周期,新建继承自UnityPlayerActivity的自定义Activity在获得显示在其他应用权限后启动Service的代码。在AndroidMainfest的Activity 那么替换为我们的自定义Activity来启动项目,添加自定义Service。(详见参考1)
咋还不行啊(发现流程)
尝试了上述方案,网上说的Unity所有其他有用操作,和AI生成的的诸多方案后程序仍然运行在黑色背景的小窗口上,笔者非常绝望,这时看到了参考3提到的UnityPlayer图层关系和源码,发现了以下关系:
class UnityPlayer extends FrameLayout{
private J mGlView;
...
}
class J extends FrameLayout {
private a a;
...
}
class a extends SurfaceView {
...
}
所以UnityPlayer是FrameLayout套着FrameLayout套着SurfaceView这样三层View嵌套的关系。
及这样的关系
我们Unity的画面是渲染在最上层的SurfaceView a上的。到这一步经过某些尝试可以直接试出我们的解决方案,不想看发现流程的朋友们可以直接移步解决方案👍。
实验
首先笔者不确定是导出后的安卓项目还是Unity渲染的原因导致的黑色背景,于是创建了一个自定义的SurfaceView class来显示一些随随便便的画面并测试其添加到我们layout上的行为:
package com.unity3d.player;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class TestSurfaceView extends SurfaceView implements SurfaceHolder.Callback{
private SurfaceHolder surfaceHolder;
private Paint paint;
private DrawingThread drawingThread;
public TestSurfaceView(Context context) {
super(context);
init();
}
public TestSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
surfaceHolder = getHolder();
surfaceHolder.addCallback(this);
paint = new Paint();
paint.setColor(Color.RED); // Example color for custom drawing
paint.setTextSize(50); // Example text size for custom drawing
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
drawingThread = new DrawingThread(holder);
drawingThread.setRunning(true);
drawingThread.start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Handle surface changes if needed
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
drawingThread.setRunning(false);
while (retry) {
try {
drawingThread.join();
retry = false;
} catch (InterruptedException e) {
// Retry shutting down the thread
}
}
}
private class DrawingThread extends Thread {
private SurfaceHolder surfaceHolder;
private boolean running = false;
public DrawingThread(SurfaceHolder surfaceHolder) {
this.surfaceHolder = surfaceHolder;
}
public void setRunning(boolean running) {
this.running = running;
}
@Override
public void run() {
while (running) {
Canvas canvas = null;
try {
canvas = surfaceHolder.lockCanvas();
if (canvas != null) {
synchronized (surfaceHolder) {
drawRandomPattern(canvas);
}
}
} finally {
if (canvas != null) {
surfaceHolder.unlockCanvasAndPost(canvas);
}
}
}
}
private void drawRandomPattern(Canvas canvas) {
canvas.drawColor(Color.TRANSPARENT);
// Draw a fixed pattern: for example, a grid of circles
paint.setColor(Color.RED);
int radius = 50;
int padding = 20;
int columns = (canvas.getWidth() - padding) / (2 * radius + padding);
int rows = (canvas.getHeight() - padding) / (2 * radius + padding);
for (int i = 0; i < columns; i++) {
for (int j = 0; j < rows; j++) {
float x = padding + radius + i * (2 * radius + padding);
float y = padding + radius + j * (2 * radius + padding);
canvas.drawCircle(x, y, radius, paint);
}
}
// Draw some text in the center
paint.setColor(Color.WHITE);
paint.setTextSize(50);
canvas.drawText("Hello, SurfaceView!", 50, canvas.getHeight() / 2, paint);
}
}
}
注意drawRandomPattern这个方法第一行首先把把整个图渲染为透明,然后在透明画面上渲染
测试surfaceView在自定义Layout上的行为
在自定义Service中把我们的TestSurfaceView 放到自定义layout上:
TestSurfaceView t = new TestSurfaceView(this);
((RelativeLayout) customizedLayout.findViewById(R.id.overlay_view)).addView(t);
得到以下画面:
背景还是黑色的,是我们的自定义layout出了问题吗?这时看到了 参考2 的信息,加入此行代码:
TestSurfaceView t = new TestSurfaceView(this);
t.getHolder().setFormat(PixelFormat.TRANSPARENT);//关键
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(t);
得到以下画面:
所以surfaceView默认是使用没有alpha通道的像素格式渲染的,需要通过这行代码设置为有alpha通道的像素格式。同时证明了我们自定义layout的可行性!😆
在自定义service中,取出UnityPlayer的surfaceView并设置像素格式:
mUnityPlayer = mup;
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(mUnityPlayer);
SurfaceView surfaceView = mUnityPlayer.findViewById(R.id.unitySurfaceView);
surfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);
运行,没有任何变化。。。😔 这时候已经开始怀疑是unity渲染时输出的像素值就是不透明的。遂作出以下实验:将TestSurfaceView➡️drawRandomPattern 第一行改为Color.BLACK(保留setFormat(PixelFormat.TRANSPARENT)),得到以下画面:
这说明透明背景的首要前提就是 渲染过程中真的在输出alpha为0的像素(好像是废话🥲)。
OK!得到以上结论后,通过实验判断是否Unity在输出正确的像素!!
模拟UnityPlayer的嵌套结构在自定义Layout上的行为
创建一个想UnityPlayer一样的View嵌套结构,最上层为我们的TestSurfaceView,并把这个嵌套View添加到自定义layout上运行:
FrameLayout uniPlayerSimu = new FrameLayout(this);
FrameLayout GLViewSimu = new FrameLayout(this);
TestSurfaceView t = new TestSurfaceView(this);
t.getHolder().setFormat(PixelFormat.TRANSPARENT);
GLViewSimu.addView(t);
uniPlayerSimu.addView(GLViewSimu);
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(uniPlayerSimu);
t.setZOrderOnTop(true);
最后一行 t.setZOrderOnTop(true) 是必要的,因为SurfaceView会创建一个单独的绘图层,它可能被放置在与其他View结构不同的窗口层中;所以虽然它是其他View的child,它却有可能被放置在其他View的上层或下层,导致意料之外的Z轴问题。所以我们使用这个方法把SurfaceView 放置在Z轴最上层,也就是所有View之上,如果查看UnityPlayer的源码我们会发现J类在创建a类对象时也使用了这个方法。
设置View的Alpha值
这个方法是我在询问AI相关解决方案时发现的,View.setAlpha(float)理论上会设置一个View和他所有的子View的透明度,可以让窗口变成半透明显示(可以把桌宠放到刚刚好的窗口里然后给整个View设置一个合适的Alpha来做一个缓兵之计😂)。
Unity View设置Alpha:
mUnityPlayer = mup;
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(mUnityPlayer);
SurfaceView surfaceView = mUnityPlayer.findViewById(R.id.unitySurfaceView);
surfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);
mUnityPlayer.setAlpha(0.5f);
mUnityPlayer = mup;
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(mUnityPlayer);
SurfaceView surfaceView = mUnityPlayer.findViewById(R.id.unitySurfaceView);
surfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);
surfaceView.setAlpha(0.5f);
设置mUnityPlayer的alpha,整个窗口变成半透明的;
设置surfaceView的alpha,窗口中背景为不透明的黑色,但是桌宠变成半透明了。(无法展示开发中画面,向大家致歉)
模拟Unity结构的View设置Alpha:
我们要对比Unity的行为与渲染背景为不透明像素的行为,所以TestSurfaceView➡️ drawRandomPattern 第一行保持为Color.BLACK
FrameLayout uniPlayerSimu = new FrameLayout(this);
FrameLayout GLViewSimu = new FrameLayout(this);
TestSurfaceView t = new TestSurfaceView(this);
t.getHolder().setFormat(PixelFormat.TRANSPARENT);
GLViewSimu.addView(t);
uniPlayerSimu.addView(GLViewSimu);
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(uniPlayerSimu);
t.setZOrderOnTop(true);
uniPlayerSimu.setAlpha(0.5f);
FrameLayout uniPlayerSimu = new FrameLayout(this);
FrameLayout GLViewSimu = new FrameLayout(this);
TestSurfaceView t = new TestSurfaceView(this);
t.getHolder().setFormat(PixelFormat.TRANSPARENT);
GLViewSimu.addView(t);
uniPlayerSimu.addView(GLViewSimu);
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(uniPlayerSimu);
t.setZOrderOnTop(true);
t.setAlpha(0.5f);
分别得到以下结果(图左是uniPlayerSimu.setAlpha;图右是t.setAlpha):
重点分析右图,通过设置渲染不透明像素的SurfaceView的Alpha值,整个窗口都变成半透明了;而Unity设置SurfaceView的Alpha,只有桌宠的透明度改变了,背景透明度并没有变化,这说明背景的黑色并不是Unity的SurfaceView渲染的!!!(不知道为啥左图没有变成半透明的😑😑😑)
解决方案
通过以上的实验,笔者发现了黑色背景并非UnityPlayer中的SurfaceView渲染的,那么其一定是其他某个View渲染的,而我们已经知道了UnityPlayer中的嵌套结构,只要尝试将背景颜色设为透明就可以解决这个问题。
最终发现是二层J类对象背景颜色为黑色,将其设计为透明:
mUnityPlayer = mup;
((RelativeLayout) touchLayout.findViewById(R.id.overlay_view)).addView(mUnityPlayer);
FrameLayout glView = (FrameLayout)mUnityPlayer.getChildAt(0);
glView.setBackgroundColor(Color.TRANSPARENT);
//下面两行删了也没有影响,但是是探索得到的知识,所以写在这里
SurfaceView surfaceView = mUnityPlayer.findViewById(R.id.unitySurfaceView);
surfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);
运行,解决🎉🎉🎉!!!
总结
1. SurfaceHolder默认使用没有alpha通道的颜色格式,要想输出透明的颜色,必须要使用surfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT); 当然UnityPlayer已经设置了。
2. 如果想让SurfaceView显示在正确的View层级上(一般来说就是使其放在z轴最上方),需要使用t.setZOrderOnTop(true);因为当创建一个SurfaceView并把它添加为其他View的child时,它会意料之外的位于其他View的上面或者下面。
如果SurfaceView放置于了一个View下方(View的z轴位置高于SurfaceView,View盖住了SurfaceView,View是SurfaceView的上层),这会反直觉地让上层View无法正确显示:
因为SurfaceView被放置在了View结构之外的窗口层(window layer)中来保证它会被显示,正如它的名字Surface表面视图一样,保证了SurfaceView的显示,所以导致了行为上其上层的View被丢弃/隐藏了。当然UnityPlayer中也调用了这个方法。
3. View.setAlpha()会设置一个View和其所有子View的透明度,所以当Unity最上层View mUnityPlayer 设置透明时整个窗口都透明化了,这个方法可以被当作一个权宜之计。至于为什么我模拟Unity嵌套结构的自定义View设置时没有发生同样的结果,希望有大佬来解答。
最后,以上是通过实验得出的结论,但笔者在写文章时又多看了两行源码,发现J类构造器中把背景颜色设置为了黑色。。。😐😑😓
this.setBackgroundColor(-16777216);
笔者是第一次开发Android项目,对相关原理和专有词汇不太熟悉,可能多有谬误,还望大家多多指正,指教。感谢!
参考,来源,引用
1. 用service控制UnityPlayer Activity生命周期以及使用layout让程序以浮窗(小窗口?)显示:
2. 让SurfaceView背景透明;把自定义View放置于UnityPlayer View下方:
Unity Android 背景透明 相机透明 - 简书 (jianshu.com)https://www.jianshu.com/p/a67f77cd2e623. 提醒大家Unity图层关系以及查看源码:
Android悬浮窗看这篇就够了_安卓如何把变量 放到悬浮框-CSDN博客https://blog.csdn.net/qq_37750825/article/details/1157546475. Unity Disccusion中大家的热情的Discussion:
Unity3D export to Android with transparent background? - Unity Engine - Unity Discussionshttps://discussions.unity.com/t/unity3d-export-to-android-with-transparent-background/688050/52?page=2大家都是非常好的创作者,非常感谢你们的分享❤️