[controller] rediseño y actualización en interacción de los botones atado a eventos
This commit is contained in:
parent
5eac64be96
commit
af7655b572
|
|
@ -112,12 +112,16 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
|
|
||||||
// Cassette Deck UI
|
// Cassette Deck UI
|
||||||
private LinearLayout deckContainer;
|
private LinearLayout deckContainer;
|
||||||
private Button btnServerControl;
|
private ProgressButton btnServerControl;
|
||||||
private ObjectAnimator fusionAnimator;
|
private ObjectAnimator fusionAnimator;
|
||||||
private android.animation.ObjectAnimator exploreAnimator;
|
private android.animation.ObjectAnimator exploreAnimator;
|
||||||
private boolean isServerAlive = false;
|
private boolean isServerAlive = false;
|
||||||
private boolean isNegotiating = false;
|
private boolean isNegotiating = false;
|
||||||
private boolean isProxyDegraded = false;
|
private boolean isProxyDegraded = false;
|
||||||
|
private Boolean targetServerState = null;
|
||||||
|
private String serverTransitionText = "";
|
||||||
|
private final Handler timeoutHandler = new Handler(android.os.Looper.getMainLooper());
|
||||||
|
private Runnable timeoutRunnable;
|
||||||
private String currentTargetUrl = null;
|
private String currentTargetUrl = null;
|
||||||
private long pulseStartTime = 0;
|
private long pulseStartTime = 0;
|
||||||
|
|
||||||
|
|
@ -271,14 +275,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
// Listeners
|
// Listeners
|
||||||
watchdogControl.setOnClickListener(v -> {
|
watchdogControl.setOnClickListener(v -> {
|
||||||
boolean willBeEnabled = !prefs.getWatchdogEnable();
|
boolean willBeEnabled = !prefs.getWatchdogEnable();
|
||||||
if (willBeEnabled) {
|
setWatchdogState(willBeEnabled);
|
||||||
BiometricHelper.prompt(MainActivity.this,
|
|
||||||
getString(R.string.unlock_watchdog_title),
|
|
||||||
getString(R.string.unlock_watchdog_subtitle),
|
|
||||||
() -> setWatchdogState(true));
|
|
||||||
} else {
|
|
||||||
setWatchdogState(false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
btnClearLog.setOnClickListener(this);
|
btnClearLog.setOnClickListener(this);
|
||||||
btnCopyLog.setOnClickListener(this);
|
btnCopyLog.setOnClickListener(this);
|
||||||
|
|
@ -294,30 +291,42 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
button_save.setOnClickListener(this);
|
button_save.setOnClickListener(this);
|
||||||
|
|
||||||
btnServerControl.setOnClickListener(v -> {
|
btnServerControl.setOnClickListener(v -> {
|
||||||
// We're simplifying the action for now to test
|
// Ignore clicks if we are already waiting for a state change
|
||||||
if (btnServerControl.getText().toString().contains("Launch")) {
|
if (targetServerState != null) return;
|
||||||
btnServerControl.setText("Starting...");
|
|
||||||
btnServerControl.setAlpha(0.7f); // Efecto visual de "Cargando"
|
// Freeze the transition text and define the TARGET state
|
||||||
|
serverTransitionText = !isServerAlive ? "Booting..." : "Shutting down...";
|
||||||
|
targetServerState = !isServerAlive;
|
||||||
|
|
||||||
|
// Lock the UI and start infinite animation
|
||||||
|
updateUIColorsAndVisibility();
|
||||||
|
btnServerControl.startProgress();
|
||||||
|
|
||||||
|
// Set a hard timeout (45 seconds) as a safety net
|
||||||
|
timeoutRunnable = () -> {
|
||||||
|
if (targetServerState != null) {
|
||||||
|
targetServerState = null; // Abort transition
|
||||||
|
btnServerControl.stopProgress();
|
||||||
|
updateUIColorsAndVisibility();
|
||||||
|
addToLog("Warning: Server state transition timed out.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
timeoutHandler.postDelayed(timeoutRunnable, 45000);
|
||||||
|
|
||||||
|
// Execute the corresponding script command
|
||||||
|
if (!isServerAlive) {
|
||||||
startTermuxEnvironmentVisible("--start");
|
startTermuxEnvironmentVisible("--start");
|
||||||
} else {
|
} else {
|
||||||
// TODO: We'll add the VPN biometric validation here later
|
|
||||||
btnServerControl.setText("Stopping...");
|
|
||||||
btnServerControl.setAlpha(0.7f);
|
|
||||||
startTermuxEnvironmentVisible("--stop");
|
startTermuxEnvironmentVisible("--stop");
|
||||||
|
|
||||||
// Automatically turn off the Watchdog if we take down the server.
|
// Turn off Watchdog gracefully when stopping the server manually
|
||||||
if (prefs.getWatchdogEnable()) setWatchdogState(false);
|
if (prefs.getWatchdogEnable()) {
|
||||||
|
setWatchdogState(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Logic to open the WebView (PortalActivity)
|
// Logic to open the WebView (PortalActivity)
|
||||||
// button_browse_content.setOnClickListener(v -> {
|
|
||||||
// Intent intent = new Intent(MainActivity.this, PortalActivity.class);
|
|
||||||
// // We tell the Portal exactly where to go
|
|
||||||
// String urlToLoad = prefs.getEnable() ? "http://box/home" : "http://localhost:8085/home";
|
|
||||||
// intent.putExtra("TARGET_URL", urlToLoad);
|
|
||||||
// startActivity(intent);
|
|
||||||
// });
|
|
||||||
button_browse_content.setOnClickListener(v -> {
|
button_browse_content.setOnClickListener(v -> {
|
||||||
if (currentTargetUrl != null) {
|
if (currentTargetUrl != null) {
|
||||||
Intent intent = new Intent(MainActivity.this, PortalActivity.class);
|
Intent intent = new Intent(MainActivity.this, PortalActivity.class);
|
||||||
|
|
@ -442,7 +451,13 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
// We evaluate the results
|
// We evaluate the results
|
||||||
isNegotiating = false;
|
isNegotiating = false;
|
||||||
isServerAlive = boxAlive || localAlive;
|
isServerAlive = boxAlive || localAlive;
|
||||||
isProxyDegraded = !boxAlive && localAlive; // Tunnel on, but proxy dead
|
|
||||||
|
// If VPN is ON but box/proxy is dead, the tunnel is degraded (Orange).
|
||||||
|
if (prefs.getEnable()) {
|
||||||
|
isProxyDegraded = !boxAlive;
|
||||||
|
} else {
|
||||||
|
isProxyDegraded = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (boxAlive) {
|
if (boxAlive) {
|
||||||
currentTargetUrl = "http://box/home";
|
currentTargetUrl = "http://box/home";
|
||||||
|
|
@ -690,7 +705,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUI();
|
updateUI();
|
||||||
updateUIColorsAndVisibility(isServerAlive);
|
updateUIColorsAndVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleControlClick() {
|
private void handleControlClick() {
|
||||||
|
|
@ -795,13 +810,20 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
if (vpnOn) {
|
if (vpnOn) {
|
||||||
// The passive radar must also use the proxy to test the tunnel.
|
// The passive radar must also use the proxy to test the tunnel.
|
||||||
boxAlive = pingUrl("http://box/home", true);
|
boxAlive = pingUrl("http://box/home", true);
|
||||||
isProxyDegraded = !boxAlive && localAlive;
|
isProxyDegraded = !boxAlive;
|
||||||
} else {
|
} else {
|
||||||
isProxyDegraded = false;
|
isProxyDegraded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
isServerAlive = localAlive || boxAlive;
|
isServerAlive = localAlive || boxAlive;
|
||||||
|
|
||||||
|
// STATE MACHINE: Has the target state been reached?
|
||||||
|
if (targetServerState != null && isServerAlive == targetServerState) {
|
||||||
|
targetServerState = null; // Transition complete!
|
||||||
|
timeoutHandler.removeCallbacks(timeoutRunnable); // Cancel safety net
|
||||||
|
runOnUiThread(() -> btnServerControl.stopProgress()); // Unlock button
|
||||||
|
}
|
||||||
|
|
||||||
if (vpnOn && boxAlive) {
|
if (vpnOn && boxAlive) {
|
||||||
currentTargetUrl = "http://box/home";
|
currentTargetUrl = "http://box/home";
|
||||||
} else if (localAlive) {
|
} else if (localAlive) {
|
||||||
|
|
@ -818,49 +840,72 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
boolean isVpnActive = prefs.getEnable();
|
boolean isVpnActive = prefs.getEnable();
|
||||||
boolean isWatchdogOn = prefs.getWatchdogEnable();
|
boolean isWatchdogOn = prefs.getWatchdogEnable();
|
||||||
|
|
||||||
// Draw island
|
// Draw island (Tunnel LED colors)
|
||||||
if (dashboardManager != null) {
|
if (dashboardManager != null) {
|
||||||
dashboardManager.setTunnelState(isVpnActive, isProxyDegraded);
|
dashboardManager.setTunnelState(isVpnActive, isProxyDegraded);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw main button
|
// Draw main VPN button (ESPW)
|
||||||
|
if (!isServerAlive) {
|
||||||
|
// Lock and dim the VPN button if there is no server to connect to
|
||||||
|
button_control.setEnabled(false);
|
||||||
if (isVpnActive) {
|
if (isVpnActive) {
|
||||||
button_control.setText(R.string.control_disable);
|
button_control.setText(R.string.control_disable);
|
||||||
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, isServerAlive ? R.color.btn_vpn_on : R.color.btn_vpn_on_dim));
|
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on_dim));
|
||||||
} else {
|
} else {
|
||||||
button_control.setText(R.string.control_enable);
|
button_control.setText(R.string.control_enable);
|
||||||
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, isServerAlive ? R.color.btn_vpn_off : R.color.btn_vpn_off_dim));
|
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off_dim));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unlock if server is alive
|
||||||
|
button_control.setEnabled(true);
|
||||||
|
if (isVpnActive) {
|
||||||
|
button_control.setText(R.string.control_disable);
|
||||||
|
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on));
|
||||||
|
} else {
|
||||||
|
button_control.setText(R.string.control_enable);
|
||||||
|
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Draw Explore Content button
|
// Draw Explore Content button
|
||||||
|
// Ensure it is ALWAYS visible, never GONE
|
||||||
|
button_browse_content.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
if (!isServerAlive) {
|
if (!isServerAlive) {
|
||||||
// State 1: Stopped
|
// State 1: Stopped (Greyed out)
|
||||||
stopExplorePulse();
|
stopExplorePulse();
|
||||||
button_browse_content.setEnabled(false);
|
button_browse_content.setEnabled(false);
|
||||||
button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_disabled));
|
button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_disabled));
|
||||||
button_browse_content.setAlpha(1.0f);
|
button_browse_content.setAlpha(1.0f);
|
||||||
button_browse_content.setTextColor(Color.parseColor("#888888")); // Texto grisáceo apagado
|
button_browse_content.setTextColor(Color.parseColor("#888888"));
|
||||||
} else if (isNegotiating) {
|
} else if (isNegotiating) {
|
||||||
// State 2: Negotiating
|
// State 3: Negotiating
|
||||||
button_browse_content.setEnabled(true);
|
button_browse_content.setEnabled(true);
|
||||||
button_browse_content.setTextColor(Color.WHITE);
|
button_browse_content.setTextColor(Color.WHITE);
|
||||||
// (El latido ya maneja la opacidad al 100%)
|
|
||||||
} else {
|
} else {
|
||||||
|
// State: Alive
|
||||||
stopExplorePulse();
|
stopExplorePulse();
|
||||||
button_browse_content.setEnabled(true);
|
button_browse_content.setEnabled(true);
|
||||||
button_browse_content.setTextColor(Color.WHITE);
|
button_browse_content.setTextColor(Color.WHITE);
|
||||||
button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_ready));
|
button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_ready));
|
||||||
|
|
||||||
if (isVpnActive && !isProxyDegraded) {
|
if (isVpnActive && !isProxyDegraded) {
|
||||||
// State 5: All good
|
button_browse_content.setAlpha(1.0f); // 100% Perfect state
|
||||||
button_browse_content.setAlpha(1.0f);
|
startExplorePulse();
|
||||||
} else {
|
} else {
|
||||||
// State 2: local or state 4
|
button_browse_content.setAlpha(0.6f); // Watered down fallback state
|
||||||
button_browse_content.setAlpha(0.6f);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FUSION LOGIC - Watchdog
|
// FUSION LOGIC (Watchdog & Server Control)
|
||||||
|
if (targetServerState != null) {
|
||||||
|
// STATE: COOL-OFF (Locked)
|
||||||
|
btnServerControl.setAlpha(0.6f);
|
||||||
|
btnServerControl.setText(serverTransitionText);
|
||||||
|
btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_disabled));
|
||||||
|
} else {
|
||||||
|
// STATE: NORMAL (Unlocked)
|
||||||
btnServerControl.setAlpha(1.0f);
|
btnServerControl.setAlpha(1.0f);
|
||||||
if (isServerAlive) {
|
if (isServerAlive) {
|
||||||
btnServerControl.setText("🛑 Stop Server");
|
btnServerControl.setText("🛑 Stop Server");
|
||||||
|
|
@ -880,59 +925,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
|
||||||
watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, isWatchdogOn ? R.color.btn_watchdog_on : R.color.btn_watchdog_off));
|
watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, isWatchdogOn ? R.color.btn_watchdog_on : R.color.btn_watchdog_off));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateUIColorsAndVisibility(boolean isServerAlive) {
|
|
||||||
boolean isVpnActive = prefs.getEnable();
|
|
||||||
|
|
||||||
if (!isServerAlive) {
|
|
||||||
button_control.setEnabled(false); // Disable ESPW click
|
|
||||||
button_browse_content.setVisibility(View.GONE);
|
|
||||||
stopExplorePulse(); // We stop the animation if the server dies
|
|
||||||
|
|
||||||
if (isVpnActive) {
|
|
||||||
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on_dim));
|
|
||||||
} else {
|
|
||||||
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off_dim));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
button_control.setEnabled(true); // Enable ESPW click
|
|
||||||
button_browse_content.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
// Heart rate and diluted color control
|
|
||||||
if (isVpnActive) {
|
|
||||||
startExplorePulse();
|
|
||||||
} else {
|
|
||||||
stopExplorePulse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// --- THE FUSION LOGIC (CASSETTE DECK) ---
|
|
||||||
boolean isWatchdogOn = prefs.getWatchdogEnable();
|
|
||||||
btnServerControl.setAlpha(1.0f); // Reset opacity
|
|
||||||
|
|
||||||
if (isServerAlive) {
|
|
||||||
btnServerControl.setText("🛑 Stop Server");
|
|
||||||
|
|
||||||
if (isWatchdogOn) {
|
|
||||||
// FUSION: Server alive + Watchdog active
|
|
||||||
// DELETED the stopFusionPulse() from here so the animation can live!
|
|
||||||
deckContainer.setBackgroundColor(Color.parseColor("#44FF9800")); // Bright border/background
|
|
||||||
btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on));
|
|
||||||
watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on));
|
|
||||||
} else {
|
|
||||||
// Sever alive, without Watchdog
|
|
||||||
if (fusionAnimator == null || !fusionAnimator.isRunning()) {
|
|
||||||
deckContainer.setBackgroundColor(Color.TRANSPARENT);
|
|
||||||
}
|
|
||||||
btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_danger)); // Red
|
|
||||||
watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_off));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Server offline
|
|
||||||
deckContainer.setBackgroundColor(Color.TRANSPARENT);
|
|
||||||
btnServerControl.setText("🚀 Launch Server");
|
|
||||||
btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_success)); // Verde
|
|
||||||
watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, isWatchdogOn ? R.color.btn_watchdog_on : R.color.btn_watchdog_off));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startFusionPulse() {
|
private void startFusionPulse() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
package org.iiab.controller;
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.Path;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.appcompat.widget.AppCompatButton;
|
||||||
|
|
||||||
|
public class ProgressButton extends AppCompatButton {
|
||||||
|
|
||||||
|
private Paint progressPaint;
|
||||||
|
private Paint progressBackgroundPaint;
|
||||||
|
|
||||||
|
private int progressColor;
|
||||||
|
private int progressBackgroundColor;
|
||||||
|
private Path clipPath;
|
||||||
|
private RectF rectF;
|
||||||
|
private float cornerRadius;
|
||||||
|
private int progressHeight;
|
||||||
|
|
||||||
|
// Animation variables
|
||||||
|
private float currentProgress = 0f;
|
||||||
|
private boolean isRunning = false;
|
||||||
|
private ValueAnimator animator;
|
||||||
|
|
||||||
|
public ProgressButton(Context context) {
|
||||||
|
super(context);
|
||||||
|
init(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProgressButton(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProgressButton(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(Context context, AttributeSet attrs) {
|
||||||
|
if (attrs != null) {
|
||||||
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressButton, 0, 0);
|
||||||
|
try {
|
||||||
|
progressColor = a.getColor(R.styleable.ProgressButton_progressButtonColor, ContextCompat.getColor(context, R.color.btn_danger));
|
||||||
|
progressBackgroundColor = a.getColor(R.styleable.ProgressButton_progressButtonBackgroundColor, Color.parseColor("#44888888"));
|
||||||
|
progressHeight = a.getDimensionPixelSize(R.styleable.ProgressButton_progressButtonHeight, (int) (6 * getResources().getDisplayMetrics().density));
|
||||||
|
// Note: We no longer read 'duration' from XML because the animation is infinite.
|
||||||
|
} finally {
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Safe defaults
|
||||||
|
progressColor = ContextCompat.getColor(context, R.color.btn_danger);
|
||||||
|
progressBackgroundColor = Color.parseColor("#44888888");
|
||||||
|
progressHeight = (int) (6 * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
progressPaint = new Paint();
|
||||||
|
progressPaint.setColor(progressColor);
|
||||||
|
progressPaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
progressBackgroundPaint = new Paint();
|
||||||
|
progressBackgroundPaint.setColor(progressBackgroundColor);
|
||||||
|
progressBackgroundPaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
// Initialize clipping path variables
|
||||||
|
clipPath = new Path();
|
||||||
|
rectF = new RectF();
|
||||||
|
cornerRadius = 8 * getResources().getDisplayMetrics().density;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
// Draw the background and text first
|
||||||
|
super.onDraw(canvas);
|
||||||
|
|
||||||
|
// Draw the progress bar constrained by the button's rounded corners
|
||||||
|
if (progressHeight > 0 && isRunning) {
|
||||||
|
int buttonWidth = getWidth();
|
||||||
|
int buttonHeight = getHeight();
|
||||||
|
|
||||||
|
// Calculate width based on the current animated float (0.0f to 1.0f)
|
||||||
|
int progressWidth = (int) (buttonWidth * currentProgress);
|
||||||
|
|
||||||
|
// 1. Prepare the rounded mask (matches the button's bounds)
|
||||||
|
rectF.set(0, 0, buttonWidth, buttonHeight);
|
||||||
|
clipPath.reset();
|
||||||
|
clipPath.addRoundRect(rectF, cornerRadius, cornerRadius, Path.Direction.CW);
|
||||||
|
|
||||||
|
// 2. Save canvas state and apply the mask
|
||||||
|
canvas.save();
|
||||||
|
canvas.clipPath(clipPath);
|
||||||
|
|
||||||
|
// 3. Draw the tracks
|
||||||
|
canvas.drawRect(0, buttonHeight - progressHeight, buttonWidth, buttonHeight, progressBackgroundPaint);
|
||||||
|
canvas.drawRect(0, buttonHeight - progressHeight, progressWidth, buttonHeight, progressPaint);
|
||||||
|
|
||||||
|
// 4. Restore canvas
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an infinite cyclic animation (fills and empties the bar).
|
||||||
|
* Disables the button to prevent spam clicks.
|
||||||
|
*/
|
||||||
|
public void startProgress() {
|
||||||
|
if (isRunning) return;
|
||||||
|
isRunning = true;
|
||||||
|
setEnabled(false); // Lock the button immediately
|
||||||
|
|
||||||
|
// Create an animator that goes from 0.0 to 1.0 (empty to full)
|
||||||
|
animator = ValueAnimator.ofFloat(0f, 1f);
|
||||||
|
animator.setDuration(1200); // 1.2 seconds per sweep
|
||||||
|
animator.setRepeatMode(ValueAnimator.REVERSE); // Fill up, then empty down
|
||||||
|
animator.setRepeatCount(ValueAnimator.INFINITE); // Never stop until commanded
|
||||||
|
|
||||||
|
animator.addUpdateListener(animation -> {
|
||||||
|
currentProgress = (float) animation.getAnimatedValue();
|
||||||
|
invalidate(); // Force redraw on every frame
|
||||||
|
});
|
||||||
|
|
||||||
|
animator.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the animation, clears the bar, and unlocks the button.
|
||||||
|
* To be called by the Controller when the backend confirms the state change.
|
||||||
|
*/
|
||||||
|
public void stopProgress() {
|
||||||
|
if (animator != null && animator.isRunning()) {
|
||||||
|
animator.cancel();
|
||||||
|
}
|
||||||
|
isRunning = false;
|
||||||
|
setEnabled(true); // Unlock button
|
||||||
|
currentProgress = 0f; // Reset width
|
||||||
|
invalidate(); // Clear the bar visually
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -343,7 +343,7 @@
|
||||||
android:baselineAligned="false"
|
android:baselineAligned="false"
|
||||||
android:weightSum="2">
|
android:weightSum="2">
|
||||||
|
|
||||||
<Button
|
<org.iiab.controller.ProgressButton
|
||||||
android:id="@+id/btn_server_control"
|
android:id="@+id/btn_server_control"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
|
|
@ -355,7 +355,12 @@
|
||||||
android:textColor="#FFFFFF"
|
android:textColor="#FFFFFF"
|
||||||
android:background="@drawable/rounded_button"
|
android:background="@drawable/rounded_button"
|
||||||
android:backgroundTint="@color/btn_success"
|
android:backgroundTint="@color/btn_success"
|
||||||
android:textAllCaps="false"/>
|
android:textAllCaps="false"
|
||||||
|
|
||||||
|
app:progressButtonHeight="6dp"
|
||||||
|
app:progressButtonDuration="@integer/server_cool_off_duration_ms"
|
||||||
|
app:progressButtonColor="#FF9800" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/watchdog_control"
|
android:id="@+id/watchdog_control"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,10 @@
|
||||||
<resources>
|
<resources>
|
||||||
<attr name="sectionBackground" format="color" />
|
<attr name="sectionBackground" format="color" />
|
||||||
<attr name="sectionHeaderBackground" format="color" />
|
<attr name="sectionHeaderBackground" format="color" />
|
||||||
|
<declare-styleable name="ProgressButton">
|
||||||
|
<attr name="progressButtonColor" format="color" />
|
||||||
|
<attr name="progressButtonBackgroundColor" format="color" />
|
||||||
|
<attr name="progressButtonHeight" format="dimension" />
|
||||||
|
<attr name="progressButtonDuration" format="integer" />
|
||||||
|
</declare-styleable>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<integer name="server_cool_off_duration_ms">15000</integer>
|
||||||
|
</resources>
|
||||||
Loading…
Reference in New Issue