Match Statistics

Made the Statistics View for after a Match "MultiUser" capable and filled with actual Match Statistics instead of the Player Stats.
This commit is contained in:
Alexander Doerflinger
2026-02-18 11:28:51 +01:00
parent 543a6283de
commit 847b8e1d7e
11 changed files with 538 additions and 112 deletions

View File

@@ -28,7 +28,6 @@ import androidx.preference.PreferenceManager;
import com.aldo.apps.ochecompanion.database.DatabaseHelper; import com.aldo.apps.ochecompanion.database.DatabaseHelper;
import com.aldo.apps.ochecompanion.database.objects.Player; import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.database.objects.Statistics;
import com.aldo.apps.ochecompanion.game.GameManager; import com.aldo.apps.ochecompanion.game.GameManager;
import com.aldo.apps.ochecompanion.ui.PlayerStatsView; import com.aldo.apps.ochecompanion.ui.PlayerStatsView;
import com.aldo.apps.ochecompanion.utils.CheckoutEngine; import com.aldo.apps.ochecompanion.utils.CheckoutEngine;
@@ -37,6 +36,7 @@ import com.aldo.apps.ochecompanion.utils.Log;
import com.aldo.apps.ochecompanion.utils.SoundEngine; import com.aldo.apps.ochecompanion.utils.SoundEngine;
import com.aldo.apps.ochecompanion.utils.UIConstants; import com.aldo.apps.ochecompanion.utils.UIConstants;
import com.google.android.material.button.MaterialButton; import com.google.android.material.button.MaterialButton;
import android.widget.ImageButton;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -72,6 +72,17 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
*/ */
private boolean mAreStatsShown = false; private boolean mAreStatsShown = false;
/**
* Index of the player currently shown in the stats view.
* Used for cycling through players with the navigation arrows.
*/
private int mStatsPlayerIndex = 0;
/**
* Cached list of all player states for stats navigation.
*/
private List<GameManager.PlayerState> mMatchPlayerStates;
// ======================================================================================== // ========================================================================================
// Game Manager (Singleton Business Logic Handler) // Game Manager (Singleton Business Logic Handler)
// ======================================================================================== // ========================================================================================
@@ -157,6 +168,18 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
*/ */
private PlayerStatsView mStatsView; private PlayerStatsView mStatsView;
/**
* Navigation bar for switching between player stats.
*/
private View mStatsNavBar;
/**
* Previous/Next buttons and indicator for stats navigation.
*/
private ImageButton mBtnStatsPrev;
private ImageButton mBtnStatsNext;
private TextView mTvStatsPlayerIndicator;
/** /**
* Array of three TextViews showing darts thrown in current turn. * Array of three TextViews showing darts thrown in current turn.
*/ */
@@ -277,6 +300,7 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
private void handleBackPressed() { private void handleBackPressed() {
if (mAreStatsShown) { if (mAreStatsShown) {
mStatsView.setVisibility(View.GONE); mStatsView.setVisibility(View.GONE);
mStatsNavBar.setVisibility(View.GONE);
mShowStatsBtn.setVisibility(View.VISIBLE); mShowStatsBtn.setVisibility(View.VISIBLE);
mAreStatsShown = false; mAreStatsShown = false;
return; return;
@@ -314,10 +338,34 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
mShowStatsBtn = findViewById(R.id.show_stats_btn); mShowStatsBtn = findViewById(R.id.show_stats_btn);
mStatsView = findViewById(R.id.player_stats_view); mStatsView = findViewById(R.id.player_stats_view);
// Stats navigation bar
mStatsNavBar = findViewById(R.id.stats_nav_bar);
mBtnStatsPrev = findViewById(R.id.btn_stats_prev);
mBtnStatsNext = findViewById(R.id.btn_stats_next);
mTvStatsPlayerIndicator = findViewById(R.id.tv_stats_player_indicator);
mShowStatsBtn.setOnClickListener(v -> { mShowStatsBtn.setOnClickListener(v -> {
mStatsView.setVisibility(View.VISIBLE); mStatsView.setVisibility(View.VISIBLE);
mShowStatsBtn.setVisibility(View.GONE); mShowStatsBtn.setVisibility(View.GONE);
mStatsNavBar.setVisibility(View.VISIBLE);
mAreStatsShown = true; mAreStatsShown = true;
showStatsForPlayer(mStatsPlayerIndex);
});
mBtnStatsPrev.setOnClickListener(v -> {
if (mMatchPlayerStates != null && !mMatchPlayerStates.isEmpty()) {
mStatsPlayerIndex = (mStatsPlayerIndex - 1 + mMatchPlayerStates.size())
% mMatchPlayerStates.size();
showStatsForPlayer(mStatsPlayerIndex);
}
});
mBtnStatsNext.setOnClickListener(v -> {
if (mMatchPlayerStates != null && !mMatchPlayerStates.isEmpty()) {
mStatsPlayerIndex = (mStatsPlayerIndex + 1) % mMatchPlayerStates.size();
showStatsForPlayer(mStatsPlayerIndex);
}
}); });
mSubmitTurnBtn.setOnClickListener(v -> submitTurn()); mSubmitTurnBtn.setOnClickListener(v -> submitTurn());
@@ -542,24 +590,47 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC
} }
/** /**
* Retrieves the winning player's statistics from the database and binds them to * Caches all player states from the completed match and shows stats for the
* the stats view. * winner (first player in the list, index 0, which is typically the active
* Runs database query on a background thread to avoid blocking the UI. * player at match end). The stats view will show match-specific stats.
*/ */
private void attachPlayerStats() { private void attachPlayerStats() {
new Thread(() -> { mMatchPlayerStates = mGameManager.getPlayerStates();
try { if (mMatchPlayerStates == null || mMatchPlayerStates.isEmpty()) {
final GameManager.PlayerState activePlayer = mGameManager.getActivePlayer(); Log.e(TAG, "attachPlayerStats: No player states available");
if (activePlayer == null) return;
return; }
final Player player = activePlayer.player; // Find the winner (remainingScore == 0) and start with them
final Statistics statistics = mDatabaseHelper.getStatisticsForPlayer(player.id); mStatsPlayerIndex = 0;
runOnUiThread(() -> mStatsView.bind(player, statistics)); for (int i = 0; i < mMatchPlayerStates.size(); i++) {
} catch (Exception e) { if (mMatchPlayerStates.get(i).remainingScore == 0) {
Log.e(TAG, "attachPlayerStats: Failed to retrieve player statistics", e); mStatsPlayerIndex = i;
break;
} }
}).start(); }
showStatsForPlayer(mStatsPlayerIndex);
}
/**
* Binds match-specific stats for the player at the given index to the stats
* view.
* Updates the navigation indicator to show current position (e.g., "1 / 3").
*
* @param index Index of the player in {@link #mMatchPlayerStates}
*/
private void showStatsForPlayer(final int index) {
if (mMatchPlayerStates == null || index < 0 || index >= mMatchPlayerStates.size()) {
return;
}
final GameManager.PlayerState playerState = mMatchPlayerStates.get(index);
mStatsView.bindMatchStats(playerState);
// Update navigation indicator
mTvStatsPlayerIndicator.setText(
String.format("%s (%d / %d)", playerState.name, index + 1, mMatchPlayerStates.size()));
} }
/** /**

View File

@@ -351,7 +351,8 @@ public class DatabaseHelper {
player.id, player.id,
player.username, player.username,
startingScore, // Initial score equals starting score startingScore, // Initial score equals starting score
0 // No darts thrown yet 0, // No darts thrown yet
0, 0, 0, 0, 0, 0, 0, 0, null // Per-match stats start at zero
)); ));
} }
} else { } else {
@@ -360,7 +361,9 @@ public class DatabaseHelper {
0L, // Guest has ID 0 0L, // Guest has ID 0
"GUEST", "GUEST",
startingScore, startingScore,
0)); 0, // No darts thrown yet
0, 0, 0, 0, 0, 0, 0, 0, null // Per-match stats start at zero
));
} }
// Convert to JSON // Convert to JSON

View File

@@ -8,10 +8,13 @@ import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.utils.DartsConstants; import com.aldo.apps.ochecompanion.utils.DartsConstants;
import com.aldo.apps.ochecompanion.utils.Log; import com.aldo.apps.ochecompanion.utils.Log;
import com.aldo.apps.ochecompanion.utils.MatchProgress; import com.aldo.apps.ochecompanion.utils.MatchProgress;
import com.aldo.apps.ochecompanion.utils.converters.HitDistributionConverter;
import com.aldo.apps.ochecompanion.utils.converters.MatchProgressConverter; import com.aldo.apps.ochecompanion.utils.converters.MatchProgressConverter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* GameManager: Singleton manager for handling all X01 game business logic. * GameManager: Singleton manager for handling all X01 game business logic.
@@ -195,7 +198,7 @@ public class GameManager {
/** /**
* State holder for a single player's X01 game progress. * State holder for a single player's X01 game progress.
* Tracks current match state for an individual player. * Tracks current match state and per-match statistics for an individual player.
*/ */
public static class PlayerState { public static class PlayerState {
/** /**
@@ -228,6 +231,35 @@ public class GameManager {
*/ */
public int dartsThrown = 0; public int dartsThrown = 0;
// ---- Per-Match Statistics ----
/** Total points scored in this match (for match average calculation). */
public int matchPointsScored = 0;
/** Number of busted turns in this match. */
public int matchBustCount = 0;
/** Highest single-turn score in this match. */
public int matchHighestTurnScore = 0;
/** Checkout value if this player won, 0 otherwise. */
public int matchCheckoutValue = 0;
/** Count of turns scoring 60+ points in this match. */
public int matchCount60Plus = 0;
/** Count of turns scoring 100+ points in this match. */
public int matchCount100Plus = 0;
/** Count of turns scoring 140+ points in this match. */
public int matchCount140Plus = 0;
/** Count of 180s scored in this match. */
public int matchCount180 = 0;
/** Per-match dart hit distribution for heatmap rendering. */
public Map<String, Integer> matchHitDistribution = new HashMap<>();
/** /**
* Constructs a PlayerState for a player with the specified starting score. * Constructs a PlayerState for a player with the specified starting score.
* *
@@ -240,6 +272,28 @@ public class GameManager {
this.name = player.username; this.name = player.username;
this.remainingScore = startScore; this.remainingScore = startScore;
} }
/**
* Returns the match average (points per 3 darts) for this match.
*
* @return Match average, or 0.0 if no darts have been thrown
*/
public double getMatchAverage() {
if (dartsThrown == 0)
return 0.0;
return (double) matchPointsScored / dartsThrown * 3;
}
/**
* Records a dart hit to the per-match hit distribution map.
*
* @param baseValue The dartboard number (1-20 or 25 for bull)
* @param multiplier The multiplier (1=single, 2=double, 3=triple)
*/
public void recordMatchDartHit(final int baseValue, final int multiplier) {
final String key = HitDistributionConverter.getSegmentKey(baseValue, multiplier);
matchHitDistribution = HitDistributionConverter.recordHit(matchHitDistribution, key);
}
} }
/** /**
@@ -407,12 +461,25 @@ public class GameManager {
// Restore active player index // Restore active player index
mActivePlayerIndex = progress.activePlayerIndex; mActivePlayerIndex = progress.activePlayerIndex;
// Restore player scores and darts thrown // Restore player scores, darts, and per-match stats
for (int i = 0; i < progress.players.size() && i < mPlayerStates.size(); i++) { for (int i = 0; i < progress.players.size() && i < mPlayerStates.size(); i++) {
MatchProgress.PlayerStateSnapshot snapshot = progress.players.get(i); MatchProgress.PlayerStateSnapshot snapshot = progress.players.get(i);
PlayerState state = mPlayerStates.get(i); PlayerState state = mPlayerStates.get(i);
state.remainingScore = snapshot.remainingScore; state.remainingScore = snapshot.remainingScore;
state.dartsThrown = snapshot.dartsThrown; state.dartsThrown = snapshot.dartsThrown;
// Restore per-match stats
state.matchPointsScored = snapshot.matchPointsScored;
state.matchBustCount = snapshot.matchBustCount;
state.matchHighestTurnScore = snapshot.matchHighestTurnScore;
state.matchCheckoutValue = snapshot.matchCheckoutValue;
state.matchCount60Plus = snapshot.matchCount60Plus;
state.matchCount100Plus = snapshot.matchCount100Plus;
state.matchCount140Plus = snapshot.matchCount140Plus;
state.matchCount180 = snapshot.matchCount180;
state.matchHitDistribution = snapshot.matchHitDistribution != null
? new HashMap<>(snapshot.matchHitDistribution)
: new HashMap<>();
} }
Log.d(TAG, "loadMatchProgress: Match progress loaded successfully"); Log.d(TAG, "loadMatchProgress: Match progress loaded successfully");
@@ -503,12 +570,32 @@ public class GameManager {
winner.remainingScore = 0; winner.remainingScore = 0;
winner.dartsThrown += dartsThrown; winner.dartsThrown += dartsThrown;
// Update statistics // Accumulate per-match stats for the winning turn
winner.matchPointsScored += pointsMade;
winner.matchCheckoutValue = checkoutValue;
if (pointsMade > winner.matchHighestTurnScore) {
winner.matchHighestTurnScore = pointsMade;
}
if (pointsMade >= 60)
winner.matchCount60Plus++;
if (pointsMade >= 100)
winner.matchCount100Plus++;
if (pointsMade >= 140)
winner.matchCount140Plus++;
if (pointsMade == 180)
winner.matchCount180++;
// Update career statistics
updatePlayerStats(winner, dartsThrown, pointsMade, false, checkoutValue); updatePlayerStats(winner, dartsThrown, pointsMade, false, checkoutValue);
// Record dart hits // Record dart hits to career stats (async DB)
recordTurnHitsToStatistics(winner, new ArrayList<>(mCurrentTurnDartHits)); recordTurnHitsToStatistics(winner, new ArrayList<>(mCurrentTurnDartHits));
// Record dart hits to per-match distribution (local)
for (final DartHit hit : mCurrentTurnDartHits) {
winner.recordMatchDartHit(hit.baseValue, hit.multiplier);
}
// Increment matches played for all players // Increment matches played for all players
incrementMatchesPlayed(); incrementMatchesPlayed();
@@ -557,13 +644,35 @@ public class GameManager {
if (!isBust) { if (!isBust) {
active.remainingScore = finalScore; active.remainingScore = finalScore;
active.dartsThrown += mCurrentTurnDarts.size(); active.dartsThrown += mCurrentTurnDarts.size();
// Accumulate per-match stats
active.matchPointsScored += turnTotal;
if (turnTotal > active.matchHighestTurnScore) {
active.matchHighestTurnScore = turnTotal;
}
if (turnTotal >= 60)
active.matchCount60Plus++;
if (turnTotal >= 100)
active.matchCount100Plus++;
if (turnTotal >= 140)
active.matchCount140Plus++;
if (turnTotal == 180)
active.matchCount180++;
} else {
// Track bust in per-match stats
active.matchBustCount++;
} }
updatePlayerStats(active, mCurrentTurnDarts.size(), turnTotal, isBust); updatePlayerStats(active, mCurrentTurnDarts.size(), turnTotal, isBust);
// Record dart hits // Record dart hits to career stats (async DB)
recordTurnHitsToStatistics(active, new ArrayList<>(mCurrentTurnDartHits)); recordTurnHitsToStatistics(active, new ArrayList<>(mCurrentTurnDartHits));
// Record dart hits to per-match distribution (local, even on bust for tracking)
for (final DartHit hit : mCurrentTurnDartHits) {
active.recordMatchDartHit(hit.baseValue, hit.multiplier);
}
// Rotate to next player // Rotate to next player
mActivePlayerIndex = (mActivePlayerIndex + 1) % mPlayerStates.size(); mActivePlayerIndex = (mActivePlayerIndex + 1) % mPlayerStates.size();
@@ -628,7 +737,16 @@ public class GameManager {
state.playerId, state.playerId,
state.name, state.name,
state.remainingScore, state.remainingScore,
state.dartsThrown)); state.dartsThrown,
state.matchPointsScored,
state.matchBustCount,
state.matchHighestTurnScore,
state.matchCheckoutValue,
state.matchCount60Plus,
state.matchCount100Plus,
state.matchCount140Plus,
state.matchCount180,
state.matchHitDistribution));
} }
String progressJson = MatchProgressConverter.fromProgress(progress); String progressJson = MatchProgressConverter.fromProgress(progress);
@@ -680,7 +798,16 @@ public class GameManager {
state.playerId, state.playerId,
state.name, state.name,
state.remainingScore, state.remainingScore,
state.dartsThrown)); state.dartsThrown,
state.matchPointsScored,
state.matchBustCount,
state.matchHighestTurnScore,
state.matchCheckoutValue,
state.matchCount60Plus,
state.matchCount100Plus,
state.matchCount140Plus,
state.matchCount180,
state.matchHitDistribution));
} }
match.participantData = MatchProgressConverter.fromProgress(finalProgress); match.participantData = MatchProgressConverter.fromProgress(finalProgress);

View File

@@ -22,9 +22,12 @@ import java.util.Map;
* HeatmapView: A custom high-performance rendering component that draws a * HeatmapView: A custom high-performance rendering component that draws a
* dartboard and overlays player performance data as a color-coded heatmap. * dartboard and overlays player performance data as a color-coded heatmap.
* <p> * <p>
* This custom View renders a professional dartboard visualization with color-coded * This custom View renders a professional dartboard visualization with
* performance data overlaid on each segment (singles, doubles, triples, and bulls). * color-coded
* The heatmap uses a gradient from "cold" (low frequency) to "hot" (high frequency) * performance data overlaid on each segment (singles, doubles, triples, and
* bulls).
* The heatmap uses a gradient from "cold" (low frequency) to "hot" (high
* frequency)
* based on how often a player hits each segment. * based on how often a player hits each segment.
* <p> * <p>
* Optimized Palette: * Optimized Palette:
@@ -46,7 +49,7 @@ public class HeatmapView extends View {
* Reused across all drawing operations for efficiency. * Reused across all drawing operations for efficiency.
*/ */
private final Paint mBasePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint mBasePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/** /**
* Color evaluator for interpolating between cold (green) and hot (red) colors. * Color evaluator for interpolating between cold (green) and hot (red) colors.
* Evaluates intermediate colors based on normalized hit frequency (0.0 to 1.0). * Evaluates intermediate colors based on normalized hit frequency (0.0 to 1.0).
@@ -58,28 +61,29 @@ public class HeatmapView extends View {
* Calculated from view width in onSizeChanged. * Calculated from view width in onSizeChanged.
*/ */
private float mCenterX; private float mCenterX;
/** /**
* Y-coordinate of the dartboard center in pixels. * Y-coordinate of the dartboard center in pixels.
* Calculated from view height in onSizeChanged. * Calculated from view height in onSizeChanged.
*/ */
private float mCenterY; private float mCenterY;
/** /**
* Radius of the dartboard in pixels. * Radius of the dartboard in pixels.
* Calculated as 95% of the minimum dimension (width or height) to leave margin. * Calculated as 95% of the minimum dimension (width or height) to leave margin.
* All segment boundaries are defined as factors of this radius. * All segment boundaries are defined as factors of this radius.
*/ */
private float mRadius; private float mRadius;
/** /**
* Cache of pre-calculated Path objects for all dartboard segments. * Cache of pre-calculated Path objects for all dartboard segments.
* Key format: "d" + number (doubles), "t" + number (triples), * Key format: "d" + number (doubles), "t" + number (triples),
* "s" + number + "_inner/outer" (singles), "sb" (single bull), "db" (double bull). * "s" + number + "_inner/outer" (singles), "sb" (single bull), "db" (double
* bull).
* Calculated once in calculatePaths() and reused for efficient rendering. * Calculated once in calculatePaths() and reused for efficient rendering.
*/ */
private final Map<String, Path> mSegmentPaths = new HashMap<>(); private final Map<String, Path> mSegmentPaths = new HashMap<>();
/** /**
* Player statistics object containing hit distribution data. * Player statistics object containing hit distribution data.
* Used to determine heatmap colors for each segment. * Used to determine heatmap colors for each segment.
@@ -87,6 +91,12 @@ public class HeatmapView extends View {
*/ */
private Statistics mStats; private Statistics mStats;
/**
* Raw hit distribution map. Populated by either {@link #setStats(Statistics)}
* or {@link #setHitDistribution(Map)} for per-match rendering.
*/
private Map<String, Integer> mHitDistribution;
/** /**
* Standard dartboard segment order (clockwise starting from 20 at the top). * Standard dartboard segment order (clockwise starting from 20 at the top).
* This array defines the physical layout of numbers around the dartboard * This array defines the physical layout of numbers around the dartboard
@@ -100,19 +110,21 @@ public class HeatmapView extends View {
* Constructs a new HeatmapView programmatically. * Constructs a new HeatmapView programmatically.
* Used when creating the view from code rather than XML inflation. * Used when creating the view from code rather than XML inflation.
* *
* @param context The Context the view is running in, through which it can access resources * @param context The Context the view is running in, through which it can
* access resources
*/ */
public HeatmapView(final Context context) { public HeatmapView(final Context context) {
super(context); super(context);
init(); init();
} }
/** /**
* Constructs a new HeatmapView from XML. * Constructs a new HeatmapView from XML.
* Used when inflating the view from an XML layout. * Used when inflating the view from an XML layout.
* *
* @param context The Context the view is running in, through which it can access resources * @param context The Context the view is running in, through which it can
* @param attrs The attributes of the XML tag that is inflating the view * access resources
* @param attrs The attributes of the XML tag that is inflating the view
*/ */
public HeatmapView(final Context context, @Nullable final AttributeSet attrs) { public HeatmapView(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs); super(context, attrs);
@@ -132,6 +144,19 @@ public class HeatmapView extends View {
*/ */
public void setStats(final Statistics stats) { public void setStats(final Statistics stats) {
this.mStats = stats; this.mStats = stats;
this.mHitDistribution = stats != null ? stats.getHitDistribution() : null;
invalidate();
}
/**
* Binds a raw hit distribution map to the view and triggers a redraw.
* Used for per-match heatmap rendering where no full Statistics object exists.
*
* @param hitDistribution Map of segment keys (e.g. "t20", "s5") to hit counts
*/
public void setHitDistribution(final Map<String, Integer> hitDistribution) {
this.mStats = null;
this.mHitDistribution = hitDistribution;
invalidate(); invalidate();
} }
@@ -140,8 +165,8 @@ public class HeatmapView extends View {
* Recalculates the dartboard geometry (center point and radius) and regenerates * Recalculates the dartboard geometry (center point and radius) and regenerates
* all segment paths based on the new dimensions. * all segment paths based on the new dimensions.
* *
* @param w Current width of this view * @param w Current width of this view
* @param h Current height of this view * @param h Current height of this view
* @param oldw Old width of this view (before size change) * @param oldw Old width of this view (before size change)
* @param oldh Old height of this view (before size change) * @param oldh Old height of this view (before size change)
*/ */
@@ -157,7 +182,8 @@ public class HeatmapView extends View {
} }
/** /**
* Calculates the Path objects for every segment on the dartboard based on current view size. * Calculates the Path objects for every segment on the dartboard based on
* current view size.
* <p> * <p>
* Clears the existing path cache and regenerates all paths for: * Clears the existing path cache and regenerates all paths for:
* - 20 double ring segments (90-100% radius) * - 20 double ring segments (90-100% radius)
@@ -197,33 +223,39 @@ public class HeatmapView extends View {
} }
/** /**
* Creates a closed Path representing a wedge-shaped arc segment of the dartboard. * Creates a closed Path representing a wedge-shaped arc segment of the
* dartboard.
* <p> * <p>
* Generates a path for a single dartboard segment (e.g., the triple 20 region) * Generates a path for a single dartboard segment (e.g., the triple 20 region)
* by drawing an arc along the outer boundary, then an arc along the inner boundary * by drawing an arc along the outer boundary, then an arc along the inner
* boundary
* in reverse, creating a closed wedge shape. * in reverse, creating a closed wedge shape.
* <p> * <p>
* The path is constructed by: * The path is constructed by:
* 1. Drawing an outer arc from startAngle to startAngle + sweep * 1. Drawing an outer arc from startAngle to startAngle + sweep
* 2. Drawing an inner arc from startAngle + sweep back to startAngle (negative sweep) * 2. Drawing an inner arc from startAngle + sweep back to startAngle (negative
* sweep)
* 3. Closing the path to connect the endpoints * 3. Closing the path to connect the endpoints
* *
* @param innerFactor Ratio of inner radius to base radius (0.0 to 1.0). E.g., 0.50 = 50% of radius * @param innerFactor Ratio of inner radius to base radius (0.0 to 1.0). E.g.,
* @param outerFactor Ratio of outer radius to base radius (0.0 to 1.0). E.g., 1.00 = 100% of radius * 0.50 = 50% of radius
* @param startAngle Starting angle in degrees (0° = right, increasing clockwise) * @param outerFactor Ratio of outer radius to base radius (0.0 to 1.0). E.g.,
* @param sweep Angular sweep in degrees (typically 18° for standard dartboard segments) * 1.00 = 100% of radius
* @param startAngle Starting angle in degrees (0° = right, increasing
* clockwise)
* @param sweep Angular sweep in degrees (typically 18° for standard
* dartboard segments)
* @return A closed Path object representing the arc segment * @return A closed Path object representing the arc segment
*/ */
private Path createArcPath(final float innerFactor, final float outerFactor, final float startAngle, final float sweep) { private Path createArcPath(final float innerFactor, final float outerFactor, final float startAngle,
final float sweep) {
final Path path = new Path(); final Path path = new Path();
final RectF outerRect = new RectF( final RectF outerRect = new RectF(
mCenterX - mRadius * outerFactor, mCenterY - mRadius * outerFactor, mCenterX - mRadius * outerFactor, mCenterY - mRadius * outerFactor,
mCenterX + mRadius * outerFactor, mCenterY + mRadius * outerFactor mCenterX + mRadius * outerFactor, mCenterY + mRadius * outerFactor);
);
final RectF innerRect = new RectF( final RectF innerRect = new RectF(
mCenterX - mRadius * innerFactor, mCenterY - mRadius * innerFactor, mCenterX - mRadius * innerFactor, mCenterY - mRadius * innerFactor,
mCenterX + mRadius * innerFactor, mCenterY + mRadius * innerFactor mCenterX + mRadius * innerFactor, mCenterY + mRadius * innerFactor);
);
path.arcTo(outerRect, startAngle, sweep); path.arcTo(outerRect, startAngle, sweep);
path.arcTo(innerRect, startAngle + sweep, -sweep); path.arcTo(innerRect, startAngle + sweep, -sweep);
@@ -238,10 +270,11 @@ public class HeatmapView extends View {
* 1. Early exit if no statistics are loaded * 1. Early exit if no statistics are loaded
* 2. Resolve color palette from resources (cold/hot) and hardcoded (empty) * 2. Resolve color palette from resources (cold/hot) and hardcoded (empty)
* 3. For each dartboard segment: * 3. For each dartboard segment:
* - Determine hit count from statistics * - Determine hit count from statistics
* - If zero hits: use subtle ghost color * - If zero hits: use subtle ghost color
* - If hits exist: interpolate between cold and hot based on normalized frequency * - If hits exist: interpolate between cold and hot based on normalized
* - Draw the filled path with the calculated color * frequency
* - Draw the filled path with the calculated color
* 4. Draw wireframe overlay on all segments for visual definition * 4. Draw wireframe overlay on all segments for visual definition
* <p> * <p>
* Color mapping: * Color mapping:
@@ -255,7 +288,15 @@ public class HeatmapView extends View {
@Override @Override
protected void onDraw(@NonNull final Canvas canvas) { protected void onDraw(@NonNull final Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
if (mStats == null) return; if (mHitDistribution == null)
return;
// Calculate max hits for normalization
int maxHits = 0;
for (final Integer count : mHitDistribution.values()) {
if (count != null && count > maxHits)
maxHits = count;
}
// Resolve branding colors from resources // Resolve branding colors from resources
final int coldColor = ContextCompat.getColor(getContext(), R.color.volt_green); final int coldColor = ContextCompat.getColor(getContext(), R.color.volt_green);
@@ -268,14 +309,14 @@ public class HeatmapView extends View {
final String statsKey = key.contains("_") ? key.substring(0, key.indexOf("_")) : key; final String statsKey = key.contains("_") ? key.substring(0, key.indexOf("_")) : key;
// Check if there are any hits recorded for this segment // Check if there are any hits recorded for this segment
final Integer hitCount = mStats.getHitDistribution().get(statsKey); final Integer hitCount = mHitDistribution.get(statsKey);
final int color; final int color;
if (hitCount == null || hitCount == 0) { if (hitCount == null || hitCount == 0) {
color = emptyColor; color = emptyColor;
} else { } else {
// Fetch the normalized heat (0.0 to 1.0) and evaluate against Green -> Red gradient // Normalize hit count against max and interpolate Green -> Red
final float weight = mStats.getNormalizedWeight(statsKey); final float weight = maxHits > 0 ? (float) hitCount / maxHits : 0f;
color = (int) mColorEvaluator.evaluate(weight, coldColor, hotColor); color = (int) mColorEvaluator.evaluate(weight, coldColor, hotColor);
} }

View File

@@ -124,17 +124,15 @@ public class PlayerItemView extends MaterialCardView {
* @param player The Player object containing data to display * @param player The Player object containing data to display
* @param gameAverage The game average to display (points per 3 darts) * @param gameAverage The game average to display (points per 3 darts)
* @param position The player's position (1 = 1st place, 2 = 2nd place, etc.) * @param position The player's position (1 = 1st place, 2 = 2nd place, etc.)
* @param isWinner Whether this player won (remaining score = 0)
*/ */
public void bindWithGameAverageAndPosition(@NonNull final Player player, final double gameAverage, public void bindWithGameAverageAndPosition(@NonNull final Player player, final double gameAverage,
final int position, final boolean isWinner) { final int position) {
Log.d(TAG, "bindWithGameAverageAndPosition() called with: player = [" + player Log.d(TAG, "bindWithGameAverageAndPosition() called with: player = [" + player
+ "], gameAverage = [" + gameAverage + "], position = [" + position + "], isWinner = [" + isWinner + "], gameAverage = [" + gameAverage + "], position = [" + position + "]");
+ "]");
// Build username with position indicator // Build username with position indicator
final String positionSuffix = getPositionSuffix(position); final String positionSuffix = getPositionSuffix(position);
final String displayName = isWinner final String displayName = position == 1
? "🏆 " + player.username + " (" + positionSuffix + ")" ? "🏆 " + player.username + " (" + positionSuffix + ")"
: player.username + " (" + positionSuffix + ")"; : player.username + " (" + positionSuffix + ")";

View File

@@ -4,6 +4,7 @@ import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View;
import android.widget.ScrollView; import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -11,6 +12,7 @@ import androidx.annotation.Nullable;
import com.aldo.apps.ochecompanion.R; import com.aldo.apps.ochecompanion.R;
import com.aldo.apps.ochecompanion.database.objects.Player; import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.database.objects.Statistics; import com.aldo.apps.ochecompanion.database.objects.Statistics;
import com.aldo.apps.ochecompanion.game.GameManager;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.google.android.material.imageview.ShapeableImageView; import com.google.android.material.imageview.ShapeableImageView;
@@ -18,17 +20,20 @@ import com.google.android.material.imageview.ShapeableImageView;
* PlayerStatsView: A complete dashboard component that visualizes a player's * PlayerStatsView: A complete dashboard component that visualizes a player's
* career performance, including a heatmap and detailed metrics. * career performance, including a heatmap and detailed metrics.
* <p> * <p>
* This custom {@link ScrollView} component provides a comprehensive player statistics * This custom {@link ScrollView} component provides a comprehensive player
* statistics
* display that includes: * display that includes:
* <ul> * <ul>
* <li>Player identity information (username and profile picture)</li> * <li>Player identity information (username and profile picture)</li>
* <li>High-level performance metrics (career average, first-9 average, checkout percentage, best finish)</li> * <li>High-level performance metrics (career average, first-9 average, checkout
* <li>Threshold score counters (60+, 100+, 140+, and 180 counts)</li> * percentage, best finish)</li>
* <li>Interactive dartboard heatmap visualization via {@link HeatmapView}</li> * <li>Threshold score counters (60+, 100+, 140+, and 180 counts)</li>
* <li>Interactive dartboard heatmap visualization via {@link HeatmapView}</li>
* </ul> * </ul>
* <p> * <p>
* The view is inflated from a layout resource and automatically initializes all * The view is inflated from a layout resource and automatically initializes all
* UI components. Data binding is performed via the {@link #bind(Player, Statistics)} * UI components. Data binding is performed via the
* {@link #bind(Player, Statistics)}
* method, which populates all fields with formatted player statistics. * method, which populates all fields with formatted player statistics.
* *
* @see HeatmapView * @see HeatmapView
@@ -49,6 +54,11 @@ public class PlayerStatsView extends ScrollView {
*/ */
private HeatmapView mHeatmap; private HeatmapView mHeatmap;
/**
* The label above the heatmap, hidden when showing match stats.
*/
private TextView mTvHeatmapLabel;
/** /**
* The circular profile image view that displays the player's avatar. * The circular profile image view that displays the player's avatar.
* Shows either a custom profile picture or a default user icon. * Shows either a custom profile picture or a default user icon.
@@ -85,23 +95,29 @@ public class PlayerStatsView extends ScrollView {
*/ */
private TextView mTvBestFinish; private TextView mTvBestFinish;
/** // Dynamic labels for metric cards (can switch between career/match mode)
* The {@link TextView} displaying the count of turns scoring 60 points or more. private TextView mTvCareerAvgLabel;
*/ private TextView mTvFirst9Label;
private TextView mTvCheckoutPctLabel;
private TextView mTvBestFinishLabel;
private TextView mTvCount60; private TextView mTvCount60;
/** /**
* The {@link TextView} displaying the count of turns scoring 100 points or more (century). * The {@link TextView} displaying the count of turns scoring 100 points or more
* (century).
*/ */
private TextView mTvCount100; private TextView mTvCount100;
/** /**
* The {@link TextView} displaying the count of turns scoring 140 points or more. * The {@link TextView} displaying the count of turns scoring 140 points or
* more.
*/ */
private TextView mTvCount140; private TextView mTvCount140;
/** /**
* The {@link TextView} displaying the count of perfect 180-point turns (maximum score). * The {@link TextView} displaying the count of perfect 180-point turns (maximum
* score).
*/ */
private TextView mTvCount180; private TextView mTvCount180;
@@ -110,14 +126,17 @@ public class PlayerStatsView extends ScrollView {
} }
/** /**
* Constructs a new {@link PlayerStatsView} with the specified context and attributes. * Constructs a new {@link PlayerStatsView} with the specified context and
* attributes.
* <p> * <p>
* This constructor inflates the player stats layout and initializes all UI components. * This constructor inflates the player stats layout and initializes all UI
* components.
* It performs the following operations: * It performs the following operations:
* <ol> * <ol>
* <li>Calls the superclass constructor with context and attributes</li> * <li>Calls the superclass constructor with context and attributes</li>
* <li>Inflates the {@code R.layout.player_stats_layout} resource into this view</li> * <li>Inflates the {@code R.layout.player_stats_layout} resource into this
* <li>Initializes all child views via {@link #initViews()}</li> * view</li>
* <li>Initializes all child views via {@link #initViews()}</li>
* </ol> * </ol>
* <p> * <p>
* This constructor is typically invoked when the view is inflated from XML. * This constructor is typically invoked when the view is inflated from XML.
@@ -134,15 +153,19 @@ public class PlayerStatsView extends ScrollView {
} }
/** /**
* Initializes all UI component references by finding views in the inflated layout. * Initializes all UI component references by finding views in the inflated
* layout.
* <p> * <p>
* This method binds the following UI components: * This method binds the following UI components:
* <ul> * <ul>
* <li><b>Identity:</b> Profile image and username text view</li> * <li><b>Identity:</b> Profile image and username text view</li>
* <li><b>Performance Metrics:</b> Career average, first-9 average, checkout percentage, * <li><b>Performance Metrics:</b> Career average, first-9 average, checkout
* and best finish text views</li> * percentage,
* <li><b>Threshold Counters:</b> Four text views for 60+, 100+, 140+, and 180 counts</li> * and best finish text views</li>
* <li><b>Visualization:</b> The {@link HeatmapView} for dartboard hit distribution</li> * <li><b>Threshold Counters:</b> Four text views for 60+, 100+, 140+, and 180
* counts</li>
* <li><b>Visualization:</b> The {@link HeatmapView} for dartboard hit
* distribution</li>
* </ul> * </ul>
* <p> * <p>
* This method is called once during construction and establishes references * This method is called once during construction and establishes references
@@ -150,6 +173,7 @@ public class PlayerStatsView extends ScrollView {
*/ */
private void initViews() { private void initViews() {
mHeatmap = findViewById(R.id.statsHeatmap); mHeatmap = findViewById(R.id.statsHeatmap);
mTvHeatmapLabel = findViewById(R.id.tvHeatmapLabel);
mIvAvatar = findViewById(R.id.ivPlayerAvatar); mIvAvatar = findViewById(R.id.ivPlayerAvatar);
mTvUsername = findViewById(R.id.tvUsername); mTvUsername = findViewById(R.id.tvUsername);
mTvCareerAvg = findViewById(R.id.tvCareerAvgValue); mTvCareerAvg = findViewById(R.id.tvCareerAvgValue);
@@ -157,6 +181,12 @@ public class PlayerStatsView extends ScrollView {
mTvCheckoutPct = findViewById(R.id.tvCheckoutPctValue); mTvCheckoutPct = findViewById(R.id.tvCheckoutPctValue);
mTvBestFinish = findViewById(R.id.tvBestFinishValue); mTvBestFinish = findViewById(R.id.tvBestFinishValue);
// Dynamic labels
mTvCareerAvgLabel = findViewById(R.id.tvCareerAvgLabel);
mTvFirst9Label = findViewById(R.id.tvFirst9Label);
mTvCheckoutPctLabel = findViewById(R.id.tvCheckoutPctLabel);
mTvBestFinishLabel = findViewById(R.id.tvBestFinishLabel);
// Threshold counters // Threshold counters
mTvCount60 = findViewById(R.id.tvCount60); mTvCount60 = findViewById(R.id.tvCount60);
mTvCount100 = findViewById(R.id.tvCount100); mTvCount100 = findViewById(R.id.tvCount100);
@@ -167,29 +197,45 @@ public class PlayerStatsView extends ScrollView {
/** /**
* Binds both the player identity and their accumulated stats to the UI. * Binds both the player identity and their accumulated stats to the UI.
* <p> * <p>
* This method populates all UI components with the provided player and statistics data. * This method populates all UI components with the provided player and
* statistics data.
* The binding process is organized into four logical sections: * The binding process is organized into four logical sections:
* <ol> * <ol>
* <li><b>Identity:</b> Displays the player's username in uppercase and loads their * <li><b>Identity:</b> Displays the player's username in uppercase and loads
* profile picture using Glide (or shows a default icon if no picture is set)</li> * their
* <li><b>High-Level Metrics:</b> Formats and displays career average, first-9 average, * profile picture using Glide (or shows a default icon if no picture is
* checkout percentage (with % symbol), and highest checkout value</li> * set)</li>
* <li><b>Threshold Totals:</b> Displays counts for 60+, 100+, 140+, and 180 point turns</li> * <li><b>High-Level Metrics:</b> Formats and displays career average, first-9
* <li><b>Heatmap Rendering:</b> Passes statistics to the {@link HeatmapView} for * average,
* visual dartboard representation</li> * checkout percentage (with % symbol), and highest checkout value</li>
* <li><b>Threshold Totals:</b> Displays counts for 60+, 100+, 140+, and 180
* point turns</li>
* <li><b>Heatmap Rendering:</b> Passes statistics to the {@link HeatmapView}
* for
* visual dartboard representation</li>
* </ol> * </ol>
* <p> * <p>
* If either parameter is {@code null}, the method logs an error and returns early * If either parameter is {@code null}, the method logs an error and returns
* early
* without modifying the UI. * without modifying the UI.
* *
* @param player The {@link Player} object containing identity information (username, profile picture) * @param player The {@link Player} object containing identity information
* @param stats The {@link Statistics} object containing all accumulated performance metrics * (username, profile picture)
* @param stats The {@link Statistics} object containing all accumulated
* performance metrics
*/ */
public void bind(@NonNull final Player player, @NonNull final Statistics stats) { public void bind(@NonNull final Player player, @NonNull final Statistics stats) {
if (player == null || stats == null) { if (player == null || stats == null) {
Log.e(TAG, "bind: Cannot bind, return"); Log.e(TAG, "bind: Cannot bind, return");
return; return;
} }
// Restore labels to career mode
mTvCareerAvgLabel.setText("Career Avg");
mTvFirst9Label.setText("First 9 Avg");
mTvCheckoutPctLabel.setText("Checkout %");
mTvBestFinishLabel.setText("High Finish");
// 1. Identity // 1. Identity
mTvUsername.setText(player.username.toUpperCase()); mTvUsername.setText(player.username.toUpperCase());
if (player.profilePictureUri != null) { if (player.profilePictureUri != null) {
@@ -211,6 +257,66 @@ public class PlayerStatsView extends ScrollView {
mTvCount180.setText(String.valueOf(stats.getCount180())); mTvCount180.setText(String.valueOf(stats.getCount180()));
// 4. Heatmap Rendering // 4. Heatmap Rendering
mTvHeatmapLabel.setText("Impact Heatmap");
mTvHeatmapLabel.setVisibility(View.VISIBLE);
mHeatmap.setVisibility(View.VISIBLE);
mHeatmap.setStats(stats); mHeatmap.setStats(stats);
} }
/**
* Binds match-specific statistics from a {@link GameManager.PlayerState} to the
* UI.
* <p>
* Unlike {@link #bind(Player, Statistics)} which shows career stats, this
* method
* shows stats from the just-completed match only. Labels are updated to reflect
* match context (e.g., "Match Avg" instead of "Career Avg").
* The heatmap is hidden since per-match dart distribution is not tracked.
*
* @param playerState The player's match state containing per-match statistics
*/
public void bindMatchStats(@NonNull final GameManager.PlayerState playerState) {
if (playerState == null) {
Log.e(TAG, "bindMatchStats: Cannot bind null playerState");
return;
}
final Player player = playerState.player;
// 1. Identity
mTvUsername.setText(player.username.toUpperCase());
if (player.profilePictureUri != null) {
Glide.with(getContext()).load(player.profilePictureUri)
.placeholder(R.drawable.ic_users)
.into(mIvAvatar);
} else {
mIvAvatar.setImageResource(R.drawable.ic_users);
}
// 2. Switch labels to match mode
mTvCareerAvgLabel.setText("Match Avg");
mTvFirst9Label.setText("Darts Thrown");
mTvCheckoutPctLabel.setText("Checkout");
mTvBestFinishLabel.setText("High Turn");
// 3. Match-specific metrics
mTvCareerAvg.setText(String.format("%.1f", playerState.getMatchAverage()));
mTvFirst9.setText(String.valueOf(playerState.dartsThrown));
mTvCheckoutPct.setText(playerState.matchCheckoutValue > 0
? String.valueOf(playerState.matchCheckoutValue)
: "");
mTvBestFinish.setText(String.valueOf(playerState.matchHighestTurnScore));
// 4. Threshold Totals (match-specific)
mTvCount60.setText(String.valueOf(playerState.matchCount60Plus));
mTvCount100.setText(String.valueOf(playerState.matchCount100Plus));
mTvCount140.setText(String.valueOf(playerState.matchCount140Plus));
mTvCount180.setText(String.valueOf(playerState.matchCount180));
// 5. Heatmap — render per-match dart hit distribution
mTvHeatmapLabel.setVisibility(View.VISIBLE);
mTvHeatmapLabel.setText("Match Heatmap");
mHeatmap.setVisibility(View.VISIBLE);
mHeatmap.setHitDistribution(playerState.matchHitDistribution);
}
} }

View File

@@ -176,7 +176,7 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
final Player player = participantData.player; final Player player = participantData.player;
final double gameAverage = participantData.gameAverage; final double gameAverage = participantData.gameAverage;
final boolean isWinner = participantData.score == 0; // Winner has 0 remaining score final boolean isWinner = participantData.score == 0; // Winner has 0 remaining score
mItemView.bindWithGameAverageAndPosition(player, gameAverage, position, isWinner); mItemView.bindWithGameAverageAndPosition(player, gameAverage, position);
} }
} }
} }

View File

@@ -1,6 +1,8 @@
package com.aldo.apps.ochecompanion.utils; package com.aldo.apps.ochecompanion.utils;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* MatchProgress: A serializable snapshot of an ongoing X01 session. * MatchProgress: A serializable snapshot of an ongoing X01 session.
@@ -28,22 +30,46 @@ public class MatchProgress {
/** The total number of darts thrown by the player so far. */ /** The total number of darts thrown by the player so far. */
public int dartsThrown; public int dartsThrown;
// ---- Per-Match Statistics ----
public int matchPointsScored;
public int matchBustCount;
public int matchHighestTurnScore;
public int matchCheckoutValue;
public int matchCount60Plus;
public int matchCount100Plus;
public int matchCount140Plus;
public int matchCount180;
public Map<String, Integer> matchHitDistribution;
/** Default constructor required for serialization/deserialization. */ /** Default constructor required for serialization/deserialization. */
public PlayerStateSnapshot() {} public PlayerStateSnapshot() {
}
/** /**
* Constructs a new PlayerStateSnapshot with the specified values. * Constructs a new PlayerStateSnapshot from a full set of match state values.
*
* @param playerId The unique ID of the player (0 for guests)
* @param name The display name of the player
* @param remainingScore The player's current remaining score in the match
* @param dartsThrown The total number of darts thrown by the player so far
*/ */
public PlayerStateSnapshot(final long playerId, final String name, final int remainingScore, final int dartsThrown) { public PlayerStateSnapshot(final long playerId, final String name,
final int remainingScore, final int dartsThrown,
final int matchPointsScored, final int matchBustCount,
final int matchHighestTurnScore, final int matchCheckoutValue,
final int matchCount60Plus, final int matchCount100Plus,
final int matchCount140Plus, final int matchCount180,
final Map<String, Integer> matchHitDistribution) {
this.playerId = playerId; this.playerId = playerId;
this.name = name; this.name = name;
this.remainingScore = remainingScore; this.remainingScore = remainingScore;
this.dartsThrown = dartsThrown; this.dartsThrown = dartsThrown;
this.matchPointsScored = matchPointsScored;
this.matchBustCount = matchBustCount;
this.matchHighestTurnScore = matchHighestTurnScore;
this.matchCheckoutValue = matchCheckoutValue;
this.matchCount60Plus = matchCount60Plus;
this.matchCount100Plus = matchCount100Plus;
this.matchCount140Plus = matchCount140Plus;
this.matchCount180 = matchCount180;
this.matchHitDistribution = matchHitDistribution != null
? new HashMap<>(matchHitDistribution)
: new HashMap<>();
} }
} }
} }

View File

@@ -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="@android:color/white"
android:pathData="M15.41,16.59L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.59Z" />
</vector>

View File

@@ -210,4 +210,47 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone"/> android:visibility="gone"/>
<!-- Player switching navigation (overlaid on stats view) -->
<LinearLayout
android:id="@+id/stats_nav_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="10dp"
android:background="@color/surface_primary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent">
<ImageButton
android:id="@+id/btn_stats_prev"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_chevron_left"
android:contentDescription="Previous player"
app:tint="@color/volt_green" />
<TextView
android:id="@+id/tv_stats_player_indicator"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:fontFamily="sans-serif-medium"
android:textColor="@color/text_secondary"
android:textSize="13sp"
tools:text="1 / 3" />
<ImageButton
android:id="@+id/btn_stats_next"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_chevron_right"
android:contentDescription="Next player"
app:tint="@color/volt_green" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -53,6 +53,7 @@
<!-- CENTERPIECE: HEATMAP --> <!-- CENTERPIECE: HEATMAP -->
<TextView <TextView
android:id="@+id/tvHeatmapLabel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Impact Heatmap" android:text="Impact Heatmap"
@@ -74,12 +75,12 @@
<LinearLayout style="@style/StatsCardStyle"> <LinearLayout style="@style/StatsCardStyle">
<TextView android:id="@+id/tvCareerAvgValue" style="@style/StatsValueStyle" tools:text="94.2" /> <TextView android:id="@+id/tvCareerAvgValue" style="@style/StatsValueStyle" tools:text="94.2" />
<TextView android:text="Career Avg" style="@style/StatsLabelStyle" /> <TextView android:id="@+id/tvCareerAvgLabel" android:text="Career Avg" style="@style/StatsLabelStyle" />
</LinearLayout> </LinearLayout>
<LinearLayout style="@style/StatsCardStyle" android:layout_marginStart="12dp"> <LinearLayout style="@style/StatsCardStyle" android:layout_marginStart="12dp">
<TextView android:id="@+id/tvFirst9Value" style="@style/StatsValueStyle" tools:text="102.5" /> <TextView android:id="@+id/tvFirst9Value" style="@style/StatsValueStyle" tools:text="102.5" />
<TextView android:text="First 9 Avg" style="@style/StatsLabelStyle" /> <TextView android:id="@+id/tvFirst9Label" android:text="First 9 Avg" style="@style/StatsLabelStyle" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@@ -93,12 +94,12 @@
<LinearLayout style="@style/StatsCardStyle"> <LinearLayout style="@style/StatsCardStyle">
<TextView android:id="@+id/tvCheckoutPctValue" style="@style/StatsValueStyle" tools:text="38.5%" /> <TextView android:id="@+id/tvCheckoutPctValue" style="@style/StatsValueStyle" tools:text="38.5%" />
<TextView android:text="Checkout %" style="@style/StatsLabelStyle" /> <TextView android:id="@+id/tvCheckoutPctLabel" android:text="Checkout %" style="@style/StatsLabelStyle" />
</LinearLayout> </LinearLayout>
<LinearLayout style="@style/StatsCardStyle" android:layout_marginStart="12dp"> <LinearLayout style="@style/StatsCardStyle" android:layout_marginStart="12dp">
<TextView android:id="@+id/tvBestFinishValue" style="@style/StatsValueStyle" tools:text="170" /> <TextView android:id="@+id/tvBestFinishValue" style="@style/StatsValueStyle" tools:text="170" />
<TextView android:text="High Finish" style="@style/StatsLabelStyle" /> <TextView android:id="@+id/tvBestFinishLabel" android:text="High Finish" style="@style/StatsLabelStyle" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>