first
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" />
|
||||
|
||||
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<!-- 权限基本用不到,小孩子不懂事调试用的 -->
|
||||
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:screenOrientation="landscape"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action
|
||||
android:name="android.intent.action.MAIN" />
|
||||
<category
|
||||
android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,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<Module, ShortcutButton> shortcutButtons = new HashMap<>();
|
||||
private final List<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Module> modules = new ArrayList<>();
|
||||
private static final List<ModuleToggleListener> listeners = new ArrayList<>();
|
||||
private static final List<ShortcutToggleListener> 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<Module> getAllModules() {
|
||||
return new ArrayList<>(modules);
|
||||
}
|
||||
|
||||
public static List<Module> getModulesByCategory(ModuleCategory category) {
|
||||
List<Module> 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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.phoenix.gui.module;
|
||||
|
||||
public interface ModuleToggleListener {
|
||||
void onModuleToggled(Module module);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.phoenix.gui.module;
|
||||
|
||||
public interface ShortcutToggleListener {
|
||||
void onShortcutToggled(Module module, boolean enabled);
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<ArraylistModule> modules;
|
||||
private Map<String, ArraylistItemView> 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<View> 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<ArraylistModule> getModules() { return this.modules; }
|
||||
}
|
||||
@@ -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 "动态取色不可用";
|
||||
}
|
||||
}
|
||||
@@ -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<MenuView> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<Module, ModuleItemView> 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<Module> 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<Module, ModuleItemView> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<OnThemeColorChangeListener> 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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
// 工具类
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<TaskItem> tasks = new CopyOnWriteArrayList<>();
|
||||
private final List<StateChangeListener> 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<TaskItem> 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<TaskItem> getVisibleTasks() {
|
||||
List<TaskItem> activeSwitchTasks = new ArrayList<>();
|
||||
List<TaskItem> 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<TaskItem> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<DynamicIslandManager.TaskItem> 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<DynamicIslandManager.TaskItem> 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String> modes;
|
||||
private final OnModeChangedListener onModeChanged;
|
||||
|
||||
private int selectedIndex;
|
||||
private final List<TextView> chips = new ArrayList<>();
|
||||
private final List<GradientDrawable> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<ScalableWidget> scalableWidgets = new ArrayList<>();
|
||||
private final List<TextView> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,206 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- 容器 -->
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/gui_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#F5F5F5"
|
||||
android:visibility="gone">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/dynamic_island_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|center_horizontal"
|
||||
android:layout_marginTop="24dp">
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/float_ball_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/menus_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/shortcuts_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/arraylist_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/debug_panel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_margin="8dp"
|
||||
android:background="#E0FFFFFF"
|
||||
android:elevation="8dp"
|
||||
android:padding="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="GUI 控制"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_start"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="显示 GUI"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_stop"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="隐藏 GUI"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="灵动岛测试"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_test_progress"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="测试进度"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_test_switch"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="测试开关"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_hide_all"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="隐藏全部"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Arraylist 测试"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_toggle_arraylist"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="切换 Arraylist"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_add_random_module"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="添加随机"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_remove_random_module"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="移除随机"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_reset_arraylist"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="重置"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="设置"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_username"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:hint="用户名"
|
||||
android:text="User"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="缩放"
|
||||
android:textSize="10sp" />
|
||||
|
||||
<!-- 缩放我推荐是不要用,难搞 -->
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seek_scale"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="150"
|
||||
android:progress="70" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_save"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:text="保存设置"
|
||||
android:textSize="11sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_toggle_debug"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_margin="8dp"
|
||||
android:elevation="8dp"
|
||||
android:text="M"
|
||||
android:textSize="6sp" />
|
||||
|
||||
<!-- 爱慕超人 -->
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||
</style>
|
||||
|
||||
<style name="AppTheme" parent="Base.AppTheme" />
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Phoen1xGUI</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Base application theme using AppCompat -->
|
||||
<style name="Theme.Phoen1xGUI" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Primary brand color -->
|
||||
<item name="colorPrimary">#96CCFF</item>
|
||||
<item name="colorPrimaryDark">#1a1a2e</item>
|
||||
<item name="colorAccent">#4CAF50</item>
|
||||
|
||||
<!-- Status bar color -->
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
|
||||
<!-- Window flags for edge-to-edge -->
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:windowTranslucentStatus">false</item>
|
||||
<item name="android:windowTranslucentNavigation">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older than API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,5 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.aapt2FromMavenOverride=/home/ph/app/aapt2
|
||||
@@ -0,0 +1,26 @@
|
||||
[versions]
|
||||
agp = "8.13.0"
|
||||
kotlin = "2.0.21"
|
||||
coreKtx = "1.10.1"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
lifecycleRuntimeKtx = "2.6.1"
|
||||
appcompat = "1.6.1"
|
||||
activity = "1.8.0"
|
||||
material = "1.12.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#Fri Oct 03 19:51:02 CST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,7 @@
|
||||
使用 gradle 8.13 构建
|
||||
aide 是构建不了的
|
||||
androidide 需要自己迁移 src
|
||||
售出不退
|
||||
含少量屎山
|
||||
|
||||
灵动岛名称为 Phoen1xGUI 的即为 v2.0 版本,该版本优化了性能。
|
||||