如何將QtActivity添加至Android Studio項目中

如何將QtActivity添加至Android Studio項目中的圖1

本文翻譯自:How to add QtActivity to an Android Studio Project

原文作者:Qt Group Qt for Android Automotive 工程師 Nikunj Arora

校審:Sam Wang

Qt Design Studio,Qt Creator和Squish等出色工具可用于設計、開發和測試Android應用。然而,有時我們可能需要將Qt框架的某些功能集成到已有的Android Studio項目中。本篇博文將演示如何將Qt for Android項目集成到Android Studio項目中。

Qt for Android主要用于在單個Activity或Service中使用Qt。因此,其導航功能與常規Android應用的實現并不完全相同。另外,由于Android系統的特性,在使用公共Android SDK時,無法將QtActivity嵌入到另一個Activity中。

構建并運行演示項目

我們將制作簡單的應用來演示如何在Android Studio項目中使用QtActivity。在這個應用中,我們從Android端向Qt發送消息,根據Android上的按鈕來更改QML矩形的顏色。

我們在Qt端有一個矩形,根據Android Activity來改變其顏色。

在Android端,我們只有兩個按鈕,可以將矩形顏色設置為綠色或青色。

如何將QtActivity添加至Android Studio項目中的圖2

構建面向Android平臺的Qt項目。您可以從https://doc.qt.io/qtcreator/creator-building-targets.html獲取使用 Qt Creator構建的說明。這將在Qt項目的構建目錄中創建一個android-build文件夾。這個文件夾是獨立的Android項目,您可以在Android Studio中打開并編輯。在我們的演示項目中,要將部分文件復制到Android Studio項目中。

下文中<Android Project>指由Android Studio創建的包含Android項目的文件夾。

<Qt Build>指使用QtCreator構建面向Android的項目時生成的android-build文件夾。常見路徑如:/QtProjects/build-ProjectName-Qt_version-DebugOrRelease/android-build。

1. 復制<Qt Build>/libs中的文件到<Android Project>/app/libs

2. 復制<Qt Build>/assets/文件夾到<Android Project>/app/

從<Qt Build>中需要復制的文件夾如下圖所示

如何將QtActivity添加至Android Studio項目中的圖3

3. 復制<Qt Build>/res/values/libs.xml 到<Android Project>/app/src/main/res/values

4. 在<Qt Build>/gradle.properties中復制qtAndroidDir屬性到<Android Project>/gradle.properties

在<Qt Build>/gradle.properties中需要復制的屬性如下

...

qtAndroidDir=/home/user/Qt/6.4.2/android_arm64_v8a/src/android/java

...

5. 從<Qt Build>/AndroidManifest.xml 中復制QtActivity的標簽,將其粘貼到<Android Project>的AndroidManifest.xml中,并刪除MAIN Intent過濾器。同時復制相關權限。

同時將屬性android:launchMode從singleTop更改為singleInstance。這有助于在Android Activity和QtActivity之間進行導航。

下圖顯示了<Android Project>的AndroidManifest.xml中的activity 標簽(節選)

<activity

android:name="org.qtproject.qt.android.bindings.QtActivity"

android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"

android:label="appAndroidTest"

android:launchMode="singleInstance"

android:screenOrientation="unspecified"

android:exported="true">

<meta-data

android:name="android.app.lib_name"

android:value="appAndroidTest" />

<meta-data

android:name="android.app.arguments"

android:value="" />

<meta-data

android:name="android.app.extract_android_style"

android:value="minimal" />

</activity>

6. 在<Android Project>/app/build.gradle中添加以下代碼

...

dependencies {

implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])

}

...

android {

...

// Extract native libraries from the APK

packagingOptions.jniLibs.useLegacyPackaging true

sourceSets {

main {

java.srcDirs += [qtAndroidDir + '/src']

aidl.srcDirs += [qtAndroidDir + '/src']

resources.srcDirs = ['resources']

assets.srcDirs = ['assets']

jniLibs.srcDirs = ['libs']

}

}

}

實現原理

建立Qt和Android之間的通信

由于我們將本地庫復制到了Android Studio項目中,因此我們可以使用Java Native Interface (JNI)在Java和C++之間進行通信。幸運的是,Qt提供了許多輔助函數來處理這個問題。對于通信,我們將使用JSON字符串,因為它們在QML中是本地可訪問的(QML具有JavaScript引擎)。

注意:每次修改Qt項目時,您需要再次從<Qt Build>復制資源和庫文件夾到<Android Project>(前一節中的步驟1和2)。

Qt端

在Qt端,我們創建了名為JniMessenger的新類。以下是頭文件。

class JniMessenger : public QObject

{

Q_OBJECT

QML_NAMED_ELEMENT(JniMessenger)

QML_SINGLETON

private:

explicit JniMessenger(QObject *parent = nullptr);

public:

Q_INVOKABLE void sendMessageToJava(const QString &message);

static JniMessenger *instance();

static JniMessenger *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine);

signals:

void messageReceivedFromJava(QString message);

};

該類作為單例暴露給QML。每當有來自Java的消息時,就會信號被觸發。因為我們將該類設置為單例,所以需要將構造函數設為私有,并添加一個&rdquo;create&rdquo;函數。在QML中實例化此類時,將調用此函數。請在https://doc.qt.io/qt-6/qqmlengine.html#QML_SINGLETON獲取有關向QML暴露單例類的更多信息。

JniMessenger *JniMessenger::instance()

{

static JniMessenger instance;

return &instance;

}

JniMessenger *JniMessenger::create(QQmlEngine *qmlEngine, QJSEngine *jsEngine)

{

Q_UNUSED(qmlEngine)

JniMessenger *singletonInstance = JniMessenger::instance();

// The engine must have the same thread affinity as the singleton.

Q_ASSERT(jsEngine->thread() == singletonInstance->thread());

return singletonInstance;

}

將此函數添加到類中,用于向Java發送消息。此函數調用Java端的靜態方法,我們稍后將創建該方法。使用新的JNI語法&ldquo;Q_DECLARE_JNI_CLASS(javaMessageHandlerClass, com/example/androidapp/MainActivity)&rdquo;定義了javaMessageHandlerClass。您可以閱讀Volker的https://www.qt.io/blog/unstringifying-android-development-with-qt-6.4這篇博文來了解關于新JNI語法的更多內容。該語法用預定義的類和類型簽名代替JNI方法調用中使用的字符串簽名。

void JniMessenger::sendMessageToJava(const QString &message)

{

QJniObject::callStaticMethod<void, jstring>(

QtJniTypes::className<QtJniTypes::javaMessageHandlerClass>(),

"receiveMessageFromQt",

QJniObject::fromString(message).object<jstring>());

}

接下來在類的外部添加以下代碼。sendMessageToQt函數是一個在Java中聲明并在此處定義的本地方法。它將消息從Java發送到Qt。JNI_OnLoad函數由JNI調用,您應該在這里注冊所有本地方法。通過使用新的JNI語法,如代碼所示,本地方法的注冊變得更容易。

void sendMessageToQt(JNIEnv *env, jclass cls, jstring message)

{

Q_UNUSED(cls)

QString string =  env->GetStringUTFChars(message, nullptr);

emit JniMessenger::instance()->messageReceivedFromJava(string);

}

Q_DECLARE_JNI_NATIVE_METHOD(sendMessageToQt)

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)

{

Q_UNUSED(vm)

Q_UNUSED(reserved)

static bool initialized = false;

if (initialized)

return JNI_VERSION_1_6;

initialized = true;

// get the JNIEnv pointer.

QJniEnvironment env;

if (!env.isValid())

return JNI_ERR;

const jclass javaClass = env.findClass(

QtJniTypes::className<QtJniTypes::javaMessageHandlerClass>());

if (!javaClass)

return JNI_ERR;

static JNINativeMethod methods[] = { Q_JNI_NATIVE_METHOD(sendMessageToQt) };

// register our native methods

if (!env.registerNativeMethods(javaClass, methods, std::size(methods)))

return JNI_ERR;

return JNI_VERSION_1_6;

在main函數中添加以下代碼:

QTimer::singleShot(0, [argc, argv]() {

if (argc > 1) {

emit JniMessenger::instance()->messageReceivedFromJava(argv[1]);

}

});

將QtActivity作為第二個Activity加載的一個問題是,本地庫還沒有加載完畢,所以我們還不能立即調用本地方法。因此,我們使用Android Intents在第一次傳遞數據時進行操作,然后再繼續執行常規的本地函數調用。Android端給出了正確地將Intent數據發送到Qt的方法。0毫秒延遲的QTimer::singleShot使得發出的信號進入主事件循環,確保 QML能夠及時捕獲信號。

最后,在QML側,為按鈕添加功能并建立連接以便監聽信號。當按鈕被點擊時,它會向Java發送一個包含單一屬性的JSON字符串:navigate:back。這個信號通知Java再次加載第一個Activity的開始部分。當我們從Java接收到消息時,我們會尋找color屬性,并將矩形的顏色設定為該屬性值。

Window {

...

Rectangle {

focus: true

Keys.onReleased: function(event) {

if (event.key === Qt.Key_Back) {

console.log("Back key pressed");

event.accepted = true;

JniMessenger.sendMessageToJava(JSON.stringify(

{

navigate: "back"

}

));

}

}

}

Connections {

target: JniMessenger

function onMessageReceivedFromJava(message) {

const data = JSON.parse(message);

for (let key in data) {

if (data.hasOwnProperty(key)) {

console.log("Setting " + key + " to " + data[key]);

if (data.color) {

rectangle.color = data.color;

}

}

}

}

}

...

}

Android端

在Android端,我們添加了新的靜態方法以便從Qt端調用。MessageFromQtListener是用于添加來自Qt消息偵聽器的接口。我們還有一個已經在Qt端定義的本地方法 sendMessageToQt。以下是MainActivity 類:

public class MainActivity extends AppCompatActivity {

private interface MessageFromQtListener {

public void onMessage(String message);

};

private static final String TAG = "Android/MainActivity";

private static boolean firstTime = true;

private JsonHandler jsonHandler;

private static MessageFromQtListener m_messageListener;

public static native void sendMessageToQt(String message);

public static void setOnMessageFromQtListener(MessageFromQtListener listener) {

m_messageListener = listener;

}

public static void receiveMessageFromQt(String message) {

Log.d(TAG, "Message received from Qt: " + message);

if (m_messageListener != null) {

m_messageListener.onMessage(message);

}

}

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

jsonHandler = new JsonHandler();

MainActivity.setOnMessageFromQtListener(new MessageFromQtListener() {

@Override

public void onMessage(String message) {

try {

JSONObject jsonObject = new JSONObject(message);

Map<String, Object> json = JsonHandler.toMap(jsonObject);

if (json.containsKey("navigate") &&

Objects.equals((String) json.get("navigate"), "back")) {

Intent intent = new Intent(getApplicationContext(), MainActivity.class);

startActivity(intent);

}

} catch (JSONException e) {

Log.e(TAG, "Not valid JSON: " + message);

}

}

});

}

public void onBtnClicked(View view) {

String color = ((Button) view).getText().toString().toLowerCase();

Log.d(TAG, color);

Intent intent = new Intent(MainActivity.this, QtActivity.class);

intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);

if (!firstTime) {

MainActivity.sendMessageToQt(jsonHandler.buildJson("color", color).toString());

} else {

intent.putExtra("applicationArguments",

jsonHandler.buildJson("color", color).toString());

firstTime = false;

}

startActivity(intent);

}

}

onBtnClicked由XML調用,并向QML發送更新顏色的JSON字符串。如&ldquo;Qt端&rdquo;一節所解釋的那樣,在QtActivity加載之前,我們不能調用本地方法,因此我們使用m_qtLibsLoaded變量來判斷。如果它的值為true,我們就使用常規的本地方法。如果它的值為false,我們就加載QtActivity并向其傳遞Intent數據。QtActivity在加載庫時查找名為applicationArguments的意圖字段,并將其作為環境變量傳遞給C++代碼。以上這些可以在main函數中使用argv參數獲取。

以上就是從Android Studio項目中運行QtActivity所需的所有操作。

登錄后免費查看全文
立即登錄
App下載
技術鄰APP
工程師必備
  • 項目客服
  • 培訓客服
  • 平臺客服

TOP