From 501e5b37fcce78ecf5804a59a51180dd905cfa00 Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Tue, 1 Apr 2025 08:48:22 -0400 Subject: [PATCH] fix: Improve XP bar animation smoothness and text sync --- .../com/pixelmintdrop/ui/XPProgressBar.kt | 135 +++++++++++------- 1 file changed, 86 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt b/app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt index f752e75..8120c3e 100644 --- a/app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt +++ b/app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt @@ -1,5 +1,7 @@ package com.pixelmintdrop.ui +import android.animation.Animator +import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.content.Context import android.graphics.Canvas @@ -55,17 +57,19 @@ class XPProgressBar @JvmOverloads constructor( private val backgroundRect = RectF() private val cornerRadius = 25f - // Progress animation + // Progress animation state private var progressAnimator: ValueAnimator? = null - private var currentProgress = 0f - private var targetProgress = 0f + private var currentProgress = 0f // Current visual progress percentage (0-100) + private var startProgress = 0f // Progress percentage at animation start + private var targetProgress = 0f // Progress percentage at animation end - // XP values - private var currentXP = 0L - private var xpForNextLevel = 100L - private var playerLevel = 1 + // XP values state + private var currentXP = 0L // Final XP value for the current level after gain + private var xpForNextLevel = 100L // XP needed for the next level (can change on level up) + private var playerLevel = 1 // Final player level after gain + private var animatingStartXP = 0L // XP value when animation started - // Level up animation + // Level up animation state private var isLevelingUp = false private var levelUpAnimator: ValueAnimator? = null private var levelBadgeScale = 1f @@ -74,19 +78,21 @@ class XPProgressBar @JvmOverloads constructor( private var themeColor = Color.WHITE /** - * Set the player's current level and XP values + * Set the player's current level and XP values (typically called for initial state) */ fun setXPValues(level: Int, currentXP: Long, xpForNextLevel: Long) { this.playerLevel = level this.currentXP = currentXP this.xpForNextLevel = xpForNextLevel + this.animatingStartXP = currentXP // Assume start XP is current XP if not animating - // Update progress value - targetProgress = calculateProgressPercentage() + // Calculate progress based on these initial values + this.targetProgress = calculateProgressPercentage(currentXP, xpForNextLevel) // If not animating, set current progress immediately if (progressAnimator == null || !progressAnimator!!.isRunning) { - currentProgress = targetProgress + this.currentProgress = this.targetProgress + this.startProgress = this.targetProgress // Ensure start=target if not animating } invalidate() @@ -107,46 +113,61 @@ class XPProgressBar @JvmOverloads constructor( * Animate adding XP to the bar */ fun animateXPGain(xpGained: Long, newLevel: Int, newCurrentXP: Long, newXPForNextLevel: Long) { - // Store original values before animation - val startXP = currentXP - val startLevel = playerLevel + // Store values *before* the gain for animation reference + val initialXP = this.currentXP + val initialXPForNext = this.xpForNextLevel + val initialLevel = this.playerLevel - // Calculate percentage before XP gain - val startProgress = calculateProgressPercentage() + // Calculate start and target percentages + this.startProgress = calculateProgressPercentage(initialXP, initialXPForNext) + this.targetProgress = calculateProgressPercentage(newCurrentXP, newXPForNextLevel) - // Update to new values - playerLevel = newLevel - currentXP = newCurrentXP - xpForNextLevel = newXPForNextLevel + // Store the starting XP for text interpolation + this.animatingStartXP = initialXP - // Calculate new target progress - targetProgress = calculateProgressPercentage() + // Update member variables to the *final* state + this.playerLevel = newLevel + this.currentXP = newCurrentXP + this.xpForNextLevel = newXPForNextLevel // Determine if level up occurred - isLevelingUp = startLevel < newLevel + isLevelingUp = initialLevel < newLevel - // Animate progress bar + // Animate progress bar percentage progressAnimator?.cancel() progressAnimator = ValueAnimator.ofFloat(startProgress, targetProgress).apply { duration = 1500 // 1.5 seconds animation interpolator = AccelerateDecelerateInterpolator() addUpdateListener { animation -> + // Update the visual progress percentage currentProgress = animation.animatedValue as Float - invalidate() + invalidate() // Trigger redraw on each frame } - // When animation completes, trigger level up animation if needed - if (isLevelingUp) { - levelUpAnimation() - } + // Add listener to handle animation end and level up effect + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + // Ensure final state is precise + currentProgress = targetProgress + // Trigger level up visual pulse *after* bar animation completes + if (isLevelingUp) { + levelUpAnimation() + } + invalidate() + } + }) start() } + // Set the currentProgress to startProgress before starting animation + // (ValueAnimator starts from its current value, ensure it starts from startProgress) + // currentProgress = startProgress // REMOVED + // invalidate() // Initial draw at start state // REMOVED } /** - * Create a level up animation effect + * Create a level up animation effect (pulse the level badge) */ private fun levelUpAnimation() { levelUpAnimator?.cancel() @@ -158,20 +179,19 @@ class XPProgressBar @JvmOverloads constructor( levelBadgeScale = animation.animatedValue as Float invalidate() } - start() } } /** - * Calculate the current progress percentage + * Calculate the progress percentage given specific XP values */ - private fun calculateProgressPercentage(): Float { - return if (xpForNextLevel > 0) { - (currentXP.toFloat() / xpForNextLevel.toFloat()) * 100f + private fun calculateProgressPercentage(xp: Long, maxXP: Long): Float { + return if (maxXP > 0) { + (xp.toFloat() / maxXP.toFloat()) * 100f } else { - 0f - } + 0f // Avoid division by zero, return 0% + }.coerceIn(0f, 100f) // Ensure percentage is always between 0 and 100 } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { @@ -179,9 +199,8 @@ class XPProgressBar @JvmOverloads constructor( // Update progress bar dimensions based on view size val verticalPadding = h * 0.2f - // Increase left margin to prevent level badge from being cut off backgroundRect.set( - h * 0.6f, // Increased from 0.5f to 0.6f for more space + h * 0.6f, verticalPadding, w - paddingRight.toFloat(), h - verticalPadding @@ -195,14 +214,14 @@ class XPProgressBar @JvmOverloads constructor( override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - // Draw level badge with adjusted position + // Draw level badge (pulses on level up) val badgeRadius = height * 0.3f * levelBadgeScale - val badgeCenterX = height * 0.35f // Adjusted from 0.25f to 0.35f to match new position + val badgeCenterX = height * 0.35f val badgeCenterY = height * 0.5f canvas.drawCircle(badgeCenterX, badgeCenterY, badgeRadius, levelBadgePaint) canvas.drawText( - playerLevel.toString(), + playerLevel.toString(), // Always show the final level badgeCenterX, badgeCenterY + (levelBadgeTextPaint.textSize / 3), levelBadgeTextPaint @@ -211,32 +230,50 @@ class XPProgressBar @JvmOverloads constructor( // Draw background bar canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint) - // Draw progress bar + // Calculate progress bar width based on animated currentProgress + val progressBarWidth = backgroundRect.width() * currentProgress / 100f progressRect.set( backgroundRect.left, backgroundRect.top, - backgroundRect.left + (backgroundRect.width() * currentProgress / 100f), + backgroundRect.left + progressBarWidth, backgroundRect.bottom ) - // Only draw if there is progress to show + // Draw the actual progress portion if (progressRect.width() > 0) { - // Draw actual progress bar canvas.drawRoundRect(progressRect, cornerRadius, cornerRadius, progressPaint) } - // Draw progress text - val progressText = "${currentXP}/${xpForNextLevel} XP" + // --- Draw Progress Text --- + val isAnimating = progressAnimator?.isRunning ?: false + val displayedXP: Long + val displayedMaxXP = this.xpForNextLevel // Denominator always shows the target max XP + + if (isAnimating) { + // Calculate displayed XP based on the visual progress percentage (0-100) + // relative to the max XP for the current/new level. + displayedXP = (currentProgress / 100.0 * displayedMaxXP).toLong() + } else { + // When not animating, use the final accurate values + displayedXP = this.currentXP + } + + // Clamp displayed XP to be between 0 and the max for the level visually + val safeDisplayedXP = displayedXP.coerceIn(0L, displayedMaxXP) + + val progressText = "${safeDisplayedXP}/${displayedMaxXP} XP" canvas.drawText( progressText, backgroundRect.centerX(), backgroundRect.centerY() + (textPaint.textSize / 3), textPaint ) + // --- End Progress Text --- } override fun onDetachedFromWindow() { super.onDetachedFromWindow() + // Clean up animators to prevent leaks progressAnimator?.cancel() levelUpAnimator?.cancel() }