package com.aethex.os; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.Activity; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.GradientDrawable; import android.os.Handler; import android.os.Looper; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.LinearInterpolator; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import androidx.core.content.res.ResourcesCompat; /** * Fullscreen overlay animation for clearance switching. * Shows a cinematic 5-step animation: fade-in → spinner → text → progress line → fade-out. */ public class ClearanceSwitchOverlay { public interface Callback { void onSwitchComplete(); } private static final String OVERLAY_TAG = "clearance_switch_overlay"; /** * Shows the clearance switch overlay animation. * * @param activity The current activity * @param targetMode The clearance mode being switched to (ThemeManager.CLEARANCE_CORP or CLEARANCE_FOUNDATION) * @param callback Called when animation completes and theme should be applied */ public static void show(Activity activity, String targetMode, Callback callback) { if (activity == null || activity.isFinishing() || activity.isDestroyed()) { if (callback != null) callback.onSwitchComplete(); return; } activity.runOnUiThread(() -> { // Remove any existing overlay FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView(); View existing = decorView.findViewWithTag(OVERLAY_TAG); if (existing != null) { decorView.removeView(existing); } // Build the overlay FrameLayout overlay = createOverlay(activity, targetMode); decorView.addView(overlay); // Run the 5-step animation runAnimation(activity, overlay, targetMode, callback); }); } private static FrameLayout createOverlay(Activity activity, String targetMode) { int bgColor = ThemeManager.getSwitchOverlayBackground(targetMode); int spinnerColor = ThemeManager.getSwitchSpinnerColor(targetMode); String label = ThemeManager.getSwitchLabel(targetMode); FrameLayout overlay = new FrameLayout(activity); overlay.setTag(OVERLAY_TAG); overlay.setLayoutParams(new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); overlay.setBackgroundColor(bgColor); overlay.setAlpha(0f); overlay.setClickable(true); // Block touches below // Center content container LinearLayout center = new LinearLayout(activity); center.setOrientation(LinearLayout.VERTICAL); center.setGravity(Gravity.CENTER); FrameLayout.LayoutParams centerParams = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); centerParams.gravity = Gravity.CENTER; center.setLayoutParams(centerParams); // Spinner ring (circular border) View spinner = new View(activity); int spinnerSize = dpToPx(activity, 48); LinearLayout.LayoutParams spinnerParams = new LinearLayout.LayoutParams(spinnerSize, spinnerSize); spinnerParams.gravity = Gravity.CENTER_HORIZONTAL; spinner.setLayoutParams(spinnerParams); GradientDrawable spinnerBg = new GradientDrawable(); spinnerBg.setShape(GradientDrawable.OVAL); spinnerBg.setColor(Color.TRANSPARENT); spinnerBg.setStroke(dpToPx(activity, 3), spinnerColor); spinner.setBackground(spinnerBg); spinner.setAlpha(0f); spinner.setTag("switch_spinner"); center.addView(spinner); // Label text TextView labelView = new TextView(activity); labelView.setText(label); labelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); labelView.setTextColor(Color.argb((int) (255 * 0.8f), 255, 255, 255)); labelView.setLetterSpacing(0.2f); try { Typeface font = ResourcesCompat.getFont(activity, R.font.source_code_pro); if (font != null) labelView.setTypeface(font, Typeface.BOLD); } catch (Exception ignored) { labelView.setTypeface(Typeface.MONOSPACE, Typeface.BOLD); } LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); labelParams.gravity = Gravity.CENTER_HORIZONTAL; labelParams.topMargin = dpToPx(activity, 20); labelView.setLayoutParams(labelParams); labelView.setAlpha(0f); labelView.setTag("switch_label"); center.addView(labelView); // Sub-label (mode name) TextView subLabel = new TextView(activity); String modeName = ThemeManager.CLEARANCE_CORP.equals(targetMode) ? "CORP CLEARANCE" : "FOUNDATION CLEARANCE"; subLabel.setText(modeName); subLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10); subLabel.setTextColor(spinnerColor); try { Typeface font = ResourcesCompat.getFont(activity, R.font.source_code_pro); if (font != null) subLabel.setTypeface(font); } catch (Exception ignored) { subLabel.setTypeface(Typeface.MONOSPACE); } LinearLayout.LayoutParams subParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); subParams.gravity = Gravity.CENTER_HORIZONTAL; subParams.topMargin = dpToPx(activity, 6); subLabel.setLayoutParams(subParams); subLabel.setAlpha(0f); subLabel.setTag("switch_sub_label"); center.addView(subLabel); overlay.addView(center); // Bottom progress line View progressLine = new View(activity); int lineHeight = dpToPx(activity, 2); FrameLayout.LayoutParams lineParams = new FrameLayout.LayoutParams(0, lineHeight); lineParams.gravity = Gravity.BOTTOM; progressLine.setLayoutParams(lineParams); progressLine.setBackgroundColor(spinnerColor); progressLine.setTag("switch_progress"); overlay.addView(progressLine); return overlay; } private static void runAnimation(Activity activity, FrameLayout overlay, String targetMode, Callback callback) { Handler handler = new Handler(Looper.getMainLooper()); int spinnerColor = ThemeManager.getSwitchSpinnerColor(targetMode); View spinner = overlay.findViewWithTag("switch_spinner"); View label = overlay.findViewWithTag("switch_label"); View subLabel = overlay.findViewWithTag("switch_sub_label"); View progress = overlay.findViewWithTag("switch_progress"); // Play switch sound SoundManager.getInstance().play(SoundManager.Sound.SWITCH); // Step 1: Fade in overlay (200ms) overlay.animate().alpha(1f).setDuration(200).withEndAction(() -> { // Step 2: Show spinner + spin (400ms) spinner.setAlpha(1f); spinner.setScaleX(0.5f); spinner.setScaleY(0.5f); spinner.animate().scaleX(1f).scaleY(1f).setDuration(300) .setInterpolator(new AccelerateDecelerateInterpolator()).start(); ObjectAnimator rotation = ObjectAnimator.ofFloat(spinner, "rotation", 0f, 360f); rotation.setDuration(800); rotation.setRepeatCount(1); rotation.setInterpolator(new LinearInterpolator()); rotation.start(); handler.postDelayed(() -> { // Step 3: Show text (300ms) label.animate().alpha(1f).setDuration(200).start(); subLabel.animate().alpha(1f).setDuration(200).setStartDelay(100).start(); handler.postDelayed(() -> { // Step 4: Progress line sweeps across (600ms) int screenWidth = activity.getWindow().getDecorView().getWidth(); ValueAnimator lineAnim = ValueAnimator.ofInt(0, screenWidth); lineAnim.setDuration(600); lineAnim.setInterpolator(new AccelerateDecelerateInterpolator()); lineAnim.addUpdateListener(anim -> { int val = (int) anim.getAnimatedValue(); ViewGroup.LayoutParams lp = progress.getLayoutParams(); lp.width = val; progress.setLayoutParams(lp); }); lineAnim.start(); handler.postDelayed(() -> { // Step 5: Fade out (300ms) overlay.animate().alpha(0f).setDuration(300).withEndAction(() -> { // Remove overlay FrameLayout decorView = (FrameLayout) activity.getWindow().getDecorView(); decorView.removeView(overlay); // Invoke callback if (callback != null) { callback.onSwitchComplete(); } }).start(); }, 650); }, 350); }, 500); }).start(); } private static int dpToPx(Activity activity, float dp) { return Math.round(TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp, activity.getResources().getDisplayMetrics())); } }