Added a Player stats view to the GameResult

Currently only showing the stats of the winning player and also only the overall stats, has to be improved to show stats of all players and also only the stats of the match, not the overall one.
This commit is contained in:
Alexander Doerflinger
2026-02-02 14:54:06 +01:00
parent 5e627aa50c
commit 689bf2808a
6 changed files with 320 additions and 2 deletions

View File

@@ -29,6 +29,7 @@ import androidx.preference.PreferenceManager;
import com.aldo.apps.ochecompanion.database.AppDatabase;
import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.database.objects.Statistics;
import com.aldo.apps.ochecompanion.ui.PlayerStatsView;
import com.aldo.apps.ochecompanion.utils.CheckoutEngine;
import com.aldo.apps.ochecompanion.utils.DartsConstants;
import com.aldo.apps.ochecompanion.utils.SoundEngine;
@@ -198,6 +199,16 @@ public class GameActivity extends AppCompatActivity {
*/
private View btnTriple;
/**
* The Button to open the stats view.
*/
private MaterialButton mShowStatsBtn;
/**
* The {@link PlayerStatsView} to display player statistics.
*/
private PlayerStatsView mStatsView;
/**
* Array of three TextViews showing darts thrown in current turn.
*/
@@ -316,6 +327,14 @@ public class GameActivity extends AppCompatActivity {
btnDouble.setOnClickListener(v -> setMultiplier(2));
btnTriple.setOnClickListener(v -> setMultiplier(3));
mShowStatsBtn = findViewById(R.id.show_stats_btn);
mStatsView = findViewById(R.id.player_stats_view);
mShowStatsBtn.setOnClickListener(v -> {
mStatsView.setVisibility(View.VISIBLE);
mShowStatsBtn.setVisibility(View.GONE);
});
findViewById(R.id.btnSubmitTurn).setOnClickListener(v -> submitTurn());
findViewById(R.id.btnUndoDart).setOnClickListener(v -> undoLastDart());
}
@@ -876,12 +895,27 @@ public class GameActivity extends AppCompatActivity {
mSoundEngine.playWinnerSound();
}
mShowStatsBtn.setVisibility(View.VISIBLE);
attachPlayerStats();
// TODO: Consider adding:
// - Statistics display
// - Save match to database
// - Offer rematch
}
private void attachPlayerStats() {
new Thread(() -> {
try {
final Player player = mPlayerStates.get(mActivePlayerIndex).player;
final Statistics statistics = AppDatabase.getDatabase(GameActivity.this).statisticsDao().getStatisticsForPlayer(player.id);
runOnUiThread(() -> {
mStatsView.bind(player, statistics);
});
} catch (Exception e) {
Log.e(TAG, "attachPlayerStats: Failed to increment matches", e);
}
}).start();
}
/**
* Plays confetti animation and displays winner's name overlay.
* Shows full-screen dimmer with celebratory confetti effect.

View File

@@ -0,0 +1,81 @@
package com.aldo.apps.ochecompanion.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.aldo.apps.ochecompanion.R;
import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.database.objects.Statistics;
import com.bumptech.glide.Glide;
import com.google.android.material.imageview.ShapeableImageView;
/**
* PlayerStatsView: A complete dashboard component that visualizes a player's
* career performance, including a heatmap and detailed metrics.
*/
public class PlayerStatsView extends ScrollView {
// UI References
private HeatmapView mHeatmap;
private ShapeableImageView mIvAvatar;
private TextView mTvUsername, mTvCareerAvg, mTvFirst9, mTvCheckoutPct, mTvBestFinish;
private TextView mTvCount60, mTvCount100, mTvCount140, mTvCount180;
public PlayerStatsView(@NonNull final Context context) {
this(context, null);
}
public PlayerStatsView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.player_stats_layout, this, true);
initViews();
}
private void initViews() {
mHeatmap = findViewById(R.id.statsHeatmap);
mIvAvatar = findViewById(R.id.ivPlayerAvatar);
mTvUsername = findViewById(R.id.tvUsername);
mTvCareerAvg = findViewById(R.id.tvCareerAvgValue);
mTvFirst9 = findViewById(R.id.tvFirst9Value);
mTvCheckoutPct = findViewById(R.id.tvCheckoutPctValue);
mTvBestFinish = findViewById(R.id.tvBestFinishValue);
// Threshold counters
mTvCount60 = findViewById(R.id.tvCount60);
mTvCount100 = findViewById(R.id.tvCount100);
mTvCount140 = findViewById(R.id.tvCount140);
mTvCount180 = findViewById(R.id.tvCount180);
}
/**
* Binds both the player identity and their accumulated stats to the UI.
*/
public void bind(@NonNull final Player player, final @NonNull Statistics stats) {
// 1. Identity
mTvUsername.setText(player.username.toUpperCase());
if (player.profilePictureUri != null) {
Glide.with(getContext()).load(player.profilePictureUri).into(mIvAvatar);
} else {
mIvAvatar.setImageResource(R.drawable.ic_users);
}
// 2. High-Level Metrics
mTvCareerAvg.setText(String.format("%.1f", stats.getAverage()));
mTvFirst9.setText(String.format("%.1f", stats.getFirst9Average()));
mTvCheckoutPct.setText(String.format("%.1f%%", stats.getCheckoutPercentage()));
mTvBestFinish.setText(String.valueOf(stats.getHighestCheckout()));
// 3. Threshold Totals
mTvCount60.setText(String.valueOf(stats.getCount60Plus()));
mTvCount100.setText(String.valueOf(stats.getCount100Plus()));
mTvCount140.setText(String.valueOf(stats.getCount140Plus()));
mTvCount180.setText(String.valueOf(stats.getCount180()));
// 4. Heatmap Rendering
mHeatmap.setStats(stats);
}
}

View File

@@ -177,10 +177,28 @@
android:visibility="gone"
tools:text="Zander"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/show_stats_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/txt_game_show_statistics"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/winner_text"
style="@style/Widget.Oche_Button_Primary"
android:visibility="gone"/>
<nl.dionsegijn.konfetti.xml.KonfettiView
android:id="@+id/konfetti_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<com.aldo.apps.ochecompanion.ui.PlayerStatsView
android:id="@+id/player_stats_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="15dp"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp"
android:background="@color/background_primary">
<!-- HEADER: IDENTITY -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_marginBottom="24dp"
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/ivPlayerAvatar"
android:layout_width="64dp"
android:layout_height="64dp"
android:padding="2dp"
app:strokeWidth="2dp"
app:strokeColor="@color/volt_green"
app:shapeAppearanceOverlay="@style/ShapeAppearance.MaterialComponents.SmallComponent"
android:src="@drawable/ic_users" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/tvUsername"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-black"
android:textColor="@color/text_primary"
android:textSize="22sp"
tools:text="SNAKEBITE" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="PRO ANALYTICS ACTIVE"
android:textAppearance="@style/TextAppearance.Oche.Caption"
android:textColor="@color/volt_green" />
</LinearLayout>
</LinearLayout>
<!-- CENTERPIECE: HEATMAP -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Impact Heatmap"
android:textAppearance="@style/TextAppearance.Oche.Caption"
android:layout_marginBottom="8dp" />
<com.aldo.apps.ochecompanion.ui.HeatmapView
android:id="@+id/statsHeatmap"
android:layout_width="match_parent"
android:layout_height="320dp"
android:layout_marginBottom="24dp" />
<!-- METRICS GRID (Top Row) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2">
<LinearLayout style="@style/StatsCardStyle">
<TextView android:id="@+id/tvCareerAvgValue" style="@style/StatsValueStyle" tools:text="94.2" />
<TextView android:text="Career Avg" style="@style/StatsLabelStyle" />
</LinearLayout>
<LinearLayout style="@style/StatsCardStyle" android:layout_marginStart="12dp">
<TextView android:id="@+id/tvFirst9Value" style="@style/StatsValueStyle" tools:text="102.5" />
<TextView android:text="First 9 Avg" style="@style/StatsLabelStyle" />
</LinearLayout>
</LinearLayout>
<!-- METRICS GRID (Bottom Row) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal"
android:weightSum="2">
<LinearLayout style="@style/StatsCardStyle">
<TextView android:id="@+id/tvCheckoutPctValue" style="@style/StatsValueStyle" tools:text="38.5%" />
<TextView android:text="Checkout %" style="@style/StatsLabelStyle" />
</LinearLayout>
<LinearLayout style="@style/StatsCardStyle" android:layout_marginStart="12dp">
<TextView android:id="@+id/tvBestFinishValue" style="@style/StatsValueStyle" tools:text="170" />
<TextView android:text="High Finish" style="@style/StatsLabelStyle" />
</LinearLayout>
</LinearLayout>
<!-- SCORING THRESHOLDS -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Threshold Hits"
android:textAppearance="@style/TextAppearance.Oche.Caption"
android:layout_marginBottom="12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/shape_round_surface"
android:padding="16dp">
<LinearLayout style="@style/ThresholdItemStyle">
<TextView android:id="@+id/tvCount60" style="@style/ThresholdValueStyle" tools:text="450" />
<TextView android:text="60+" style="@style/ThresholdLabelStyle" />
</LinearLayout>
<LinearLayout style="@style/ThresholdItemStyle">
<TextView android:id="@+id/tvCount100" style="@style/ThresholdValueStyle" tools:text="112" />
<TextView android:text="100+" style="@style/ThresholdLabelStyle" />
</LinearLayout>
<LinearLayout style="@style/ThresholdItemStyle">
<TextView android:id="@+id/tvCount140" style="@style/ThresholdValueStyle" tools:text="42" />
<TextView android:text="140+" style="@style/ThresholdLabelStyle" />
</LinearLayout>
<LinearLayout style="@style/ThresholdItemStyle">
<TextView android:id="@+id/tvCount180" style="@style/ThresholdValueStyle" android:textColor="@color/volt_green" tools:text="12" />
<TextView android:text="180s" style="@style/ThresholdLabelStyle" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -32,6 +32,7 @@
<string name="txt_game_btn_triple">Triple</string>
<string name="txt_game_btn_bull">Bull</string>
<string name="txt_game_btn_submit">Submit Turn</string>
<string name="txt_game_show_statistics">Show Stats</string>
<!-- Preference Strings -->
<string name="pref_key_day_night_mode_auto">day_night_mode_auto</string>

View File

@@ -87,4 +87,46 @@
<item name="cornerSize">12dp</item>
</style>
<!-- Stats Dashboard Specific Styles -->
<style name="StatsCardStyle">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">80dp</item>
<item name="android:layout_weight">1</item>
<item name="android:background">@drawable/shape_round_surface</item>
<item name="android:gravity">center</item>
<item name="android:orientation">vertical</item>
</style>
<style name="StatsValueStyle">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
<item name="android:fontFamily">sans-serif-black</item>
<item name="android:textColor">@color/text_primary</item>
</style>
<style name="StatsLabelStyle">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">10sp</item>
<item name="android:textAllCaps">true</item>
<item name="android:textColor">@color/text_secondary</item>
<item name="android:fontFamily">sans-serif-medium</item>
</style>
<style name="ThresholdItemStyle">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item>
<item name="android:gravity">center</item>
<item name="android:orientation">vertical</item>
</style>
<style name="ThresholdValueStyle" parent="StatsValueStyle">
<item name="android:textSize">18sp</item>
</style>
<style name="ThresholdLabelStyle" parent="StatsLabelStyle" />
</resources>