From dea2db8cb872ca85210a2143cdf2d95adbf6d45b Mon Sep 17 00:00:00 2001 From: Phoen1x <2284257190@qq.com> Date: Sat, 8 Nov 2025 20:29:38 +0800 Subject: [PATCH] first --- .gitignore | 15 + app/.gitignore | 1 + app/build.gradle.kts | 57 ++ app/proguard-rules.pro | 21 + app/src/main/AndroidManifest.xml | 45 ++ .../java/com/phoenix/gui/MainActivity.java | 424 +++++++++++++ .../com/phoenix/gui/config/ConfigManager.java | 108 ++++ .../java/com/phoenix/gui/module/Module.java | 91 +++ .../phoenix/gui/module/ModuleCategory.java | 19 + .../com/phoenix/gui/module/ModuleManager.java | 124 ++++ .../gui/module/ModuleToggleListener.java | 5 + .../gui/module/ShortcutToggleListener.java | 5 + .../module/impl/combat/FightBotModule.java | 20 + .../gui/module/impl/combat/HitBoxModule.java | 20 + .../impl/combat/InfiniteAuraModule.java | 20 + .../module/impl/combat/KillAuraModule.java | 55 ++ .../module/impl/combat/LockBackModule.java | 20 + .../gui/module/impl/combat/RideModule.java | 20 + .../module/impl/movement/AirJumpModule.java | 20 + .../module/impl/movement/FastStopModule.java | 20 + .../gui/module/impl/movement/FlyModule.java | 20 + .../module/impl/movement/JekBackModule.java | 20 + .../module/impl/movement/ScaffoldModule.java | 20 + .../gui/module/impl/movement/SpeedModule.java | 20 + .../gui/module/impl/player/AntiBotModule.java | 20 + .../gui/module/impl/player/BlinkModule.java | 20 + .../gui/module/impl/player/NameTagModule.java | 20 + .../gui/module/impl/player/PhasesModule.java | 20 + .../module/impl/player/TeleportModule.java | 20 + .../gui/module/impl/visual/DisableModule.java | 20 + .../gui/module/impl/visual/FovModule.java | 20 + .../gui/module/impl/visual/GodModeModule.java | 20 + .../module/impl/visual/InfiniteYModule.java | 20 + .../gui/module/impl/visual/OnPosModule.java | 20 + .../gui/module/impl/visual/ThemeModule.java | 39 ++ .../module/impl/world/AimAssetsModule.java | 20 + .../module/impl/world/AutoClickModule.java | 20 + .../gui/module/impl/world/FuckerModule.java | 20 + .../gui/module/impl/world/GameModeModule.java | 20 + .../module/impl/world/RemoteStopModule.java | 20 + .../com/phoenix/gui/ui/AnimationUtils.java | 103 ++++ .../com/phoenix/gui/ui/ArraylistItemView.java | 130 ++++ .../com/phoenix/gui/ui/ArraylistModule.java | 47 ++ .../com/phoenix/gui/ui/ArraylistView.java | 170 ++++++ .../phoenix/gui/ui/DynamicColorExtractor.java | 159 +++++ .../com/phoenix/gui/ui/FloatBallView.java | 232 +++++++ .../java/com/phoenix/gui/ui/MenuView.java | 516 ++++++++++++++++ .../com/phoenix/gui/ui/ModuleItemView.java | 355 +++++++++++ .../com/phoenix/gui/ui/ShortcutButton.java | 299 +++++++++ .../java/com/phoenix/gui/ui/ThemeManager.java | 139 +++++ app/src/main/java/com/phoenix/gui/ui/UI.java | 256 ++++++++ .../gui/ui/dynamic/AnimatedTaskItemView.java | 60 ++ .../gui/ui/dynamic/CollapsedContentView.java | 301 +++++++++ .../gui/ui/dynamic/DynamicIslandManager.java | 357 +++++++++++ .../gui/ui/dynamic/DynamicIslandView.java | 569 ++++++++++++++++++ .../gui/ui/dynamic/DynamicIslandWindow.java | 107 ++++ .../phoenix/gui/ui/dynamic/GlowDrawable.java | 66 ++ .../gui/ui/dynamic/MaterialProgressBar.java | 161 +++++ .../gui/ui/dynamic/MaterialSwitchView.java | 206 +++++++ .../gui/ui/dynamic/ProgressItemView.java | 186 ++++++ .../phoenix/gui/ui/dynamic/SheenDrawable.java | 77 +++ .../gui/ui/dynamic/SwitchItemView.java | 187 ++++++ .../phoenix/gui/ui/dynamic/TaskItemView.java | 26 + .../gui/ui/widgets/ColorPickerView.java | 139 +++++ .../phoenix/gui/ui/widgets/CustomSlider.java | 130 ++++ .../phoenix/gui/ui/widgets/CustomSwitch.java | 123 ++++ .../com/phoenix/gui/ui/widgets/ModeChips.java | 226 +++++++ .../phoenix/gui/ui/widgets/SubMenuPanel.java | 498 +++++++++++++++ .../drawable-v24/ic_launcher_foreground.xml | 30 + .../res/drawable/ic_launcher_background.xml | 170 ++++++ .../res/drawable/ic_launcher_foreground.xml | 30 + app/src/main/res/drawable/ic_notification.xml | 10 + app/src/main/res/layout/activity_main.xml | 206 +++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes app/src/main/res/values-night/colors.xml | 2 + app/src/main/res/values-night/themes.xml | 9 + app/src/main/res/values/colors.xml | 10 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 18 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + build.gradle.kts | 5 + gradle.properties | 24 + gradle/libs.versions.toml | 26 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++++++ gradlew.bat | 89 +++ readme.md | 7 + settings.gradle.kts | 23 + 101 files changed, 8251 insertions(+) create mode 100644 .gitignore create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/phoenix/gui/MainActivity.java create mode 100644 app/src/main/java/com/phoenix/gui/config/ConfigManager.java create mode 100644 app/src/main/java/com/phoenix/gui/module/Module.java create mode 100644 app/src/main/java/com/phoenix/gui/module/ModuleCategory.java create mode 100644 app/src/main/java/com/phoenix/gui/module/ModuleManager.java create mode 100644 app/src/main/java/com/phoenix/gui/module/ModuleToggleListener.java create mode 100644 app/src/main/java/com/phoenix/gui/module/ShortcutToggleListener.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/combat/FightBotModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/combat/HitBoxModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/combat/InfiniteAuraModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/combat/KillAuraModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/combat/LockBackModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/combat/RideModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/movement/AirJumpModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/movement/FastStopModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/movement/FlyModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/movement/JekBackModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/movement/ScaffoldModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/movement/SpeedModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/player/AntiBotModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/player/BlinkModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/player/NameTagModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/player/PhasesModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/player/TeleportModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/visual/DisableModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/visual/FovModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/visual/GodModeModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/visual/InfiniteYModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/visual/OnPosModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/visual/ThemeModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/world/AimAssetsModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/world/AutoClickModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/world/FuckerModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/world/GameModeModule.java create mode 100644 app/src/main/java/com/phoenix/gui/module/impl/world/RemoteStopModule.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/AnimationUtils.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/ArraylistItemView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/ArraylistModule.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/ArraylistView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/DynamicColorExtractor.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/FloatBallView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/MenuView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/ModuleItemView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/ShortcutButton.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/ThemeManager.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/UI.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/AnimatedTaskItemView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/CollapsedContentView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandManager.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandWindow.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/GlowDrawable.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialProgressBar.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialSwitchView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/ProgressItemView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/SheenDrawable.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/SwitchItemView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/dynamic/TaskItemView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/widgets/ColorPickerView.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/widgets/CustomSlider.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/widgets/CustomSwitch.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/widgets/ModeChips.java create mode 100644 app/src/main/java/com/phoenix/gui/ui/widgets/SubMenuPanel.java create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_notification.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 readme.md create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..f8e7c7f --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.phoenix.gui" + compileSdk = 36 + + defaultConfig { + applicationId = "com.phoenix.gui" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + buildFeatures { + viewBinding = true + } + + // for debug + packaging { + jniLibs.keepDebugSymbols += "**/*.so" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.activity) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8d88d8b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/MainActivity.java b/app/src/main/java/com/phoenix/gui/MainActivity.java new file mode 100644 index 0000000..9f74b62 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/MainActivity.java @@ -0,0 +1,424 @@ +package com.phoenix.gui; + +import android.content.*; +import android.graphics.Color; +import android.os.*; +import android.view.View; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.*; +import androidx.activity.ComponentActivity; +import com.phoenix.gui.config.ConfigManager; +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleManager; +import com.phoenix.gui.module.ModuleToggleListener; +import com.phoenix.gui.module.ShortcutToggleListener; +import com.phoenix.gui.ui.ArraylistModule; +import com.phoenix.gui.ui.ArraylistView; +import com.phoenix.gui.ui.DynamicColorExtractor; +import com.phoenix.gui.ui.FloatBallView; +import com.phoenix.gui.ui.ShortcutButton; +import com.phoenix.gui.ui.dynamic.DynamicIslandWindow; +import com.phoenix.gui.ui.ThemeManager; +import com.phoenix.gui.ui.UI; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +public class MainActivity extends ComponentActivity implements ModuleToggleListener, ShortcutToggleListener { + + private boolean wifiState = true; + + private Button btnStart, btnStop, btnTestProgress, btnTestSwitch, btnHideAll, btnSave, btnToggleDebug, btnToggleArraylist; + private Button btnAddRandom, btnRemoveRandom, btnResetArraylist; + private EditText etUsername; + private SeekBar seekScale; + private ScrollView debugPanel; + private FrameLayout shortcutsContainer; + + private final Map shortcutButtons = new HashMap<>(); + private final List allTestModules = new ArrayList<>(); + private final Random random = new Random(); + + @Override + protected void onCreate(Bundle b) { + super.onCreate(b); + requestWindowFeature(Window.FEATURE_NO_TITLE); + + View decorView = getWindow().getDecorView(); + int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + decorView.setSystemUiVisibility(uiOptions); + + setContentView(R.layout.activity_main); + + UI.init(this); // 封装 + + initAllTestModules(); + bindViews(); + loadConfig(); + setListeners(); + updateBtnState(); + + toast("MainActivity ready"); + } + + private void initAllTestModules() { + // 下面那个随机测试的,可以随便填点啥 + } + + private void bindViews() { + btnStart = findViewById(R.id.btn_start); + btnStop = findViewById(R.id.btn_stop); + btnTestProgress = findViewById(R.id.btn_test_progress); + btnTestSwitch = findViewById(R.id.btn_test_switch); + btnHideAll = findViewById(R.id.btn_hide_all); + btnSave = findViewById(R.id.btn_save); + btnToggleDebug = findViewById(R.id.btn_toggle_debug); + btnToggleArraylist = findViewById(R.id.btn_toggle_arraylist); + btnAddRandom = findViewById(R.id.btn_add_random_module); + btnRemoveRandom = findViewById(R.id.btn_remove_random_module); + btnResetArraylist = findViewById(R.id.btn_reset_arraylist); + + etUsername = findViewById(R.id.et_username); + seekScale = findViewById(R.id.seek_scale); + debugPanel = findViewById(R.id.debug_panel); + shortcutsContainer = findViewById(R.id.shortcuts_container); + } + + private void loadConfig() { + etUsername.setText(ConfigManager.getDynamicIslandUsername()); + seekScale.setProgress((int) (ConfigManager.getDynamicIslandScale() * 100)); + } + + private void saveConfig() { + String name = etUsername.getText().toString().trim(); + float scale = seekScale.getProgress() / 100f; + + ConfigManager.setDynamicIslandUsername(name); + ConfigManager.setDynamicIslandScale(scale); + + DynamicIslandWindow island = UI.getDynamicIsland(); + if (island != null && island.isShowing()) { + island.updateConfig(scale, name); + } + + toast("saved"); + } + + private void setListeners() { + btnStart.setOnClickListener(v -> startGUI()); + btnStop.setOnClickListener(v -> stopGUI()); + btnSave.setOnClickListener(v -> saveConfig()); + btnToggleDebug.setOnClickListener(v -> toggleDebugPanel()); + + if (btnToggleArraylist != null) { + btnToggleArraylist.setOnClickListener(v -> toggleArraylist()); + } + + if (btnAddRandom != null) { + btnAddRandom.setOnClickListener(v -> addRandomModule()); + } + + if (btnRemoveRandom != null) { + btnRemoveRandom.setOnClickListener(v -> removeRandomModule()); + } + + if (btnResetArraylist != null) { + btnResetArraylist.setOnClickListener(v -> { + addDefaultArraylistModules(); + toast("Arraylist reset"); + }); + } + + btnTestProgress.setOnClickListener(v -> { + showProgress(); + toast("test progress"); + }); + + btnTestSwitch.setOnClickListener(v -> showSwitch()); + + btnHideAll.setOnClickListener(v -> { + hideAll(); + toast("hide all"); + }); + } + + private void updateBtnState() { + boolean isRunning = UI.isShowing(); + btnStart.setEnabled(!isRunning); + btnStop.setEnabled(isRunning); + btnTestProgress.setEnabled(isRunning); + btnTestSwitch.setEnabled(isRunning); + btnHideAll.setEnabled(isRunning); + + boolean arraylistControlsEnabled = isRunning && UI.getArraylistView() != null; + + if (btnToggleArraylist != null) { + btnToggleArraylist.setEnabled(isRunning); + } + + if (btnAddRandom != null) { + btnAddRandom.setEnabled(arraylistControlsEnabled); + } + + if (btnRemoveRandom != null) { + btnRemoveRandom.setEnabled(arraylistControlsEnabled); + } + + if (btnResetArraylist != null) { + btnResetArraylist.setEnabled(arraylistControlsEnabled); + } + } + + private void startGUI() { + if (UI.isShowing()) return; + + try { + UI.show(this); + updateBtnState(); + toast("GUI started"); + } catch (Exception e) { + e.printStackTrace(); + toast("Failed to start GUI: " + e.getMessage()); + } + } + + private void stopGUI() { + if (!UI.isShowing()) return; + + try { + if (!shortcutButtons.isEmpty()) { + for (ShortcutButton button : shortcutButtons.values()) { + try { + button.destroy(); + } catch (Exception e) { + e.printStackTrace(); + } + } + shortcutButtons.clear(); + shortcutsContainer.removeAllViews(); + } + + UI.hide(); + updateBtnState(); + toast("GUI stopped"); + } catch (Exception e) { + e.printStackTrace(); + toast("Failed to stop GUI: " + e.getMessage()); + } + } + + private void addDefaultArraylistModules() { + ArraylistView arraylistView = UI.getArraylistView(); + if (arraylistView == null) return; + + arraylistView.clearModules(); // 是的你没看错这个才是真的能用的 + // arraylistView.addModule(new ArraylistModule("like this")); + } + + private void addRandomModule() { + ArraylistView arraylistView = UI.getArraylistView(); + if (arraylistView == null || allTestModules.isEmpty()) return; + + String moduleName = allTestModules.get(random.nextInt(allTestModules.size())); + arraylistView.addModule(new ArraylistModule(moduleName)); + toast("Added: " + moduleName); + } + + private void removeRandomModule() { + ArraylistView arraylistView = UI.getArraylistView(); + if (arraylistView != null && !arraylistView.getModules().isEmpty()) { + int indexToRemove = random.nextInt(arraylistView.getModules().size()); + String moduleName = arraylistView.getModules().get(indexToRemove).getName(); + arraylistView.removeModule(moduleName); + toast("Removed: " + moduleName); + } else { + toast("Arraylist is empty"); + } + } + + private void toggleArraylist() { + ArraylistView arraylist = UI.getArraylistView(); + if (arraylist == null) { + toast("请先启动 GUI"); + } else { + if (arraylist.getVisibility() == View.VISIBLE) { + arraylist.hide(); + toast("Arraylist 已隐藏"); + } else { + arraylist.show(); + toast("Arraylist 已显示"); + } + } + updateBtnState(); + } + + private void toggleDynamicIsland() { + DynamicIslandWindow island = UI.getDynamicIsland(); + if (island == null) { + ConfigManager.setDynamicIslandEnabled(true); + toast("请重启 GUI 显示灵动岛"); + } else { + try { + island.hide(); + ConfigManager.setDynamicIslandEnabled(false); + toast("灵动岛已隐藏"); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void showProgress() { + DynamicIslandWindow island = UI.getDynamicIsland(); + if (island == null || !island.isShowing()) return; + + try { + island.addOrUpdateProgress("test_progress", "progress", "im subtitle", null, 0.65f, null); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void showSwitch() { + DynamicIslandWindow island = UI.getDynamicIsland(); + if (island == null || !island.isShowing()) return; + + try { + wifiState = !wifiState; + island.addSwitch("test_switch", "im a function", wifiState); + toast("test switch -> " + (wifiState ? "ON" : "OFF")); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void hideAll() { + DynamicIslandWindow island = UI.getDynamicIsland(); + if (island != null && island.isShowing()) { + try { + island.hideAllTasks(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void toggleDebugPanel() { + if (debugPanel.getVisibility() == View.VISIBLE) { + debugPanel.setVisibility(View.GONE); + } else { + debugPanel.setVisibility(View.VISIBLE); + } + } + + @Override + public void onModuleToggled(Module module) { + DynamicIslandWindow island = UI.getDynamicIsland(); + if (island != null && island.isShowing()) { + try { + island.addSwitch("module_" + module.getName(), module.getName(), module.isEnabled()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + ShortcutButton button = shortcutButtons.get(module); + if (button != null) { + try { + button.updateStyle(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + ArraylistView arraylistView = UI.getArraylistView(); + if (arraylistView != null) { + try { + if (module.isEnabled()) { + arraylistView.addModule(module.getName()); + } else { + arraylistView.removeModule(module.getName()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void onShortcutToggled(Module module, boolean enabled) { + if (enabled) { + createShortcut(module); + } else { + removeShortcut(module); + } + } + + private void createShortcut(Module module) { + if (!shortcutButtons.containsKey(module)) { + try { + ShortcutButton button = new ShortcutButton(this, module); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER + ); + button.setLayoutParams(params); + shortcutsContainer.addView(button); + button.show(); + shortcutButtons.put(module, button); + } catch (Exception e) { + e.printStackTrace(); + toast("创建快捷按钮失败: " + e.getMessage()); + } + } + } + + private void removeShortcut(Module module) { + ShortcutButton button = shortcutButtons.get(module); + if (button != null) { + try { + button.hide(); + button.postDelayed(() -> { + shortcutsContainer.removeView(button); + button.destroy(); + }, 250); + } catch (Exception e) { + e.printStackTrace(); + } + shortcutButtons.remove(module); + } + } + + private void toast(String s) { + Toast.makeText(this, s, Toast.LENGTH_SHORT).show(); + android.util.Log.d("PHOENIX", s); // me。 + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (UI.isShowing()) { + stopGUI(); + } + + UI.cleanup(); + } + + @Override + public void onBackPressed() { + if (UI.isShowing()) { + stopGUI(); // 回家吃饭 + } else { + super.onBackPressed(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/config/ConfigManager.java b/app/src/main/java/com/phoenix/gui/config/ConfigManager.java new file mode 100644 index 0000000..01ae585 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/config/ConfigManager.java @@ -0,0 +1,108 @@ +package com.phoenix.gui.config; + +import android.content.Context; +import android.content.SharedPreferences; + +public class ConfigManager { + + private static final String PREF_NAME = "phoenix_gui_config"; + private static SharedPreferences sharedPreferences; + + private static final String KEY_DYNAMIC_ISLAND_ENABLED = "dynamic_island_enabled"; + private static final String KEY_DYNAMIC_ISLAND_SCALE = "dynamic_island_scale"; + private static final String KEY_DYNAMIC_ISLAND_USERNAME = "dynamic_island_username"; + + // 初始化 + public static void init(Context context) { + if (sharedPreferences == null) { + sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } + } + + private static String getKey(String moduleName, String configKey) { + return moduleName + "_" + configKey; + } + private static SharedPreferences getPrefs() { + if (sharedPreferences == null) { + throw new IllegalStateException("ConfigManager not initialized. Call init() first."); + } + return sharedPreferences; + } + public static float getFloat(String moduleName, String key, float defaultValue) { + try { + return getPrefs().getFloat(moduleName + "_" + key, defaultValue); + } catch (ClassCastException e) { + + try { + return (float) getPrefs().getInt(moduleName + "_" + key, (int) defaultValue); + } catch (Exception e2) { + return defaultValue; + } + } + } + + public static void saveInt(String moduleName, String configKey, int value) { + if (sharedPreferences == null) return; + sharedPreferences.edit().putInt(getKey(moduleName, configKey), value).apply(); + } + + public static int getInt(String moduleName, String configKey, int defaultValue) { + if (sharedPreferences == null) return defaultValue; + return sharedPreferences.getInt(getKey(moduleName, configKey), defaultValue); + } + public static void saveBoolean(String moduleName, String configKey, boolean value) { + if (sharedPreferences == null) return; + sharedPreferences.edit().putBoolean(getKey(moduleName, configKey), value).apply(); + } + + public static boolean getBoolean(String moduleName, String configKey, boolean defaultValue) { + if (sharedPreferences == null) return defaultValue; + return sharedPreferences.getBoolean(getKey(moduleName, configKey), defaultValue); + } + + public static void saveModuleConfig(String moduleName, String configKey, int value) { + saveInt(moduleName, configKey, value); + } + + public static void saveModuleConfig(String moduleName, String configKey, boolean value) { + saveBoolean(moduleName, configKey, value); + } + + public static boolean getDynamicIslandEnabled() { + return getPrefs().getBoolean(KEY_DYNAMIC_ISLAND_ENABLED, true); + } + + public static void setDynamicIslandEnabled(boolean enabled) { + getPrefs().edit() + .putBoolean(KEY_DYNAMIC_ISLAND_ENABLED, enabled) + .apply(); + } + + public static float getDynamicIslandScale() { + return getPrefs().getFloat(KEY_DYNAMIC_ISLAND_SCALE, 0.7f); + } + + public static void setDynamicIslandScale(float scale) { + getPrefs().edit() + .putFloat(KEY_DYNAMIC_ISLAND_SCALE, scale) + .apply(); + } + + public static String getDynamicIslandUsername() { + return getPrefs().getString(KEY_DYNAMIC_ISLAND_USERNAME, "User"); + } + + public static void setDynamicIslandUsername(String username) { + getPrefs().edit() + .putString(KEY_DYNAMIC_ISLAND_USERNAME, username) + .apply(); + } + + public static void clearAll() { + getPrefs().edit().clear().apply(); + } + + private ConfigManager() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/Module.java b/app/src/main/java/com/phoenix/gui/module/Module.java new file mode 100644 index 0000000..03e50b0 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/Module.java @@ -0,0 +1,91 @@ +package com.phoenix.gui.module; + +import com.phoenix.gui.ui.widgets.SubMenuPanel; + +public abstract class Module { + private final String name; + private final ModuleCategory category; + private final String description; + private boolean isEnabled = false; + + private boolean shortcutEnabled = false; + + public Module(String name, ModuleCategory category, String description) { + this.name = name; + this.category = category; + this.description = description; + } + + public Module(String name, ModuleCategory category) { + this(name, category, ""); + } + + public String getName() { + return name; + } + + public ModuleCategory getCategory() { + return category; + } + + public String getDescription() { + return description; + } + + public boolean isEnabled() { + return isEnabled; + } + + public boolean isShortcutEnabled() { + return shortcutEnabled; + } + + public boolean supportsShortcut() { + return true; + } + + public void toggle() { + if (isEnabled) { + disable(); + } else { + enable(); + } + } + + public void enable() { + if (!isEnabled) { + isEnabled = true; + onEnable(); + ModuleManager.notifyModuleToggled(this); + } + } + + public void disable() { + if (isEnabled) { + isEnabled = false; + onDisable(); + ModuleManager.notifyModuleToggled(this); + } + } + + protected void onEnable() {} + + protected void onDisable() {} + + public void configureSubMenu(SubMenuPanel subMenu) { + + if (supportsShortcut()) { + subMenu.addShortcutSwitch(shortcutEnabled); + } + } + + protected void onShortcutToggled(boolean enabled) {} + + public void toggleShortcut(boolean enabled) { + if (shortcutEnabled != enabled) { + shortcutEnabled = enabled; + onShortcutToggled(enabled); + ModuleManager.notifyShortcutToggled(this, enabled); + } + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/ModuleCategory.java b/app/src/main/java/com/phoenix/gui/module/ModuleCategory.java new file mode 100644 index 0000000..11cd2a6 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/ModuleCategory.java @@ -0,0 +1,19 @@ +package com.phoenix.gui.module; + +public enum ModuleCategory { + COMBAT("Combat"), + MOVEMENT("Movement"), + PLAYER("Player"), + VISUAL("Visual"), + WORLD("World"); + + private final String displayName; + + ModuleCategory(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/ModuleManager.java b/app/src/main/java/com/phoenix/gui/module/ModuleManager.java new file mode 100644 index 0000000..3b38125 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/ModuleManager.java @@ -0,0 +1,124 @@ +package com.phoenix.gui.module; + +import com.phoenix.gui.module.impl.combat.*; +import com.phoenix.gui.module.impl.movement.*; +import com.phoenix.gui.module.impl.player.*; +import com.phoenix.gui.module.impl.visual.*; +import com.phoenix.gui.module.impl.world.*; + +import java.util.ArrayList; +import java.util.List; + +public class ModuleManager { + private static final List modules = new ArrayList<>(); + private static final List listeners = new ArrayList<>(); + private static final List shortcutListeners = new ArrayList<>(); + + static { + registerModules(); + } + + private static void registerModules() { //注册 + + register(new HitBoxModule()); + register(new InfiniteAuraModule()); + register(new KillAuraModule()); + register(new FightBotModule()); + register(new RideModule()); + register(new LockBackModule()); + + register(new AirJumpModule()); + register(new SpeedModule()); + register(new FastStopModule()); + register(new ScaffoldModule()); + register(new JekBackModule()); + register(new FlyModule()); + + register(new AntiBotModule()); + register(new BlinkModule()); + register(new NameTagModule()); + register(new PhasesModule()); + register(new TeleportModule()); + + register(new ThemeModule()); + register(new FovModule()); + register(new DisableModule()); + register(new GodModeModule()); + register(new OnPosModule()); + register(new InfiniteYModule()); + + + register(new FuckerModule()); + register(new RemoteStopModule()); + register(new GameModeModule()); + register(new AutoClickModule()); + register(new AimAssetsModule()); + } + + private static void register(Module module) { + modules.add(module); + } + + public static List getAllModules() { + return new ArrayList<>(modules); + } + + public static List getModulesByCategory(ModuleCategory category) { + List result = new ArrayList<>(); + for (Module module : modules) { + if (module.getCategory() == category) { + result.add(module); + } + } + return result; + } + + public static Module getModuleByName(String name) { + for (Module module : modules) { + if (module.getName().equalsIgnoreCase(name)) { + return module; + } + } + return null; + } + + public static void addToggleListener(ModuleToggleListener listener) { + listeners.add(listener); + } + + public static void removeToggleListener(ModuleToggleListener listener) { + listeners.remove(listener); + } + + public static void addShortcutListener(ShortcutToggleListener listener) { + shortcutListeners.add(listener); + } + + public static void removeShortcutListener(ShortcutToggleListener listener) { + shortcutListeners.remove(listener); + } + + public static void notifyModuleToggled(Module module) { + for (ModuleToggleListener listener : listeners) { + listener.onModuleToggled(module); + } + } + + public static void notifyShortcutToggled(Module module, boolean enabled) { + for (ShortcutToggleListener listener : shortcutListeners) { + listener.onShortcutToggled(module, enabled); + } + } + + public static void disableAll() { + for (Module module : modules) { + if (module.isEnabled()) { + module.disable(); + } + } + } + + private ModuleManager() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/ModuleToggleListener.java b/app/src/main/java/com/phoenix/gui/module/ModuleToggleListener.java new file mode 100644 index 0000000..9becf9e --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/ModuleToggleListener.java @@ -0,0 +1,5 @@ +package com.phoenix.gui.module; + +public interface ModuleToggleListener { + void onModuleToggled(Module module); +} diff --git a/app/src/main/java/com/phoenix/gui/module/ShortcutToggleListener.java b/app/src/main/java/com/phoenix/gui/module/ShortcutToggleListener.java new file mode 100644 index 0000000..8b31984 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/ShortcutToggleListener.java @@ -0,0 +1,5 @@ +package com.phoenix.gui.module; + +public interface ShortcutToggleListener { + void onShortcutToggled(Module module, boolean enabled); +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/FightBotModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/FightBotModule.java new file mode 100644 index 0000000..e62923f --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/FightBotModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.combat; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class FightBotModule extends Module { + public FightBotModule() { + super("FightBot", ModuleCategory.COMBAT); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/HitBoxModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/HitBoxModule.java new file mode 100644 index 0000000..912bc04 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/HitBoxModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.combat; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class HitBoxModule extends Module { + public HitBoxModule() { + super("HitBox", ModuleCategory.COMBAT); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/InfiniteAuraModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/InfiniteAuraModule.java new file mode 100644 index 0000000..e6551af --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/InfiniteAuraModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.combat; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class InfiniteAuraModule extends Module { + public InfiniteAuraModule() { + super("InfiniteAura", ModuleCategory.COMBAT); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/KillAuraModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/KillAuraModule.java new file mode 100644 index 0000000..7615cb9 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/KillAuraModule.java @@ -0,0 +1,55 @@ +package com.phoenix.gui.module.impl.combat; + +import com.phoenix.gui.config.ConfigManager; +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; +import com.phoenix.gui.ui.widgets.SubMenuPanel; + +import java.util.Arrays; + +public class KillAuraModule extends Module { + private float range = 4.0f; + private int cps = 10; + private int targetMode = 0; + private boolean autoBlock = false; + + public KillAuraModule() { + super("KillAura", ModuleCategory.COMBAT, "囊囊囊"); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } + + @Override + public void configureSubMenu(SubMenuPanel subMenu) { + + subMenu.addSlider("Range", 300, 800, (int)(range * 100), "%.2f", value -> { + range = value / 100.0f; + + }); + + subMenu.addSlider("CPS", 1, 20, cps, "%d", value -> { + cps = value; + + }); + + subMenu.addMode("Target", Arrays.asList("Single", "Multi"), targetMode, (index, mode) -> { + targetMode = index; + + }); + + subMenu.addSwitch("Auto Block", autoBlock, enabled -> { + autoBlock = enabled; + + }); + + super.configureSubMenu(subMenu); + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/LockBackModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/LockBackModule.java new file mode 100644 index 0000000..645a86c --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/LockBackModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.combat; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class LockBackModule extends Module { + public LockBackModule() { + super("LockBack", ModuleCategory.COMBAT); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/combat/RideModule.java b/app/src/main/java/com/phoenix/gui/module/impl/combat/RideModule.java new file mode 100644 index 0000000..861c7cc --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/combat/RideModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.combat; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class RideModule extends Module { + public RideModule() { + super("Ride", ModuleCategory.COMBAT); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/AirJumpModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/AirJumpModule.java new file mode 100644 index 0000000..d8e4d6a --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/AirJumpModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.movement; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class AirJumpModule extends Module { + public AirJumpModule() { + super("AirJump", ModuleCategory.MOVEMENT); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/FastStopModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/FastStopModule.java new file mode 100644 index 0000000..608f0d5 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/FastStopModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.movement; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class FastStopModule extends Module { + public FastStopModule() { + super("FastStop", ModuleCategory.MOVEMENT); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/FlyModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/FlyModule.java new file mode 100644 index 0000000..2a48c4e --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/FlyModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.movement; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class FlyModule extends Module { + public FlyModule() { + super("Fly", ModuleCategory.MOVEMENT); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/JekBackModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/JekBackModule.java new file mode 100644 index 0000000..22c1058 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/JekBackModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.movement; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class JekBackModule extends Module { + public JekBackModule() { + super("JekBack", ModuleCategory.MOVEMENT); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/ScaffoldModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/ScaffoldModule.java new file mode 100644 index 0000000..05dad5a --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/ScaffoldModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.movement; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class ScaffoldModule extends Module { + public ScaffoldModule() { + super("Scaffold", ModuleCategory.MOVEMENT); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/movement/SpeedModule.java b/app/src/main/java/com/phoenix/gui/module/impl/movement/SpeedModule.java new file mode 100644 index 0000000..36bca82 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/movement/SpeedModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.movement; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class SpeedModule extends Module { + public SpeedModule() { + super("Speed", ModuleCategory.MOVEMENT); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/player/AntiBotModule.java b/app/src/main/java/com/phoenix/gui/module/impl/player/AntiBotModule.java new file mode 100644 index 0000000..31f660f --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/player/AntiBotModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.player; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class AntiBotModule extends Module { + public AntiBotModule() { + super("AntiBot", ModuleCategory.PLAYER); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/player/BlinkModule.java b/app/src/main/java/com/phoenix/gui/module/impl/player/BlinkModule.java new file mode 100644 index 0000000..da10f2d --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/player/BlinkModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.player; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class BlinkModule extends Module { + public BlinkModule() { + super("Blink", ModuleCategory.PLAYER); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/player/NameTagModule.java b/app/src/main/java/com/phoenix/gui/module/impl/player/NameTagModule.java new file mode 100644 index 0000000..812f9fa --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/player/NameTagModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.player; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class NameTagModule extends Module { + public NameTagModule() { + super("NameTag", ModuleCategory.PLAYER); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/player/PhasesModule.java b/app/src/main/java/com/phoenix/gui/module/impl/player/PhasesModule.java new file mode 100644 index 0000000..878fa10 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/player/PhasesModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.player; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class PhasesModule extends Module { + public PhasesModule() { + super("Phases", ModuleCategory.PLAYER); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/player/TeleportModule.java b/app/src/main/java/com/phoenix/gui/module/impl/player/TeleportModule.java new file mode 100644 index 0000000..386943a --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/player/TeleportModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.player; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class TeleportModule extends Module { + public TeleportModule() { + super("Teleport", ModuleCategory.PLAYER); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/DisableModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/DisableModule.java new file mode 100644 index 0000000..9d3301f --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/DisableModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.visual; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class DisableModule extends Module { + public DisableModule() { + super("Disable", ModuleCategory.VISUAL); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/FovModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/FovModule.java new file mode 100644 index 0000000..329c180 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/FovModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.visual; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class FovModule extends Module { + public FovModule() { + super("Fov", ModuleCategory.VISUAL); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/GodModeModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/GodModeModule.java new file mode 100644 index 0000000..c416ad4 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/GodModeModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.visual; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class GodModeModule extends Module { + public GodModeModule() { + super("GodMode", ModuleCategory.VISUAL); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/InfiniteYModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/InfiniteYModule.java new file mode 100644 index 0000000..33b727b --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/InfiniteYModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.visual; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class InfiniteYModule extends Module { + public InfiniteYModule() { + super("InfiniteY", ModuleCategory.VISUAL); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/OnPosModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/OnPosModule.java new file mode 100644 index 0000000..13184e5 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/OnPosModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.visual; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class OnPosModule extends Module { + public OnPosModule() { + super("OnPos", ModuleCategory.VISUAL); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/visual/ThemeModule.java b/app/src/main/java/com/phoenix/gui/module/impl/visual/ThemeModule.java new file mode 100644 index 0000000..5892826 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/visual/ThemeModule.java @@ -0,0 +1,39 @@ +package com.phoenix.gui.module.impl.visual; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; +import com.phoenix.gui.ui.widgets.ColorPickerView; +import com.phoenix.gui.ui.widgets.SubMenuPanel; + +public class ThemeModule extends Module { + + public ThemeModule() { + super("Theme", ModuleCategory.VISUAL, "Customize the UI colors and themes."); + } + + // 这个模块没有简单的开关功能,所以 onEnable/onDisable 为空 + @Override + protected void onEnable() {} + + @Override + protected void onDisable() {} + + // 这个模块不支持快捷方式 + @Override + public boolean supportsShortcut() { + return false; + } + + @Override + public void configureSubMenu(SubMenuPanel subMenu) { + // 添加一行说明文字 + subMenu.addText("Select a preset theme or create your own color below."); + + // 添加预设主题按钮 + subMenu.addThemeButtons(); + + // 添加自定义颜色选择器 + ColorPickerView colorPicker = new ColorPickerView(subMenu.getContext()); + subMenu.addCustomView(colorPicker); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/module/impl/world/AimAssetsModule.java b/app/src/main/java/com/phoenix/gui/module/impl/world/AimAssetsModule.java new file mode 100644 index 0000000..2b6e5d4 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/world/AimAssetsModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.world; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class AimAssetsModule extends Module { + public AimAssetsModule() { + super("AimAssets", ModuleCategory.WORLD); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/world/AutoClickModule.java b/app/src/main/java/com/phoenix/gui/module/impl/world/AutoClickModule.java new file mode 100644 index 0000000..3e4e03c --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/world/AutoClickModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.world; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class AutoClickModule extends Module { + public AutoClickModule() { + super("AutoClick", ModuleCategory.WORLD); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/world/FuckerModule.java b/app/src/main/java/com/phoenix/gui/module/impl/world/FuckerModule.java new file mode 100644 index 0000000..b12b7f9 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/world/FuckerModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.world; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class FuckerModule extends Module { + public FuckerModule() { + super("Fucker", ModuleCategory.WORLD); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/world/GameModeModule.java b/app/src/main/java/com/phoenix/gui/module/impl/world/GameModeModule.java new file mode 100644 index 0000000..59468b1 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/world/GameModeModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.world; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class GameModeModule extends Module { + public GameModeModule() { + super("GameMode", ModuleCategory.WORLD); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/module/impl/world/RemoteStopModule.java b/app/src/main/java/com/phoenix/gui/module/impl/world/RemoteStopModule.java new file mode 100644 index 0000000..26759f8 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/module/impl/world/RemoteStopModule.java @@ -0,0 +1,20 @@ +package com.phoenix.gui.module.impl.world; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; + +public class RemoteStopModule extends Module { + public RemoteStopModule() { + super("RemoteStop", ModuleCategory.WORLD); + } + + @Override + protected void onEnable() { + + } + + @Override + protected void onDisable() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/AnimationUtils.java b/app/src/main/java/com/phoenix/gui/ui/AnimationUtils.java new file mode 100644 index 0000000..754f4c3 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/AnimationUtils.java @@ -0,0 +1,103 @@ +package com.phoenix.gui.ui; + +import android.animation.ValueAnimator; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; + +public class AnimationUtils { + + public static void expand(View view, long duration) { + + view.measure( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + int targetHeight = view.getMeasuredHeight(); + + view.getLayoutParams().height = 0; + view.setVisibility(View.VISIBLE); + view.setAlpha(0f); + + ValueAnimator animator = ValueAnimator.ofInt(0, targetHeight); + animator.setDuration(duration); + animator.setInterpolator(new DecelerateInterpolator()); + animator.addUpdateListener(animation -> { + int value = (int) animation.getAnimatedValue(); + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + layoutParams.height = value; + view.setLayoutParams(layoutParams); + }); + animator.start(); + + view.animate() + .alpha(1f) + .setDuration(duration) + .setInterpolator(new DecelerateInterpolator()) + .start(); + } + + public static void expand(View view) { + expand(view, 300); + } + + public static void collapse(View view, long duration) { + int initialHeight = view.getHeight(); + + ValueAnimator animator = ValueAnimator.ofInt(initialHeight, 0); + animator.setDuration(duration); + animator.setInterpolator(new DecelerateInterpolator()); + animator.addUpdateListener(animation -> { + int value = (int) animation.getAnimatedValue(); + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + layoutParams.height = value; + view.setLayoutParams(layoutParams); + }); + animator.start(); + + view.animate() + .alpha(0f) + .setDuration(duration) + .setInterpolator(new DecelerateInterpolator()) + .withEndAction(() -> { + view.setVisibility(View.GONE); + view.getLayoutParams().height = 0; + }) + .start(); + } + + public static void collapse(View view) { + collapse(view, 300); + } + + public static void fadeIn(View view, long duration) { + view.setAlpha(0f); + view.setVisibility(View.VISIBLE); + view.animate() + .alpha(1f) + .setDuration(duration) + .setInterpolator(new DecelerateInterpolator()) + .start(); + } + + public static void fadeIn(View view) { + fadeIn(view, 300); + } + + public static void fadeOut(View view, long duration) { + view.animate() + .alpha(0f) + .setDuration(duration) + .setInterpolator(new DecelerateInterpolator()) + .withEndAction(() -> view.setVisibility(View.GONE)) + .start(); + } + + public static void fadeOut(View view) { + fadeOut(view, 300); + } + + private AnimationUtils() { + + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/ArraylistItemView.java b/app/src/main/java/com/phoenix/gui/ui/ArraylistItemView.java new file mode 100644 index 0000000..015cd42 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/ArraylistItemView.java @@ -0,0 +1,130 @@ +package com.phoenix.gui.ui; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.*; +import android.graphics.drawable.Drawable; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; + +public class ArraylistItemView extends FrameLayout implements ThemeManager.OnThemeColorChangeListener { + + public enum PositionType { + SINGLE, FIRST, MIDDLE, LAST + } + + private TextView textView; + private Paint backgroundPaint; + private Path clipPath; + private float cornerRadius; + private int customTextColor = -1; + private float measuredTextWidth = 0; + private PositionType positionType = PositionType.MIDDLE; + + private Paint shadowPaint; + private Path shadowPath; + + public ArraylistItemView(Context context) { + super(context); + init(); + } + public ArraylistItemView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + public ArraylistItemView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + setWillNotDraw(false); + setLayerType(LAYER_TYPE_SOFTWARE, null); + + setClipChildren(false); + setClipToPadding(false); + + int shadowSpace = dpToPx(8); + setPadding(shadowSpace, shadowSpace, shadowSpace, shadowSpace); + + cornerRadius = dpToPx(3); + + textView = new TextView(getContext()); + textView.setTextColor(ThemeManager.getThemeColor()); + textView.setTextSize(10); + textView.setTypeface(textView.getTypeface(), Typeface.BOLD); + textView.setGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL); + textView.setPadding(dpToPx(6), dpToPx(2), dpToPx(6), dpToPx(2)); + + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + ); + params.gravity = Gravity.END; + addView(textView, params); + + backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + backgroundPaint.setColor(0xB2000000); + backgroundPaint.setStyle(Paint.Style.FILL); + + // REMOVED: sheenDrawable initialization + + clipPath = new Path(); + shadowPath = new Path(); + + shadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + shadowPaint.setColor(0xFF000000); + shadowPaint.setMaskFilter(new BlurMaskFilter(dpToPx(6), BlurMaskFilter.Blur.NORMAL)); + + ThemeManager.addListener(this); + } + + @Override + protected void onDraw(Canvas canvas) { + int width = getWidth(); + int height = getHeight(); + + if (width <= 0 || height <= 0) { + super.onDraw(canvas); + return; + } + + RectF backgroundBounds = new RectF( + getPaddingLeft(), getPaddingTop(), + width - getPaddingRight(), height - getPaddingBottom() + ); + + drawSelectiveShadow(canvas, backgroundBounds); + drawDynamicBackground(canvas, backgroundBounds); + + super.onDraw(canvas); + } + + @Override + public void onThemeColorChanged(int newColor) { + if (customTextColor == -1) { + textView.setTextColor(newColor); + } + invalidate(); + } + + public void cleanup() { + ThemeManager.removeListener(this); + } + + + // 下面由 gemini 2.5 pro 编写。够屎山。你要是想改你就去找 claude 吧,千万别用 gemini。 + public void setPositionType(PositionType type) { if (this.positionType != type) { this.positionType = type; invalidate(); } } + private void drawSelectiveShadow(Canvas canvas, RectF bounds) { shadowPath.reset(); float spread = dpToPx(0.5f); switch (positionType) { case SINGLE: shadowPath.addRoundRect(bounds, cornerRadius, cornerRadius, Path.Direction.CW); break; case FIRST: shadowPath.moveTo(bounds.left + cornerRadius, bounds.top); shadowPath.lineTo(bounds.right - cornerRadius, bounds.top); shadowPath.quadTo(bounds.right, bounds.top, bounds.right, bounds.top + cornerRadius); shadowPath.lineTo(bounds.right, bounds.bottom + spread); shadowPath.lineTo(bounds.left, bounds.bottom + spread); shadowPath.lineTo(bounds.left, bounds.top + cornerRadius); shadowPath.quadTo(bounds.left, bounds.top, bounds.left + cornerRadius, bounds.top); break; case MIDDLE: shadowPath.moveTo(bounds.right, bounds.top - spread); shadowPath.lineTo(bounds.right, bounds.bottom + spread); shadowPath.lineTo(bounds.left, bounds.bottom + spread); shadowPath.lineTo(bounds.left, bounds.top - spread); break; case LAST: shadowPath.moveTo(bounds.left, bounds.top - spread); shadowPath.lineTo(bounds.right, bounds.top - spread); shadowPath.lineTo(bounds.right, bounds.bottom - cornerRadius); shadowPath.quadTo(bounds.right, bounds.bottom, bounds.right - cornerRadius, bounds.bottom); shadowPath.lineTo(bounds.left + cornerRadius, bounds.bottom); shadowPath.quadTo(bounds.left, bounds.bottom, bounds.left, bounds.bottom - cornerRadius); shadowPath.lineTo(bounds.left, bounds.top - spread); break; } shadowPath.close(); canvas.drawPath(shadowPath, shadowPaint); } + private void drawDynamicBackground(Canvas canvas, RectF bounds) { clipPath.reset(); clipPath.moveTo(bounds.left + cornerRadius, bounds.top); if (positionType == PositionType.FIRST || positionType == PositionType.SINGLE) { clipPath.lineTo(bounds.right - cornerRadius, bounds.top); clipPath.quadTo(bounds.right, bounds.top, bounds.right, bounds.top + cornerRadius); } else { clipPath.lineTo(bounds.right, bounds.top); } if (positionType == PositionType.LAST || positionType == PositionType.SINGLE) { clipPath.lineTo(bounds.right, bounds.bottom - cornerRadius); clipPath.quadTo(bounds.right, bounds.bottom, bounds.right - cornerRadius, bounds.bottom); } else { clipPath.lineTo(bounds.right, bounds.bottom); } clipPath.lineTo(bounds.left + cornerRadius, bounds.bottom); clipPath.quadTo(bounds.left, bounds.bottom, bounds.left, bounds.bottom - cornerRadius); clipPath.lineTo(bounds.left, bounds.top + cornerRadius); clipPath.quadTo(bounds.left, bounds.top, bounds.left + cornerRadius, bounds.top); clipPath.close(); canvas.drawPath(clipPath, backgroundPaint); } + public void setText(String text) { textView.setText(text); measureTextWidth(); } + private void measureTextWidth() { TextPaint paint = textView.getPaint(); String text = textView.getText().toString(); measuredTextWidth = paint.measureText(text) + textView.getPaddingLeft() + textView.getPaddingRight(); } + public float getMeasuredTextWidth() { return measuredTextWidth; } + public void setTextColor(int color) { customTextColor = color; textView.setTextColor(color); } + private int dpToPx(float dp) { return (int) (dp * getContext().getResources().getDisplayMetrics().density); } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/ArraylistModule.java b/app/src/main/java/com/phoenix/gui/ui/ArraylistModule.java new file mode 100644 index 0000000..371eb20 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/ArraylistModule.java @@ -0,0 +1,47 @@ +package com.phoenix.gui.ui; + +public class ArraylistModule { + private String name; + private int color; + private boolean useThemeColor; + + public ArraylistModule(String name) { + this.name = name; + this.color = ThemeManager.getTextPrimary(); + this.useThemeColor = true; + } + + public ArraylistModule(String name, int color) { + this.name = name; + this.color = color; + this.useThemeColor = false; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getColor() { + if (useThemeColor) { + return ThemeManager.getTextPrimary(); + } + return color; + } + + public void setColor(int color) { + this.color = color; + this.useThemeColor = false; + } + + public void setUseThemeColor(boolean use) { + this.useThemeColor = use; + } + + public boolean isUseThemeColor() { + return useThemeColor; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/ArraylistView.java b/app/src/main/java/com/phoenix/gui/ui/ArraylistView.java new file mode 100644 index 0000000..5ac3d43 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/ArraylistView.java @@ -0,0 +1,170 @@ +package com.phoenix.gui.ui; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.LayoutTransition; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.widget.LinearLayout; +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ArraylistView extends LinearLayout { + private List modules; + private Map itemViews; + + public ArraylistView(Context context) { + super(context); + init(); + } + public ArraylistView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + public ArraylistView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + setOrientation(VERTICAL); + setGravity(Gravity.RIGHT | Gravity.TOP); + modules = new ArrayList<>(); + itemViews = new HashMap<>(); + + setClipChildren(false); + setClipToPadding(false); + + int paddingTop = dpToPx(16); + setPadding(0, paddingTop, 0, 0); + + setupLayoutAnimations(); + } + + private void setupLayoutAnimations() { + LayoutTransition transition = new LayoutTransition(); + + transition.setDuration(300); + + transition.setAnimator(LayoutTransition.APPEARING, null); + transition.setAnimator(LayoutTransition.DISAPPEARING, null); + + this.setLayoutTransition(transition); + } + + private void rebuildViews() { + List viewsToShow = new ArrayList<>(); + int moduleCount = modules.size(); + + for (int i = 0; i < moduleCount; i++) { + ArraylistModule module = modules.get(i); + ArraylistItemView itemView = itemViews.get(module.getName()); + + if (itemView != null) { + viewsToShow.add(itemView); + + if (moduleCount == 1) { + itemView.setPositionType(ArraylistItemView.PositionType.SINGLE); + } else if (i == 0) { + itemView.setPositionType(ArraylistItemView.PositionType.FIRST); + } else if (i == moduleCount - 1) { + itemView.setPositionType(ArraylistItemView.PositionType.LAST); + } else { + itemView.setPositionType(ArraylistItemView.PositionType.MIDDLE); + } + + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) itemView.getLayoutParams(); + if (params == null) { + params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + } + params.gravity = Gravity.RIGHT; + + int shadowSpaceInPx = dpToPx(8); + if (i > 0) { + params.topMargin = -(shadowSpaceInPx * 2); + } else { + params.topMargin = 0; + } + itemView.setLayoutParams(params); + } + } + + for (int i = getChildCount() - 1; i >= 0; i--) { + View child = getChildAt(i); + if (!viewsToShow.contains(child)) { + removeViewAt(i); + } + } + + for (int i = 0; i < viewsToShow.size(); i++) { + View view = viewsToShow.get(i); + if (indexOfChild(view) != i) { + if (view.getParent() != null) { + removeView(view); + } + addView(view, i); + } + } + } + + public void addModule(String moduleName) { addModule(new ArraylistModule(moduleName)); } + public void addModule(String moduleName, int color) { addModule(new ArraylistModule(moduleName, color)); } + public void addModule(ArraylistModule module) { + if (itemViews.containsKey(module.getName())) { return; } + modules.add(module); + ArraylistItemView itemView = new ArraylistItemView(getContext()); + itemView.setText(module.getName()); + if (!module.isUseThemeColor()) { itemView.setTextColor(module.getColor()); } + + itemViews.put(module.getName(), itemView); + + itemView.setAlpha(0f); + itemView.setTranslationX(dpToPx(20)); + + sortModules(); + rebuildViews(); + + animateItemIn(itemView); + } + + public void removeModule(String moduleName) { + ArraylistModule toRemove = null; + for (ArraylistModule module : modules) { if (module.getName().equals(moduleName)) { toRemove = module; break; } } + if (toRemove != null) { + modules.remove(toRemove); + ArraylistItemView itemView = itemViews.get(moduleName); + if (itemView != null) { + animateItemOut(itemView, () -> { + itemViews.remove(moduleName); + itemView.cleanup(); + sortModules(); + rebuildViews(); + }); + } + } // 下面也是 gemini 写的,由于我懒得动。所以建议不要动。 + } + private void sortModules() { Collections.sort(modules, (m1, m2) -> { ArraylistItemView v1 = itemViews.get(m1.getName()); ArraylistItemView v2 = itemViews.get(m2.getName()); if (v1 == null || v2 == null) { return Integer.compare(m2.getName().length(), m1.getName().length()); } float width1 = v1.getMeasuredTextWidth(); float width2 = v2.getMeasuredTextWidth(); int widthCompare = Float.compare(width2, width1); if (widthCompare != 0) { return widthCompare; } return m1.getName().compareTo(m2.getName()); }); } + private void animateItemIn(ArraylistItemView itemView) { ValueAnimator alphaAnimator = ValueAnimator.ofFloat(0f, 1f); alphaAnimator.setDuration(300); alphaAnimator.setInterpolator(new DecelerateInterpolator()); alphaAnimator.addUpdateListener(animation -> { float value = (float) animation.getAnimatedValue(); itemView.setAlpha(value); }); ValueAnimator translationAnimator = ValueAnimator.ofFloat(dpToPx(20), 0f); translationAnimator.setDuration(300); translationAnimator.setInterpolator(new DecelerateInterpolator()); translationAnimator.addUpdateListener(animation -> { float value = (float) animation.getAnimatedValue(); itemView.setTranslationX(value); }); alphaAnimator.start(); translationAnimator.start(); } + private void animateItemOut(ArraylistItemView itemView, Runnable onComplete) { ValueAnimator alphaAnimator = ValueAnimator.ofFloat(itemView.getAlpha(), 0f); alphaAnimator.setDuration(250); alphaAnimator.setInterpolator(new DecelerateInterpolator()); alphaAnimator.addUpdateListener(animation -> { float value = (float) animation.getAnimatedValue(); itemView.setAlpha(value); }); ValueAnimator translationAnimator = ValueAnimator.ofFloat(itemView.getTranslationX(), dpToPx(20)); translationAnimator.setDuration(250); translationAnimator.setInterpolator(new DecelerateInterpolator()); translationAnimator.addUpdateListener(animation -> { float value = (float) animation.getAnimatedValue(); itemView.setTranslationX(value); }); translationAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (onComplete != null) { onComplete.run(); } } }); alphaAnimator.start(); translationAnimator.start(); } + public void clearModules() { for (ArraylistItemView itemView : itemViews.values()) { itemView.cleanup(); } modules.clear(); itemViews.clear(); removeAllViews(); } + public void show() { setVisibility(VISIBLE); } + public void hide() { setVisibility(GONE); } + private int dpToPx(float dp) { return (int) (dp * getContext().getResources().getDisplayMetrics().density); } + public List getModules() { return this.modules; } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/DynamicColorExtractor.java b/app/src/main/java/com/phoenix/gui/ui/DynamicColorExtractor.java new file mode 100644 index 0000000..29838dd --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/DynamicColorExtractor.java @@ -0,0 +1,159 @@ +package com.phoenix.gui.ui; + +import android.app.WallpaperColors; +import android.app.WallpaperManager; +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +public class DynamicColorExtractor { + + private static final String TAG = "DynamicColorExtractor"; + private final Context context; + private WallpaperManager wallpaperManager; + private WallpaperManager.OnColorsChangedListener colorsChangedListener; + + public DynamicColorExtractor(Context context) { + this.context = context.getApplicationContext(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + wallpaperManager = WallpaperManager.getInstance(context); + } + } + + public void init() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + extractAndApplyColors(); + startListening(); + } else { + Log.w(TAG, "nah i cant use dc"); + ThemeManager.applyDefaultTheme(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O_MR1) + private void extractAndApplyColors() { + try { + WallpaperColors wallpaperColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM); + + if (wallpaperColors != null) { + + Color primaryColor = wallpaperColors.getPrimaryColor(); + + if (primaryColor != null) { + int color = primaryColor.toArgb(); + + int adjustedColor = adjustColorForUI(color); + + Log.d(TAG, String.format("color: #%08X -> #%08X", color, adjustedColor)); + + ThemeManager.setThemeColor(adjustedColor); + + Color secondaryColor = wallpaperColors.getSecondaryColor(); + if (secondaryColor != null) { + int secColor = secondaryColor.toArgb(); + + ThemeManager.setGradientStart(adjustedColor); + ThemeManager.setGradientEnd(adjustColorForUI(secColor)); + } + + Color tertiaryColor = wallpaperColors.getTertiaryColor(); + if (tertiaryColor != null) { + Log.d(TAG, "Tertiary color: #" + Integer.toHexString(tertiaryColor.toArgb())); + } + } else { + + ThemeManager.applyDefaultTheme(); + } + } else { + + ThemeManager.applyDefaultTheme(); + } + } catch (Exception e) { + + ThemeManager.applyDefaultTheme(); + } + } + + private int adjustColorForUI(int color) { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + + if (hsv[1] < 0.5f) { + hsv[1] = Math.min(hsv[1] * 1.5f, 0.7f); + } + + if (hsv[2] < 0.6f) { + hsv[2] = 0.7f; + } else if (hsv[2] > 0.9f) { + hsv[2] = 0.85f; + } + + return Color.HSVToColor(hsv); + } + + @RequiresApi(api = Build.VERSION_CODES.O_MR1) + private void startListening() { + if (colorsChangedListener == null) { + colorsChangedListener = new WallpaperManager.OnColorsChangedListener() { + @Override + public void onColorsChanged(WallpaperColors colors, int which) { + if ((which & WallpaperManager.FLAG_SYSTEM) != 0) { + + extractAndApplyColors(); + } + } + }; + + wallpaperManager.addOnColorsChangedListener(colorsChangedListener, null); + + } + } + + @RequiresApi(api = Build.VERSION_CODES.O_MR1) + public void stopListening() { + if (colorsChangedListener != null && wallpaperManager != null) { + wallpaperManager.removeOnColorsChangedListener(colorsChangedListener); + colorsChangedListener = null; + + } + } + + public void refresh() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + extractAndApplyColors(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.S) + public String getColorSchemeInfo() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + try { + WallpaperColors colors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM); + if (colors != null) { + StringBuilder info = new StringBuilder(); + info.append("主题色方案: "); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + int colorHints = colors.getColorHints(); + if ((colorHints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0) { + info.append("亮色背景 "); + } + if ((colorHints & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0) { + info.append("深色主题 "); + } + } + + info.append("\n当前主题色: #").append(Integer.toHexString(ThemeManager.getThemeColor())); + + return info.toString(); + } + } catch (Exception e) { + Log.e(TAG, "failed", e); + } + } + return "动态取色不可用"; + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/FloatBallView.java b/app/src/main/java/com/phoenix/gui/ui/FloatBallView.java new file mode 100644 index 0000000..58ad7a6 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/FloatBallView.java @@ -0,0 +1,232 @@ +package com.phoenix.gui.ui; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import com.phoenix.gui.config.ConfigManager; +import com.phoenix.gui.module.ModuleCategory; + +import java.util.ArrayList; +import java.util.List; + +public class FloatBallView extends LinearLayout { + + private final List menuViews = new ArrayList<>(); + private boolean menusVisible = false; + + private final LinearLayout outerBall; + private final LinearLayout innerBall; + + private final FrameLayout menusContainer; + + public FloatBallView(Context context, FrameLayout menusContainer) { + super(context); + this.menusContainer = menusContainer; + + ConfigManager.init(context); + + setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER + )); + + outerBall = new LinearLayout(context); + int size = (int) (getScreenHeight(context) * 0.05); + outerBall.setLayoutParams(new LayoutParams(size, size)); + + GradientDrawable gradient = new GradientDrawable(); + gradient.setColors(new int[]{ThemeManager.getGradientStart(), ThemeManager.getGradientEnd()}); + gradient.setGradientType(GradientDrawable.LINEAR_GRADIENT); + gradient.setShape(GradientDrawable.OVAL); + outerBall.setBackground(gradient); + outerBall.setGravity(Gravity.CENTER); + + innerBall = new LinearLayout(context); + int innerSize = (int) (getScreenHeight(context) * 0.03); + innerBall.setLayoutParams(new LayoutParams(innerSize, innerSize)); + + GradientDrawable innerDrawable = new GradientDrawable(); + innerDrawable.setColor(ThemeManager.getTextPrimary()); + innerDrawable.setShape(GradientDrawable.OVAL); + innerBall.setBackground(innerDrawable); + + outerBall.addView(innerBall); + + LinearLayout controlView = new LinearLayout(context); + controlView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + controlView.addView(outerBall); + addView(controlView); + + setupTouchListeners(); + + createMenus(); + } + + private void createMenus() { + menuViews.add(new MenuView(getContext(), ModuleCategory.PLAYER, MenuView.MenuPosition.POSITION_1)); + menuViews.add(new MenuView(getContext(), ModuleCategory.WORLD, MenuView.MenuPosition.POSITION_2)); + menuViews.add(new MenuView(getContext(), ModuleCategory.MOVEMENT, MenuView.MenuPosition.POSITION_3)); + menuViews.add(new MenuView(getContext(), ModuleCategory.COMBAT, MenuView.MenuPosition.POSITION_4)); + menuViews.add(new MenuView(getContext(), ModuleCategory.VISUAL, MenuView.MenuPosition.POSITION_5)); + } + + private void setupTouchListeners() { + final float[] downPos = new float[2]; + final boolean[] hasMoved = {false}; + final long[] downTime = {0}; + + setOnTouchListener((v, event) -> { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + downPos[0] = event.getX(); + downPos[1] = event.getY(); + hasMoved[0] = false; + downTime[0] = System.currentTimeMillis(); + + animate() + .scaleX(0.9f) + .scaleY(0.9f) + .setDuration(100) + .start(); + return true; + + case MotionEvent.ACTION_MOVE: + float moveX = event.getX(); + float moveY = event.getY(); + float deltaX = moveX - downPos[0]; + float deltaY = moveY - downPos[1]; + + if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) { + hasMoved[0] = true; + setTranslationX(getTranslationX() + deltaX); + setTranslationY(getTranslationY() + deltaY); + } + return true; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + animate() + .scaleX(1f) + .scaleY(1f) + .setDuration(100) + .start(); + + long clickDuration = System.currentTimeMillis() - downTime[0]; + if (!hasMoved[0] && clickDuration < 300) { + toggleMenus(); + } + return true; + } + return false; + }); + } + + private void toggleMenus() { + menusVisible = !menusVisible; + + if (menusVisible) { + showMenus(); + } else { + hideMenus(); + } + } + + private void showMenus() { + if (menusContainer == null) return; + + menusContainer.removeAllViews(); + menusContainer.setVisibility(View.VISIBLE); + + for (MenuView menu : menuViews) { + if (menu.getParent() != null) { + ((FrameLayout) menu.getParent()).removeView(menu); + } + + menusContainer.addView(menu); + menu.show(); + } + } + + private void hideMenus() { + if (menusContainer == null) return; + + for (MenuView menu : menuViews) { + menu.hide(); + } + + postDelayed(() -> { + try { + for (MenuView menu : menuViews) { + if (menu.getParent() != null) { + ((FrameLayout) menu.getParent()).removeView(menu); + } + } + menusContainer.setVisibility(View.GONE); + } catch (Exception e) { + e.printStackTrace(); + } + }, 300); + } + + public void show() { + setVisibility(View.VISIBLE); + setAlpha(0f); + setScaleX(0.8f); + setScaleY(0.8f); + animate() + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + .setDuration(250) + .start(); + } + + public void hide() { + if (menusVisible) { + hideMenus(); + menusVisible = false; + } + + animate() + .alpha(0f) + .scaleX(0.8f) + .scaleY(0.8f) + .setDuration(250) + .withEndAction(() -> setVisibility(View.GONE)) + .start(); + } + + public void destroy() { + try { + for (MenuView menu : menuViews) { + if (menu.getParent() != null) { + ((FrameLayout) menu.getParent()).removeView(menu); + } + menu.destroy(); + } + menuViews.clear(); + + if (menusContainer != null) { + menusContainer.removeAllViews(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private int getScreenHeight(Context context) { + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getRealMetrics(metrics); + return metrics.heightPixels; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/MenuView.java b/app/src/main/java/com/phoenix/gui/ui/MenuView.java new file mode 100644 index 0000000..7d35657 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/MenuView.java @@ -0,0 +1,516 @@ +package com.phoenix.gui.ui; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewOutlineProvider; +import android.view.WindowManager; +import android.view.animation.DecelerateInterpolator; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import android.view.ViewGroup; +import com.phoenix.gui.module.Module; +import com.phoenix.gui.module.ModuleCategory; +import com.phoenix.gui.module.ModuleManager; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MenuView extends LinearLayout implements ThemeManager.OnThemeColorChangeListener { + + private static final String PREF_NAME = "menu_view_settings"; + private static final String KEY_SCALE_FACTOR = "scale_factor"; + + private final Context context; + private final ModuleCategory category; + private final MenuPosition position; + private final SharedPreferences preferences; + + private boolean isExpanded = false; + + private final LinearLayout titleBar; + private final ScrollView scrollView; + private final LinearLayout contentLayout; + + private final Map moduleItems = new HashMap<>(); + private ValueAnimator expandAnimator; + private final Handler handler = new Handler(Looper.getMainLooper()); + + private float touchStartX = 0f; + private float touchStartY = 0f; + private boolean isDragging = false; + private final int touchSlop; + + private float scaleFactor = 1.0f; + + private final float baseScaleFactor; + + private static final long ANIMATION_DURATION = 250L; + private static final float CORNER_RADIUS_DP = 8f; + + private static int globalMaxWidth = 0; + + private final Runnable statusChecker = new Runnable() { + @Override + public void run() { + if (getVisibility() == View.VISIBLE && isExpanded) { + refreshAllModuleStates(); + handler.postDelayed(this, 500); + } + } + }; + + public enum MenuPosition { + POSITION_1, POSITION_2, POSITION_3, POSITION_4, POSITION_5 + } + + public MenuView(Context context, ModuleCategory category, MenuPosition position) { + super(context); + this.context = context; + this.category = category; + this.position = position; + this.preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + this.touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + this.baseScaleFactor = calculateBaseScaleFactor() * 0.75f; + + this.scaleFactor = 1.0f; + + setOrientation(VERTICAL); + + List modules = ModuleManager.getModulesByCategory(category); + + GradientDrawable bg = new GradientDrawable(); + bg.setColor(ThemeManager.getBgPrimary()); + bg.setCornerRadius(scaled(CORNER_RADIUS_DP)); + setBackground(bg); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setClipToOutline(true); + setOutlineProvider(ViewOutlineProvider.BACKGROUND); + setElevation(scaled(12)); + } else { + setClipChildren(true); + setClipToPadding(true); + } + + ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + setLayoutParams(layoutParams); + + int titleHeight = scaled(36); + int spacing = scaled(12); + setTranslationY(position.ordinal() * (titleHeight + spacing)); + + titleBar = createTitleBar(); + addView(titleBar); + + scrollView = new ScrollView(context); + scrollView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 0)); + scrollView.setVerticalScrollBarEnabled(false); + scrollView.setVisibility(View.GONE); + scrollView.setClipChildren(false); + scrollView.setClipToPadding(false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + scrollView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + contentLayout = new LinearLayout(context); + contentLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + contentLayout.setOrientation(VERTICAL); + contentLayout.setPadding(0, 0, 0, 0); + contentLayout.setClipChildren(false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + contentLayout.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + for (Module module : modules) { + ModuleItemView item = new ModuleItemView(context, module, this, baseScaleFactor * scaleFactor); + moduleItems.put(module, item); + contentLayout.addView(item); + } + + scrollView.addView(contentLayout); + addView(scrollView); + + setupTouchListeners(); + + ThemeManager.addListener(this); + } + + private void refreshAllModuleStates() { + for (Map.Entry entry : moduleItems.entrySet()) { + Module module = entry.getKey(); + ModuleItemView item = entry.getValue(); + item.syncWithModuleState(module.isEnabled()); + } + } + + private float calculateBaseScaleFactor() { + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getRealMetrics(metrics); + float dpi = metrics.densityDpi; + + if (dpi <= 240) { + return 0.7f; + } else if (dpi <= 320) { + return 0.85f; + } else if (dpi <= 480) { + return 1.0f; + } else if (dpi <= 640) { + return 1.15f; + } else { + return 1.3f; + } + } + + private int scaled(int baseDp) { + return (int) (dpToPx(baseDp) * baseScaleFactor * scaleFactor); + } + + private float scaled(float baseDp) { + return dpToPx((int) baseDp) * baseScaleFactor * scaleFactor; + } + + public float getScaleFactor() { + return scaleFactor; + } + + public void setScaleFactor(float factor) { + factor = Math.max(0.5f, Math.min(2.0f, factor)); + if (Math.abs(this.scaleFactor - factor) < 0.01f) { + return; + } + + this.scaleFactor = factor; + + preferences.edit() + .putFloat(KEY_SCALE_FACTOR, factor) + .apply(); + + rebuildUI(); + } + + public void increaseScale() { + setScaleFactor(scaleFactor * 1.1f); + } + + public void decreaseScale() { + setScaleFactor(scaleFactor * 0.9f); + } + + public void resetScale() { + setScaleFactor(1.0f); + } + + private void rebuildUI() { + boolean wasExpanded = isExpanded; + + if (isExpanded) { + collapseContent(); + } + + GradientDrawable bg = (GradientDrawable) getBackground(); + bg.setCornerRadius(scaled(CORNER_RADIUS_DP)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setElevation(scaled(12)); + } + + updateTitleBar(); + + for (ModuleItemView item : moduleItems.values()) { + item.updateScale(baseScaleFactor * scaleFactor); + } + + globalMaxWidth = 0; + measureInitialSize(); + + if (wasExpanded) { + handler.postDelayed(this::expandContent, 100); + } + } + + private void updateTitleBar() { + int height = scaled(36); + titleBar.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, height)); + titleBar.setPadding(scaled(14), 0, scaled(14), 0); + + GradientDrawable bg = new GradientDrawable(); + bg.setColor(ThemeManager.getBgSecondary()); + bg.setCornerRadius(scaled(CORNER_RADIUS_DP)); + titleBar.setBackground(bg); + + TextView textView = (TextView) titleBar.getChildAt(0); + textView.setTextSize(11.5f * baseScaleFactor * scaleFactor); + } + + private LinearLayout createTitleBar() { + LinearLayout bar = new LinearLayout(context); + int height = scaled(36); + bar.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, height)); + bar.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); + bar.setPadding(scaled(14), 0, scaled(14), 0); + + GradientDrawable bg = new GradientDrawable(); + bg.setColor(ThemeManager.getBgSecondary()); + bg.setCornerRadius(scaled(CORNER_RADIUS_DP)); + bar.setBackground(bg); + + TextView textView = new TextView(context); + textView.setText(category.getDisplayName()); + textView.setTextSize(11.5f * baseScaleFactor * scaleFactor); + textView.setTextColor(ThemeManager.getTextPrimary()); + textView.setTypeface(Typeface.DEFAULT_BOLD); + bar.addView(textView); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + bar.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + return bar; + } + + private void setupTouchListeners() { + final float[] lastRaw = new float[2]; + + titleBar.setOnTouchListener((v, event) -> { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + touchStartX = event.getX(); + touchStartY = event.getY(); + lastRaw[0] = event.getRawX(); + lastRaw[1] = event.getRawY(); + isDragging = false; + return true; + + case MotionEvent.ACTION_MOVE: + float deltaX = Math.abs(event.getX() - touchStartX); + float deltaY = Math.abs(event.getY() - touchStartY); + + if (!isDragging && (deltaX > touchSlop || deltaY > touchSlop)) { + isDragging = true; + } + + if (isDragging) { + float nowRawX = event.getRawX(); + float nowRawY = event.getRawY(); + float moveDeltaX = nowRawX - lastRaw[0]; + float moveDeltaY = nowRawY - lastRaw[1]; + MenuView.this.setTranslationX(MenuView.this.getTranslationX() + moveDeltaX); + MenuView.this.setTranslationY(MenuView.this.getTranslationY() + moveDeltaY); + lastRaw[0] = nowRawX; + lastRaw[1] = nowRawY; + } + return true; + + case MotionEvent.ACTION_UP: + if (!isDragging) { + toggleExpand(); + } + isDragging = false; + return true; + + case MotionEvent.ACTION_CANCEL: + isDragging = false; + return true; + } + return false; + }); + } + + private void toggleExpand() { + if (expandAnimator != null) expandAnimator.cancel(); + + if (isExpanded) { + collapseContent(); + } else { + expandContent(); + } + } + + private void expandContent() { + isExpanded = true; + scrollView.setVisibility(View.VISIBLE); + + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics screenSize = new DisplayMetrics(); + windowManager.getDefaultDisplay().getRealMetrics(screenSize); + + contentLayout.measure( + MeasureSpec.makeMeasureSpec(globalMaxWidth, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) + ); + int maxHeight = (int) (screenSize.heightPixels * 0.5); + int targetHeight = Math.min(contentLayout.getMeasuredHeight(), maxHeight); + + scrollView.setAlpha(0f); + LayoutParams lp = (LayoutParams) scrollView.getLayoutParams(); + lp.height = 0; + scrollView.setLayoutParams(lp); + + expandAnimator = ValueAnimator.ofInt(0, targetHeight); + expandAnimator.setDuration(ANIMATION_DURATION); + expandAnimator.setInterpolator(new DecelerateInterpolator()); + expandAnimator.addUpdateListener(animator -> { + int value = (int) animator.getAnimatedValue(); + LayoutParams params = (LayoutParams) scrollView.getLayoutParams(); + params.height = value; + scrollView.setLayoutParams(params); + scrollView.setAlpha((float) value / targetHeight); + }); + expandAnimator.start(); + + handler.post(statusChecker); + } + + private void collapseContent() { + isExpanded = false; + int startHeight = scrollView.getHeight(); + + closeAllSubMenus(); + + handler.removeCallbacks(statusChecker); + + expandAnimator = ValueAnimator.ofInt(startHeight, 0); + expandAnimator.setDuration(ANIMATION_DURATION); + expandAnimator.setInterpolator(new DecelerateInterpolator()); + expandAnimator.addUpdateListener(animator -> { + int value = (int) animator.getAnimatedValue(); + LayoutParams params = (LayoutParams) scrollView.getLayoutParams(); + params.height = value; + scrollView.setLayoutParams(params); + if (startHeight > 0) { + scrollView.setAlpha((float) value / startHeight); + } + }); + expandAnimator.addListener(new android.animation.AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(android.animation.Animator animation) { + scrollView.setVisibility(View.GONE); + } + }); + expandAnimator.start(); + } + + public void show() { + setVisibility(View.VISIBLE); + measureInitialSize(); + + setAlpha(0f); + setScaleX(0.8f); + setScaleY(0.8f); + animate() + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + .setDuration(ANIMATION_DURATION) + .setInterpolator(new DecelerateInterpolator()) + .start(); + + if (isExpanded) { + handler.post(statusChecker); + } + } + + private void measureInitialSize() { + int maxWidth = globalMaxWidth; + + titleBar.measure( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) + ); + maxWidth = Math.max(maxWidth, titleBar.getMeasuredWidth()); + + for (ModuleItemView item : moduleItems.values()) { + item.measure( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) + ); + maxWidth = Math.max(maxWidth, item.getMeasuredWidth()); + } + + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); +DisplayMetrics screenSize = new DisplayMetrics(); +windowManager.getDefaultDisplay().getRealMetrics(screenSize); +int smallerDimension = Math.min(screenSize.widthPixels, screenSize.heightPixels); +int minWidth = (int) (smallerDimension * 0.3); +int maxAllowedWidth = (int) (smallerDimension * 0.5); +maxWidth = Math.max(minWidth, Math.min(maxWidth, maxAllowedWidth)); // 你说屎吧是屎 但是好像还有可读性 + maxWidth = Math.max(minWidth, Math.min(maxWidth, maxAllowedWidth)); + + if (maxWidth > globalMaxWidth) { + globalMaxWidth = maxWidth; + } + + ViewGroup.LayoutParams params = getLayoutParams(); + if (params != null) { + params.width = globalMaxWidth; + setLayoutParams(params); + } + } + + public void hide() { + handler.removeCallbacks(statusChecker); + + if (expandAnimator != null) expandAnimator.cancel(); + animate().cancel(); + + for (ModuleItemView item : moduleItems.values()) { + item.cancelAllAnimations(); + } + + animate() + .alpha(0f) + .scaleX(0.8f) + .scaleY(0.8f) + .setDuration(ANIMATION_DURATION) + .setInterpolator(new DecelerateInterpolator()) + .withEndAction(() -> setVisibility(View.GONE)) + .start(); + } + + public void closeAllSubMenus() { + for (ModuleItemView item : moduleItems.values()) { + item.closeSubMenuIfOpen(); + } + } + + public String getScaleInfo() { + return String.format("基础缩放: %.2f, 用户缩放: %.2f, 总缩放: %.2f", //debug + baseScaleFactor, scaleFactor, baseScaleFactor * scaleFactor); + } + + @Override + public void onThemeColorChanged(int newColor) { + for (ModuleItemView item : moduleItems.values()) { + item.updateThemeColor(); + } + } + + public void destroy() { + ThemeManager.removeListener(this); + if (expandAnimator != null) expandAnimator.cancel(); + handler.removeCallbacksAndMessages(null); + } + + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/ModuleItemView.java b/app/src/main/java/com/phoenix/gui/ui/ModuleItemView.java new file mode 100644 index 0000000..c9ee428 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/ModuleItemView.java @@ -0,0 +1,355 @@ +package com.phoenix.gui.ui; + +import android.animation.ArgbEvaluator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.os.Handler; +import android.os.Looper; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.OvershootInterpolator; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.ui.widgets.SubMenuPanel; + +public class ModuleItemView extends LinearLayout { + + private final Module module; + private final MenuView parentMenu; + + private final FrameLayout mainContainer; + private final TextView normalText; + private final TextView boldText; + private final SubMenuPanel subMenuPanel; + + private ValueAnimator colorAnimator; + private ValueAnimator touchStateAnimator; + private final Handler longPressHandler = new Handler(Looper.getMainLooper()); + private Runnable longPressRunnable; + private boolean isLongPressing = false; + + private float touchStartX = 0f; + private float touchStartY = 0f; + private boolean hasMoved = false; + private final int touchSlop; + + private float scaleFactor = 1.0f; + + private boolean lastKnownState = false; + + private static final long LONG_PRESS_DELAY = 500L; + private static final float PRESS_SCALE = 0.95f; + private static final int PRESS_ANIMATION_DURATION = 150; + private static final int RELEASE_ANIMATION_DURATION = 250; + private static final int CORNER_RADIUS_DP = 8; + private final float cornerRadiusPx; + private final TimeInterpolator pressInterpolator = new DecelerateInterpolator(); + private final TimeInterpolator releaseInterpolator = new OvershootInterpolator(1.5f); + + public ModuleItemView(Context context, Module module, MenuView parentMenu, float scaleFactor) { + super(context); + this.module = module; + this.parentMenu = parentMenu; + this.touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + this.lastKnownState = module.isEnabled(); + this.scaleFactor = scaleFactor; + this.cornerRadiusPx = dpToPx(CORNER_RADIUS_DP); + + setOrientation(VERTICAL); + setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + + mainContainer = createMainContainer(); + addView(mainContainer); + + normalText = createNormalText(); + boldText = createBoldText(); + mainContainer.addView(normalText); + mainContainer.addView(boldText); + + subMenuPanel = new SubMenuPanel(context, module, scaleFactor); + addView(subMenuPanel); + + module.configureSubMenu(subMenuPanel); + + subMenuPanel.setOnStateChangedListener(() -> { + + }); + + updateVisualState(module.isEnabled(), false); + + setupTouchListeners(); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { + setLayerType(LAYER_TYPE_HARDWARE, null); + } + } + + public void syncWithModuleState(boolean currentState) { + if (lastKnownState != currentState) { + lastKnownState = currentState; + updateVisualState(currentState, true); + } + } + + public void updateScale(float scaleFactor) { + this.scaleFactor = scaleFactor; + + LayoutParams params = (LayoutParams) mainContainer.getLayoutParams(); + params.setMargins(0, 0, 0, 0); + mainContainer.setLayoutParams(params); + + float textSize = 11f * scaleFactor; + int paddingV = scaled(7); + int paddingH = scaled(12); + + normalText.setTextSize(textSize); + normalText.setPadding(paddingH, paddingV, paddingH, paddingV); + + boldText.setTextSize(textSize); + boldText.setPadding(paddingH, paddingV, paddingH, paddingV); + + subMenuPanel.updateScale(scaleFactor); + + requestLayout(); + } + + private int scaled(int baseDp) { + return (int) (dpToPx(baseDp) * scaleFactor); + } + + private FrameLayout createMainContainer() { + FrameLayout container = new FrameLayout(getContext()); + + LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + params.setMargins(0, 0, 0, 0); + container.setLayoutParams(params); + + GradientDrawable initialBackground = new GradientDrawable(); + initialBackground.setColor(Color.TRANSPARENT); + initialBackground.setCornerRadius(0f); + container.setBackground(initialBackground); + + return container; + } + + + + private TextView createNormalText() { + TextView textView = new TextView(getContext()); + textView.setText(module.getName()); + textView.setTextSize(11f * scaleFactor); + textView.setTextColor(ThemeManager.getTextPrimary()); + textView.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); + int paddingV = scaled(7); + int paddingH = scaled(12); + textView.setPadding(paddingH, paddingV, paddingH, paddingV); + textView.setTypeface(Typeface.DEFAULT); + textView.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT + )); + return textView; + } + + private TextView createBoldText() { + TextView textView = new TextView(getContext()); + textView.setText(module.getName()); + textView.setTextSize(11f * scaleFactor); + textView.setTextColor(ThemeManager.getTextOnTheme()); + textView.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); + int paddingV = scaled(7); + int paddingH = scaled(12); + textView.setPadding(paddingH, paddingV, paddingH, paddingV); + textView.setTypeface(Typeface.DEFAULT_BOLD); + textView.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT + )); + textView.setAlpha(0f); + return textView; + } + + + + private void setupTouchListeners() { + mainContainer.setOnTouchListener((v, event) -> { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + touchStartX = event.getX(); + touchStartY = event.getY(); + hasMoved = false; + isLongPressing = false; + + longPressRunnable = () -> { + if (!hasMoved) { + isLongPressing = true; + onLongPress(); + } + }; + longPressHandler.postDelayed(longPressRunnable, LONG_PRESS_DELAY); + + + animateTouchState(true); + return true; + + case MotionEvent.ACTION_MOVE: + float deltaX = Math.abs(event.getX() - touchStartX); + float deltaY = Math.abs(event.getY() - touchStartY); + + boolean isOutOfBounds = event.getX() < 0 || event.getX() > getWidth() || event.getY() < 0 || event.getY() > getHeight(); + + if (!hasMoved && (deltaX > touchSlop || deltaY > touchSlop || isOutOfBounds)) { + hasMoved = true; + cancelLongPressDetection(); + + animateTouchState(false); + } + return true; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + cancelLongPressDetection(); + + animateTouchState(false); + + if (!hasMoved && !isLongPressing) { + module.toggle(); + lastKnownState = module.isEnabled(); + updateVisualState(lastKnownState, true); + } + + isLongPressing = false; + hasMoved = false; + return true; + } + return false; + }); + } + + private void animateTouchState(boolean pressed) { + if (touchStateAnimator != null) { + touchStateAnimator.cancel(); + } + + + final float startScale = mainContainer.getScaleX(); + final float endScale = pressed ? PRESS_SCALE : 1.0f; + + final GradientDrawable background = (GradientDrawable) mainContainer.getBackground(); + final float startRadius = background.getCornerRadii() != null ? background.getCornerRadii()[0] : 0f; + final float endRadius = pressed ? cornerRadiusPx : 0f; + + + touchStateAnimator = ValueAnimator.ofFloat(0f, 1f); + touchStateAnimator.setDuration(pressed ? PRESS_ANIMATION_DURATION : RELEASE_ANIMATION_DURATION); + touchStateAnimator.setInterpolator(pressed ? pressInterpolator : releaseInterpolator); + + touchStateAnimator.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + float currentScale = startScale + (endScale - startScale) * fraction; + float currentRadius = startRadius + (endRadius - startRadius) * fraction; + + mainContainer.setScaleX(currentScale); + mainContainer.setScaleY(currentScale); + background.setCornerRadius(currentRadius); + }); + + touchStateAnimator.start(); + } + + + private void onLongPress() { + performHapticFeedback(android.view.HapticFeedbackConstants.LONG_PRESS); + subMenuPanel.toggle(); + } + + private void cancelLongPressDetection() { + if (longPressRunnable != null) { + longPressHandler.removeCallbacks(longPressRunnable); + } + longPressRunnable = null; + } + + + public void updateVisualState(boolean isEnabled, boolean animated) { + if (animated) { + animateToState(isEnabled); + } else { + setStateImmediately(isEnabled); + } + } + + private void setStateImmediately(boolean isEnabled) { + GradientDrawable bg = (GradientDrawable) mainContainer.getBackground(); + bg.setColor(isEnabled ? ThemeManager.getThemeColor() : Color.TRANSPARENT); + bg.setCornerRadius(0f); + + normalText.setAlpha(isEnabled ? 0f : 1f); + boldText.setAlpha(isEnabled ? 1f : 0f); + } + + private void animateToState(boolean isEnabled) { + if (colorAnimator != null) { + colorAnimator.cancel(); + } + + GradientDrawable bg = (GradientDrawable) mainContainer.getBackground(); + + int fromColor = (bg.getColor() != null) ? bg.getColor().getDefaultColor() : Color.TRANSPARENT; + int toColor = isEnabled ? ThemeManager.getThemeColor() : Color.TRANSPARENT; + + colorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), fromColor, toColor); + colorAnimator.setDuration(150); + colorAnimator.addUpdateListener(animator -> { + bg.setColor((int) animator.getAnimatedValue()); + }); + colorAnimator.start(); + + normalText.animate().alpha(isEnabled ? 0f : 1f).setDuration(150).start(); + boldText.animate().alpha(isEnabled ? 1f : 0f).setDuration(150).start(); + } + + + public void updateThemeColor() { + if (module.isEnabled()) { + GradientDrawable bg = (GradientDrawable) mainContainer.getBackground(); + bg.setColor(ThemeManager.getThemeColor()); + } + } + + public void closeSubMenuIfOpen() { + if (subMenuPanel.isOpen()) { + subMenuPanel.forceCollapse(); + } + } + + public void cancelAllAnimations() { + if (colorAnimator != null) colorAnimator.cancel(); + + if (touchStateAnimator != null) touchStateAnimator.cancel(); + mainContainer.animate().cancel(); //保留 + normalText.animate().cancel(); + boldText.animate().cancel(); + cancelLongPressDetection(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + cancelAllAnimations(); + longPressHandler.removeCallbacksAndMessages(null); + } + + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/ShortcutButton.java b/app/src/main/java/com/phoenix/gui/ui/ShortcutButton.java new file mode 100644 index 0000000..890702a --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/ShortcutButton.java @@ -0,0 +1,299 @@ +package com.phoenix.gui.ui; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.phoenix.gui.module.Module; + +public class ShortcutButton extends LinearLayout implements ThemeManager.OnThemeColorChangeListener { + + private static final String PREF_NAME = "shortcut_button_positions"; + private static final String KEY_PREFIX_X = "button_x_"; + private static final String KEY_PREFIX_Y = "button_y_"; + + private final Module module; + private final SharedPreferences preferences; + + private final TextView textView; + private final GradientDrawable background; + + private int currentBackgroundColor; + private int fixedTextWidth = -1; + + public ShortcutButton(Context context, Module module) { + super(context); + this.module = module; + this.preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + + setOrientation(HORIZONTAL); + setGravity(Gravity.CENTER); + + background = new GradientDrawable(); + currentBackgroundColor = module.isEnabled() + ? ThemeManager.getThemeColor() + : ThemeManager.getBgDisabled(); + background.setColor(currentBackgroundColor); + background.setCornerRadius(dpToPx(20)); + setBackground(background); + + int paddingH = dpToPx(12); + int paddingV = dpToPx(8); + setPadding(paddingH, paddingV, paddingH, paddingV); + + textView = new TextView(context); + textView.setText(module.getName()); + textView.setTextSize(11f); + int textColor = module.isEnabled() + ? ThemeManager.getTextOnTheme() + : ThemeManager.getTextPrimary(); + textView.setTextColor(textColor); + textView.setTypeface(module.isEnabled() + ? Typeface.DEFAULT_BOLD + : Typeface.DEFAULT); + textView.setGravity(Gravity.CENTER); + + fixedTextWidth = calculateMaxTextWidth(); + LayoutParams textParams = new LayoutParams(fixedTextWidth, LayoutParams.WRAP_CONTENT); + textView.setLayoutParams(textParams); + + addView(textView); + + + loadPosition(); + + setupTouchListeners(); + + ThemeManager.addListener(this); + + setLayerType(LAYER_TYPE_HARDWARE, null); + } + + private int calculateMaxTextWidth() { + Paint paint = new Paint(); + paint.setTextSize(textView.getTextSize()); + + paint.setTypeface(Typeface.DEFAULT); + float normalWidth = paint.measureText(module.getName()); + + paint.setTypeface(Typeface.DEFAULT_BOLD); + float boldWidth = paint.measureText(module.getName()); + + return (int) Math.ceil(Math.max(normalWidth, boldWidth)); + } + + private void loadPosition() { + String key = getPositionKey(); + + boolean hasSavedPosition = preferences.contains(KEY_PREFIX_X + key) + && preferences.contains(KEY_PREFIX_Y + key); + + if (hasSavedPosition) { + float x = preferences.getInt(KEY_PREFIX_X + key, 0); + float y = preferences.getInt(KEY_PREFIX_Y + key, 0); + setTranslationX(x); + setTranslationY(y); + } else { + generateRandomPosition(); + } + } + + private void generateRandomPosition() { + WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getRealMetrics(metrics); + + int estimatedWidth = dpToPx(150); + int estimatedHeight = dpToPx(50); + + int margin = dpToPx(30); + + int minX = margin - metrics.widthPixels / 2 + estimatedWidth / 2; + int maxX = metrics.widthPixels / 2 - estimatedWidth / 2 - margin; + int minY = margin - metrics.heightPixels / 2 + estimatedHeight / 2; + int maxY = metrics.heightPixels / 2 - estimatedHeight / 2 - margin; + + if (maxX > minX) { + setTranslationX(minX + (int) (Math.random() * (maxX - minX))); + } + + if (maxY > minY) { + setTranslationY(minY + (int) (Math.random() * (maxY - minY))); + } + } + + private void savePosition() { + String key = getPositionKey(); + preferences.edit() + .putInt(KEY_PREFIX_X + key, (int) getTranslationX()) + .putInt(KEY_PREFIX_Y + key, (int) getTranslationY()) + .apply(); + } + + private String getPositionKey() { + return module.getName().replaceAll("[^a-zA-Z0-9]", "_"); + } + + private void setupTouchListeners() { + final float[] downPos = new float[2]; + final boolean[] hasMoved = {false}; + final long[] downTime = {0}; + + setOnTouchListener((v, event) -> { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + downPos[0] = event.getX(); + downPos[1] = event.getY(); + hasMoved[0] = false; + downTime[0] = System.currentTimeMillis(); + + animate() + .scaleX(0.9f) + .scaleY(0.9f) + .setDuration(100) + .start(); + return true; + + case MotionEvent.ACTION_MOVE: + float moveX = event.getX(); + float moveY = event.getY(); + float deltaX = moveX - downPos[0]; + float deltaY = moveY - downPos[1]; + + if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) { + hasMoved[0] = true; + setTranslationX(getTranslationX() + deltaX); + setTranslationY(getTranslationY() + deltaY); + } + return true; + + case MotionEvent.ACTION_UP: + + animate() + .scaleX(1f) + .scaleY(1f) + .setDuration(100) + .start(); + + long clickDuration = System.currentTimeMillis() - downTime[0]; + + if (!hasMoved[0] && clickDuration < 300) { + module.toggle(); + updateStyle(); + } else if (hasMoved[0]) { + savePosition(); + } + + hasMoved[0] = false; + return true; + + case MotionEvent.ACTION_CANCEL: + + animate() + .scaleX(1f) + .scaleY(1f) + .setDuration(100) + .start(); + hasMoved[0] = false; + return true; + } + return false; + }); + } + + public void updateStyle() { + boolean isEnabled = module.isEnabled(); + int targetColor = isEnabled ? ThemeManager.getThemeColor() : ThemeManager.getBgDisabled(); + int textColor = isEnabled ? ThemeManager.getTextOnTheme() : ThemeManager.getTextPrimary(); + Typeface targetTypeface = isEnabled ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT; + + ValueAnimator colorAnimator = ValueAnimator.ofArgb(currentBackgroundColor, targetColor); + colorAnimator.setDuration(150); + colorAnimator.addUpdateListener(animator -> { + int color = (int) animator.getAnimatedValue(); + background.setColor(color); + currentBackgroundColor = color; + }); + colorAnimator.start(); + + textView.animate() + .alpha(0.5f) + .setDuration(75) + .withEndAction(() -> { + textView.setTextColor(textColor); + textView.setTypeface(targetTypeface); + textView.animate() + .alpha(1f) + .setDuration(75) + .start(); + }) + .start(); + } + + public void show() { + setVisibility(VISIBLE); + setAlpha(0f); + setScaleX(0.5f); + setScaleY(0.5f); + + animate() + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + .setDuration(200) + .start(); + } + + public void hide() { + animate() + .alpha(0f) + .scaleX(0.5f) + .scaleY(0.5f) + .setDuration(200) + .withEndAction(() -> setVisibility(GONE)) + .start(); + } + + @Override + public void onThemeColorChanged(int newColor) { + if (module.isEnabled()) { + ValueAnimator colorAnimator = ValueAnimator.ofArgb(currentBackgroundColor, newColor); + colorAnimator.setDuration(150); + colorAnimator.addUpdateListener(animator -> { + int color = (int) animator.getAnimatedValue(); + background.setColor(color); + currentBackgroundColor = color; + }); + colorAnimator.start(); + } + } + + public void destroy() { + clearAnimation(); + if (animate() != null) { + animate().cancel(); + } + if (textView != null && textView.animate() != null) { + textView.animate().cancel(); + } + + ThemeManager.removeListener(this); + + savePosition(); + } + + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/ThemeManager.java b/app/src/main/java/com/phoenix/gui/ui/ThemeManager.java new file mode 100644 index 0000000..5d4dff4 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/ThemeManager.java @@ -0,0 +1,139 @@ +package com.phoenix.gui.ui; + +import android.graphics.Color; +import com.phoenix.gui.config.ConfigManager; // <-- 使用您的 ConfigManager + +import java.util.ArrayList; +import java.util.List; + +public class ThemeManager { + + private static final String CONFIG_MODULE_NAME = "GlobalTheme"; + private static final String CONFIG_KEY_THEME_COLOR = "theme_color"; + private static final int DEFAULT_THEME_COLOR = 0xFF96CCFF; + + private static int themeColor = DEFAULT_THEME_COLOR; + + private static int bgPrimary = 0xFF292D2C; + private static int bgSecondary = 0xFF1F2221; + private static int bgDisabled = 0xFF333333; + + private static int textPrimary = 0xFFFFFFFF; + private static int textSecondary = 0xFFE0E0E0; + private static int textTertiary = 0xFFAAAAAA; + private static int textOnTheme = 0xFF1A1A1A; + + private static int stateDisabled = 0xFF555555; + + private static int glowColor = 0x66000000; + private static int glassBackground = 0xE6000000; + + private static int gradientStart = 0xFFa7ff8a; + private static int gradientEnd = 0xFF82c1fb; + + private static final List listeners = new ArrayList<>(); + + public interface OnThemeColorChangeListener { + void onThemeColorChanged(int newColor); + } + + private static void saveThemeColor() { + + ConfigManager.saveModuleConfig(CONFIG_MODULE_NAME, CONFIG_KEY_THEME_COLOR, themeColor); + } + + public static int getThemeColor() { + return themeColor; + } + + public static void setThemeColor(int color) { + if (themeColor != color) { + themeColor = color; + saveThemeColor(); + notifyListeners(); + } + } + + public static float[] getThemeColorHSV() { + float[] hsv = new float[3]; + Color.colorToHSV(themeColor, hsv); + return hsv; + } + + public static int getBgPrimary() { return bgPrimary; } + public static void setBgPrimary(int color) { bgPrimary = color; notifyListeners(); } + public static int getBgSecondary() { return bgSecondary; } + public static void setBgSecondary(int color) { bgSecondary = color; notifyListeners(); } + public static int getBgDisabled() { return bgDisabled; } + public static void setBgDisabled(int color) { bgDisabled = color; notifyListeners(); } + public static int getTextPrimary() { return textPrimary; } + public static void setTextPrimary(int color) { textPrimary = color; notifyListeners(); } + public static int getTextSecondary() { return textSecondary; } + public static void setTextSecondary(int color) { textSecondary = color; notifyListeners(); } + public static int getTextTertiary() { return textTertiary; } + public static void setTextTertiary(int color) { textTertiary = color; notifyListeners(); } + public static int getTextOnTheme() { return textOnTheme; } + public static void setTextOnTheme(int color) { textOnTheme = color; notifyListeners(); } + public static int getStateDisabled() { return stateDisabled; } + public static void setStateDisabled(int color) { stateDisabled = color; notifyListeners(); } + public static int getGlowColor() { return glowColor; } + public static void setGlowColor(int color) { glowColor = color; notifyListeners(); } + public static int getGlassBackground() { return glassBackground; } + public static void setGlassBackground(int color) { glassBackground = color; notifyListeners(); } + public static int getGradientStart() { return gradientStart; } + public static void setGradientStart(int color) { gradientStart = color; notifyListeners(); } + public static int getGradientEnd() { return gradientEnd; } + public static void setGradientEnd(int color) { gradientEnd = color; notifyListeners(); } + + public static void applyDefaultTheme() { + setThemeColor(DEFAULT_THEME_COLOR); + } + + public static void applyGreenTheme() { + setThemeColor(0xFF4CAF50); + } + + public static void applyPurpleTheme() { + setThemeColor(0xFFBB86FC); + } + + public static void applyOrangeTheme() { + setThemeColor(0xFFFF9800); + } + + public static void applyPinkTheme() { + setThemeColor(0xFFE91E63); + } + + public static void setThemeColorRGB(int r, int g, int b) { + setThemeColor(Color.rgb(r, g, b)); + } + + public static void setThemeColorHSV(float hue, float saturation, float value) { + setThemeColor(Color.HSVToColor(new float[]{hue, saturation, value})); + } + + public static void addListener(OnThemeColorChangeListener listener) { + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + + public static void removeListener(OnThemeColorChangeListener listener) { + listeners.remove(listener); + } + + private static void notifyListeners() { + for (OnThemeColorChangeListener listener : new ArrayList<>(listeners)) { + try { + listener.onThemeColorChanged(themeColor); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private ThemeManager() { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/UI.java b/app/src/main/java/com/phoenix/gui/ui/UI.java new file mode 100644 index 0000000..d359e51 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/UI.java @@ -0,0 +1,256 @@ +package com.phoenix.gui.ui; + +import android.app.Activity; +import android.content.Context; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import com.phoenix.gui.R; +import com.phoenix.gui.config.ConfigManager; +import com.phoenix.gui.module.ModuleManager; +import com.phoenix.gui.module.ModuleToggleListener; +import com.phoenix.gui.module.ShortcutToggleListener; +import com.phoenix.gui.ui.dynamic.DynamicIslandWindow; + +// 封装 + +public class UI { + private static DynamicColorExtractor dynamicColorExtractor; + private static boolean initialized = false; + private static boolean isShowing = false; + + // UI + private static FloatBallView floatBall; + private static DynamicIslandWindow dynamicIsland; + private static ArraylistView arraylistView; + + // 容器引用 + private static FrameLayout guiContainer; + private static FrameLayout floatBallContainer; + private static FrameLayout menusContainer; + private static FrameLayout dynamicIslandContainer; + private static FrameLayout arraylistContainer; + + // activity 引用 + private static Activity currentActivity; + + + public static void init(Context ctx) { + if (initialized) { + return; + } + + + ConfigManager.init(ctx); + + + dynamicColorExtractor = new DynamicColorExtractor(ctx); + dynamicColorExtractor.init(); + + initialized = true; + } + + public static void show(Activity activity) { + if (isShowing) { + return; + } + + try { + currentActivity = activity; + + // 绑定容器 + bindContainers(activity); + + // 显示主容器 + if (guiContainer != null) { + guiContainer.setVisibility(View.VISIBLE); + } + + // 初始化ppp + initFloatBall(); + initDynamicIsland(); + initArraylist(); + + // 注册 + if (activity instanceof ModuleToggleListener) { + ModuleManager.addToggleListener((ModuleToggleListener) activity); + } + if (activity instanceof ShortcutToggleListener) { + ModuleManager.addShortcutListener((ShortcutToggleListener) activity); + } + + isShowing = true; + } catch (Exception e) { + e.printStackTrace(); + } + } + + + public static void hide() { + if (!isShowing) { + return; // 回家吃饭 + } + + try { + // 移除 + if (currentActivity instanceof ModuleToggleListener) { + ModuleManager.removeToggleListener((ModuleToggleListener) currentActivity); + } + if (currentActivity instanceof ShortcutToggleListener) { + ModuleManager.removeShortcutListener((ShortcutToggleListener) currentActivity); + } + + // 清理 + if (arraylistView != null) { + arraylistView.clearModules(); + arraylistView.hide(); + arraylistView = null; + } + + // 清理 + if (dynamicIsland != null) { + dynamicIsland.hide(); + dynamicIsland = null; + } + + // 还是清理 + if (floatBall != null) { + floatBall.hide(); + floatBall.destroy(); + floatBall = null; + } + + // 清空垃圾桶 + if (floatBallContainer != null) floatBallContainer.removeAllViews(); + if (menusContainer != null) menusContainer.removeAllViews(); + if (arraylistContainer != null) arraylistContainer.removeAllViews(); + + // 隐藏 + if (guiContainer != null) { + guiContainer.setVisibility(View.GONE); + } + + isShowing = false; + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void bindContainers(Activity activity) { + guiContainer = activity.findViewById(R.id.gui_container); + floatBallContainer = activity.findViewById(R.id.float_ball_container); + menusContainer = activity.findViewById(R.id.menus_container); + dynamicIslandContainer = activity.findViewById(R.id.dynamic_island_container); + arraylistContainer = activity.findViewById(R.id.arraylist_container); + } + + + private static void initFloatBall() { + try { + if (floatBall != null) { + floatBall.destroy(); + floatBall = null; + } + + if (floatBallContainer != null) { + floatBallContainer.removeAllViews(); + floatBall = new FloatBallView(currentActivity, menusContainer); + floatBallContainer.addView(floatBall); + floatBall.show(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + + private static void initDynamicIsland() { + boolean enabled = ConfigManager.getDynamicIslandEnabled(); + if (!enabled) return; + + try { + if (dynamicIsland != null) { + dynamicIsland.hide(); + dynamicIsland = null; + } + + if (dynamicIslandContainer != null) { + dynamicIslandContainer.removeAllViews(); + dynamicIsland = new DynamicIslandWindow(currentActivity, dynamicIslandContainer); + dynamicIsland.show(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + + private static void initArraylist() { + try { + if (arraylistView != null) { + arraylistView.clearModules(); + arraylistView = null; + } + + if (arraylistContainer != null) { + arraylistContainer.removeAllViews(); + arraylistView = new ArraylistView(currentActivity); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ); + arraylistContainer.addView(arraylistView, params); + addDefaultArraylistModules(); + arraylistView.show(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + + private static void addDefaultArraylistModules() { + if (arraylistView == null) return; + + arraylistView.clearModules(); + // arraylistView.addModule(new ArraylistModule("like this")); + + } + + + public static void cleanup() { + hide(); + + if (dynamicColorExtractor != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + dynamicColorExtractor.stopListening(); + dynamicColorExtractor = null; + } + + currentActivity = null; + initialized = false; + } + + public static DynamicColorExtractor getDynamicColorExtractor() { + return dynamicColorExtractor; + } + + + public static DynamicIslandWindow getDynamicIsland() { + return dynamicIsland; + } + + + public static ArraylistView getArraylistView() { + return arraylistView; + } + + + public static boolean isShowing() { + return isShowing; + } + + private UI() { + // 工具类 + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/AnimatedTaskItemView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/AnimatedTaskItemView.java new file mode 100644 index 0000000..078902c --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/AnimatedTaskItemView.java @@ -0,0 +1,60 @@ +package com.phoenix.gui.ui.dynamic; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; + +public abstract class AnimatedTaskItemView extends TaskItemView { + + private final Handler updateHandler = new Handler(Looper.getMainLooper()); + private final Runnable updateRunnable = new Runnable() { + @Override + public void run() { + updateContent(); + updateHandler.postDelayed(this, 16); + } + }; + + private boolean isUpdating = false; + + public AnimatedTaskItemView(@NonNull Context context, DynamicIslandManager.TaskItem task, float scale) { + super(context, task, scale); + } + + protected void startUpdating() { + if (!isUpdating) { + isUpdating = true; + updateHandler.post(updateRunnable); + } + } + + protected void stopUpdating() { + if (isUpdating) { + isUpdating = false; + updateHandler.removeCallbacks(updateRunnable); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + startUpdating(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + stopUpdating(); + } + + @Override + protected void onVisibilityChanged(android.view.View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + if (visibility == VISIBLE) { + startUpdating(); + } else { + stopUpdating(); + } + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/CollapsedContentView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/CollapsedContentView.java new file mode 100644 index 0000000..1362ecb --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/CollapsedContentView.java @@ -0,0 +1,301 @@ +package com.phoenix.gui.ui.dynamic; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.animation.OvershootInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import com.phoenix.gui.ui.ThemeManager; + +public class CollapsedContentView extends LinearLayout implements ThemeManager.OnThemeColorChangeListener { + + private ImageView brandIcon; + private TextView brandText; + + private ImageView userIcon; + private TextView userText; + + private ImageView fpsIcon; + private TextView fpsNumberText; + private TextView fpsLabelText; + private FrameLayout fpsNumberContainer; + + private DynamicIslandManager manager; + private float scale = 0.7f; + private int currentFps = 0; + + public CollapsedContentView(@NonNull Context context) { + super(context); + init(context); + } + + private void init(Context context) { + setOrientation(HORIZONTAL); + setGravity(Gravity.CENTER); + setPadding( + (int) dpToPx(12 * scale), 0, + (int) dpToPx(12 * scale), 0 + ); + + createBrandModule(context); + addSeparator(context); + + createUserModule(context); + addSeparator(context); + + createFpsModule(context); + } + + private void createBrandModule(Context context) { + LinearLayout module = new LinearLayout(context); + module.setOrientation(HORIZONTAL); + module.setGravity(Gravity.CENTER_VERTICAL); + + brandIcon = new ImageView(context); + try { + int iconId = context.getResources().getIdentifier("icon", "drawable", context.getPackageName()); + if (iconId != 0) { + brandIcon.setImageResource(iconId); + } else { + brandIcon.setImageResource(android.R.drawable.ic_menu_info_details); + } + } catch (Exception e) { + brandIcon.setImageResource(android.R.drawable.ic_menu_info_details); + } + // 获取颜色。 + brandIcon.setColorFilter(ThemeManager.getThemeColor()); + int iconSize = (int) dpToPx(16 * scale); + module.addView(brandIcon, new LayoutParams(iconSize, iconSize)); + + module.addView(createSpacer(context, 4 * scale), new LayoutParams( + (int) dpToPx(4 * scale), 0 + )); + + brandText = createStyledText(context, "Phoen1xGUI", ThemeManager.getThemeColor(), true); // 神必小小小opai + module.addView(brandText); + + addView(module); + } + + private void createUserModule(Context context) { + LinearLayout module = new LinearLayout(context); + module.setOrientation(HORIZONTAL); + module.setGravity(Gravity.CENTER_VERTICAL); + + userIcon = new ImageView(context); + userIcon.setImageResource(android.R.drawable.ic_menu_myplaces); + userIcon.setColorFilter(com.phoenix.gui.ui.ThemeManager.getTextSecondary()); + int iconSize = (int) dpToPx(16 * scale); + module.addView(userIcon, new LayoutParams(iconSize, iconSize)); + + module.addView(createSpacer(context, 4 * scale), new LayoutParams( + (int) dpToPx(4 * scale), 0 + )); + + userText = createStyledText(context, "Test", com.phoenix.gui.ui.ThemeManager.getTextSecondary(), false); // 用户。 + module.addView(userText); + + addView(module); + } + + private void createFpsModule(Context context) { + LinearLayout module = new LinearLayout(context); + module.setOrientation(HORIZONTAL); + module.setGravity(Gravity.CENTER_VERTICAL); + + fpsIcon = new ImageView(context); + fpsIcon.setImageResource(android.R.drawable.ic_menu_manage); + fpsIcon.setColorFilter(com.phoenix.gui.ui.ThemeManager.getTextSecondary()); + int iconSize = (int) dpToPx(16 * scale); + module.addView(fpsIcon, new LayoutParams(iconSize, iconSize)); + + module.addView(createSpacer(context, 4 * scale), new LayoutParams( + (int) dpToPx(4 * scale), 0 + )); + + fpsNumberContainer = new FrameLayout(context); + fpsNumberContainer.setClipChildren(false); + fpsNumberContainer.setClipToPadding(false); + module.addView(fpsNumberContainer, new LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + )); + + fpsNumberText = createStyledText(context, "0", com.phoenix.gui.ui.ThemeManager.getTextSecondary(), false); + fpsNumberContainer.addView(fpsNumberText, new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT + )); + + fpsLabelText = createStyledText(context, " FPS", com.phoenix.gui.ui.ThemeManager.getTextSecondary(), false); + module.addView(fpsLabelText); + + addView(module); + } + + private TextView createStyledText(Context context, String text, int color, boolean bold) { + TextView textView = new TextView(context); + textView.setText(text); + textView.setTextColor(color); + textView.setTextSize(13 * scale); + textView.setTypeface(null, bold ? Typeface.BOLD : Typeface.NORMAL); + textView.setSingleLine(true); + textView.setIncludeFontPadding(false); + return textView; + } + + private TextView createSeparator(Context context) { + TextView separator = new TextView(context); + separator.setText(" • "); + + int separatorColor = (com.phoenix.gui.ui.ThemeManager.getTextSecondary() & 0x00FFFFFF) | 0x66000000; + separator.setTextColor(separatorColor); + separator.setTextSize(13 * scale); + separator.setIncludeFontPadding(false); + LayoutParams params = new LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + ); + params.setMargins( + (int) dpToPx(4 * scale), 0, + (int) dpToPx(4 * scale), 0 + ); + return separator; + } + + private void addSeparator(Context context) { + addView(createSeparator(context)); + } + + private android.view.View createSpacer(Context context, float widthDp) { + return new android.view.View(context); + } + + public void setManager(DynamicIslandManager manager) { + this.manager = manager; + updateContent(); + } + + public void updateContent() { + if (manager == null) return; + + userText.setText(manager.getPersistentText()); + } + + public void updateFps(int fps) { + if (currentFps == fps) return; + + final String newFpsText = String.valueOf(fps); + currentFps = fps; + + fpsNumberText.animate() + .translationY(-dpToPx(20 * scale)) + .alpha(0f) + .scaleX(0.8f) + .scaleY(0.8f) + .setDuration(150) + .setInterpolator(new androidx.interpolator.view.animation.FastOutSlowInInterpolator()) + .withEndAction(() -> { + final int startWidth = fpsNumberContainer.getWidth(); + + Paint textPaint = fpsNumberText.getPaint(); + final int endWidth = (int) Math.ceil(textPaint.measureText(newFpsText)); + + fpsNumberText.setText(newFpsText); + + fpsNumberText.setTranslationY(dpToPx(20 * scale)); + fpsNumberText.setAlpha(0f); + fpsNumberText.setScaleX(1.2f); + fpsNumberText.setScaleY(1.2f); + + fpsNumberText.animate() + .translationY(0f) + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + .setDuration(400) + .setInterpolator(new OvershootInterpolator(2.5f)) + .start(); + + if (startWidth != endWidth) { + ValueAnimator widthAnimator = ValueAnimator.ofInt(startWidth, endWidth); + widthAnimator.setDuration(400); + widthAnimator.setInterpolator(new OvershootInterpolator(2.5f)); + widthAnimator.addUpdateListener(animation -> { + int animatedWidth = (Integer) animation.getAnimatedValue(); + ViewGroup.LayoutParams params = fpsNumberContainer.getLayoutParams(); + params.width = animatedWidth; + fpsNumberContainer.setLayoutParams(params); + }); + widthAnimator.start(); + } + }) + .start(); + } + + public void updateConfig(float scale, String text) { + this.scale = scale; + userText.setText(text); + + float textSize = 13 * scale; + brandText.setTextSize(textSize); + userText.setTextSize(textSize); + fpsNumberText.setTextSize(textSize); + fpsLabelText.setTextSize(textSize); + + int iconSize = (int) dpToPx(16 * scale); + updateIconSize(brandIcon, iconSize); + updateIconSize(userIcon, iconSize); + updateIconSize(fpsIcon, iconSize); + + setPadding( + (int) dpToPx(12 * scale), 0, + (int) dpToPx(12 * scale), 0 + ); + + requestLayout(); + } + + private void updateIconSize(ImageView icon, int size) { + if (icon != null) { + LayoutParams params = (LayoutParams) icon.getLayoutParams(); + params.width = size; + params.height = size; + icon.setLayoutParams(params); + } + } + + private float dpToPx(float dp) { + return dp * getResources().getDisplayMetrics().density; + } + + @Override + public void onThemeColorChanged(int newColor) { + if (brandIcon != null) { + brandIcon.setColorFilter(newColor); + } + if (brandText != null) { + brandText.setTextColor(newColor); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + ThemeManager.addListener(this); + onThemeColorChanged(ThemeManager.getThemeColor()); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + ThemeManager.removeListener(this); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandManager.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandManager.java new file mode 100644 index 0000000..2fb28b3 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandManager.java @@ -0,0 +1,357 @@ +package com.phoenix.gui.ui.dynamic; + +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; + +public class DynamicIslandManager { + + public static final long VALUE_PROGRESS_TIMEOUT_MS = 1000L; + public static final long SWITCH_DISPLAY_DURATION_MS = 500L; + public static final long TIME_PROGRESS_GRACE_PERIOD_MS = 1000L; + + public static class TaskItem { + public enum Type { SWITCH, PROGRESS } + + public final Type type; + public final String identifier; + public String text; + public String subtitle; + public boolean switchState; + public Drawable icon; + public boolean isTimeBased; + public long lastUpdateTime; + public boolean isAwaitingData; + public boolean removing; + public long duration; + public float displayProgress; + public float targetProgress; + public boolean isVisuallyHidden; + + private Runnable hideRunnable; + private Runnable progressRunnable; + + public TaskItem(Type type, String identifier, String text, @Nullable String subtitle) { + this.type = type; + this.identifier = identifier; + this.text = text; + this.subtitle = subtitle; + this.switchState = false; + this.icon = null; + this.isTimeBased = false; + this.lastUpdateTime = System.currentTimeMillis(); + this.isAwaitingData = false; + this.removing = false; + this.duration = 0; + this.displayProgress = 1.0f; + this.targetProgress = 1.0f; + this.isVisuallyHidden = false; + } + + public void cancelJobs(Handler handler) { + if (hideRunnable != null) { + handler.removeCallbacks(hideRunnable); + hideRunnable = null; + } + if (progressRunnable != null) { + handler.removeCallbacks(progressRunnable); + progressRunnable = null; + } + } + } + + public interface StateChangeListener { + void onTasksChanged(); + void onExpandedStateChanged(boolean isExpanded); + void onConfigChanged(float scale, String persistentText); + } + + private float scale = 0.7f; + private String persistentText = "User"; + private final List tasks = new CopyOnWriteArrayList<>(); + private final List listeners = new CopyOnWriteArrayList<>(); + private final Handler handler = new Handler(Looper.getMainLooper()); + private int currentFps = 0; + + private final Runnable updateRunnable = new Runnable() { + @Override + public void run() { + updateTasks(); + handler.postDelayed(this, 250); + } + }; + + public DynamicIslandManager(float initialScale, String initialText) { + this.scale = Math.max(0.5f, Math.min(2.0f, initialScale)); + this.persistentText = initialText; + handler.post(updateRunnable); + } + + public float getScale() { return scale; } + public String getPersistentText() { return persistentText; } + public List getTasks() { return new ArrayList<>(tasks); } + public int getCurrentFps() { return currentFps; } + public void setCurrentFps(int fps) { this.currentFps = fps; } + + public boolean isExpanded() { + for (TaskItem task : tasks) { + if (!task.removing && !task.isVisuallyHidden) return true; + } + return false; + } + + public List getVisibleTasks() { + List activeSwitchTasks = new ArrayList<>(); + List otherTasks = new ArrayList<>(); + + for (TaskItem task : tasks) { + if (task.removing || task.isVisuallyHidden) continue; + + if (task.type == TaskItem.Type.SWITCH) { + activeSwitchTasks.add(task); + } else { + otherTasks.add(task); + } + } + + if (!activeSwitchTasks.isEmpty()) { + return activeSwitchTasks; + } else { + return otherTasks; + } + } + + public void updateConfig(float newScale, String newText) { + this.scale = Math.max(0.5f, Math.min(2.0f, newScale)); + this.persistentText = newText; + notifyConfigChanged(); + } + + public void addSwitch(String identifier, String text, boolean state) { + android.util.Log.d("DynamicIsland", String.format("addSwitch: id=%s, text=%s, state=%s", identifier, text, state)); + + String mainTitle = "功能开关"; + String subTitle = text + "|已被" + (state ? "开启" : "关闭"); + + int taskIndex = findTaskIndex(identifier); + TaskItem task; + + if (taskIndex != -1) { + task = tasks.get(taskIndex); + task.cancelJobs(handler); + task.text = mainTitle; + task.subtitle = subTitle; + task.switchState = state; + task.lastUpdateTime = System.currentTimeMillis(); + task.duration = SWITCH_DISPLAY_DURATION_MS; + task.removing = false; + task.isTimeBased = true; + + startTimeBasedAnimation(task); + android.util.Log.d("DynamicIsland", "Updated existing switch task"); + } else { + task = new TaskItem(TaskItem.Type.SWITCH, identifier, mainTitle, subTitle); + task.switchState = state; + task.duration = SWITCH_DISPLAY_DURATION_MS; + task.isTimeBased = true; + + startTimeBasedAnimation(task); + tasks.add(0, task); + android.util.Log.d("DynamicIsland", "Created new switch task"); + } + + notifyTasksChanged(); + } + + public void addOrUpdateProgress(String identifier, String text, + @Nullable String subtitle, @Nullable Drawable icon, + @Nullable Float progress, @Nullable Long duration) { + int taskIndex = findTaskIndex(identifier); + if (taskIndex != -1) { + updateProgressInternal(tasks.get(taskIndex), text, subtitle, progress, duration); + } else { + addProgressInternal(identifier, text, subtitle, icon, progress, duration); + } + notifyTasksChanged(); + } + + public void removeTask(String identifier) { + int taskIndex = findTaskIndex(identifier); + if (taskIndex != -1) { + TaskItem task = tasks.get(taskIndex); + if (task.removing) return; + + task.removing = true; + task.cancelJobs(handler); + notifyTasksChanged(); + + handler.postDelayed(() -> { + tasks.remove(task); + notifyTasksChanged(); + }, 500); + } + } + + public void hide() { + for (TaskItem task : tasks) { + task.cancelJobs(handler); + } + tasks.clear(); + notifyTasksChanged(); + } + + public void destroy() { + handler.removeCallbacksAndMessages(null); + hide(); + } + + public void addListener(StateChangeListener listener) { listeners.add(listener); } + public void removeListener(StateChangeListener listener) { listeners.remove(listener); } + + private void notifyTasksChanged() { + boolean expanded = isExpanded(); + for (StateChangeListener listener : listeners) { + listener.onTasksChanged(); + listener.onExpandedStateChanged(expanded); + } + } + + private void notifyConfigChanged() { + for (StateChangeListener listener : listeners) { + handler.post(() -> listener.onConfigChanged(scale, persistentText)); + } + } + + private int findTaskIndex(String identifier) { + for (int i = 0; i < tasks.size(); i++) { + if (tasks.get(i).identifier.equals(identifier)) return i; + } + return -1; + } + + private void updateTasks() { + if (tasks.isEmpty()) return; + long currentTime = System.currentTimeMillis(); + boolean hasChanged = false; + + Iterator iterator = tasks.iterator(); + while (iterator.hasNext()) { + TaskItem task = iterator.next(); + if (task.removing) continue; + + boolean shouldBeMarkedForRemoval = false; + if (task.isTimeBased) { + if (task.displayProgress <= 0.01f && !task.isAwaitingData) { + task.isAwaitingData = true; + task.lastUpdateTime = currentTime; + } + if (task.isAwaitingData && (currentTime - task.lastUpdateTime > TIME_PROGRESS_GRACE_PERIOD_MS)) { + shouldBeMarkedForRemoval = true; + } + } + else if (task.type == TaskItem.Type.PROGRESS) { + if (currentTime - task.lastUpdateTime > VALUE_PROGRESS_TIMEOUT_MS) { + shouldBeMarkedForRemoval = true; + } + } + + if (shouldBeMarkedForRemoval) { + task.removing = true; + hasChanged = true; + handler.postDelayed(() -> { + tasks.remove(task); + notifyTasksChanged(); + }, 500); + } + } + if (hasChanged) { + notifyTasksChanged(); + } + } + + private void addProgressInternal(String identifier, String text, String subtitle, + Drawable icon, Float progressValue, Long duration) { + TaskItem newTask = new TaskItem(TaskItem.Type.PROGRESS, identifier, text, subtitle); + newTask.icon = icon != null ? icon.mutate() : null; + if (progressValue != null) { + newTask.isTimeBased = false; + newTask.displayProgress = 0f; + animateProgressTo(newTask, progressValue, 400); + } else { + newTask.isTimeBased = true; + newTask.duration = duration != null ? duration : 5000L; + startTimeBasedAnimation(newTask); + } + tasks.add(0, newTask); + } + + private void updateProgressInternal(TaskItem task, String text, String subtitle, + Float progressValue, Long duration) { + task.text = text; + task.subtitle = subtitle; + task.lastUpdateTime = System.currentTimeMillis(); + task.isAwaitingData = false; + task.removing = false; + task.cancelJobs(handler); + + if (progressValue != null) { + task.isTimeBased = false; + animateProgressTo(task, progressValue, 400); + } else { + task.isTimeBased = true; + task.duration = duration != null ? duration : 5000L; + startTimeBasedAnimation(task); + } + } + + private void animateProgressTo(TaskItem task, float targetProgress, long durationMs) { + task.cancelJobs(handler); + task.targetProgress = Math.max(0f, Math.min(1f, targetProgress)); + final float startProgress = task.displayProgress; + final float delta = task.targetProgress - startProgress; + + if (Math.abs(delta) < 0.001f) return; + + final long startTime = System.currentTimeMillis(); + final boolean isSwitch = task.type == TaskItem.Type.SWITCH; + + task.progressRunnable = new Runnable() { + @Override + public void run() { + long elapsed = System.currentTimeMillis() - startTime; + float fraction = Math.min(1f, elapsed / (float) durationMs); + float easedFraction = (float) (1 - Math.pow(1 - fraction, 3)); + task.displayProgress = startProgress + delta * easedFraction; + + if (!isSwitch) { + notifyTasksChanged(); + } + + if (fraction < 1f) { + handler.postDelayed(this, 16); + } else { + task.displayProgress = task.targetProgress; + task.progressRunnable = null; + + notifyTasksChanged(); + } + } + }; + handler.post(task.progressRunnable); + } + + private void startTimeBasedAnimation(TaskItem task) { + task.cancelJobs(handler); + + if (task.displayProgress < 1.0f) { + animateProgressTo(task, 1.0f, 500); + handler.postDelayed(() -> animateProgressTo(task, 0f, task.duration), 500); + } else { + animateProgressTo(task, 0f, task.duration); + } + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandView.java new file mode 100644 index 0000000..d7bf098 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandView.java @@ -0,0 +1,569 @@ +package com.phoenix.gui.ui.dynamic; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.*; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.view.Choreographer; +import android.view.Gravity; +import android.view.View; +import android.view.animation.OvershootInterpolator; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; + +import com.phoenix.gui.ui.ThemeManager; + +import java.util.ArrayList; +import java.util.List; + +public class DynamicIslandView extends FrameLayout implements DynamicIslandManager.StateChangeListener, ThemeManager.OnThemeColorChangeListener { + + private static final float COLLAPSED_HEIGHT_DP = 36f; + private static final float EXPANDED_CORNER_RADIUS_DP = 28f; + private static final float ITEM_HEIGHT_DP = 52f; + private static final float VIEW_PADDING_DP = 12f; + private static final float HORIZONTAL_ITEM_PADDING_DP = 24f; + private static final float ICON_SIZE_DP = 40f; + private static final float ICON_SPACING_DP = 12f; + private static final float GLOW_BLUR_RADIUS_DP = 12f; + private static final float GLOW_SPREAD_RADIUS_DP = 4f; + private static final float SHADOW_SAFETY_MARGIN_DP = 8f; + private static final int SIZE_ANIMATION_DURATION_MS = 350; + private static final int CONTENT_FADE_OUT_MS = 100; + private static final int CONTENT_FADE_IN_MS = 200; + + private DynamicIslandManager manager; + + private Paint backgroundPaint; + private Paint glowPaint; + private RectF backgroundRect = new RectF(); + private RectF glowRect = new RectF(); + + private float sheenPosition = -1.0f; + private ValueAnimator sheenAnimator; + + private CollapsedContentView collapsedContent; + private FrameLayout expandedContainer; + + private float currentHeight; + private float currentWidth; + private float currentCornerRadius; + private ValueAnimator sizeAnimator; + private boolean wasExpanded = false; + + private TextPaint textPaint; + + private int frameCount = 0; + private long lastFpsTime = System.nanoTime(); + + public DynamicIslandView(@NonNull Context context) { + super(context); + init(context); + } + + public DynamicIslandView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + private void init(Context context) { + setWillNotDraw(false); + setLayerType(LAYER_TYPE_SOFTWARE, null); + setClipChildren(false); + setClipToPadding(false); + + textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + + initPaints(); + initSheenAnimation(); + initContentViews(context); + startFpsMonitor(); + } + + private void initPaints() { + glowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + glowPaint.setColor(ThemeManager.getGlowColor()); + glowPaint.setStyle(Paint.Style.FILL); + glowPaint.setMaskFilter(new BlurMaskFilter(dpToPx(GLOW_BLUR_RADIUS_DP), BlurMaskFilter.Blur.NORMAL)); + + backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + backgroundPaint.setColor(ThemeManager.getGlassBackground()); + backgroundPaint.setStyle(Paint.Style.FILL); + } + + private void initSheenAnimation() { + sheenAnimator = ValueAnimator.ofFloat(-1.0f, 2.0f); + sheenAnimator.setDuration(2500); + sheenAnimator.setStartDelay(500); + sheenAnimator.setInterpolator(new android.view.animation.LinearInterpolator()); + sheenAnimator.setRepeatCount(ValueAnimator.INFINITE); + sheenAnimator.setRepeatMode(ValueAnimator.RESTART); + sheenAnimator.addUpdateListener(animation -> { + sheenPosition = (float) animation.getAnimatedValue(); + invalidate(); + }); + sheenAnimator.start(); + } + + private void initContentViews(Context context) { + collapsedContent = new CollapsedContentView(context); + collapsedContent.setVisibility(VISIBLE); + LayoutParams collapsedParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + collapsedParams.gravity = Gravity.CENTER; + addView(collapsedContent, collapsedParams); + + expandedContainer = new FrameLayout(context); + expandedContainer.setVisibility(GONE); + expandedContainer.setAlpha(0f); + expandedContainer.setBackgroundColor(Color.TRANSPARENT); + LayoutParams expandedParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + expandedParams.gravity = Gravity.CENTER; + addView(expandedContainer, expandedParams); + } + + private void startFpsMonitor() { + Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + frameCount++; + long elapsedNanos = frameTimeNanos - lastFpsTime; + if (elapsedNanos >= 1_000_000_000L) { + if (manager != null) { + manager.setCurrentFps(frameCount); + if (collapsedContent != null) { + collapsedContent.updateFps(frameCount); + } + } + frameCount = 0; + lastFpsTime = frameTimeNanos; + } + Choreographer.getInstance().postFrameCallback(this); + } + }); + } + + public void setManager(DynamicIslandManager manager) { + if (this.manager != null) { + this.manager.removeListener(this); + } + this.manager = manager; + this.manager.addListener(this); + + if (collapsedContent != null) { + collapsedContent.setManager(manager); + } + + updateContent(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + float scale = manager != null ? manager.getScale() : 0.7f; + int glowPadding = (int) (dpToPx(GLOW_BLUR_RADIUS_DP + GLOW_SPREAD_RADIUS_DP) * scale); + int safetyMargin = (int) (dpToPx(SHADOW_SAFETY_MARGIN_DP) * scale); + int totalPadding = glowPadding + safetyMargin; + + int contentWidth = (int) currentWidth; + int contentHeight = (int) currentHeight; + + if (currentWidth == 0 || currentHeight == 0) { + boolean isExpanded = manager != null && manager.isExpanded(); + if (isExpanded) { + contentHeight = calculateExpandedHeight(scale); + contentWidth = calculateExpandedWidth(scale); + } else { + contentHeight = (int) (dpToPx(COLLAPSED_HEIGHT_DP) * scale); + contentWidth = calculateCollapsedWidth(scale); + } + currentHeight = contentHeight; + currentWidth = contentWidth; + currentCornerRadius = isExpanded ? dpToPx(EXPANDED_CORNER_RADIUS_DP) * scale : contentHeight / 2f; + } + + int contentWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.AT_MOST); + int contentHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY); + + measureChild(collapsedContent, contentWidthSpec, contentHeightSpec); + measureChild(expandedContainer, MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY), contentHeightSpec); + + setMeasuredDimension( + contentWidth + totalPadding * 2, + contentHeight + totalPadding * 2 + ); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + float scale = manager != null ? manager.getScale() : 0.7f; + int glowPadding = (int) (dpToPx(GLOW_BLUR_RADIUS_DP + GLOW_SPREAD_RADIUS_DP) * scale); + int safetyMargin = (int) (dpToPx(SHADOW_SAFETY_MARGIN_DP) * scale); + int totalPadding = glowPadding + safetyMargin; + + int contentLeft = totalPadding; + int contentTop = totalPadding; + int contentRight = getWidth() - totalPadding; + int contentBottom = getHeight() - totalPadding; + + int contentWidth = contentRight - contentLeft; + int collapsedWidth = collapsedContent.getMeasuredWidth(); + int collapsedLeft = contentLeft + (contentWidth - collapsedWidth) / 2; + collapsedContent.layout(collapsedLeft, contentTop, collapsedLeft + collapsedWidth, contentBottom); + + expandedContainer.layout(contentLeft, contentTop, contentRight, contentBottom); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + float scale = manager != null ? manager.getScale() : 0.7f; + int glowPadding = (int) (dpToPx(GLOW_BLUR_RADIUS_DP + GLOW_SPREAD_RADIUS_DP) * scale); + int safetyMargin = (int) (dpToPx(SHADOW_SAFETY_MARGIN_DP) * scale); + int totalPadding = glowPadding + safetyMargin; + + float contentLeft = totalPadding; + float contentTop = totalPadding; + float contentRight = getWidth() - totalPadding; + float contentBottom = getHeight() - totalPadding; + + backgroundRect.set(contentLeft, contentTop, contentRight, contentBottom); + + float spreadPx = dpToPx(GLOW_SPREAD_RADIUS_DP) * scale; + glowRect.set( + contentLeft - spreadPx, + contentTop - spreadPx, + contentRight + spreadPx, + contentBottom + spreadPx + ); + + canvas.drawRoundRect(glowRect, currentCornerRadius, currentCornerRadius, glowPaint); + canvas.drawRoundRect(backgroundRect, currentCornerRadius, currentCornerRadius, backgroundPaint); + drawSheen(canvas, backgroundRect, currentCornerRadius); + } + + private void drawSheen(Canvas canvas, RectF rect, float cornerRadius) { + float gradientWidth = rect.width() * 0.5f; + float gradientStart = rect.width() * sheenPosition; + + LinearGradient gradient = new LinearGradient( + gradientStart - gradientWidth, rect.top, + gradientStart, rect.bottom, + new int[]{0x00FFFFFF, 0x14FFFFFF, 0x00FFFFFF}, + new float[]{0f, 0.5f, 1f}, + Shader.TileMode.CLAMP + ); + + Paint sheenPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + sheenPaint.setShader(gradient); + + canvas.save(); + canvas.clipRect(rect); + canvas.drawRoundRect(rect, cornerRadius, cornerRadius, sheenPaint); + canvas.restore(); + } + + @Override + public void onTasksChanged() { + updateContent(); + post(() -> animateSizeToFitContent()); + } + + @Override + public void onExpandedStateChanged(boolean isExpanded) { + if (wasExpanded != isExpanded) { + wasExpanded = isExpanded; + updateContent(); + animateToState(isExpanded); + } + } + + @Override + public void onConfigChanged(float scale, String persistentText) { + if (collapsedContent != null) { + collapsedContent.updateConfig(scale, persistentText); + } + requestLayout(); + } + + private void updateContent() { + if (manager == null) return; + boolean isExpanded = manager.isExpanded(); + + if (isExpanded) { + LinearLayout taskContainer = null; + if (expandedContainer.getChildCount() > 0 && expandedContainer.getChildAt(0) instanceof LinearLayout) { + taskContainer = (LinearLayout) expandedContainer.getChildAt(0); + } + + if (taskContainer == null) { + rebuildExpandedContent(); + } else { + if (!areTasksMatching(taskContainer)) { + rebuildExpandedContent(); + } else { + updateExistingContent(taskContainer); + } + } + } + + if (collapsedContent != null) { + collapsedContent.updateContent(); + } + } + + private boolean areTasksMatching(LinearLayout taskContainer) { + List tasks = manager.getVisibleTasks(); + + if (taskContainer.getChildCount() != tasks.size()) { + android.util.Log.d("DynamicIsland", String.format( + "Tasks not matching: count mismatch (view=%d, task=%d)", + taskContainer.getChildCount(), tasks.size() + )); + return false; + } + + for (int i = 0; i < tasks.size(); i++) { + View child = taskContainer.getChildAt(i); + if (!(child instanceof TaskItemView)) { + android.util.Log.d("DynamicIsland", "Tasks not matching: child is not TaskItemView"); + return false; + } + + TaskItemView view = (TaskItemView) child; + DynamicIslandManager.TaskItem task = tasks.get(i); + + if (!view.task.identifier.equals(task.identifier)) { + android.util.Log.d("DynamicIsland", String.format( + "Tasks not matching: identifier mismatch at %d (view=%s, task=%s)", + i, view.task.identifier, task.identifier + )); + return false; + } + + if (view.task.type != task.type) { + android.util.Log.d("DynamicIsland", String.format( + "Tasks not matching: type mismatch at %d (view=%s, task=%s)", + i, view.task.type, task.type + )); + return false; + } + } + + android.util.Log.d("DynamicIsland", "Tasks matching, using updateExistingContent"); + return true; + } + + private void updateExistingContent(LinearLayout taskContainer) { + List tasks = manager.getVisibleTasks(); + android.util.Log.d("DynamicIsland", String.format("updateExistingContent: %d tasks", tasks.size())); + + for (int i = 0; i < tasks.size(); i++) { + if (i >= taskContainer.getChildCount()) break; + + View child = taskContainer.getChildAt(i); + if (!(child instanceof TaskItemView)) continue; + + TaskItemView view = (TaskItemView) child; + DynamicIslandManager.TaskItem newTask = tasks.get(i); + + view.task = newTask; + + view.updateContent(); + } + } + + private void rebuildExpandedContent() { + android.util.Log.d("DynamicIsland", "rebuildExpandedContent called"); + expandedContainer.removeAllViews(); + LinearLayout taskContainer = new LinearLayout(getContext()); + taskContainer.setOrientation(LinearLayout.VERTICAL); + taskContainer.setPadding( + 0, (int) (dpToPx(VIEW_PADDING_DP) * manager.getScale()), + 0, (int) (dpToPx(VIEW_PADDING_DP) * manager.getScale()) + ); + for (DynamicIslandManager.TaskItem task : manager.getVisibleTasks()) { + TaskItemView itemView = createTaskItemView(task); + taskContainer.addView(itemView); + } + expandedContainer.addView(taskContainer, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + } + + private TaskItemView createTaskItemView(DynamicIslandManager.TaskItem task) { + switch (task.type) { + case SWITCH: + return new SwitchItemView(getContext(), task, manager.getScale()); + case PROGRESS: + return new ProgressItemView(getContext(), task, manager.getScale()); + default: + throw new IllegalArgumentException("Unknown task type: " + task.type); + } + } + + private void animateSizeToFitContent() { + if (manager == null) return; + boolean isExpanded = manager.isExpanded(); + float scale = manager.getScale(); + + float targetHeight = isExpanded ? calculateExpandedHeight(scale) : dpToPx(COLLAPSED_HEIGHT_DP) * scale; + float targetWidth = isExpanded ? calculateExpandedWidth(scale) : calculateCollapsedWidth(scale); + float targetCorner = isExpanded ? dpToPx(EXPANDED_CORNER_RADIUS_DP) * scale : targetHeight / 2f; + + if (Math.abs(targetHeight - currentHeight) > 1f || Math.abs(targetWidth - currentWidth) > 1f) { + animateSizeWithSpring(targetHeight, targetWidth, targetCorner); + } + } + + private void animateToState(boolean expanded) { + if (sizeAnimator != null) sizeAnimator.cancel(); + float scale = manager != null ? manager.getScale() : 0.7f; + float targetHeight = expanded ? calculateExpandedHeight(scale) : dpToPx(COLLAPSED_HEIGHT_DP) * scale; + float targetWidth = expanded ? calculateExpandedWidth(scale) : calculateCollapsedWidth(scale); + float targetCorner = expanded ? dpToPx(EXPANDED_CORNER_RADIUS_DP) * scale : targetHeight / 2f; + + if (expanded) { + collapsedContent.animate().alpha(0f).scaleX(0.9f).scaleY(0.9f).setDuration(CONTENT_FADE_OUT_MS) + .setInterpolator(new FastOutSlowInInterpolator()).withEndAction(() -> collapsedContent.setVisibility(GONE)).start(); + expandedContainer.setVisibility(VISIBLE); + expandedContainer.setAlpha(0f); + expandedContainer.setScaleX(0.9f); + expandedContainer.setScaleY(0.9f); + expandedContainer.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(CONTENT_FADE_IN_MS) + .setInterpolator(new FastOutSlowInInterpolator()).start(); + } else { + expandedContainer.animate().alpha(0f).scaleX(0.9f).scaleY(0.9f).setDuration(CONTENT_FADE_OUT_MS) + .setInterpolator(new FastOutSlowInInterpolator()).withEndAction(() -> expandedContainer.setVisibility(GONE)).start(); + collapsedContent.setVisibility(VISIBLE); + collapsedContent.setAlpha(0f); + collapsedContent.setScaleX(0.9f); + collapsedContent.setScaleY(0.9f); + collapsedContent.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(CONTENT_FADE_IN_MS) + .setInterpolator(new FastOutSlowInInterpolator()).start(); + } + animateSizeWithSpring(targetHeight, targetWidth, targetCorner); + } + + private void animateSizeWithSpring(float targetHeight, float targetWidth, float targetCorner) { + final float startHeight = currentHeight; + final float startWidth = currentWidth; + final float startCorner = currentCornerRadius; + + if (sizeAnimator != null) sizeAnimator.cancel(); + + sizeAnimator = ValueAnimator.ofFloat(0f, 1f); + sizeAnimator.setDuration(SIZE_ANIMATION_DURATION_MS); + sizeAnimator.setInterpolator(new OvershootInterpolator(0.8f)); + + sizeAnimator.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + currentHeight = startHeight + (targetHeight - startHeight) * fraction; + currentWidth = startWidth + (targetWidth - startWidth) * fraction; + currentCornerRadius = startCorner + (targetCorner - startCorner) * fraction; + requestLayout(); + }); + + sizeAnimator.start(); + } + + private int calculateExpandedHeight(float scale) { + if (manager == null) return (int) (dpToPx(COLLAPSED_HEIGHT_DP) * scale); + int totalHeight = 0; + for (DynamicIslandManager.TaskItem task : manager.getVisibleTasks()) { + switch (task.type) { + case SWITCH: + totalHeight += dpToPx(ITEM_HEIGHT_DP) * scale; + break; + case PROGRESS: + totalHeight += dpToPx(ITEM_HEIGHT_DP + 8 + 12) * scale; + break; + } + } + return totalHeight + (int) (dpToPx(VIEW_PADDING_DP * 2) * scale); + } + + private int calculateExpandedWidth(float scale) { + if (manager == null) return (int) (dpToPx(280) * scale); + + int minWidth = (int) (dpToPx(280) * scale); + int maxWidth = minWidth; + + for (DynamicIslandManager.TaskItem task : manager.getVisibleTasks()) { + int requiredWidth = calculateTaskWidth(task, scale); + maxWidth = Math.max(maxWidth, requiredWidth); + } + + int absoluteMaxWidth = (int) (dpToPx(450) * scale); + return Math.min(maxWidth, absoluteMaxWidth); + } + + private int calculateCollapsedWidth(float scale) { + if (collapsedContent == null) return (int) (dpToPx(280) * scale); + + int widthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + int heightSpec = MeasureSpec.makeMeasureSpec((int)(dpToPx(COLLAPSED_HEIGHT_DP) * scale), MeasureSpec.EXACTLY); + collapsedContent.measure(widthSpec, heightSpec); + + int contentWidth = collapsedContent.getMeasuredWidth(); + int padding = (int) (dpToPx(40) * scale); + return contentWidth + padding; + } + + private int calculateTaskWidth(DynamicIslandManager.TaskItem task, float scale) { + float sidePadding = dpToPx(HORIZONTAL_ITEM_PADDING_DP * 2) * scale; + float iconWidth = 0; + float spacing = 0; + + if (task.icon != null || task.type == DynamicIslandManager.TaskItem.Type.SWITCH) { + if (task.type == DynamicIslandManager.TaskItem.Type.SWITCH) iconWidth = dpToPx(52) * scale; + else iconWidth = dpToPx(ICON_SIZE_DP) * scale; + spacing = dpToPx(ICON_SPACING_DP) * scale; + } + + textPaint.setTextSize(spToPx(15 * scale)); + textPaint.setTypeface(Typeface.DEFAULT_BOLD); + float titleWidth = textPaint.measureText(task.text); + + float subtitleWidth = 0; + if (task.subtitle != null) { + textPaint.setTextSize(spToPx(12 * scale)); + textPaint.setTypeface(Typeface.DEFAULT); + subtitleWidth = textPaint.measureText(task.subtitle.replace("|", " ")); + } + + float textWidth = Math.max(titleWidth, subtitleWidth); + float extraPadding = dpToPx(32) * scale; + + return (int) (sidePadding + iconWidth + spacing + textWidth + extraPadding); + } + + private float dpToPx(float dp) { + return dp * getResources().getDisplayMetrics().density; + } + + private int spToPx(float sp) { + return (int) (sp * getResources().getDisplayMetrics().scaledDensity); + } + + @Override + public void onThemeColorChanged(int newColor) { + invalidate(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + ThemeManager.addListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (sheenAnimator != null) sheenAnimator.cancel(); + if (sizeAnimator != null) sizeAnimator.cancel(); + // 停止监听应该是 + ThemeManager.removeListener(this); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandWindow.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandWindow.java new file mode 100644 index 0000000..05bc3b0 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/DynamicIslandWindow.java @@ -0,0 +1,107 @@ +package com.phoenix.gui.ui.dynamic; + +import android.content.Context; +import android.view.View; +import android.widget.FrameLayout; +import androidx.annotation.NonNull; + +public class DynamicIslandWindow { + + private final Context context; + private final FrameLayout container; + private DynamicIslandView dynamicIslandView; + private DynamicIslandManager manager; + private boolean isShowing = false; + + public DynamicIslandWindow(@NonNull Context context, FrameLayout container) { + this.context = context.getApplicationContext(); + this.container = container; + } + + public void show() { + if (isShowing) return; + + android.content.SharedPreferences prefs = context.getSharedPreferences("SettingsPrefs", Context.MODE_PRIVATE); + float scale = prefs.getFloat("dynamicIslandScale", 0.7f); + String username = prefs.getString("dynamicIslandUsername", "User"); + manager = new DynamicIslandManager(scale, username); + + dynamicIslandView = new DynamicIslandView(context); + dynamicIslandView.setManager(manager); + dynamicIslandView.setAlpha(0.80f); + + if (container != null) { + container.removeAllViews(); + container.addView(dynamicIslandView, new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT + )); + container.setVisibility(View.VISIBLE); + isShowing = true; + } + } + + public void hide() { + if (!isShowing) return; + + try { + if (container != null) { + container.removeAllViews(); + container.setVisibility(View.GONE); + } + + if (dynamicIslandView != null) { + dynamicIslandView = null; + } + + if (manager != null) { + manager.destroy(); + manager = null; + } + + isShowing = false; + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void updateConfig(float scale, String username) { + if (manager != null) { + manager.updateConfig(scale, username); + } + } + + public void addSwitch(String identifier, String text, boolean state) { + if (manager != null) { + manager.addSwitch(identifier, text, state); + } + } + + public void addOrUpdateProgress(String identifier, String text, String subtitle, + android.graphics.drawable.Drawable icon, + Float progress, Long duration) { + if (manager != null) { + manager.addOrUpdateProgress(identifier, text, subtitle, icon, progress, duration); + } + } + + public void removeTask(String identifier) { + if (manager != null) { + manager.removeTask(identifier); + } + } + + public void hideAllTasks() { + if (manager != null) { + manager.hide(); + } + } + + public boolean isShowing() { + return isShowing; + } + + public DynamicIslandManager getManager() { + return manager; + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/GlowDrawable.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/GlowDrawable.java new file mode 100644 index 0000000..1c92768 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/GlowDrawable.java @@ -0,0 +1,66 @@ +package com.phoenix.gui.ui.dynamic; + +import android.graphics.*; +import android.graphics.drawable.Drawable; +import androidx.annotation.NonNull; + +public class GlowDrawable extends Drawable { + + private final Paint paint; + private float cornerRadius = 0; + private float blurRadius = 0; + private float spreadRadius = 0; + + public GlowDrawable() { + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColor(0x66000000); + paint.setStyle(Paint.Style.FILL); + } + + public void setCornerRadius(float radius) { + this.cornerRadius = radius; + invalidateSelf(); + } + + public void setBlurRadius(float radius) { + this.blurRadius = radius; + if (radius > 0) { + paint.setMaskFilter(new BlurMaskFilter(radius, BlurMaskFilter.Blur.NORMAL)); + } else { + paint.setMaskFilter(null); + } + invalidateSelf(); + } + + public void setSpreadRadius(float radius) { + this.spreadRadius = radius; + invalidateSelf(); + } + + @Override + public void draw(@NonNull Canvas canvas) { + Rect bounds = getBounds(); + RectF rect = new RectF( + bounds.left - spreadRadius, + bounds.top - spreadRadius, + bounds.right + spreadRadius, + bounds.bottom + spreadRadius + ); + canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint); + } + + @Override + public void setAlpha(int alpha) { + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + paint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialProgressBar.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialProgressBar.java new file mode 100644 index 0000000..acf9321 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialProgressBar.java @@ -0,0 +1,161 @@ +package com.phoenix.gui.ui.dynamic; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.*; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.LinearInterpolator; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class MaterialProgressBar extends View { + + private static final float DEFAULT_HEIGHT_DP = 8f; + + private float progress = 0f; + + private float sheenPosition = 0f; + private ValueAnimator sheenAnimator; + + private Paint backgroundPaint; + private Paint progressPaint; + + private float barHeight; + private float cornerRadius; + + public MaterialProgressBar(@NonNull Context context) { + super(context); + init(context); + } + + public MaterialProgressBar(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + private void init(Context context) { + float density = context.getResources().getDisplayMetrics().density; + barHeight = DEFAULT_HEIGHT_DP * density; + cornerRadius = barHeight / 2f; + + backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + backgroundPaint.setStyle(Paint.Style.FILL); + + int backgroundColor = (com.phoenix.gui.ui.ThemeManager.getThemeColor() & 0x00FFFFFF) | 0x26000000; + backgroundPaint.setColor(backgroundColor); + + progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + progressPaint.setStyle(Paint.Style.FILL); + progressPaint.setColor(com.phoenix.gui.ui.ThemeManager.getThemeColor()); + + startSheenAnimation(); + } + + private void startSheenAnimation() { + sheenAnimator = ValueAnimator.ofFloat(0f, 2f); + sheenAnimator.setDuration(1000); + sheenAnimator.setInterpolator(new LinearInterpolator()); + sheenAnimator.setRepeatCount(ValueAnimator.INFINITE); + sheenAnimator.setRepeatMode(ValueAnimator.RESTART); + + sheenAnimator.addUpdateListener(animation -> { + sheenPosition = (float) animation.getAnimatedValue(); + invalidate(); + }); + + sheenAnimator.start(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = (int) (barHeight + getPaddingTop() + getPaddingBottom()); + setMeasuredDimension(width, height); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + super.onDraw(canvas); + + float width = getWidth() - getPaddingLeft() - getPaddingRight(); + float left = getPaddingLeft(); + float top = getPaddingTop(); + float right = left + width; + float bottom = top + barHeight; + + RectF backgroundRect = new RectF(left, top, right, bottom); + canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint); + + float progressWidth = Math.max(width * progress, barHeight); + float progressRight = left + progressWidth; + + if (progress > 0.001f) { + + canvas.save(); + + RectF progressRect = new RectF(left, top, progressRight, bottom); + canvas.clipRect(progressRect); + + canvas.drawRoundRect(progressRect, cornerRadius, cornerRadius, progressPaint); + + drawSheen(canvas, progressRect); + + canvas.restore(); + } + } + + private void drawSheen(Canvas canvas, RectF rect) { + float gradientWidth = rect.width() * 0.3f; + float gradientStart = rect.width() * (sheenPosition - 1f); + + int colorSheen = (com.phoenix.gui.ui.ThemeManager.getTextPrimary() & 0x00FFFFFF) | 0x4D000000; + + LinearGradient gradient = new LinearGradient( + rect.left + gradientStart, + rect.top, + rect.left + gradientStart + gradientWidth, + rect.top, + new int[]{0x00FFFFFF, colorSheen, 0x00FFFFFF}, + new float[]{0f, 0.5f, 1f}, + Shader.TileMode.CLAMP + ); + + Paint sheenPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + sheenPaint.setShader(gradient); + + canvas.drawRoundRect(rect, cornerRadius, cornerRadius, sheenPaint); + } + + public void setProgress(float progress) { + this.progress = Math.max(0f, Math.min(1f, progress)); + invalidate(); + } + + public float getProgress() { + return progress; + } + + public void setBarHeight(float heightDp) { + float density = getResources().getDisplayMetrics().density; + this.barHeight = heightDp * density; + this.cornerRadius = barHeight / 2f; + requestLayout(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (sheenAnimator != null) { + sheenAnimator.cancel(); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (sheenAnimator != null && !sheenAnimator.isRunning()) { + sheenAnimator.start(); + } + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialSwitchView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialSwitchView.java new file mode 100644 index 0000000..9172901 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/MaterialSwitchView.java @@ -0,0 +1,206 @@ +package com.phoenix.gui.ui.dynamic; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.*; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.view.animation.OvershootInterpolator; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.phoenix.gui.ui.ThemeManager; + +public class MaterialSwitchView extends View implements ThemeManager.OnThemeColorChangeListener { + + private static final float TRACK_WIDTH_DP = 52f; + private static final float TRACK_HEIGHT_DP = 32f; + private static final float THUMB_SIZE_DP = 24f; + private static final float THUMB_PADDING_DP = 4f; + + private boolean isChecked = false; + private float thumbPosition = 0f; + private float trackColorFraction = 0f; + + private ValueAnimator thumbAnimator; + private ValueAnimator colorAnimator; + + private Paint trackPaint; + private Paint thumbPaint; + + private float trackWidth; + private float trackHeight; + private float thumbSize; + private float thumbPadding; + private float trackRadius; + private float thumbRadius; + + public MaterialSwitchView(@NonNull Context context) { + super(context); + init(context); + } + + public MaterialSwitchView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + private void init(Context context) { + float density = context.getResources().getDisplayMetrics().density; + trackWidth = TRACK_WIDTH_DP * density; + trackHeight = TRACK_HEIGHT_DP * density; + thumbSize = THUMB_SIZE_DP * density; + thumbPadding = THUMB_PADDING_DP * density; + trackRadius = trackHeight / 2f; + thumbRadius = thumbSize / 2f; + + trackPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + trackPaint.setStyle(Paint.Style.FILL); + + thumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + thumbPaint.setStyle(Paint.Style.FILL); + thumbPaint.setColor(com.phoenix.gui.ui.ThemeManager.getTextPrimary()); + thumbPaint.setShadowLayer( + 4f * density, + 0f, + 2f * density, + 0x40000000 + ); + + setLayerType(LAYER_TYPE_SOFTWARE, null); + + setClickable(false); + setFocusable(false); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = (int) (trackWidth + getPaddingLeft() + getPaddingRight()); + int height = (int) (trackHeight + getPaddingTop() + getPaddingBottom()); + setMeasuredDimension(width, height); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + super.onDraw(canvas); + + float centerX = getWidth() / 2f; + float centerY = getHeight() / 2f; + + float trackLeft = centerX - trackWidth / 2f; + float trackTop = centerY - trackHeight / 2f; + float trackRight = centerX + trackWidth / 2f; + float trackBottom = centerY + trackHeight / 2f; + + int colorTrackChecked = ThemeManager.getThemeColor(); + int colorTrackUnchecked = (com.phoenix.gui.ui.ThemeManager.getTextPrimary() & 0x00FFFFFF) | 0x4D000000; + int trackColor = interpolateColor(colorTrackUnchecked, colorTrackChecked, trackColorFraction); + trackPaint.setColor(trackColor); + + RectF trackRect = new RectF(trackLeft, trackTop, trackRight, trackBottom); + canvas.drawRoundRect(trackRect, trackRadius, trackRadius, trackPaint); + + float thumbTravelDistance = trackWidth - thumbSize - 2 * thumbPadding; + float thumbCenterX = trackLeft + thumbPadding + thumbRadius + thumbTravelDistance * thumbPosition; + float thumbCenterY = centerY; + + canvas.drawCircle(thumbCenterX, thumbCenterY, thumbRadius, thumbPaint); + } + + public void setChecked(boolean checked, boolean animate) { + if (this.isChecked == checked) return; + + this.isChecked = checked; + + if (animate) { + animateToPosition(checked ? 1f : 0f); + animateTrackColor(checked ? 1f : 0f); + } else { + thumbPosition = checked ? 1f : 0f; + trackColorFraction = checked ? 1f : 0f; + invalidate(); + } + } + + public void setChecked(boolean checked) { + setChecked(checked, true); + } + + public boolean isChecked() { + return isChecked; + } + + private void animateToPosition(float targetPosition) { + if (thumbAnimator != null) { + thumbAnimator.cancel(); + } + + thumbAnimator = ValueAnimator.ofFloat(thumbPosition, targetPosition); + thumbAnimator.setDuration(200); + thumbAnimator.setInterpolator(new OvershootInterpolator(1.2f)); + thumbAnimator.addUpdateListener(animation -> { + thumbPosition = (float) animation.getAnimatedValue(); + invalidate(); + }); + thumbAnimator.start(); + } + + private void animateTrackColor(float targetFraction) { + if (colorAnimator != null) { + colorAnimator.cancel(); + } + + colorAnimator = ValueAnimator.ofFloat(trackColorFraction, targetFraction); + colorAnimator.setDuration(150); + colorAnimator.addUpdateListener(animation -> { + trackColorFraction = (float) animation.getAnimatedValue(); + invalidate(); + }); + colorAnimator.start(); + } + + private int interpolateColor(int colorA, int colorB, float fraction) { + int alphaA = Color.alpha(colorA); + int redA = Color.red(colorA); + int greenA = Color.green(colorA); + int blueA = Color.blue(colorA); + + int alphaB = Color.alpha(colorB); + int redB = Color.red(colorB); + int greenB = Color.green(colorB); + int blueB = Color.blue(colorB); + + int alpha = (int) (alphaA + (alphaB - alphaA) * fraction); + int red = (int) (redA + (redB - redA) * fraction); + int green = (int) (greenA + (greenB - greenA) * fraction); + int blue = (int) (blueA + (blueB - blueA) * fraction); + + return Color.argb(alpha, red, green, blue); + } + + @Override + public void onThemeColorChanged(int newColor) { + + if (isChecked) { + invalidate(); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + ThemeManager.addListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (thumbAnimator != null) { + thumbAnimator.cancel(); + } + if (colorAnimator != null) { + colorAnimator.cancel(); + } + ThemeManager.removeListener(this); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/ProgressItemView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/ProgressItemView.java new file mode 100644 index 0000000..7c5299a --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/ProgressItemView.java @@ -0,0 +1,186 @@ +package com.phoenix.gui.ui.dynamic; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import com.phoenix.gui.ui.ThemeManager; + +public class ProgressItemView extends AnimatedTaskItemView implements ThemeManager.OnThemeColorChangeListener { + + private int colorPrimary; + private int colorOnPrimary; + private GradientDrawable iconBackground; + + private static final int ICON_SIZE_DP = 40; + private static final int ICON_CORNER_RADIUS_DP = 14; + private static final int ICON_INNER_SIZE_DP = 24; + + private ImageView iconView; + private TextView titleText; + private TextView subtitleText; + private MaterialProgressBar progressBar; + + public ProgressItemView(@NonNull Context context, DynamicIslandManager.TaskItem task, float scale) { + super(context, task, scale); + } + + @Override + protected void init(Context context) { + colorPrimary = ThemeManager.getThemeColor(); + colorOnPrimary = ThemeManager.getTextOnTheme(); + + setLayoutParams(new LayoutParams( + LayoutParams.MATCH_PARENT, + (int) (dpToPx(52 + 8 + 12) * scale) + )); + + LinearLayout mainContainer = new LinearLayout(context); + mainContainer.setOrientation(LinearLayout.VERTICAL); + mainContainer.setGravity(Gravity.CENTER_VERTICAL); + addView(mainContainer, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + + LinearLayout topContainer = new LinearLayout(context); + topContainer.setOrientation(LinearLayout.HORIZONTAL); + topContainer.setGravity(Gravity.CENTER_VERTICAL); + topContainer.setPadding( + (int) (dpToPx(24) * scale), 0, + (int) (dpToPx(24) * scale), 0 + ); + mainContainer.addView(topContainer, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + (int) (dpToPx(52) * scale) + )); + + iconView = new ImageView(context); + iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + int iconSize = (int) (dpToPx(ICON_SIZE_DP) * scale); + + iconBackground = new GradientDrawable(); + iconBackground.setShape(GradientDrawable.RECTANGLE); + iconBackground.setCornerRadius(dpToPx(ICON_CORNER_RADIUS_DP) * scale); + iconBackground.setColor(adjustAlpha(colorPrimary, 0.5f)); + iconView.setBackground(iconBackground); + + int iconPadding = (int) ((dpToPx(ICON_SIZE_DP) - dpToPx(ICON_INNER_SIZE_DP)) / 2 * scale); + iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding); + iconView.setColorFilter(colorOnPrimary); + + LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(iconSize, iconSize); + topContainer.addView(iconView, iconParams); + + topContainer.addView(new android.view.View(context), new LinearLayout.LayoutParams( + (int) (dpToPx(12) * scale), 0 + )); + + LinearLayout textContainer = new LinearLayout(context); + textContainer.setOrientation(LinearLayout.VERTICAL); + textContainer.setGravity(Gravity.CENTER_VERTICAL); + topContainer.addView(textContainer, new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.MATCH_PARENT, 1f + )); + + titleText = new TextView(context); + titleText.setTextColor(com.phoenix.gui.ui.ThemeManager.getTextPrimary()); + titleText.setTextSize(15 * scale); + titleText.setTypeface(null, Typeface.BOLD); + titleText.setSingleLine(true); + titleText.setIncludeFontPadding(false); + textContainer.addView(titleText); + + subtitleText = new TextView(context); + subtitleText.setTextColor(com.phoenix.gui.ui.ThemeManager.getTextSecondary()); + subtitleText.setTextSize(12 * scale); + subtitleText.setSingleLine(true); + subtitleText.setIncludeFontPadding(false); + LinearLayout.LayoutParams subtitleParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + subtitleParams.topMargin = (int) (dpToPx(2) * scale); + textContainer.addView(subtitleText, subtitleParams); + + mainContainer.addView(new android.view.View(context), new LinearLayout.LayoutParams( + 0, (int) (dpToPx(6) * scale) + )); + + progressBar = new MaterialProgressBar(context); + progressBar.setBarHeight(8 * scale); + LinearLayout.LayoutParams progressParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + (int) (dpToPx(8) * scale) + ); + progressParams.setMargins( + (int) (dpToPx(24) * scale), + 0, + (int) (dpToPx(24) * scale), + 0 + ); + mainContainer.addView(progressBar, progressParams); + + mainContainer.addView(new android.view.View(context), new LinearLayout.LayoutParams( + 0, (int) (dpToPx(6) * scale) + )); + + updateContent(); + } + + @Override + protected void updateContent() { + + titleText.setText(task.text); + + if (task.subtitle != null && !task.subtitle.isEmpty()) { + subtitleText.setText(task.subtitle); + subtitleText.setVisibility(VISIBLE); + } else { + subtitleText.setVisibility(GONE); + } + + if (task.icon != null) { + iconView.setImageDrawable(task.icon); + iconView.setVisibility(VISIBLE); + } else { + iconView.setVisibility(GONE); + } + + progressBar.setProgress(task.displayProgress); + } + + private int adjustAlpha(int color, float alpha) { + int alphaInt = Math.round(Color.alpha(color) * alpha); + return Color.argb(alphaInt, Color.red(color), Color.green(color), Color.blue(color)); + } + + @Override + public void onThemeColorChanged(int newColor) { + this.colorPrimary = newColor; + this.colorOnPrimary = ThemeManager.getTextOnTheme(); + + if (iconBackground != null) { + iconBackground.setColor(adjustAlpha(colorPrimary, 0.5f)); + } + if (iconView != null) { + iconView.setColorFilter(colorOnPrimary); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + ThemeManager.addListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + ThemeManager.removeListener(this); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/SheenDrawable.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/SheenDrawable.java new file mode 100644 index 0000000..f63d71b --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/SheenDrawable.java @@ -0,0 +1,77 @@ +package com.phoenix.gui.ui.dynamic; + +import android.animation.ValueAnimator; +import android.graphics.*; +import android.graphics.drawable.Drawable; +import android.view.animation.LinearInterpolator; +import androidx.annotation.NonNull; + +public class SheenDrawable extends Drawable { + + private final Paint paint; + private float cornerRadius = 0; + private float sheenPosition = -1.0f; + private ValueAnimator animator; + + public SheenDrawable() { + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + startAnimation(); + } + + public void setCornerRadius(float radius) { + this.cornerRadius = radius; + invalidateSelf(); + } + + private void startAnimation() { + animator = ValueAnimator.ofFloat(-1.0f, 2.0f); + animator.setDuration(2500); + animator.setStartDelay(500); + animator.setInterpolator(new LinearInterpolator()); + animator.setRepeatCount(ValueAnimator.INFINITE); + animator.setRepeatMode(ValueAnimator.RESTART); + + animator.addUpdateListener(animation -> { + sheenPosition = (float) animation.getAnimatedValue(); + invalidateSelf(); + }); + + animator.start(); + } + + @Override + public void draw(@NonNull Canvas canvas) { + Rect bounds = getBounds(); + + float gradientWidth = bounds.width() * 0.5f; + float gradientStart = bounds.width() * sheenPosition; + + LinearGradient gradient = new LinearGradient( + gradientStart - gradientWidth, 0, + gradientStart, bounds.height(), + new int[]{0x00FFFFFF, 0x14FFFFFF, 0x00FFFFFF}, + new float[]{0f, 0.5f, 1f}, + Shader.TileMode.CLAMP + ); + + paint.setShader(gradient); + + RectF rect = new RectF(bounds); + canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint); + } + + @Override + public void setAlpha(int alpha) { + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + paint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/SwitchItemView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/SwitchItemView.java new file mode 100644 index 0000000..48ffc29 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/SwitchItemView.java @@ -0,0 +1,187 @@ +package com.phoenix.gui.ui.dynamic; + +import android.content.Context; +import android.graphics.Typeface; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import com.phoenix.gui.ui.ThemeManager; +import java.util.Objects; + +public class SwitchItemView extends TaskItemView implements ThemeManager.OnThemeColorChangeListener { + + private int colorPrimary; + private MaterialSwitchView switchView; + private TextView titleText; + private LinearLayout subtitleContainer; + + private String lastRenderedText = null; + private String lastRenderedSubtitle = null; + private boolean lastRenderedState = false; + private boolean isInitialized = false; + + public SwitchItemView(@NonNull Context context, DynamicIslandManager.TaskItem task, float scale) { + super(context, task, scale); + } + + @Override + protected void init(Context context) { + colorPrimary = ThemeManager.getThemeColor(); + + setLayoutParams(new LayoutParams( + LayoutParams.MATCH_PARENT, + (int) (dpToPx(52) * scale) + )); + setPadding( + (int) (dpToPx(24) * scale), 0, + (int) (dpToPx(24) * scale), 0 + ); + + LinearLayout container = new LinearLayout(context); + container.setOrientation(LinearLayout.HORIZONTAL); + container.setGravity(Gravity.CENTER_VERTICAL); + container.setClickable(false); + addView(container, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + + switchView = new MaterialSwitchView(context); + switchView.setScaleX(scale * 1.1f); + switchView.setScaleY(scale * 1.1f); + switchView.setClickable(false); + LinearLayout.LayoutParams switchParams = new LinearLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + container.addView(switchView, switchParams); + + LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams( + (int) (dpToPx(12) * scale), 0 + ); + container.addView(new android.view.View(context), spacerParams); + + LinearLayout textContainer = new LinearLayout(context); + textContainer.setOrientation(LinearLayout.VERTICAL); + textContainer.setGravity(Gravity.CENTER_VERTICAL); + container.addView(textContainer, new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.MATCH_PARENT, 1f + )); + + titleText = new TextView(context); + titleText.setTextColor(com.phoenix.gui.ui.ThemeManager.getTextPrimary()); + titleText.setTextSize(15 * scale); + titleText.setTypeface(null, Typeface.BOLD); + titleText.setSingleLine(true); + titleText.setIncludeFontPadding(false); + textContainer.addView(titleText, new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT + )); + + subtitleContainer = new LinearLayout(context); + subtitleContainer.setOrientation(LinearLayout.HORIZONTAL); + subtitleContainer.setGravity(Gravity.CENTER_VERTICAL); + LinearLayout.LayoutParams subtitleContainerParams = new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT + ); + subtitleContainerParams.topMargin = (int) (dpToPx(2) * scale); + textContainer.addView(subtitleContainer, subtitleContainerParams); + + updateContent(); + } + + @Override + protected void updateContent() { + + if (!isInitialized) { + switchView.setChecked(task.switchState, false); + titleText.setText(task.text); + updateSubtitle(); + + lastRenderedText = task.text; + lastRenderedSubtitle = task.subtitle; + lastRenderedState = task.switchState; + isInitialized = true; + + android.util.Log.d("SwitchItemView", String.format( + "First render: id=%s, state=%s", task.identifier, task.switchState + )); + return; + } + + if (lastRenderedState != task.switchState) { + switchView.setChecked(task.switchState, true); + lastRenderedState = task.switchState; + + android.util.Log.d("SwitchItemView", String.format( + "State changed: id=%s, new=%s", task.identifier, task.switchState + )); + } + + if (!Objects.equals(lastRenderedText, task.text)) { + titleText.setText(task.text); + lastRenderedText = task.text; + } + + if (!Objects.equals(lastRenderedSubtitle, task.subtitle)) { + updateSubtitle(); + lastRenderedSubtitle = task.subtitle; + } + } + + private void updateSubtitle() { + subtitleContainer.removeAllViews(); + + if (task.subtitle != null && task.subtitle.contains("|")) { + String[] parts = task.subtitle.split("\\|", 2); + + TextView part1 = new TextView(getContext()); + part1.setText(parts[0]); + part1.setTextColor(colorPrimary); + part1.setTextSize(12 * scale); + part1.setTypeface(null, Typeface.BOLD); + part1.setSingleLine(true); + part1.setIncludeFontPadding(false); + subtitleContainer.addView(part1); + + LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams((int) (dpToPx(4) * scale), 0); + subtitleContainer.addView(new android.view.View(getContext()), spacerParams); + + TextView part2 = new TextView(getContext()); + part2.setText(parts[1]); + part2.setTextColor(com.phoenix.gui.ui.ThemeManager.getTextSecondary()); + part2.setTextSize(12 * scale); + part2.setSingleLine(true); + part2.setIncludeFontPadding(false); + subtitleContainer.addView(part2); + + } else if (task.subtitle != null) { + TextView subtitle = new TextView(getContext()); + subtitle.setText(task.subtitle); + subtitle.setTextColor(com.phoenix.gui.ui.ThemeManager.getTextSecondary()); + subtitle.setTextSize(12 * scale); + subtitle.setSingleLine(true); + subtitle.setIncludeFontPadding(false); + subtitleContainer.addView(subtitle); + } + } + + @Override + public void onThemeColorChanged(int newColor) { + this.colorPrimary = newColor; + updateSubtitle(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + ThemeManager.addListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + ThemeManager.removeListener(this); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/dynamic/TaskItemView.java b/app/src/main/java/com/phoenix/gui/ui/dynamic/TaskItemView.java new file mode 100644 index 0000000..d017c31 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/dynamic/TaskItemView.java @@ -0,0 +1,26 @@ +package com.phoenix.gui.ui.dynamic; + +import android.content.Context; +import android.widget.FrameLayout; +import androidx.annotation.NonNull; + +public abstract class TaskItemView extends FrameLayout { + + public DynamicIslandManager.TaskItem task; + protected float scale; + + public TaskItemView(@NonNull Context context, DynamicIslandManager.TaskItem task, float scale) { + super(context); + this.task = task; + this.scale = scale; + init(context); + } + + protected abstract void init(Context context); + + protected abstract void updateContent(); + + protected float dpToPx(float dp) { + return dp * getResources().getDisplayMetrics().density; + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/widgets/ColorPickerView.java b/app/src/main/java/com/phoenix/gui/ui/widgets/ColorPickerView.java new file mode 100644 index 0000000..6945043 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/widgets/ColorPickerView.java @@ -0,0 +1,139 @@ +package com.phoenix.gui.ui.widgets; + +// powered by qp + +import android.content.Context; +import android.graphics.*; +import android.graphics.drawable.GradientDrawable; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import com.phoenix.gui.ui.ThemeManager; + +public class ColorPickerView extends LinearLayout { + + private final ColorPaletteView paletteView; + private final SeekBar hueSlider; + private float currentHue; + private float currentSat; + private float currentVal; + + public ColorPickerView(Context context) { + super(context); + setOrientation(VERTICAL); + setPadding(0, dpToPx(5), 0, 0); + + float[] initialHsv = ThemeManager.getThemeColorHSV(); + currentHue = initialHsv[0]; + currentSat = initialHsv[1]; + currentVal = initialHsv[2]; + + paletteView = new ColorPaletteView(context); + addView(paletteView, new LayoutParams(LayoutParams.MATCH_PARENT, dpToPx(100))); + + hueSlider = new SeekBar(context); + hueSlider.setMax(360); + hueSlider.setProgress((int) currentHue); + + int[] hueColors = new int[361]; + for (int i = 0; i < hueColors.length; i++) { + hueColors[i] = Color.HSVToColor(new float[]{i, 1f, 1f}); + } + GradientDrawable hueGradient = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, hueColors); + hueSlider.setProgressDrawable(hueGradient); + + LayoutParams sliderParams = new LayoutParams(LayoutParams.MATCH_PARENT, dpToPx(10)); + sliderParams.setMargins(0, dpToPx(12), 0, 0); + hueSlider.setLayoutParams(sliderParams); + hueSlider.getThumb().setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)); + + hueSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + currentHue = progress; + paletteView.setHue(currentHue); + updateThemeColor(); + } + } + @Override public void onStartTrackingTouch(SeekBar seekBar) {} + @Override public void onStopTrackingTouch(SeekBar seekBar) {} + }); + + addView(hueSlider); + } + + private void updateThemeColor() { + float[] hsv = {currentHue, currentSat, currentVal}; + int color = Color.HSVToColor(hsv); + ThemeManager.setThemeColor(color); + } + + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } + + private class ColorPaletteView extends View { + private final Paint saturationPaint, valuePaint; + private final Paint indicatorPaint; + private Shader valueShader, saturationShader; + private float hue; + private float indicatorX, indicatorY; + + public ColorPaletteView(Context context) { + super(context); + saturationPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + valuePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + indicatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + indicatorPaint.setStyle(Paint.Style.STROKE); + indicatorPaint.setStrokeWidth(dpToPx(2)); + indicatorPaint.setColor(Color.WHITE); + indicatorPaint.setShadowLayer(dpToPx(2), 0, 0, Color.BLACK); + setHue(currentHue); + } + + public void setHue(float hue) { + this.hue = hue; + if (getWidth() > 0) { + int hueColor = Color.HSVToColor(new float[]{hue, 1f, 1f}); + saturationShader = new LinearGradient(0, 0, getWidth(), 0, Color.WHITE, hueColor, Shader.TileMode.CLAMP); + saturationPaint.setShader(saturationShader); + } + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + valueShader = new LinearGradient(0, 0, 0, h, Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP); + valuePaint.setShader(valueShader); + setHue(hue); + indicatorX = w * currentSat; + indicatorY = h * (1 - currentVal); + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.drawRect(0, 0, getWidth(), getHeight(), saturationPaint); + canvas.drawRect(0, 0, getWidth(), getHeight(), valuePaint); + canvas.drawCircle(indicatorX, indicatorY, dpToPx(6), indicatorPaint); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + indicatorX = Math.max(0, Math.min(getWidth(), event.getX())); + indicatorY = Math.max(0, Math.min(getHeight(), event.getY())); + currentSat = indicatorX / getWidth(); + currentVal = 1 - (indicatorY / getHeight()); + updateThemeColor(); + invalidate(); + return true; + } + return super.onTouchEvent(event); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSlider.java b/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSlider.java new file mode 100644 index 0000000..aa1bb9c --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSlider.java @@ -0,0 +1,130 @@ +package com.phoenix.gui.ui.widgets; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.view.MotionEvent; +import android.view.View; + +import com.phoenix.gui.config.ConfigManager; +import com.phoenix.gui.ui.ThemeManager; + +public class CustomSlider extends View implements ThemeManager.OnThemeColorChangeListener { + private final String moduleName; + private final String configKey; + private final int min; + private final int max; + private final OnValueChangedListener onValueChanged; + + private float progress = 0f; + private float scaleFactor = 1.0f; + + private final Paint trackPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint thumbFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint thumbStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + public interface OnValueChangedListener { + void onValueChanged(int value); + } + + public CustomSlider(Context context, String moduleName, String configKey, + int min, int max, int initialValue, OnValueChangedListener listener) { + super(context); + this.moduleName = moduleName; + this.configKey = configKey; + this.min = min; + this.max = max; + this.onValueChanged = listener; + + updatePaints(); + + thumbFillPaint.setColor(ThemeManager.getTextPrimary()); + + setValue(initialValue, false); + + ThemeManager.addListener(this); + } + + public void updateScale(float scaleFactor) { + this.scaleFactor = scaleFactor; + updatePaints(); + invalidate(); + } + + private void updatePaints() { + trackPaint.setStrokeCap(Paint.Cap.ROUND); + trackPaint.setStrokeWidth(scaledDp(3f)); + thumbStrokePaint.setStyle(Paint.Style.STROKE); + thumbStrokePaint.setStrokeWidth(scaledDp(2f)); + } + + private float scaledDp(float baseDp) { + return dpToPx(baseDp) * scaleFactor; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + int themeColor = ThemeManager.getThemeColor(); + thumbStrokePaint.setColor(themeColor); + + float trackY = getHeight() / 2f; + float thumbRadius = getHeight() / 2f - scaledDp(3f); + float thumbX = thumbRadius + (getWidth() - 2 * thumbRadius) * progress; + + trackPaint.setColor(ThemeManager.getStateDisabled()); + canvas.drawLine(thumbX, trackY, getWidth() - thumbRadius, trackY, trackPaint); + + trackPaint.setColor(themeColor); + canvas.drawLine(thumbRadius, trackY, thumbX, trackY, trackPaint); + + canvas.drawCircle(thumbX, trackY, thumbRadius, thumbFillPaint); + canvas.drawCircle(thumbX, trackY, thumbRadius, thumbStrokePaint); + } + + public void setValue(int value, boolean notify) { + progress = (float)(value - min) / (max - min); + if (notify) { + onValueChanged.onValueChanged(value); + + ConfigManager.saveModuleConfig(moduleName, configKey, value); + } + invalidate(); + } + + public int getValue() { + return min + (int)(progress * (max - min)); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + float newProgress = event.getX() / getWidth(); + progress = Math.max(0f, Math.min(1f, newProgress)); + int currentValue = getValue(); + onValueChanged.onValueChanged(currentValue); + ConfigManager.saveModuleConfig(moduleName, configKey, currentValue); + invalidate(); + return true; + } + return super.onTouchEvent(event); + } + + @Override + public void onThemeColorChanged(int newColor) { + invalidate(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + ThemeManager.removeListener(this); + } + + private float dpToPx(float dp) { + return dp * getResources().getDisplayMetrics().density; + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSwitch.java b/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSwitch.java new file mode 100644 index 0000000..051c5f4 --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/widgets/CustomSwitch.java @@ -0,0 +1,123 @@ +package com.phoenix.gui.ui.widgets; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.view.View; +import android.view.animation.DecelerateInterpolator; + +import com.phoenix.gui.config.ConfigManager; +import com.phoenix.gui.ui.ThemeManager; + +public class CustomSwitch extends View implements ThemeManager.OnThemeColorChangeListener { + private final String moduleName; + private final String configKey; + private final OnStateChangedListener onStateChanged; + + private boolean isChecked = false; + private float thumbX = 0f; + private float scaleFactor = 1.0f; + + private final Paint trackPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint thumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final RectF trackRect = new RectF(); + private ValueAnimator thumbAnimator; + + public interface OnStateChangedListener { + void onStateChanged(boolean enabled); + } + + public CustomSwitch(Context context, String moduleName, String configKey, + boolean initialState, OnStateChangedListener listener) { + super(context); + this.moduleName = moduleName; + this.configKey = configKey; + this.onStateChanged = listener; + + thumbPaint.setColor(ThemeManager.getTextPrimary()); + setChecked(initialState, false); + + setOnClickListener(v -> setChecked(!isChecked, true)); + + ThemeManager.addListener(this); + } + + public void updateScale(float scaleFactor) { + this.scaleFactor = scaleFactor; + + setChecked(isChecked, false); + invalidate(); + } + + private float scaledDp(float baseDp) { + return dpToPx(baseDp) * scaleFactor; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + int themeColor = ThemeManager.getThemeColor(); + + trackRect.set(0f, 0f, getWidth(), getHeight()); + trackPaint.setColor(isChecked ? themeColor : ThemeManager.getStateDisabled()); + canvas.drawRoundRect(trackRect, getHeight() / 2f, getHeight() / 2f, trackPaint); + + float thumbRadius = getHeight() / 2f - scaledDp(2f); + canvas.drawCircle(thumbX, getHeight() / 2f, thumbRadius, thumbPaint); + } + + public void setChecked(boolean checked, boolean animate) { + if (isChecked == checked && thumbAnimator != null && thumbAnimator.isRunning()) return; + + isChecked = checked; + float thumbRadius = getHeight() / 2f - scaledDp(2f); + float targetX = isChecked ? getWidth() - thumbRadius - scaledDp(2f) : thumbRadius + scaledDp(2f); + + if (animate) { + if (thumbAnimator != null) thumbAnimator.cancel(); + thumbAnimator = ValueAnimator.ofFloat(thumbX, targetX); + thumbAnimator.setDuration(200); + thumbAnimator.setInterpolator(new DecelerateInterpolator()); + thumbAnimator.addUpdateListener(animation -> { + thumbX = (float) animation.getAnimatedValue(); + invalidate(); + }); + thumbAnimator.start(); + } else { + thumbX = targetX; + invalidate(); + } + + onStateChanged.onStateChanged(isChecked); + ConfigManager.saveModuleConfig(moduleName, configKey, isChecked); + } + + public boolean isChecked() { + return isChecked; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + setChecked(isChecked, false); + } + + @Override + public void onThemeColorChanged(int newColor) { + invalidate(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + ThemeManager.removeListener(this); + if (thumbAnimator != null) thumbAnimator.cancel(); + } + + private float dpToPx(float dp) { + return dp * getResources().getDisplayMetrics().density; + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/widgets/ModeChips.java b/app/src/main/java/com/phoenix/gui/ui/widgets/ModeChips.java new file mode 100644 index 0000000..3c3b26c --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/widgets/ModeChips.java @@ -0,0 +1,226 @@ +package com.phoenix.gui.ui.widgets; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.OvershootInterpolator; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.phoenix.gui.config.ConfigManager; +import com.phoenix.gui.ui.ThemeManager; + +import java.util.ArrayList; +import java.util.List; + +public class ModeChips extends LinearLayout implements ThemeManager.OnThemeColorChangeListener { + private final String moduleName; + private final String configKey; + private final List modes; + private final OnModeChangedListener onModeChanged; + + private int selectedIndex; + private final List chips = new ArrayList<>(); + private final List chipBackgrounds = new ArrayList<>(); + private float scaleFactor = 1.0f; + + public interface OnModeChangedListener { + void onModeChanged(int index, String mode); + } + + public ModeChips(Context context, String moduleName, String configKey, + List modes, int initialMode, OnModeChangedListener listener) { + super(context); + this.moduleName = moduleName; + this.configKey = configKey; + this.modes = modes; + this.onModeChanged = listener; + this.selectedIndex = initialMode; + + setOrientation(HORIZONTAL); + + for (int i = 0; i < modes.size(); i++) { + TextView chip = createChip(modes.get(i), i); + chips.add(chip); + addView(chip); + + if (i < modes.size() - 1) { + addView(createSpacer()); + } + } + + updateSelection(initialMode, false); + ThemeManager.addListener(this); + } + + public void updateScale(float scaleFactor) { + this.scaleFactor = scaleFactor; + + for (TextView chip : chips) { + chip.setTextSize(11f * scaleFactor); + int paddingH = scaled(12); + int paddingV = scaled(6); + chip.setPadding(paddingH, paddingV, paddingH, paddingV); + } + + for (int i = 0; i < getChildCount(); i++) { + if (getChildAt(i) instanceof LinearLayout) { + LinearLayout spacer = (LinearLayout) getChildAt(i); + if (spacer.getChildCount() == 0) { + spacer.setLayoutParams(new LayoutParams(scaled(4), LayoutParams.MATCH_PARENT)); + } + } + } + + updateSelection(selectedIndex, false); + + requestLayout(); + } + + private int scaled(int baseDp) { + return (int) (dpToPx(baseDp) * scaleFactor); + } + + private TextView createChip(String text, final int index) { + TextView chip = new TextView(getContext()); + chip.setText(text); + chip.setTextSize(11f * scaleFactor); + chip.setGravity(Gravity.CENTER); + chip.setPadding(scaled(12), scaled(6), scaled(12), scaled(6)); + + LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + params.setMargins(0, 0, 0, 0); + chip.setLayoutParams(params); + + GradientDrawable bg = new GradientDrawable(); + bg.setCornerRadius(scaled(8)); + chipBackgrounds.add(bg); + chip.setBackground(bg); + + chip.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + + v.animate() + .scaleX(0.9f) + .scaleY(0.9f) + .setDuration(100) + .start(); + return true; + + case MotionEvent.ACTION_UP: + + v.animate() + .scaleX(1f) + .scaleY(1f) + .setDuration(100) + .start(); + + if (event.getX() >= 0 && event.getX() <= v.getWidth() && + event.getY() >= 0 && event.getY() <= v.getHeight()) { + + if (selectedIndex != index) { + updateSelection(index, true); + } + } + return true; + + case MotionEvent.ACTION_CANCEL: + + v.animate() + .scaleX(1f) + .scaleY(1f) + .setDuration(100) + .start(); + return true; + } + return false; + } + }); + + return chip; + } + + private LinearLayout createSpacer() { + LinearLayout spacer = new LinearLayout(getContext()); + spacer.setLayoutParams(new LayoutParams(scaled(4), LayoutParams.MATCH_PARENT)); + return spacer; + } + + private void updateSelection(int newIndex, boolean animate) { + int oldIndex = selectedIndex; + selectedIndex = newIndex; + + for (int i = 0; i < chips.size(); i++) { + applyChipStyle(i, i == newIndex); + } + + if (animate && newIndex >= 0 && newIndex < chips.size()) { + TextView newChip = chips.get(newIndex); + newChip.setScaleX(0.9f); + newChip.setScaleY(0.9f); + newChip.animate() + .scaleX(1f) + .scaleY(1f) + .setDuration(200) + .setInterpolator(new OvershootInterpolator(2f)) + .start(); + + onModeChanged.onModeChanged(newIndex, modes.get(newIndex)); + ConfigManager.saveModuleConfig(moduleName, configKey, newIndex); + } + } + + private void applyChipStyle(int index, boolean selected) { + if (index < 0 || index >= chips.size()) return; + + TextView chip = chips.get(index); + GradientDrawable bg = chipBackgrounds.get(index); + + if (selected) { + bg.setColor(ThemeManager.getThemeColor()); + bg.setCornerRadius(scaled(8)); + chip.setTextColor(ThemeManager.getTextOnTheme()); + chip.setTypeface(Typeface.DEFAULT_BOLD); + } else { + bg.setColor(ThemeManager.getBgDisabled()); + bg.setCornerRadius(scaled(8)); + chip.setTextColor(ThemeManager.getTextSecondary()); + chip.setTypeface(Typeface.DEFAULT); + } + } + + public String getSelectedMode() { + return modes.get(selectedIndex); + } + + public int getSelectedIndex() { + return selectedIndex; + } + + @Override + public void onThemeColorChanged(int newColor) { + updateSelection(selectedIndex, false); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + ThemeManager.removeListener(this); + + for (TextView chip : chips) { + chip.animate().cancel(); + } + } + + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } +} diff --git a/app/src/main/java/com/phoenix/gui/ui/widgets/SubMenuPanel.java b/app/src/main/java/com/phoenix/gui/ui/widgets/SubMenuPanel.java new file mode 100644 index 0000000..607982c --- /dev/null +++ b/app/src/main/java/com/phoenix/gui/ui/widgets/SubMenuPanel.java @@ -0,0 +1,498 @@ +package com.phoenix.gui.ui.widgets; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.phoenix.gui.module.Module; +import com.phoenix.gui.ui.ThemeManager; + +import java.util.ArrayList; +import java.util.List; + +public class SubMenuPanel extends LinearLayout implements ThemeManager.OnThemeColorChangeListener { + private final Module module; + private boolean isOpen = false; + private ValueAnimator heightAnimator; + private Runnable onStateChangedListener; + + private float scaleFactor = 1.0f; + + private final List scalableWidgets = new ArrayList<>(); + private final List themeButtons = new ArrayList<>(); + private final int[] themeColors = { + 0xFF96CCFF, // Default + 0xFF4CAF50, // Green + 0xFFBB86FC, // Purple + 0xFFFF9800, // Orange + 0xFFE91E63 // Pink + }; + + public SubMenuPanel(Context context, Module module, float scaleFactor) { + super(context); + this.module = module; + this.scaleFactor = scaleFactor; + + setOrientation(VERTICAL); + setBackgroundColor(Color.TRANSPARENT); + updatePadding(); + setVisibility(View.GONE); + setAlpha(0f); + + LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, 0); + params.setMargins(scaled(6), scaled(2), scaled(6), 0); + setLayoutParams(params); + } + + public SubMenuPanel(Context context, Module module) { + this(context, module, 1.0f); + } + + public void updateScale(float scaleFactor) { + this.scaleFactor = scaleFactor; + updatePadding(); + LayoutParams params = (LayoutParams) getLayoutParams(); + params.setMargins(scaled(6), scaled(2), scaled(6), 0); + setLayoutParams(params); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + updateChildScale(child); + } + for (ScalableWidget widget : scalableWidgets) { + widget.updateScale(scaleFactor); + } + requestLayout(); + } + + private void updatePadding() { + int padding = scaled(4); + setPadding(padding, 0, padding, scaled(4)); + } + + private void updateChildScale(View view) { + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + ViewGroup.LayoutParams lp = group.getLayoutParams(); + if (lp instanceof MarginLayoutParams) { + MarginLayoutParams mlp = (MarginLayoutParams) lp; + mlp.topMargin = scaled(2); + mlp.bottomMargin = scaled(2); + } + for (int i = 0; i < group.getChildCount(); i++) { + updateChildScale(group.getChildAt(i)); + } + } else if (view instanceof TextView) { + TextView tv = (TextView) view; + float baseSize = 11f; + if (tv.getCurrentTextColor() == ThemeManager.getTextTertiary()) { + baseSize = 10f; + } + tv.setTextSize(baseSize * scaleFactor); + } + } + + private int scaled(int baseDp) { + return (int) (dpToPx(baseDp) * scaleFactor); + } + + public void addSlider(String label, int min, int max, int defaultValue, String format, + CustomSlider.OnValueChangedListener onValueChanged) { + LinearLayout container = new LinearLayout(getContext()); + container.setOrientation(VERTICAL); + LayoutParams containerParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + containerParams.setMargins(0, scaled(2), 0, scaled(2)); + container.setLayoutParams(containerParams); + + LinearLayout labelRow = new LinearLayout(getContext()); + labelRow.setOrientation(HORIZONTAL); + + TextView labelText = new TextView(getContext()); + labelText.setText(label); + labelText.setTextSize(11f * scaleFactor); + labelText.setTextColor(ThemeManager.getTextSecondary()); + LayoutParams labelParams = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); + labelText.setLayoutParams(labelParams); + labelRow.addView(labelText); + + TextView valueText = new TextView(getContext()); + valueText.setTextSize(11f * scaleFactor); + valueText.setTextColor(ThemeManager.getTextSecondary()); + labelRow.addView(valueText); + + container.addView(labelRow); + + CustomSlider slider = new CustomSlider(getContext(), module.getName(), label.toLowerCase(), + min, max, defaultValue, value -> { + if ("%.2f".equals(format)) { + valueText.setText(String.format("%.2f", value / 100.0)); + } else { + valueText.setText(String.valueOf(value)); + } + onValueChanged.onValueChanged(value); + }); + + slider.updateScale(scaleFactor); + LayoutParams sliderParams = new LayoutParams(LayoutParams.MATCH_PARENT, scaled(20)); + sliderParams.setMargins(0, scaled(2), 0, 0); + container.addView(slider, sliderParams); + scalableWidgets.add(new ScalableWidget(slider, labelText, valueText, sliderParams, 20)); + if ("%.2f".equals(format)) { + valueText.setText(String.format("%.2f", defaultValue / 100.0)); + } else { + valueText.setText(String.valueOf(defaultValue)); + } + addView(container); + } + + public void addShortcutSwitch(boolean defaultValue) { + LinearLayout container = new LinearLayout(getContext()); + container.setOrientation(HORIZONTAL); + container.setGravity(Gravity.CENTER_VERTICAL); + LayoutParams containerParams = new LayoutParams(LayoutParams.MATCH_PARENT, scaled(28)); + containerParams.setMargins(0, scaled(2), 0, scaled(2)); + container.setLayoutParams(containerParams); + + TextView labelText = new TextView(getContext()); + labelText.setText("Shortcut"); + labelText.setTextSize(11f * scaleFactor); + labelText.setTextColor(ThemeManager.getTextSecondary()); + LayoutParams labelParams = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); + labelText.setLayoutParams(labelParams); + container.addView(labelText); + + CustomSwitch switchView = new CustomSwitch(getContext(), module.getName(), "shortcut", + defaultValue, enabled -> { + module.toggleShortcut(enabled); + }); + + switchView.updateScale(scaleFactor); + LayoutParams switchParams = new LayoutParams(scaled(32), scaled(18)); + container.addView(switchView, switchParams); + scalableWidgets.add(new ScalableWidget(switchView, labelText, containerParams, switchParams, 28, 32, 18)); + addView(container); + } + + public void addSwitch(String label, boolean defaultValue, CustomSwitch.OnStateChangedListener onStateChanged) { + LinearLayout container = new LinearLayout(getContext()); + container.setOrientation(HORIZONTAL); + container.setGravity(Gravity.CENTER_VERTICAL); + LayoutParams containerParams = new LayoutParams(LayoutParams.MATCH_PARENT, scaled(28)); + containerParams.setMargins(0, scaled(2), 0, scaled(2)); + container.setLayoutParams(containerParams); + + TextView labelText = new TextView(getContext()); + labelText.setText(label); + labelText.setTextSize(11f * scaleFactor); + labelText.setTextColor(ThemeManager.getTextSecondary()); + LayoutParams labelParams = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); + labelText.setLayoutParams(labelParams); + container.addView(labelText); + + CustomSwitch switchView = new CustomSwitch(getContext(), module.getName(), label.toLowerCase(), + defaultValue, onStateChanged); + + switchView.updateScale(scaleFactor); + LayoutParams switchParams = new LayoutParams(scaled(32), scaled(18)); + container.addView(switchView, switchParams); + scalableWidgets.add(new ScalableWidget(switchView, labelText, containerParams, switchParams, 28, 32, 18)); + addView(container); + } + + public void addMode(String label, List modes, int defaultMode, ModeChips.OnModeChangedListener onModeChanged) { + LinearLayout container = new LinearLayout(getContext()); + container.setOrientation(VERTICAL); + LayoutParams containerParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + containerParams.setMargins(0, scaled(2), 0, scaled(2)); + container.setLayoutParams(containerParams); + + TextView labelText = new TextView(getContext()); + labelText.setText(label); + labelText.setTextSize(11f * scaleFactor); + labelText.setTextColor(ThemeManager.getTextSecondary()); + LayoutParams labelParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + labelParams.setMargins(0, 0, 0, scaled(2)); + labelText.setLayoutParams(labelParams); + container.addView(labelText); + + HorizontalScrollView scrollView = new HorizontalScrollView(getContext()); + scrollView.setHorizontalScrollBarEnabled(false); + scrollView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); + + ModeChips chips = new ModeChips(getContext(), module.getName(), label.toLowerCase(), + modes, defaultMode, onModeChanged); + + chips.updateScale(scaleFactor); + scrollView.addView(chips); + container.addView(scrollView); + scalableWidgets.add(new ScalableWidget(chips, labelText, null, null, 0)); + addView(container); + } + + public void addText(String text) { + TextView textView = new TextView(getContext()); + textView.setText(text); + textView.setTextSize(10f * scaleFactor); + textView.setTextColor(ThemeManager.getTextTertiary()); + LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + params.setMargins(0, scaled(2), 0, scaled(2)); + textView.setLayoutParams(params); + addView(textView); + } + + + public void addCustomView(View view) { + if (view.getLayoutParams() == null) { + LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + params.setMargins(0, scaled(4), 0, scaled(4)); + view.setLayoutParams(params); + } + addView(view); + } + + public void addThemeButtons() { + themeButtons.clear(); + LinearLayout container = new LinearLayout(getContext()); + container.setOrientation(HORIZONTAL); + + String[] themeNames = {"Default", "Green", "Purple", "Orange", "Pink"}; + Runnable[] themeActions = { + ThemeManager::applyDefaultTheme, + ThemeManager::applyGreenTheme, + ThemeManager::applyPurpleTheme, + ThemeManager::applyOrangeTheme, + ThemeManager::applyPinkTheme + }; + + for(int i = 0; i < themeNames.length; i++) { + TextView button = new TextView(getContext()); + button.setText(themeNames[i]); + button.setTextSize(10f * scaleFactor); + button.setGravity(Gravity.CENTER); + int paddingH = scaled(8); + int paddingV = scaled(5); + button.setPadding(paddingH, paddingV, paddingH, paddingV); + + + button.setBackground(new GradientDrawable()); + + final Runnable action = themeActions[i]; + button.setOnClickListener(v -> action.run()); + + LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + if (i > 0) { + params.leftMargin = scaled(4); + } + container.addView(button, params); + themeButtons.add(button); + } + + HorizontalScrollView scrollView = new HorizontalScrollView(getContext()); + scrollView.setHorizontalScrollBarEnabled(false); + scrollView.addView(container); + addCustomView(scrollView); + + updateThemeButtonStyles(); + } + + private void updateThemeButtonStyles() { + int currentThemeColor = ThemeManager.getThemeColor(); + for (int i = 0; i < themeButtons.size(); i++) { + TextView button = themeButtons.get(i); + GradientDrawable bg = (GradientDrawable) button.getBackground(); + + boolean isSelected = currentThemeColor == themeColors[i]; + + if (isSelected) { + bg.setColor(ThemeManager.getThemeColor()); + button.setTextColor(ThemeManager.getTextOnTheme()); + } else { + bg.setColor(ThemeManager.getBgDisabled()); + button.setTextColor(ThemeManager.getTextPrimary()); + } + bg.setCornerRadius(scaled(6)); + } + } + + + + public void toggle() { + toggle(null); + } + + public void toggle(Runnable onComplete) { + if (isOpen) { + collapse(onComplete); + } else { + expand(onComplete); + } + } + + private void expand(Runnable onComplete) { + isOpen = true; + setVisibility(View.VISIBLE); + measure( + MeasureSpec.makeMeasureSpec(((ViewGroup)getParent()).getWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) + ); + int targetHeight = getMeasuredHeight(); + if (heightAnimator != null) heightAnimator.cancel(); + getLayoutParams().height = 0; + heightAnimator = ValueAnimator.ofInt(0, targetHeight); + heightAnimator.setDuration(250); + heightAnimator.addUpdateListener(animation -> { + ViewGroup.LayoutParams params = getLayoutParams(); + params.height = (int) animation.getAnimatedValue(); + setLayoutParams(params); + }); + heightAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + ViewGroup.LayoutParams params = getLayoutParams(); + params.height = LayoutParams.WRAP_CONTENT; + setLayoutParams(params); + if (onComplete != null) onComplete.run(); + if (onStateChangedListener != null) onStateChangedListener.run(); + } + }); + heightAnimator.start(); + setAlpha(0f); + animate().alpha(1f).setDuration(250).start(); + } + + public void collapse(Runnable onComplete) { + if (!isOpen) { + if (onComplete != null) onComplete.run(); + return; + } + isOpen = false; + int currentHeight = getHeight(); + getLayoutParams().height = currentHeight; + if (heightAnimator != null) heightAnimator.cancel(); + animate().alpha(0f).setDuration(250).start(); + heightAnimator = ValueAnimator.ofInt(currentHeight, 0); + heightAnimator.setDuration(250); + heightAnimator.addUpdateListener(animation -> { + ViewGroup.LayoutParams params = getLayoutParams(); + params.height = (int) animation.getAnimatedValue(); + setLayoutParams(params); + }); + heightAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setVisibility(View.GONE); + if (onComplete != null) onComplete.run(); + if (onStateChangedListener != null) onStateChangedListener.run(); + } + }); + heightAnimator.start(); + } + + public void forceCollapse() { + if (heightAnimator != null) heightAnimator.cancel(); + isOpen = false; + setVisibility(View.GONE); + setAlpha(0f); + getLayoutParams().height = 0; + } + + public boolean isOpen() { + return isOpen; + } + + public void setOnStateChangedListener(Runnable listener) { + onStateChangedListener = listener; + } + + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } + + // 3. 实现 onThemeColorChanged 方法 + @Override + public void onThemeColorChanged(int newColor) { + updateThemeButtonStyles(); + } + + // 4. 注册和注销监听器 + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + ThemeManager.addListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + ThemeManager.removeListener(this); + } + + private class ScalableWidget { + View widget; + TextView label; + TextView valueText; + LayoutParams sliderParams; + LayoutParams containerParams; + LayoutParams switchParams; + int baseSliderHeight; + int baseContainerHeight; + int baseSwitchWidth; + int baseSwitchHeight; + + ScalableWidget(View widget, TextView label, TextView valueText, LayoutParams sliderParams, int baseSliderHeight) { + this.widget = widget; + this.label = label; + this.valueText = valueText; + this.sliderParams = sliderParams; + this.baseSliderHeight = baseSliderHeight; + } + + ScalableWidget(View widget, TextView label, LayoutParams containerParams, LayoutParams switchParams, + int baseContainerHeight, int baseSwitchWidth, int baseSwitchHeight) { + this.widget = widget; + this.label = label; + this.containerParams = containerParams; + this.switchParams = switchParams; + this.baseContainerHeight = baseContainerHeight; + this.baseSwitchWidth = baseSwitchWidth; + this.baseSwitchHeight = baseSwitchHeight; + } + + void updateScale(float scale) { + if (label != null) { + label.setTextSize(11f * scale); + } + if (valueText != null) { + valueText.setTextSize(11f * scale); + } + if (sliderParams != null) { + sliderParams.height = scaled(baseSliderHeight); + sliderParams.setMargins(0, scaled(2), 0, 0); + } + if (containerParams != null) { + containerParams.height = scaled(baseContainerHeight); + containerParams.setMargins(0, scaled(2), 0, scaled(2)); + } + if (switchParams != null) { + switchParams.width = scaled(baseSwitchWidth); + switchParams.height = scaled(baseSwitchHeight); + } + if (widget instanceof CustomSlider) { + ((CustomSlider) widget).updateScale(scale); + } else if (widget instanceof CustomSwitch) { + ((CustomSwitch) widget).updateScale(scale); + } else if (widget instanceof ModeChips) { + ((ModeChips) widget).updateScale(scale); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 0000000..fb69f7b --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..58b90a4 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +