轉載:https://blog.csdn.net/xssdmx/article/details/107315493
Android全局桌面寵物 Unity方案實現
最近接到一個任務是Android設備上實現一個全局的指引動畫,開始想着就用普通動畫控件或者svga、lottie控件實現,最近正好在學習Unity,所以試着用unity實現。經過三天努力,居然實現了。話不多說,馬上開始:
1、准備素材
在愛給網找到一個蝴蝶3D模型,然后通過3Dmax導出為FBX模型,然后倒入到unity里,具體操作相對比較簡單,只是說明一下,模型紋理需要跟導出文件放在一起,否者到unity里面沒有紋理皮膚。然后就是在unity修改Animation Type為Legacy,以及動畫循環設置:Wrap Mode設置為Loop。
模型下載地址:http://www.aigei.com/3d/model/lactation/
2、導出透明Unity工程
Unity導出透明應用比較麻煩,耗時最多,找了很久只有這個文章有提及:
https://www.jianshu.com/p/a67f77cd2e62
也看了英文原地址:
https://forum.unity.com/threads/unity3d-export-to-android-with-transparent-background.512129/
於是開始嘗試,按照文檔所說,只需要修改兩個地方就能實現:
1、修改Main Camera 的背景顏色為 Solid Color,並且透明度設置為0。
2、導出設置勾選preserveFramebufferAlpha。
我裝的Unity 5.5.0f3 (64-bit)和Unity 2019.1.0a8 (64-bit),沒有找到文檔所說的preserveFramebufferAlpha選項,按文檔所說的2018.1版本可以,我又裝了Unity 2018.1.1f1 (64-bit),選項是有了,但是按照設置導出還是不透明。
反復排查和對比,發現我的顏色設置透明度不起作用,只顯示6位顏色,而沒有透明度顯示:
只能嘗試下載其他版本。
找到官方操作手冊,發現這個版本也有設置選項
https://docs.unity3d.com/cn/2017.4/Manual/class-PlayerSettingsAndroid.html
於是嘗試下了Unity 2017.4.2f2 (64-bit)版本,看到了8位的顏色值,心里大喜:
本來想着直接導出apk運行,但是一直報錯,導出失敗,懷疑是版本比較舊,嘗試更換了舊版本NDKK和jdk版本,以及自定義了Gradle,都不行。最后只好導出工程,然后自己創建Android工程編譯。還算順利很快運行實現了效果。
中間還出現個小問題,就是渲染的圖像顏色不對,有點過曝光。后來發現是camera顏色值問題,設置成#00000000后解決。
3、做全局window窗口
Unity直接導出的工程是activity顯示動畫,要做全局widow,需要把UnityPlayer放service里生成。我直接拷貝相關方法,放到service里生成,然后放入系統window,結果什么都不顯示。
看UnityPlayer生成傳入的context居然是activity類型,頓時心的都涼了。
我不會輕易放棄,我通過Application保存了全局的activity和UnityPlayer,然后從window里面獲取,驗證可行性,多次嘗試都是顯示空白。
public class MainApp extends Application {
private static Activity mActivity;
static UnityPlayer mUnityPlayer;
public static Application app;
@Override
public void onCreate() {
super.onCreate();
app = this;
}
public static Activity getActivity() {
return mActivity;
}
public static void setActivity(Activity activity) {
mActivity = activity;
}
public static UnityPlayer getUnityPlayer() {
return mUnityPlayer;
}
public static void setUnityPlayer(UnityPlayer unityPlayer) {
mUnityPlayer = unityPlayer;
}
}
偶然間發現,UnityPlayer在activity里面生成,並且生命周期全部放activity,只是把UnityPlayer加載到window窗口,居然可以實現。只是activity不能切換后台,切換到后台動畫就暫停了。
嘗試各參數,最后發現是 mUnityPlayer.windowFocusChanged(true);這句對顯示有關鍵作用。
於是推到重來,new UnityPlayer用了getApplicationContext,成功!
所以改變service全部生成,結果成功。
代碼如下:
package com.Company.bgTest;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.IBinder;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import com.unity3d.player.UnityPlayer;
/** * @author hardy * @name My Application * @class name:com.Company.bgTest * @class describe: * @time 2020/7/10 15:47 * @change * @chang time * @class describe */
public class MainService extends Service {
//Log用的TAG
private static final String TAG = "MainService";
//要引用的布局文件.
LinearLayout toucherLayout;
//布局參數.
WindowManager.LayoutParams params;
//實例化的WindowManager.
WindowManager windowManager;
//狀態欄高度.(接下來會用到)
int statusBarHeight = -1;
protected UnityPlayer mUnityPlayer;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "MainService Created");
//OnCreate中來生成懸浮窗.
createToucher();
}
private void createToucher() {
//賦值WindowManager&LayoutParam.
params = new WindowManager.LayoutParams();
windowManager = (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE);
//設置type.系統提示型窗口,一般都在應用程序窗口之上.
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
//設置效果為背景透明.
params.format = PixelFormat.RGBA_8888;
//設置flags.不可聚焦及不可使用按鈕對懸浮窗進行操控.
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//設置窗口初始停靠位置.
params.gravity = Gravity.LEFT | Gravity.TOP;
params.x = 0;
params.y = 0;
//設置懸浮窗口長寬數據.
//注意,這里的width和height均使用px而非dp.這里我偷了個懶
//如果你想完全對應布局設置,需要先獲取到機器的dpi
//px與dp的換算為px = dp * (dpi / 160).
params.width = 400;
params.height = 600;
LayoutInflater inflater = LayoutInflater.from(getApplication());
//獲取浮動窗口視圖所在布局.
toucherLayout = (LinearLayout) inflater.inflate(R.layout.pet_window, null);
//添加toucherlayout
windowManager.addView(toucherLayout, params);
Log.i(TAG, "toucherlayout-->left:" + toucherLayout.getLeft());
Log.i(TAG, "toucherlayout-->right:" + toucherLayout.getRight());
Log.i(TAG, "toucherlayout-->top:" + toucherLayout.getTop());
Log.i(TAG, "toucherlayout-->bottom:" + toucherLayout.getBottom());
//主動計算出當前View的寬高信息.
toucherLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
//用於檢測狀態欄高度.
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
}
Log.i(TAG, "狀態欄高度為:" + statusBarHeight);
mUnityPlayer = new UnityPlayer(this.getApplicationContext());
// mUnityPlayer = MainApp.getUnityPlayer();
((RelativeLayout) toucherLayout.findViewById(R.id.rl_pet)).addView(mUnityPlayer);
mUnityPlayer.start();
mUnityPlayer.resume();
mUnityPlayer.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
//ImageButton我放在了布局中心,布局一共300dp
params.x = (int) event.getRawX() - 150;
//這就是狀態欄偏移量用的地方
params.y = (int) event.getRawY() - 150 - statusBarHeight;
windowManager.updateViewLayout(toucherLayout,params);
return false;
}
});
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
mUnityPlayer.windowFocusChanged(true);
return super.onStartCommand(intent, flags, startId);
}
// Quit Unity
@Override public void onDestroy ()
{
mUnityPlayer.pause();
mUnityPlayer.stop();
mUnityPlayer.quit();
super.onDestroy();
}
}
另外值得一提的是全局懸浮窗需要設置權限,參考 https://www.jianshu.com/p/ac63c57d2555:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW"/>
以及代碼判斷用戶手動開啟:
//當AndroidSDK>=23及Android版本6.0及以上時,需要獲取OVERLAY_PERMISSION. //使用canDrawOverlays用於檢查,下面為其源碼。其中也提醒了需要在manifest文件中添加權限. /** * Checks if the specified context can draw on top of other apps. As of API * level 23, an app cannot draw on top of other apps unless it declares the * {@link android.Manifest.permission#SYSTEM_ALERT_WINDOW} permission in its * manifest, <em>and</em> the user specifically grants the app this * capability. To prompt the user to grant this approval, the app must send an * intent with the action * {@link android.provider.Settings#ACTION_MANAGE_OVERLAY_PERMISSION}, which * causes the system to display a permission management screen. * */ if (Build.VERSION.SDK_INT >= 23) { if (Settings.canDrawOverlays(UnityPlayerActivity.this)) { Intent intent = new Intent(UnityPlayerActivity.this, MainService.class);
Toast.makeText(UnityPlayerActivity.this, "已開啟Toucher", Toast.LENGTH_SHORT).show();
// startService(intent);
// finish();// moveTaskToBack(true);
} else {
//若沒有權限,提示獲取.
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
Toast.makeText(UnityPlayerActivity.this, "需要取得權限以使用懸浮窗", Toast.LENGTH_SHORT).show();
startActivity(intent);
}
} else {
//SDK在23以下,不用管.
Intent intent = new Intent(UnityPlayerActivity.this, MainService.class);startService(intent); moveTaskToBack(true);
// finish();
}