- 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