Skip to content

Commit 9d89409

Browse files
committed
Add Android support
1 parent 0bbc48a commit 9d89409

30 files changed

+1367
-18
lines changed

examples/android/.gitignore

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# C++ source
2+
app/src/main/cpp/src
3+
app/src/main/cpp/third_party
4+
5+
# Built application files
6+
*.apk
7+
*.aar
8+
*.ap_
9+
*.aab
10+
11+
# Files for the ART/Dalvik VM
12+
*.dex
13+
14+
# Java class files
15+
*.class
16+
17+
# Generated files
18+
bin/
19+
gen/
20+
out/
21+
# Uncomment the following line in case you need and you don't have the release build type files in your app
22+
# release/
23+
24+
# Gradle files
25+
.gradle/
26+
build/
27+
28+
# Local configuration file (sdk path, etc)
29+
local.properties
30+
31+
# Proguard folder generated by Eclipse
32+
proguard/
33+
34+
# Log Files
35+
*.log
36+
37+
# Android Studio Navigation editor temp files
38+
.navigation/
39+
40+
# Android Studio captures folder
41+
captures/
42+
43+
# IntelliJ
44+
*.iml
45+
.idea/workspace.xml
46+
.idea/tasks.xml
47+
.idea/gradle.xml
48+
.idea/assetWizardSettings.xml
49+
.idea/dictionaries
50+
.idea/libraries
51+
.idea/jarRepositories.xml
52+
# Android Studio 3 in .gitignore file.
53+
.idea/caches
54+
.idea/modules.xml
55+
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
56+
.idea/navEditor.xml
57+
58+
# Keystore files
59+
# Uncomment the following lines if you do not want to check your keystore files in.
60+
#*.jks
61+
#*.keystore
62+
63+
# External native build folder generated in Android Studio 2.2 and later
64+
.externalNativeBuild
65+
.cxx/
66+
67+
# Google Services (e.g. APIs or Firebase)
68+
# google-services.json
69+
70+
# Freeline
71+
freeline.py
72+
freeline/
73+
freeline_project_description.json
74+
75+
# fastlane
76+
fastlane/report.xml
77+
fastlane/Preview.html
78+
fastlane/screenshots
79+
fastlane/test_output
80+
fastlane/readme.md
81+
82+
# Version control
83+
vcs.xml
84+
85+
# lint
86+
lint/intermediates/
87+
lint/generated/
88+
lint/outputs/
89+
lint/tmp/
90+
# lint/reports/
91+
92+
# Android Profiling
93+
*.hprof

examples/android/app/build.gradle

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
apply plugin: 'com.android.application'
2+
3+
android {
4+
compileSdkVersion 36
5+
ndkVersion '28.1.13356709'
6+
7+
defaultConfig {
8+
applicationId "graphics.revector.demo"
9+
minSdkVersion 24
10+
targetSdkVersion 36
11+
12+
// Shader compilation directives, put glsl shaders to app/src/main/shaders.
13+
// Android Studio will pick them up and compile them into APK/assets/shaders.
14+
// KNOWN ISSUE: if shaders having errors, it takes long time for gradle to timeout.
15+
// But it will eventually time out and complain about shader compiling.
16+
shaders {
17+
glslcArgs.addAll(['-c', '-g'])
18+
}
19+
externalNativeBuild {
20+
cmake {
21+
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
22+
arguments '-DANDROID_TOOLCHAIN=clang', '-DANDROID_STL=c++_static', "-DCMAKE_BUILD_TYPE=Release"
23+
}
24+
}
25+
}
26+
externalNativeBuild {
27+
cmake {
28+
version '3.18.1'
29+
path 'src/main/cpp/CMakeLists.txt'
30+
}
31+
}
32+
buildTypes.release.minifyEnabled = false
33+
buildFeatures.prefab = true
34+
namespace 'graphics.revector.demo'
35+
}
36+
37+
dependencies {
38+
implementation 'androidx.appcompat:appcompat:1.6.1'
39+
implementation 'androidx.games:games-activity:1.2.1'
40+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
4+
5+
<application
6+
android:label="@string/app_name"
7+
android:theme="@style/Application.Fullscreen">
8+
9+
<activity
10+
android:name=".MainActivity"
11+
android:configChanges="orientation|keyboardHidden"
12+
android:exported="true">
13+
<!-- Tell GameActivity the name of our .so file.
14+
This will be optional after the release 1.1.0 -->
15+
<meta-data
16+
android:name="android.app.lib_name"
17+
android:value="revector_android_demo" />
18+
<intent-filter>
19+
<action android:name="android.intent.action.MAIN" />
20+
<category android:name="android.intent.category.LAUNCHER" />
21+
</intent-filter>
22+
</activity>
23+
</application>
24+
</manifest>
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
#include <android/asset_manager_jni.h>
2+
#include <android/log.h>
3+
#include <game-activity/native_app_glue/android_native_app_glue.h>
4+
5+
#include <iostream>
6+
#include <random>
7+
8+
// clang-format off
9+
#include "src/vulkan_wrapper.h"
10+
#include "src/app.h"
11+
// clang-format on
12+
13+
revector::App *app;
14+
15+
constexpr bool USE_VULKAN = false;
16+
bool vulkan_initialized = false;
17+
18+
constexpr int DPI_STANDARD = 96;
19+
20+
using namespace revector;
21+
22+
using Pathfinder::Vec2;
23+
using Pathfinder::Vec3;
24+
25+
class MyNode : public Node {
26+
std::shared_ptr<ToggleButtonGroup> button_group;
27+
28+
void custom_ready() override {
29+
auto vbox_container = std::make_shared<VBoxContainer>();
30+
vbox_container->set_separation(8);
31+
vbox_container->set_position({10, 10});
32+
add_child(vbox_container);
33+
34+
{
35+
auto button = std::make_shared<Button>();
36+
button->container_sizing.flag_h = ContainerSizingFlag::ShrinkStart;
37+
vbox_container->add_child(button);
38+
39+
auto callback = []() { Logger::info("Button triggered"); };
40+
button->connect_signal("triggered", callback);
41+
}
42+
43+
{
44+
auto button = std::make_shared<Button>();
45+
button->set_icon_normal(std::make_shared<VectorImage>(get_asset_dir("icons/Node_Button.svg")));
46+
button->container_sizing.flag_h = ContainerSizingFlag::ShrinkStart;
47+
vbox_container->add_child(button);
48+
}
49+
50+
{
51+
auto check_button = std::make_shared<CheckButton>();
52+
check_button->container_sizing.flag_h = ContainerSizingFlag::ShrinkStart;
53+
vbox_container->add_child(check_button);
54+
55+
auto callback = [](bool toggled) { Logger::info("Button toggled"); };
56+
check_button->connect_signal("toggled", callback);
57+
}
58+
59+
{
60+
auto container_group = std::make_shared<VBoxContainer>();
61+
container_group->set_separation(8);
62+
container_group->set_position({10, 200});
63+
add_child(container_group);
64+
65+
auto label = std::make_shared<Label>();
66+
label->set_text("Toggle Button Group");
67+
container_group->add_child(label);
68+
69+
button_group = std::make_shared<ToggleButtonGroup>();
70+
71+
for (int i = 0; i < 3; ++i) {
72+
auto check_button = std::make_shared<RadioButton>();
73+
check_button->container_sizing.flag_h = ContainerSizingFlag::ShrinkStart;
74+
container_group->add_child(check_button);
75+
button_group->add_button(check_button);
76+
}
77+
}
78+
}
79+
};
80+
81+
// Process the next main command.
82+
void handle_cmd(android_app *app_ctx, int32_t cmd) {
83+
switch (cmd) {
84+
case APP_CMD_START:
85+
__android_log_print(ANDROID_LOG_INFO, "revector", "APP_CMD_START");
86+
break;
87+
case APP_CMD_RESUME:
88+
__android_log_print(ANDROID_LOG_INFO, "revector", "APP_CMD_RESUME");
89+
break;
90+
case APP_CMD_PAUSE:
91+
__android_log_print(ANDROID_LOG_INFO, "revector", "APP_CMD_PAUSE");
92+
break;
93+
case APP_CMD_STOP:
94+
__android_log_print(ANDROID_LOG_INFO, "revector", "APP_CMD_STOP");
95+
break;
96+
case APP_CMD_DESTROY:
97+
__android_log_print(ANDROID_LOG_INFO, "revector", "APP_CMD_DESTROY");
98+
break;
99+
case APP_CMD_INIT_WINDOW: {
100+
__android_log_print(ANDROID_LOG_INFO, "revector", "APP_CMD_INIT_WINDOW");
101+
102+
AConfiguration *config = AConfiguration_new();
103+
AConfiguration_fromAssetManager(config, app_ctx->activity->assetManager);
104+
int32_t dpi = AConfiguration_getDensity(config);
105+
float dpi_scale = (float)dpi / DPI_STANDARD;
106+
AConfiguration_delete(config);
107+
__android_log_print(ANDROID_LOG_INFO, "revector", "DPI: %d, Expected scale factor: %.1f", dpi, dpi_scale);
108+
109+
auto window_size =
110+
Pathfinder::Vec2I(ANativeWindow_getWidth(app_ctx->window), ANativeWindow_getHeight(app_ctx->window));
111+
112+
app = new revector::App(app_ctx->window, app_ctx->activity->assetManager, window_size, true, USE_VULKAN);
113+
app->set_custom_scaling_factor(dpi_scale);
114+
115+
app->get_tree_root()->add_child(std::make_shared<MyNode>());
116+
} break;
117+
case APP_CMD_TERM_WINDOW: {
118+
__android_log_print(ANDROID_LOG_INFO, "revector", "APP_CMD_TERM_WINDOW");
119+
120+
app->single_run_cleanup();
121+
122+
// The window is being hidden or closed or rotated, clean it up.
123+
delete app;
124+
app = nullptr;
125+
} break;
126+
default:
127+
__android_log_print(ANDROID_LOG_INFO, "revector", "Event not handled: %d", cmd);
128+
}
129+
}
130+
131+
void handle_motion_event(GameActivityMotionEvent *event) {
132+
if (!app) {
133+
return;
134+
}
135+
136+
int32_t actionMasked = event->action & AMOTION_EVENT_ACTION_MASK;
137+
int32_t pointerIndex = 0;
138+
139+
// Only get pointer index for specific actions (DOWN/UP)
140+
if (actionMasked == AMOTION_EVENT_ACTION_POINTER_DOWN || actionMasked == AMOTION_EVENT_ACTION_POINTER_UP) {
141+
pointerIndex =
142+
(event->action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
143+
}
144+
145+
// Get X/Y coordinates for the affected pointer
146+
float x = GameActivityPointerAxes_getAxisValue(&event->pointers[pointerIndex], AMOTION_EVENT_AXIS_X);
147+
float y = GameActivityPointerAxes_getAxisValue(&event->pointers[pointerIndex], AMOTION_EVENT_AXIS_Y);
148+
149+
float x_pos = x / app->get_scaling_factor();
150+
float y_pos = y / app->get_scaling_factor();
151+
152+
auto input_server = InputServer::get_singleton();
153+
154+
switch (actionMasked) {
155+
case AMOTION_EVENT_ACTION_DOWN: {
156+
InputEvent input_event{};
157+
input_event.type = InputEventType::MouseButton;
158+
input_event.args.mouse_button.button = 0;
159+
input_event.args.mouse_button.pressed = true;
160+
input_event.args.mouse_button.position = {(float)x_pos, (float)y_pos};
161+
input_server->input_queue.push_back(input_event);
162+
input_server->cursor_position = {(float)x_pos, (float)y_pos};
163+
} break;
164+
case AMOTION_EVENT_ACTION_UP: {
165+
InputEvent input_event{};
166+
input_event.type = InputEventType::MouseButton;
167+
input_event.args.mouse_button.button = 0;
168+
input_event.args.mouse_button.pressed = false;
169+
input_event.args.mouse_button.position = {(float)x_pos, (float)y_pos};
170+
input_server->input_queue.push_back(input_event);
171+
172+
input_server->cursor_position = {(float)x_pos, (float)y_pos};
173+
} break;
174+
case AMOTION_EVENT_ACTION_HOVER_MOVE:
175+
case AMOTION_EVENT_ACTION_MOVE: {
176+
InputEvent input_event{};
177+
input_event.type = InputEventType::MouseMotion;
178+
input_event.args.mouse_motion.position = {(float)x_pos, (float)y_pos};
179+
input_server->last_cursor_position = input_server->cursor_position;
180+
181+
input_server->cursor_position = {(float)x_pos, (float)y_pos};
182+
183+
input_event.args.mouse_motion.relative = input_server->cursor_position - input_server->last_cursor_position;
184+
input_server->input_queue.push_back(input_event);
185+
} break;
186+
case AMOTION_EVENT_ACTION_SCROLL: {
187+
__android_log_print(ANDROID_LOG_INFO, "Pathfinder", "INPUT: SCROLL (%.1f, %.1f)", x, y);
188+
} break;
189+
}
190+
}
191+
192+
void android_main(struct android_app *app_ctx) {
193+
// Set the callback to process system events.
194+
app_ctx->onAppCmd = handle_cmd;
195+
196+
// In NativeActivity, you would do this: app->onInputEvent = engine_handle_input;
197+
// In GameActivity, you should NOT do this. The field is unused/removed for input.
198+
199+
// It's also recommended to nullify any filters to ensure all input is received:
200+
android_app_set_key_event_filter(app_ctx, nullptr);
201+
android_app_set_motion_event_filter(app_ctx, nullptr);
202+
203+
if (USE_VULKAN && !vulkan_initialized) {
204+
InitVulkan();
205+
vulkan_initialized = true;
206+
}
207+
208+
// Used to poll the events in the main loop.
209+
int events;
210+
android_poll_source *source;
211+
212+
// Main loop.
213+
do {
214+
if (ALooper_pollOnce(0, nullptr, &events, (void **)&source) >= 0) {
215+
if (source != nullptr) {
216+
source->process(app_ctx, source);
217+
}
218+
}
219+
220+
// 2. Get the current input buffer
221+
android_input_buffer *inputBuffer = android_app_swap_input_buffers(app_ctx);
222+
223+
// 3. Process Motion Events (Touch/Controller Analog)
224+
if (inputBuffer && inputBuffer->motionEventsCount) {
225+
for (int i = 0; i < inputBuffer->motionEventsCount; i++) {
226+
// Process inputBuffer->motionEvents[i] (a GameActivityMotionEvent*)
227+
handle_motion_event(&inputBuffer->motionEvents[i]);
228+
}
229+
// Clear the motion events queue for the next frame
230+
android_app_clear_motion_events(inputBuffer);
231+
}
232+
233+
if (app) {
234+
app->single_run();
235+
}
236+
} while (app_ctx->destroyRequested == 0);
237+
}

0 commit comments

Comments
 (0)