fix: Improve XP bar animation smoothness and text sync

This commit is contained in:
cmclark00 2025-04-01 08:48:22 -04:00
parent eb962268ca
commit 501e5b37fc

View file

@ -1,5 +1,7 @@
package com.pixelmintdrop.ui package com.pixelmintdrop.ui
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
@ -55,17 +57,19 @@ class XPProgressBar @JvmOverloads constructor(
private val backgroundRect = RectF() private val backgroundRect = RectF()
private val cornerRadius = 25f private val cornerRadius = 25f
// Progress animation // Progress animation state
private var progressAnimator: ValueAnimator? = null private var progressAnimator: ValueAnimator? = null
private var currentProgress = 0f private var currentProgress = 0f // Current visual progress percentage (0-100)
private var targetProgress = 0f private var startProgress = 0f // Progress percentage at animation start
private var targetProgress = 0f // Progress percentage at animation end
// XP values // XP values state
private var currentXP = 0L private var currentXP = 0L // Final XP value for the current level after gain
private var xpForNextLevel = 100L private var xpForNextLevel = 100L // XP needed for the next level (can change on level up)
private var playerLevel = 1 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 isLevelingUp = false
private var levelUpAnimator: ValueAnimator? = null private var levelUpAnimator: ValueAnimator? = null
private var levelBadgeScale = 1f private var levelBadgeScale = 1f
@ -74,19 +78,21 @@ class XPProgressBar @JvmOverloads constructor(
private var themeColor = Color.WHITE 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) { fun setXPValues(level: Int, currentXP: Long, xpForNextLevel: Long) {
this.playerLevel = level this.playerLevel = level
this.currentXP = currentXP this.currentXP = currentXP
this.xpForNextLevel = xpForNextLevel this.xpForNextLevel = xpForNextLevel
this.animatingStartXP = currentXP // Assume start XP is current XP if not animating
// Update progress value // Calculate progress based on these initial values
targetProgress = calculateProgressPercentage() this.targetProgress = calculateProgressPercentage(currentXP, xpForNextLevel)
// If not animating, set current progress immediately // If not animating, set current progress immediately
if (progressAnimator == null || !progressAnimator!!.isRunning) { if (progressAnimator == null || !progressAnimator!!.isRunning) {
currentProgress = targetProgress this.currentProgress = this.targetProgress
this.startProgress = this.targetProgress // Ensure start=target if not animating
} }
invalidate() invalidate()
@ -107,46 +113,61 @@ class XPProgressBar @JvmOverloads constructor(
* Animate adding XP to the bar * Animate adding XP to the bar
*/ */
fun animateXPGain(xpGained: Long, newLevel: Int, newCurrentXP: Long, newXPForNextLevel: Long) { fun animateXPGain(xpGained: Long, newLevel: Int, newCurrentXP: Long, newXPForNextLevel: Long) {
// Store original values before animation // Store values *before* the gain for animation reference
val startXP = currentXP val initialXP = this.currentXP
val startLevel = playerLevel val initialXPForNext = this.xpForNextLevel
val initialLevel = this.playerLevel
// Calculate percentage before XP gain // Calculate start and target percentages
val startProgress = calculateProgressPercentage() this.startProgress = calculateProgressPercentage(initialXP, initialXPForNext)
this.targetProgress = calculateProgressPercentage(newCurrentXP, newXPForNextLevel)
// Update to new values // Store the starting XP for text interpolation
playerLevel = newLevel this.animatingStartXP = initialXP
currentXP = newCurrentXP
xpForNextLevel = newXPForNextLevel
// Calculate new target progress // Update member variables to the *final* state
targetProgress = calculateProgressPercentage() this.playerLevel = newLevel
this.currentXP = newCurrentXP
this.xpForNextLevel = newXPForNextLevel
// Determine if level up occurred // Determine if level up occurred
isLevelingUp = startLevel < newLevel isLevelingUp = initialLevel < newLevel
// Animate progress bar // Animate progress bar percentage
progressAnimator?.cancel() progressAnimator?.cancel()
progressAnimator = ValueAnimator.ofFloat(startProgress, targetProgress).apply { progressAnimator = ValueAnimator.ofFloat(startProgress, targetProgress).apply {
duration = 1500 // 1.5 seconds animation duration = 1500 // 1.5 seconds animation
interpolator = AccelerateDecelerateInterpolator() interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { animation -> addUpdateListener { animation ->
// Update the visual progress percentage
currentProgress = animation.animatedValue as Float currentProgress = animation.animatedValue as Float
invalidate() invalidate() // Trigger redraw on each frame
} }
// When animation completes, trigger level up animation if needed // Add listener to handle animation end and level up effect
if (isLevelingUp) { addListener(object : AnimatorListenerAdapter() {
levelUpAnimation() 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() 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() { private fun levelUpAnimation() {
levelUpAnimator?.cancel() levelUpAnimator?.cancel()
@ -158,20 +179,19 @@ class XPProgressBar @JvmOverloads constructor(
levelBadgeScale = animation.animatedValue as Float levelBadgeScale = animation.animatedValue as Float
invalidate() invalidate()
} }
start() start()
} }
} }
/** /**
* Calculate the current progress percentage * Calculate the progress percentage given specific XP values
*/ */
private fun calculateProgressPercentage(): Float { private fun calculateProgressPercentage(xp: Long, maxXP: Long): Float {
return if (xpForNextLevel > 0) { return if (maxXP > 0) {
(currentXP.toFloat() / xpForNextLevel.toFloat()) * 100f (xp.toFloat() / maxXP.toFloat()) * 100f
} else { } 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) { 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 // Update progress bar dimensions based on view size
val verticalPadding = h * 0.2f val verticalPadding = h * 0.2f
// Increase left margin to prevent level badge from being cut off
backgroundRect.set( backgroundRect.set(
h * 0.6f, // Increased from 0.5f to 0.6f for more space h * 0.6f,
verticalPadding, verticalPadding,
w - paddingRight.toFloat(), w - paddingRight.toFloat(),
h - verticalPadding h - verticalPadding
@ -195,14 +214,14 @@ class XPProgressBar @JvmOverloads constructor(
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
// Draw level badge with adjusted position // Draw level badge (pulses on level up)
val badgeRadius = height * 0.3f * levelBadgeScale 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 val badgeCenterY = height * 0.5f
canvas.drawCircle(badgeCenterX, badgeCenterY, badgeRadius, levelBadgePaint) canvas.drawCircle(badgeCenterX, badgeCenterY, badgeRadius, levelBadgePaint)
canvas.drawText( canvas.drawText(
playerLevel.toString(), playerLevel.toString(), // Always show the final level
badgeCenterX, badgeCenterX,
badgeCenterY + (levelBadgeTextPaint.textSize / 3), badgeCenterY + (levelBadgeTextPaint.textSize / 3),
levelBadgeTextPaint levelBadgeTextPaint
@ -211,32 +230,50 @@ class XPProgressBar @JvmOverloads constructor(
// Draw background bar // Draw background bar
canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint) 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( progressRect.set(
backgroundRect.left, backgroundRect.left,
backgroundRect.top, backgroundRect.top,
backgroundRect.left + (backgroundRect.width() * currentProgress / 100f), backgroundRect.left + progressBarWidth,
backgroundRect.bottom backgroundRect.bottom
) )
// Only draw if there is progress to show // Draw the actual progress portion
if (progressRect.width() > 0) { if (progressRect.width() > 0) {
// Draw actual progress bar
canvas.drawRoundRect(progressRect, cornerRadius, cornerRadius, progressPaint) canvas.drawRoundRect(progressRect, cornerRadius, cornerRadius, progressPaint)
} }
// Draw progress text // --- Draw Progress Text ---
val progressText = "${currentXP}/${xpForNextLevel} XP" 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( canvas.drawText(
progressText, progressText,
backgroundRect.centerX(), backgroundRect.centerX(),
backgroundRect.centerY() + (textPaint.textSize / 3), backgroundRect.centerY() + (textPaint.textSize / 3),
textPaint textPaint
) )
// --- End Progress Text ---
} }
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
super.onDetachedFromWindow() super.onDetachedFromWindow()
// Clean up animators to prevent leaks
progressAnimator?.cancel() progressAnimator?.cancel()
levelUpAnimator?.cancel() levelUpAnimator?.cancel()
} }