Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions config/nativephp.php
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,29 @@
'color_on_primary' => env('NATIVEPHP_ANDROID_COLOR_ON_PRIMARY', '#FFFFFF'),
],

/*
|--------------------------------------------------------------------------
| Android Splash Screen
|--------------------------------------------------------------------------
|
| style: 'image' - keep the full-bleed Compose splash overlay
| (current behavior; default).
| 'native' - rely solely on the held system splash
| (icon-on-background, jump-free) on API 31+.
| background: Splash window background color (light). Hex #RRGGBB or
| #AARRGGBB. Wrap in quotes in .env ('#' starts a comment).
| background_night: Splash window background color (dark mode).
|
| Provide public/splash.png (and optional public/splash-dark.png). On API < 31
| the full-bleed overlay is always used as a fallback regardless of style.
|
*/
'splash' => [
'style' => env('NATIVEPHP_ANDROID_SPLASH_STYLE', 'image'),
'background' => env('NATIVEPHP_ANDROID_SPLASH_BACKGROUND', '#FFFFFF'),
'background_night' => env('NATIVEPHP_ANDROID_SPLASH_BACKGROUND_NIGHT', '#000000'),
],

/*
|--------------------------------------------------------------------------
| Android Build Configuration
Expand Down
1 change: 1 addition & 0 deletions resources/androidstudio/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ android {

dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.material)
implementation(libs.androidx.constraintlayout)
Expand Down
2 changes: 1 addition & 1 deletion resources/androidstudio/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
android:label="NativePHP"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidPHP"
android:theme="@style/Theme.AndroidPHP.Splash"
tools:targetApi="31">
<property
android:name="android.app.16kb_pages.enabled"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import android.os.Handler
import android.util.Log
import android.webkit.CookieManager
import androidx.fragment.app.FragmentActivity
import android.os.Build
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.activity.compose.setContent
import com.nativephp.mobile.bridge.PHPBridge
import com.nativephp.mobile.bridge.PHPQueueWorker
Expand Down Expand Up @@ -70,14 +72,22 @@ class MainActivity : FragmentActivity(), WebViewProvider {
// Status bar style configuration - replaced during build
private val statusBarStyle = "REPLACE_STATUS_BAR_STYLE"

// Splash style configuration - replaced during build
private val splashStyle = "REPLACE_SPLASH_STYLE"

companion object {
// Static instance holder for accessing MainActivity from other activities
var instance: MainActivity? = null
private set
}

override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
// Hold the system splash until ready only in native style on API 31+
if (splashStyle == "native" && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
splashScreen.setKeepOnScreenCondition { showSplash }
}
instance = this

// Android 15 edge-to-edge compatibility fix
Expand Down Expand Up @@ -898,9 +908,10 @@ class MainActivity : FragmentActivity(), WebViewProvider {
}
)

// Splash overlay with fade animation (full screen, no insets)
// Full-bleed overlay in image style, or as a fallback below API 31
val useOverlay = splashStyle == "image" || Build.VERSION.SDK_INT < Build.VERSION_CODES.S
AnimatedVisibility(
visible = showSplash,
visible = showSplash && useOverlay,
exit = fadeOut(animationSpec = tween(300))
) {
SplashScreen()
Expand Down
9 changes: 9 additions & 0 deletions resources/androidstudio/app/src/main/res/drawable/splash.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1dp"
android:height="1dp"
android:viewportWidth="1"
android:viewportHeight="1">
<path
android:fillColor="#00000000"
android:pathData="M0,0h1v1h-1z" />
</vector>
2 changes: 2 additions & 0 deletions resources/androidstudio/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
agp = "8.13.2"
kotlin = "2.0.0"
coreKtx = "1.13.1"
coreSplashscreen = "1.0.1"
appcompat = "1.7.1"
material = "1.12.0"
constraintlayout = "2.2.1"
Expand All @@ -21,6 +22,7 @@ playServicesLocation = "21.0.1"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
- [https://nativephp.com/docs/mobile/3/the-basics/native-functions] Use these docs for the native bridge system: the `nativephp_call()` function, PHP facades (Biometrics, Browser, Camera from `Native\Mobile\Facades\`), the `#nativephp` JavaScript package alias with full TypeScript types, `On()`/`Off()` event methods in JS, and Vue.js microphone recording as an integration example
- [https://nativephp.com/docs/mobile/3/the-basics/native-components] Use these docs for the EDGE native UI system overview: Blade-style syntax rendered as truly native components (bottom-nav, top-bar, side-nav), hot reload without recompile, and platform-specific rendering per iOS/Android design guidelines
- [https://nativephp.com/docs/mobile/3/the-basics/web-view] Use these docs for web view configuration: `<meta name="viewport">` with edge-to-edge display and `viewport-fit`, CSS variables `--inset-top`/`--inset-bottom`/`--inset-left`/`--inset-right` for safe area insets, the `nativephp-safe-area` helper class, and `nativephp.status_bar_style` config (auto/light/dark for Android); supports Livewire, Vue, React, Svelte, HTMX
- [https://nativephp.com/docs/mobile/3/the-basics/splash-screens] Use these docs for splash screen setup: place `public/splash.png` (light) and `public/splash-dark.png` (dark), PNG format minimum 1080x1920px, GD PHP extension required with ~2GB memory
- [https://nativephp.com/docs/mobile/3/the-basics/splash-screens] Use these docs for splash screen setup: place `public/splash.png` (light) and `public/splash-dark.png` (dark), PNG format minimum 1080x1920px, GD PHP extension required with ~2GB memory. iOS also accepts `public/splash.svg` (+ `splash-dark.svg`) as a native vector LaunchImage (no rasterizer); Android splash input is PNG-only. Android system splash is held until the WebView is ready (no phase jump) via `androidx.core:core-splashscreen`; configure `android.splash.style` (`image` = full-bleed Compose overlay, default; `native` = icon-on-background system splash on API 31+) and `android.splash.background` / `background_night` (hex). In `image` style the system splash icon is transparent so only the background color shows before the overlay. API < 31 always falls back to the full-bleed overlay.
- [https://nativephp.com/docs/mobile/3/the-basics/app-icon] Use these docs for app icon setup: place `public/icon.png` at 1024x1024px PNG with no transparency, GD PHP extension required (~2GB memory), auto-resized for all Android density variants; default NativePHP icon used if none provided
- [https://nativephp.com/docs/mobile/3/the-basics/assets] Use these docs for asset management: must run Vite build (`npm run build`) before `native:run` for JS/CSS; all app root files are bundled (use relative paths, not storage_path()); user-generated files in `public` via `Storage::disk('mobile_public')`; set `FILESYSTEM_DISK=mobile_public` in .env

Expand Down
16 changes: 12 additions & 4 deletions src/Traits/InstallsAndroid.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,14 @@ private function writeAndroidTheme(): void
$primary = $this->normalizeThemeColor(config('nativephp.android.theme.color_primary') ?: '#000000');
$primaryNight = $this->normalizeThemeColor(config('nativephp.android.theme.color_primary_night') ?: '#FFFFFF');
$onPrimary = $this->normalizeThemeColor(config('nativephp.android.theme.color_on_primary') ?: '#FFFFFF');
$splashBg = $this->normalizeThemeColor(config('nativephp.android.splash.background') ?: '#FFFFFF');
$splashBgNight = $this->normalizeThemeColor(config('nativephp.android.splash.background_night') ?: '#000000');

File::ensureDirectoryExists(dirname($valuesNightPath));

$this->components->task('Applying Android theme', function () use ($valuesPath, $valuesNightPath, $primary, $primaryNight, $onPrimary) {
File::put($valuesPath, $this->renderThemeXml($primary, $onPrimary));
File::put($valuesNightPath, $this->renderThemeXml($primaryNight, $onPrimary));
$this->components->task('Applying Android theme', function () use ($valuesPath, $valuesNightPath, $primary, $primaryNight, $onPrimary, $splashBg, $splashBgNight) {
File::put($valuesPath, $this->renderThemeXml($primary, $onPrimary, $splashBg));
File::put($valuesNightPath, $this->renderThemeXml($primaryNight, $onPrimary, $splashBgNight));

return true;
});
Expand All @@ -89,7 +91,7 @@ private function normalizeThemeColor(string $value): string
return '#'.(strlen($hex) === 6 ? 'FF'.$hex : $hex);
}

private function renderThemeXml(string $primary, string $onPrimary): string
private function renderThemeXml(string $primary, string $onPrimary, string $splashBackground): string
{
return <<<XML
<?xml version="1.0" encoding="utf-8"?>
Expand All @@ -100,12 +102,18 @@ private function renderThemeXml(string $primary, string $onPrimary): string
<item name="colorOnPrimary">{$onPrimary}</item>
<item name="colorAccent">{$primary}</item>
<item name="android:colorAccent">{$primary}</item>
<item name="android:windowBackground">{$splashBackground}</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:enforceNavigationBarContrast">false</item>
</style>
<style name="Theme.AndroidPHP.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">{$splashBackground}</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="postSplashScreenTheme">@style/Theme.AndroidPHP</item>
</style>
</resources>

XML;
Expand Down
40 changes: 39 additions & 1 deletion src/Traits/InstallsAndroidSplashScreen.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public function installAndroidSplashScreen(): void
$this->logToFile(' Dark splash (splash-dark.png): '.($hasDarkSplash ? 'found' : 'not found'));

if (! $hasLightSplash && ! $hasDarkSplash) {
$this->logToFile(' No splash screens found, skipping');
$this->logToFile(' No splash images found, using app icon as splash drawable');
$this->writeAppIconSplashFallback();

return;
}
Expand All @@ -38,6 +39,12 @@ public function installAndroidSplashScreen(): void
'xxxhdpi' => [1280, 1920],
];

// Density PNGs replace the placeholder; same-folder splash.xml + splash.png is an AAPT2 error
$placeholderXml = base_path('nativephp/android/app/src/main/res/drawable/splash.xml');
if (File::exists($placeholderXml)) {
File::delete($placeholderXml);
}

if ($hasLightSplash && $this->validateSplashImage($lightSplashPath)) {
$this->logToFile(' Generating light splash variants...');
foreach ($sizes as $density => $dimensions) {
Expand Down Expand Up @@ -93,4 +100,35 @@ private function validateSplashImage(string $splashPath): bool

return true;
}

private function writeAppIconSplashFallback(): void
{
$resDir = base_path('nativephp/android/app/src/main/res/');
$drawableDir = $resDir.'drawable';
File::ensureDirectoryExists($drawableDir);

// Remove placeholder vector so splash.xml and splash.png don't coexist (AAPT2 error)
$placeholder = $drawableDir.DIRECTORY_SEPARATOR.'splash.xml';
if (File::exists($placeholder)) {
File::delete($placeholder);
}

$densities = ['mipmap-xxxhdpi', 'mipmap-xxhdpi', 'mipmap-xhdpi', 'mipmap-hdpi', 'mipmap-mdpi'];
foreach ($densities as $density) {
$iconPath = $resDir.$density.'/ic_launcher_foreground.png';
if (File::exists($iconPath)) {
@copy($iconPath, $drawableDir.'/splash.png');

return;
}
}

// No icon found — write a 1×1 transparent PNG so resource linking succeeds
$img = imagecreatetruecolor(1, 1);
imagesavealpha($img, true);
$transparent = imagecolorallocatealpha($img, 0, 0, 0, 127);
imagefill($img, 0, 0, $transparent);
imagepng($img, $drawableDir.'/splash.png');
imagedestroy($img);
}
}
58 changes: 58 additions & 0 deletions src/Traits/InstallsSplashScreen.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ trait InstallsSplashScreen
{
public function installIosSplashScreen()
{
if ($this->installIosSvgSplash()) {
return;
}

// Define supported splash screen variants
$splashVariants = [
'splash.png' => ['filename' => 'splash.png', 'idiom' => 'universal'],
Expand Down Expand Up @@ -146,4 +150,58 @@ private function validateIosSplashScreen(string $splashPath, string $filename =

return true;
}

private function installIosSvgSplash(): bool
{
$svgPath = public_path('splash.svg');
if (! File::exists($svgPath) || ! $this->isValidSvg($svgPath)) {
return false;
}

$launchImageDir = base_path('nativephp/ios/NativePHP/Assets.xcassets/LaunchImage.imageset');
File::ensureDirectoryExists($launchImageDir);

$images = [['idiom' => 'universal', 'filename' => 'splash.svg']];
@copy($svgPath, $launchImageDir.'/splash.svg');

$darkSvg = public_path('splash-dark.svg');
if (File::exists($darkSvg) && $this->isValidSvg($darkSvg)) {
@copy($darkSvg, $launchImageDir.'/splash-dark.svg');
$images[] = [
'idiom' => 'universal',
'filename' => 'splash-dark.svg',
'appearances' => [['appearance' => 'luminosity', 'value' => 'dark']],
];
}

$contentsJson = [
'images' => $images,
'info' => ['author' => 'xcode', 'version' => 1],
'properties' => [
'pre-rendered' => true,
'preserves-vector-representation' => true,
],
];

File::put(
$launchImageDir.'/Contents.json',
json_encode($contentsJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);

return true;
}

private function isValidSvg(string $path): bool
{
$contents = @file_get_contents($path);
if ($contents === false || trim($contents) === '') {
return false;
}

$previous = libxml_use_internal_errors(true);
$doc = simplexml_load_string($contents);
libxml_use_internal_errors($previous);

return $doc !== false && strtolower($doc->getName()) === 'svg';
}
}
7 changes: 6 additions & 1 deletion src/Traits/PreparesBuild.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

trait PreparesBuild
{
use CleansEnvFile, InstallsAndroidSplashScreen, InstallsAppIcon, PlatformFileOperations;
use CleansEnvFile, InstallsAndroidSplashScreen, InstallsAppIcon, PlatformFileOperations, UpdatesSplashConfiguration;

/**
* Validate required environment variables for building
Expand Down Expand Up @@ -155,6 +155,11 @@ protected function updateAndroidConfiguration(): void
$this->logToFile(' Updating status bar style: '.config('nativephp.android.status_bar_style', 'auto'));
$this->updateStatusBarStyleConfiguration();

$this->logToFile(' Updating splash style: '.config('nativephp.android.splash.style', 'image'));
$this->updateSplashStyleConfiguration();
$this->updateSplashThemeIcon();
$this->updateSplashThemeBackground();

$this->logToFile(' Updating local properties...');
$sdkPath = config('nativephp.android.android_sdk_path');
if ($sdkPath) {
Expand Down
Loading