- Create Android project structure with build configuration and manifest - Implement PHP-to-Java bridge with JNI integration for native Android UI components - Add comprehensive UI stub functions including window creation, labels, buttons, and inputs - Develop Android Activity with native View bridge and layout management system - Integrate build scripts for compiling PHP into Android shared library and packaging APK - Include installation script with ADB deployment and application launching - Provide complete Hello World application demonstrating TypePHP Android capabilities - Add proper Git ignore rules and documentation for the Android examplemaster
parent
8dfb363422
commit
a0db503e3d
11 changed files with 695 additions and 0 deletions
@ -0,0 +1,2 @@ |
|||||||
|
/build/ |
||||||
|
/dist/ |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
package="com.swoole.typephp"> |
||||||
|
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="36" /> |
||||||
|
<application |
||||||
|
android:allowBackup="false" |
||||||
|
android:extractNativeLibs="false" |
||||||
|
android:icon="@drawable/typephp_icon" |
||||||
|
android:label="TypePHP Android" |
||||||
|
android:theme="@android:style/Theme.Material.Light.NoActionBar"> |
||||||
|
<activity |
||||||
|
android:name=".MainActivity" |
||||||
|
android:exported="true"> |
||||||
|
<intent-filter> |
||||||
|
<action android:name="android.intent.action.MAIN" /> |
||||||
|
<category android:name="android.intent.category.LAUNCHER" /> |
||||||
|
</intent-filter> |
||||||
|
</activity> |
||||||
|
</application> |
||||||
|
</manifest> |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
# TypePHP Android native app |
||||||
|
|
||||||
|
This example compiles PHP into a self-contained Android `arm64-v8a` shared |
||||||
|
library and packages a signed APK using the Android SDK command-line tools. The |
||||||
|
TypePHP code owns the control layout, name input, click counter, reset behavior, |
||||||
|
and event dispatch. Java only supplies the Activity lifecycle and a generic |
||||||
|
native View bridge. It does not require Gradle or Android Studio. |
||||||
|
|
||||||
|
Prerequisites: |
||||||
|
|
||||||
|
- Android SDK Platform/Build Tools 36 |
||||||
|
- Android NDK r27 or newer |
||||||
|
- JDK with `javac` and `keytool` |
||||||
|
- the PHPX Android SDK produced by `phpx/sdk/build-native.sh` |
||||||
|
|
||||||
|
Build: |
||||||
|
|
||||||
|
```sh |
||||||
|
export ANDROID_SDK_ROOT=/home/swoole/soft/android-sdk |
||||||
|
export ANDROID_NDK_HOME=/home/swoole/soft/android-ndk-r27d |
||||||
|
export PHPX_ANDROID_SDK_DIR=/path/to/android-arm64-v8a-sdk |
||||||
|
./build-app.sh |
||||||
|
``` |
||||||
|
|
||||||
|
The result is `dist/typephp-android-hello-debug.apk`. With an arm64 Android |
||||||
|
device connected and USB debugging enabled, install and launch it using: |
||||||
|
|
||||||
|
```sh |
||||||
|
./install-app.sh |
||||||
|
``` |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
name: typephp_android_hello |
||||||
|
version: 0.1.0 |
||||||
|
mode: lib |
||||||
|
output: dist/typephp_android_hello.so |
||||||
|
php-version: "8.4" |
||||||
|
cxx-std: c++17 |
||||||
|
target-platform: aarch64-linux-android24 |
||||||
|
build-dir: build/typephp |
||||||
|
no-progress: true |
||||||
|
|
||||||
|
sources: |
||||||
|
- php-src |
||||||
|
- cpp-src |
||||||
|
|
||||||
|
ld-flags: |
||||||
|
- -Wl,--gc-sections |
||||||
|
- -Wl,--exclude-libs,ALL |
||||||
@ -0,0 +1,105 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
set -euo pipefail |
||||||
|
|
||||||
|
project_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) |
||||||
|
compiler_root=$(cd "${project_dir}/../.." && pwd) |
||||||
|
android_sdk=${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/home/swoole/soft/android-sdk}} |
||||||
|
android_ndk=${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-/home/swoole/soft/android-ndk-r27d}} |
||||||
|
phpx_root=${PHPX_HOME:-$(cd "${compiler_root}/../phpx" && pwd)} |
||||||
|
phpx_android_sdk=${PHPX_ANDROID_SDK_DIR:-${phpx_root}/android/arm64-v8a} |
||||||
|
|
||||||
|
if [[ ! -d "${android_sdk}/build-tools" || ! -f "${android_sdk}/platforms/android-36/android.jar" ]]; then |
||||||
|
echo "Android SDK Platform 36 and Build Tools are required: ${android_sdk}" >&2 |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
if [[ ! -f "${android_ndk}/build/cmake/android.toolchain.cmake" ]]; then |
||||||
|
echo "Android NDK is required: ${android_ndk}" >&2 |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
if [[ ! -f "${phpx_android_sdk}/.typephp-android-sdk-abi" ]]; then |
||||||
|
echo "TypePHP Android SDK is required: ${phpx_android_sdk}" >&2 |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
build_tools_version=$(find "${android_sdk}/build-tools" -mindepth 1 -maxdepth 1 -type d \ |
||||||
|
-printf '%f\n' | sort -V | tail -n 1) |
||||||
|
build_tools=${android_sdk}/build-tools/${build_tools_version} |
||||||
|
android_jar=${android_sdk}/platforms/android-36/android.jar |
||||||
|
build_dir=${project_dir}/build/apk |
||||||
|
classes_dir=${build_dir}/classes |
||||||
|
package_dir=${build_dir}/package |
||||||
|
resources_dir=${build_dir}/resources |
||||||
|
compiled_resources_dir=${build_dir}/compiled-resources |
||||||
|
dist_dir=${project_dir}/dist |
||||||
|
|
||||||
|
mkdir -p \ |
||||||
|
"${classes_dir}" \ |
||||||
|
"${package_dir}/lib/arm64-v8a" \ |
||||||
|
"${resources_dir}/drawable-nodpi" \ |
||||||
|
"${compiled_resources_dir}" \ |
||||||
|
"${dist_dir}" |
||||||
|
cp -p "${compiler_root}/examples/objective-c-macos/ios-assets/AppIcon-180.png" \ |
||||||
|
"${resources_dir}/drawable-nodpi/typephp_icon.png" |
||||||
|
|
||||||
|
ANDROID_NDK_HOME="${android_ndk}" \ |
||||||
|
PHPX_HOME="${phpx_root}" \ |
||||||
|
PHPX_ANDROID_SDK_DIR="${phpx_android_sdk}" \ |
||||||
|
php "${compiler_root}/bin/tpc.php" "${project_dir}/android.yml" --no-progress |
||||||
|
|
||||||
|
javac -source 8 -target 8 -encoding UTF-8 \ |
||||||
|
-bootclasspath "${android_jar}" \ |
||||||
|
-d "${classes_dir}" \ |
||||||
|
"${project_dir}/java/com/swoole/typephp/MainActivity.java" |
||||||
|
|
||||||
|
mapfile -t class_files < <(find "${classes_dir}" -type f -name '*.class' -print) |
||||||
|
"${build_tools}/d8" --lib "${android_jar}" --min-api 24 \ |
||||||
|
--output "${package_dir}" "${class_files[@]}" |
||||||
|
|
||||||
|
unsigned_apk=${build_dir}/typephp-android-hello-unsigned.apk |
||||||
|
aligned_apk=${build_dir}/typephp-android-hello-aligned.apk |
||||||
|
signed_apk=${dist_dir}/typephp-android-hello-debug.apk |
||||||
|
"${build_tools}/aapt2" compile \ |
||||||
|
--dir "${resources_dir}" \ |
||||||
|
-o "${compiled_resources_dir}" |
||||||
|
mapfile -t compiled_resources < <(find "${compiled_resources_dir}" -type f -name '*.flat' -print) |
||||||
|
"${build_tools}/aapt2" link \ |
||||||
|
-I "${android_jar}" \ |
||||||
|
--manifest "${project_dir}/AndroidManifest.xml" \ |
||||||
|
--min-sdk-version 24 \ |
||||||
|
--target-sdk-version 36 \ |
||||||
|
--version-code 1 \ |
||||||
|
--version-name 0.1.0 \ |
||||||
|
"${compiled_resources[@]}" \ |
||||||
|
-o "${unsigned_apk}" |
||||||
|
|
||||||
|
cp -p "${dist_dir}/typephp_android_hello.so" \ |
||||||
|
"${package_dir}/lib/arm64-v8a/libtypephp_android_hello.so" |
||||||
|
( |
||||||
|
cd "${package_dir}" |
||||||
|
zip -q -0 "${unsigned_apk}" lib/arm64-v8a/libtypephp_android_hello.so |
||||||
|
zip -q "${unsigned_apk}" classes.dex |
||||||
|
) |
||||||
|
"${build_tools}/zipalign" -f -P 16 4 "${unsigned_apk}" "${aligned_apk}" |
||||||
|
|
||||||
|
debug_keystore=${build_dir}/debug.keystore |
||||||
|
if [[ ! -f "${debug_keystore}" ]]; then |
||||||
|
keytool -genkeypair -noprompt \ |
||||||
|
-keystore "${debug_keystore}" \ |
||||||
|
-storepass android \ |
||||||
|
-alias androiddebugkey \ |
||||||
|
-keypass android \ |
||||||
|
-dname 'CN=Android Debug,O=TypePHP,C=CN' \ |
||||||
|
-keyalg RSA \ |
||||||
|
-validity 10000 >/dev/null |
||||||
|
fi |
||||||
|
"${build_tools}/apksigner" sign \ |
||||||
|
--ks "${debug_keystore}" \ |
||||||
|
--ks-pass pass:android \ |
||||||
|
--key-pass pass:android \ |
||||||
|
--out "${signed_apk}" \ |
||||||
|
"${aligned_apk}" |
||||||
|
|
||||||
|
"${build_tools}/zipalign" -c -P 16 4 "${signed_apk}" |
||||||
|
"${build_tools}/apksigner" verify --verbose "${signed_apk}" |
||||||
|
echo "Built signed APK: ${signed_apk}" |
||||||
@ -0,0 +1,196 @@ |
|||||||
|
#include <jni.h> |
||||||
|
#include <phpx.h> |
||||||
|
#include <typephp_runtime.h> |
||||||
|
|
||||||
|
#include <mutex> |
||||||
|
|
||||||
|
TYPEPHP_RUNTIME_INIT_FUNCTION(typephp_android_hello); |
||||||
|
extern void php_typephp_application_did_launch(); |
||||||
|
extern void php_typephp_application_control_activated(php::Int control_id); |
||||||
|
|
||||||
|
static JavaVM *java_vm = nullptr; |
||||||
|
static jobject activity = nullptr; |
||||||
|
static jclass activity_class = nullptr; |
||||||
|
static std::once_flag runtime_once; |
||||||
|
static int runtime_status = -1; |
||||||
|
|
||||||
|
static JNIEnv *get_env() |
||||||
|
{ |
||||||
|
JNIEnv *env = nullptr; |
||||||
|
if (java_vm == nullptr || java_vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) { |
||||||
|
return nullptr; |
||||||
|
} |
||||||
|
return env; |
||||||
|
} |
||||||
|
|
||||||
|
static jmethodID method(JNIEnv *env, const char *name, const char *signature) |
||||||
|
{ |
||||||
|
return activity_class == nullptr ? nullptr : env->GetMethodID(activity_class, name, signature); |
||||||
|
} |
||||||
|
|
||||||
|
static jstring to_java_string(JNIEnv *env, const php::Str &value) |
||||||
|
{ |
||||||
|
return env->NewStringUTF(value.data()); |
||||||
|
} |
||||||
|
|
||||||
|
static php::Str from_java_string(JNIEnv *env, jstring value) |
||||||
|
{ |
||||||
|
if (value == nullptr) { |
||||||
|
return php::Str(); |
||||||
|
} |
||||||
|
const char *utf8 = env->GetStringUTFChars(value, nullptr); |
||||||
|
php::Str result(utf8 == nullptr ? "" : utf8); |
||||||
|
if (utf8 != nullptr) { |
||||||
|
env->ReleaseStringUTFChars(value, utf8); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
static int ensure_runtime() |
||||||
|
{ |
||||||
|
std::call_once(runtime_once, []() { |
||||||
|
char application_name[] = "typephp_android_hello"; |
||||||
|
char *argv[] = {application_name, nullptr}; |
||||||
|
runtime_status = TYPEPHP_RUNTIME_INIT(typephp_android_hello)(1, argv); |
||||||
|
}); |
||||||
|
return runtime_status; |
||||||
|
} |
||||||
|
|
||||||
|
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) |
||||||
|
{ |
||||||
|
java_vm = vm; |
||||||
|
return JNI_VERSION_1_6; |
||||||
|
} |
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL |
||||||
|
Java_com_swoole_typephp_MainActivity_nativeApplicationDidLaunch(JNIEnv *env, jobject instance) |
||||||
|
{ |
||||||
|
if (activity != nullptr) { |
||||||
|
env->DeleteGlobalRef(activity); |
||||||
|
} |
||||||
|
if (activity_class != nullptr) { |
||||||
|
env->DeleteGlobalRef(activity_class); |
||||||
|
} |
||||||
|
activity = env->NewGlobalRef(instance); |
||||||
|
jclass local_class = env->GetObjectClass(instance); |
||||||
|
activity_class = reinterpret_cast<jclass>(env->NewGlobalRef(local_class)); |
||||||
|
env->DeleteLocalRef(local_class); |
||||||
|
|
||||||
|
if (ensure_runtime() == 0) { |
||||||
|
php_typephp_application_did_launch(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL |
||||||
|
Java_com_swoole_typephp_MainActivity_nativeControlActivated(JNIEnv *, jobject, jint control_id) |
||||||
|
{ |
||||||
|
if (runtime_status == 0) { |
||||||
|
php_typephp_application_control_activated(static_cast<php::Int>(control_id)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void php_ui_create_window(php::Str title, php::Int width, php::Int height) |
||||||
|
{ |
||||||
|
JNIEnv *env = get_env(); |
||||||
|
jmethodID target = env == nullptr ? nullptr : method(env, "uiCreateWindow", "(Ljava/lang/String;II)V"); |
||||||
|
if (target == nullptr) { |
||||||
|
return; |
||||||
|
} |
||||||
|
jstring java_title = to_java_string(env, title); |
||||||
|
env->CallVoidMethod(activity, target, java_title, static_cast<jint>(width), static_cast<jint>(height)); |
||||||
|
env->DeleteLocalRef(java_title); |
||||||
|
} |
||||||
|
|
||||||
|
php::Int php_ui_add_image(php::Str resource_name, php::Int x, php::Int y, php::Int width, php::Int height) |
||||||
|
{ |
||||||
|
JNIEnv *env = get_env(); |
||||||
|
jmethodID target = env == nullptr ? nullptr : method(env, "uiAddImage", "(Ljava/lang/String;IIII)I"); |
||||||
|
if (target == nullptr) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
jstring java_name = to_java_string(env, resource_name); |
||||||
|
jint result = env->CallIntMethod(activity, target, java_name, static_cast<jint>(x), static_cast<jint>(y), |
||||||
|
static_cast<jint>(width), static_cast<jint>(height)); |
||||||
|
env->DeleteLocalRef(java_name); |
||||||
|
return static_cast<php::Int>(result); |
||||||
|
} |
||||||
|
|
||||||
|
php::Int php_ui_add_label(php::Str text, php::Int x, php::Int y, php::Int width, php::Int height, |
||||||
|
php::Int font_size, php::Bool bold) |
||||||
|
{ |
||||||
|
JNIEnv *env = get_env(); |
||||||
|
jmethodID target = env == nullptr ? nullptr : method(env, "uiAddLabel", "(Ljava/lang/String;IIIIIZ)I"); |
||||||
|
if (target == nullptr) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
jstring java_text = to_java_string(env, text); |
||||||
|
jint result = env->CallIntMethod(activity, target, java_text, static_cast<jint>(x), static_cast<jint>(y), |
||||||
|
static_cast<jint>(width), static_cast<jint>(height), |
||||||
|
static_cast<jint>(font_size), static_cast<jboolean>(bold)); |
||||||
|
env->DeleteLocalRef(java_text); |
||||||
|
return static_cast<php::Int>(result); |
||||||
|
} |
||||||
|
|
||||||
|
php::Int php_ui_add_text_input(php::Str hint, php::Int x, php::Int y, php::Int width, php::Int height) |
||||||
|
{ |
||||||
|
JNIEnv *env = get_env(); |
||||||
|
jmethodID target = env == nullptr ? nullptr : method(env, "uiAddTextInput", "(Ljava/lang/String;IIII)I"); |
||||||
|
if (target == nullptr) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
jstring java_hint = to_java_string(env, hint); |
||||||
|
jint result = env->CallIntMethod(activity, target, java_hint, static_cast<jint>(x), static_cast<jint>(y), |
||||||
|
static_cast<jint>(width), static_cast<jint>(height)); |
||||||
|
env->DeleteLocalRef(java_hint); |
||||||
|
return static_cast<php::Int>(result); |
||||||
|
} |
||||||
|
|
||||||
|
php::Int php_ui_add_button(php::Str title, php::Int x, php::Int y, php::Int width, php::Int height, php::Int style) |
||||||
|
{ |
||||||
|
JNIEnv *env = get_env(); |
||||||
|
jmethodID target = env == nullptr ? nullptr : method(env, "uiAddButton", "(Ljava/lang/String;IIIII)I"); |
||||||
|
if (target == nullptr) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
jstring java_title = to_java_string(env, title); |
||||||
|
jint result = env->CallIntMethod(activity, target, java_title, static_cast<jint>(x), static_cast<jint>(y), |
||||||
|
static_cast<jint>(width), static_cast<jint>(height), static_cast<jint>(style)); |
||||||
|
env->DeleteLocalRef(java_title); |
||||||
|
return static_cast<php::Int>(result); |
||||||
|
} |
||||||
|
|
||||||
|
void php_ui_set_control_text(php::Int control_id, php::Str text) |
||||||
|
{ |
||||||
|
JNIEnv *env = get_env(); |
||||||
|
jmethodID target = env == nullptr ? nullptr : method(env, "uiSetControlText", "(ILjava/lang/String;)V"); |
||||||
|
if (target == nullptr) { |
||||||
|
return; |
||||||
|
} |
||||||
|
jstring java_text = to_java_string(env, text); |
||||||
|
env->CallVoidMethod(activity, target, static_cast<jint>(control_id), java_text); |
||||||
|
env->DeleteLocalRef(java_text); |
||||||
|
} |
||||||
|
|
||||||
|
php::Str php_ui_get_control_text(php::Int control_id) |
||||||
|
{ |
||||||
|
JNIEnv *env = get_env(); |
||||||
|
jmethodID target = env == nullptr ? nullptr : method(env, "uiGetControlText", "(I)Ljava/lang/String;"); |
||||||
|
if (target == nullptr) { |
||||||
|
return php::Str(); |
||||||
|
} |
||||||
|
auto value = static_cast<jstring>(env->CallObjectMethod(activity, target, static_cast<jint>(control_id))); |
||||||
|
php::Str result = from_java_string(env, value); |
||||||
|
if (value != nullptr) { |
||||||
|
env->DeleteLocalRef(value); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
void php_ui_show_window() |
||||||
|
{ |
||||||
|
JNIEnv *env = get_env(); |
||||||
|
jmethodID target = env == nullptr ? nullptr : method(env, "uiShowWindow", "()V"); |
||||||
|
if (target != nullptr) { |
||||||
|
env->CallVoidMethod(activity, target); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
function ui_create_window(string $title, int $width, int $height): void {} |
||||||
|
|
||||||
|
function ui_add_image(string $resourceName, int $x, int $y, int $width, int $height): int {} |
||||||
|
|
||||||
|
function ui_add_label( |
||||||
|
string $text, |
||||||
|
int $x, |
||||||
|
int $y, |
||||||
|
int $width, |
||||||
|
int $height, |
||||||
|
int $fontSize, |
||||||
|
bool $bold, |
||||||
|
): int {} |
||||||
|
|
||||||
|
function ui_add_text_input(string $hint, int $x, int $y, int $width, int $height): int {} |
||||||
|
|
||||||
|
function ui_add_button(string $title, int $x, int $y, int $width, int $height, int $style): int {} |
||||||
|
|
||||||
|
function ui_set_control_text(int $controlId, string $text): void {} |
||||||
|
|
||||||
|
function ui_get_control_text(int $controlId): string {} |
||||||
|
|
||||||
|
function ui_show_window(): void {} |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
set -euo pipefail |
||||||
|
|
||||||
|
project_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) |
||||||
|
android_sdk=${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/home/swoole/soft/android-sdk}} |
||||||
|
adb=${android_sdk}/platform-tools/adb |
||||||
|
apk=${project_dir}/dist/typephp-android-hello-debug.apk |
||||||
|
|
||||||
|
[[ -x "${adb}" ]] || { echo "adb was not found: ${adb}" >&2; exit 1; } |
||||||
|
[[ -f "${apk}" ]] || { echo "Build the APK first with ./build-app.sh" >&2; exit 1; } |
||||||
|
|
||||||
|
"${adb}" install -r "${apk}" |
||||||
|
"${adb}" shell am start -n com.swoole.typephp/.MainActivity |
||||||
@ -0,0 +1,184 @@ |
|||||||
|
package com.swoole.typephp; |
||||||
|
|
||||||
|
import android.app.Activity; |
||||||
|
import android.content.res.ColorStateList; |
||||||
|
import android.graphics.Color; |
||||||
|
import android.graphics.Typeface; |
||||||
|
import android.os.Bundle; |
||||||
|
import android.util.SparseArray; |
||||||
|
import android.view.Gravity; |
||||||
|
import android.view.View; |
||||||
|
import android.view.ViewGroup; |
||||||
|
import android.widget.Button; |
||||||
|
import android.widget.EditText; |
||||||
|
import android.widget.ImageView; |
||||||
|
import android.widget.TextView; |
||||||
|
|
||||||
|
public final class MainActivity extends Activity { |
||||||
|
static { |
||||||
|
System.loadLibrary("typephp_android_hello"); |
||||||
|
} |
||||||
|
|
||||||
|
private native void nativeApplicationDidLaunch(); |
||||||
|
private native void nativeControlActivated(int controlId); |
||||||
|
|
||||||
|
private final SparseArray<View> controls = new SparseArray<>(); |
||||||
|
private int nextControlId = 1; |
||||||
|
private LogicalLayout content; |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void onCreate(Bundle state) { |
||||||
|
super.onCreate(state); |
||||||
|
nativeApplicationDidLaunch(); |
||||||
|
} |
||||||
|
|
||||||
|
private int storeControl(View control, int x, int y, int width, int height) { |
||||||
|
int controlId = nextControlId++; |
||||||
|
control.setId(controlId); |
||||||
|
controls.put(controlId, control); |
||||||
|
content.addView(control, content.new LayoutParams(x, y, width, height)); |
||||||
|
return controlId; |
||||||
|
} |
||||||
|
|
||||||
|
private void uiCreateWindow(String title, int width, int height) { |
||||||
|
setTitle(title); |
||||||
|
content = new LogicalLayout(width, height); |
||||||
|
content.setBackgroundColor(Color.rgb(247, 249, 252)); |
||||||
|
setContentView(content); |
||||||
|
} |
||||||
|
|
||||||
|
private int uiAddImage(String resourceName, int x, int y, int width, int height) { |
||||||
|
ImageView image = new ImageView(this); |
||||||
|
int resourceId = getResources().getIdentifier(resourceName, "drawable", getPackageName()); |
||||||
|
if (resourceId != 0) { |
||||||
|
image.setImageResource(resourceId); |
||||||
|
} |
||||||
|
image.setScaleType(ImageView.ScaleType.CENTER_INSIDE); |
||||||
|
return storeControl(image, x, y, width, height); |
||||||
|
} |
||||||
|
|
||||||
|
private int uiAddLabel( |
||||||
|
String text, |
||||||
|
int x, |
||||||
|
int y, |
||||||
|
int width, |
||||||
|
int height, |
||||||
|
int fontSize, |
||||||
|
boolean bold) { |
||||||
|
TextView label = new TextView(this); |
||||||
|
label.setText(text); |
||||||
|
label.setTextSize(fontSize); |
||||||
|
label.setTextColor(bold ? Color.rgb(24, 36, 54) : Color.rgb(90, 103, 122)); |
||||||
|
label.setTypeface(Typeface.DEFAULT, bold ? Typeface.BOLD : Typeface.NORMAL); |
||||||
|
label.setGravity(Gravity.CENTER); |
||||||
|
return storeControl(label, x, y, width, height); |
||||||
|
} |
||||||
|
|
||||||
|
private int uiAddTextInput(String hint, int x, int y, int width, int height) { |
||||||
|
EditText input = new EditText(this); |
||||||
|
input.setHint(hint); |
||||||
|
input.setSingleLine(true); |
||||||
|
input.setTextSize(18); |
||||||
|
input.setPadding(dp(12), 0, dp(12), 0); |
||||||
|
return storeControl(input, x, y, width, height); |
||||||
|
} |
||||||
|
|
||||||
|
private int uiAddButton(String title, int x, int y, int width, int height, int style) { |
||||||
|
Button button = new Button(this); |
||||||
|
button.setText(title); |
||||||
|
button.setTextSize(16); |
||||||
|
if (style == 1) { |
||||||
|
button.setTextColor(Color.WHITE); |
||||||
|
button.setBackgroundTintList(ColorStateList.valueOf(Color.rgb(0, 125, 214))); |
||||||
|
} else { |
||||||
|
button.setTextColor(Color.rgb(0, 92, 164)); |
||||||
|
button.setBackgroundTintList(ColorStateList.valueOf(Color.rgb(220, 236, 249))); |
||||||
|
} |
||||||
|
final int controlId = storeControl(button, x, y, width, height); |
||||||
|
button.setOnClickListener(new View.OnClickListener() { |
||||||
|
@Override |
||||||
|
public void onClick(View view) { |
||||||
|
nativeControlActivated(controlId); |
||||||
|
} |
||||||
|
}); |
||||||
|
return controlId; |
||||||
|
} |
||||||
|
|
||||||
|
private void uiSetControlText(int controlId, String text) { |
||||||
|
View control = controls.get(controlId); |
||||||
|
if (control instanceof TextView) { |
||||||
|
((TextView) control).setText(text); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private String uiGetControlText(int controlId) { |
||||||
|
View control = controls.get(controlId); |
||||||
|
return control instanceof TextView ? ((TextView) control).getText().toString() : ""; |
||||||
|
} |
||||||
|
|
||||||
|
private void uiShowWindow() { |
||||||
|
content.setVisibility(View.VISIBLE); |
||||||
|
} |
||||||
|
|
||||||
|
private int dp(float value) { |
||||||
|
return Math.round(value * getResources().getDisplayMetrics().density); |
||||||
|
} |
||||||
|
|
||||||
|
private final class LogicalLayout extends ViewGroup { |
||||||
|
private final int logicalWidth; |
||||||
|
private final int logicalHeight; |
||||||
|
|
||||||
|
private LogicalLayout(int width, int height) { |
||||||
|
super(MainActivity.this); |
||||||
|
logicalWidth = width; |
||||||
|
logicalHeight = height; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
||||||
|
int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); |
||||||
|
int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); |
||||||
|
float scaleX = (float) measuredWidth / logicalWidth; |
||||||
|
float scaleY = (float) measuredHeight / logicalHeight; |
||||||
|
for (int i = 0; i < getChildCount(); i++) { |
||||||
|
LayoutParams params = (LayoutParams) getChildAt(i).getLayoutParams(); |
||||||
|
getChildAt(i).measure( |
||||||
|
MeasureSpec.makeMeasureSpec(Math.round(params.logicalWidth * scaleX), MeasureSpec.EXACTLY), |
||||||
|
MeasureSpec.makeMeasureSpec(Math.round(params.logicalHeight * scaleY), MeasureSpec.EXACTLY)); |
||||||
|
} |
||||||
|
setMeasuredDimension(measuredWidth, measuredHeight); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
||||||
|
float scaleX = (float) (right - left) / logicalWidth; |
||||||
|
float scaleY = (float) (bottom - top) / logicalHeight; |
||||||
|
for (int i = 0; i < getChildCount(); i++) { |
||||||
|
View child = getChildAt(i); |
||||||
|
LayoutParams params = (LayoutParams) child.getLayoutParams(); |
||||||
|
int childLeft = Math.round(params.logicalX * scaleX); |
||||||
|
int childTop = Math.round(params.logicalY * scaleY); |
||||||
|
child.layout( |
||||||
|
childLeft, |
||||||
|
childTop, |
||||||
|
childLeft + child.getMeasuredWidth(), |
||||||
|
childTop + child.getMeasuredHeight()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private final class LayoutParams extends ViewGroup.LayoutParams { |
||||||
|
private final int logicalX; |
||||||
|
private final int logicalY; |
||||||
|
private final int logicalWidth; |
||||||
|
private final int logicalHeight; |
||||||
|
|
||||||
|
private LayoutParams(int x, int y, int width, int height) { |
||||||
|
super(width, height); |
||||||
|
logicalX = x; |
||||||
|
logicalY = y; |
||||||
|
logicalWidth = width; |
||||||
|
logicalHeight = height; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/** Called by the thin Android Activity after its native library is loaded. */ |
||||||
|
function typephp_application_did_launch(): void |
||||||
|
{ |
||||||
|
AndroidHelloApplication::build(); |
||||||
|
ui_show_window(); |
||||||
|
} |
||||||
|
|
||||||
|
/** Called by the generic Android View bridge for every activated control. */ |
||||||
|
function typephp_application_control_activated(int $controlId): void |
||||||
|
{ |
||||||
|
AndroidHelloApplication::handleEvent($controlId); |
||||||
|
} |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
final class AndroidHelloApplication |
||||||
|
{ |
||||||
|
private const BUTTON_PRIMARY = 1; |
||||||
|
private const BUTTON_SECONDARY = 2; |
||||||
|
|
||||||
|
private static int $nameInput = 0; |
||||||
|
private static int $statusLabel = 0; |
||||||
|
private static int $countButton = 0; |
||||||
|
private static int $resetButton = 0; |
||||||
|
private static int $clickCount = 0; |
||||||
|
|
||||||
|
public static function build(): void |
||||||
|
{ |
||||||
|
ui_create_window('TypePHP Android', 390, 844); |
||||||
|
ui_add_image('typephp_icon', 105, 36, 180, 96); |
||||||
|
ui_add_label('TypePHP Native App', 24, 142, 342, 52, 29, true); |
||||||
|
ui_add_label( |
||||||
|
"界面结构、应用状态和事件逻辑均由 TypePHP 实现。\n" . |
||||||
|
'Java 仅提供 Activity 和原生 View 桥接。', |
||||||
|
30, |
||||||
|
202, |
||||||
|
330, |
||||||
|
74, |
||||||
|
15, |
||||||
|
false, |
||||||
|
); |
||||||
|
self::$nameInput = ui_add_text_input('请输入名字', 30, 300, 330, 56); |
||||||
|
self::$statusLabel = ui_add_label('', 30, 378, 330, 74, 18, true); |
||||||
|
self::$countButton = ui_add_button( |
||||||
|
'使用 TypePHP 点击计数', |
||||||
|
30, |
||||||
|
476, |
||||||
|
330, |
||||||
|
58, |
||||||
|
self::BUTTON_PRIMARY, |
||||||
|
); |
||||||
|
self::$resetButton = ui_add_button( |
||||||
|
'重置计数', |
||||||
|
105, |
||||||
|
552, |
||||||
|
180, |
||||||
|
52, |
||||||
|
self::BUTTON_SECONDARY, |
||||||
|
); |
||||||
|
self::renderStatus(false); |
||||||
|
} |
||||||
|
|
||||||
|
public static function handleEvent(int $controlId): void |
||||||
|
{ |
||||||
|
if ($controlId === self::$resetButton) { |
||||||
|
self::$clickCount = 0; |
||||||
|
self::renderStatus(true); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($controlId !== self::$countButton) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
self::$clickCount++; |
||||||
|
self::renderStatus(false); |
||||||
|
} |
||||||
|
|
||||||
|
private static function renderStatus(bool $wasReset): void |
||||||
|
{ |
||||||
|
$name = ui_get_control_text(self::$nameInput); |
||||||
|
if ($name === '') { |
||||||
|
$name = 'Android'; |
||||||
|
} |
||||||
|
|
||||||
|
if ($wasReset) { |
||||||
|
ui_set_control_text(self::$statusLabel, $name . ',计数已重置'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (self::$clickCount === 0) { |
||||||
|
ui_set_control_text(self::$statusLabel, '输入名字,然后开始计数'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
ui_set_control_text( |
||||||
|
self::$statusLabel, |
||||||
|
'你好,' . $name . '!已点击 ' . self::$clickCount . ' 次', |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue