Fixed animation, fixed UI
This commit is contained in:
@@ -6,8 +6,11 @@
|
||||
that other apps created.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<attribution android:tag="oche_gameplay" android:label="@string/attribution_label" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.aldo.apps.ochecompanion;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.RectF;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
@@ -16,7 +19,9 @@ import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
@@ -157,6 +162,15 @@ public class AddPlayerActivity extends AppCompatActivity {
|
||||
}
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<String> requestPermissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(), isGranted -> {
|
||||
if (isGranted) {
|
||||
mGetContent.launch("image/*");
|
||||
} else {
|
||||
Toast.makeText(this, "Permission denied to read images", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Called when the activity is first created. Initializes UI and loads existing player if present.
|
||||
*
|
||||
@@ -207,7 +221,7 @@ public class AddPlayerActivity extends AppCompatActivity {
|
||||
mCropOverlay = findViewById(R.id.cropOverlay);
|
||||
|
||||
// Set up click listeners
|
||||
mProfilePictureView.setOnClickListener(v -> mGetContent.launch("image/*"));
|
||||
mProfilePictureView.setOnClickListener(v -> checkForPermissionAndLaunchImagePicker());
|
||||
mSaveButton.setOnClickListener(v -> savePlayer());
|
||||
if (mBtnDelete != null) {
|
||||
mBtnDelete.setOnClickListener(v -> deletePlayer());
|
||||
@@ -216,6 +230,32 @@ public class AddPlayerActivity extends AppCompatActivity {
|
||||
findViewById(R.id.btnCancelCrop).setOnClickListener(v -> exitCropMode());
|
||||
}
|
||||
|
||||
private void checkForPermissionAndLaunchImagePicker() {
|
||||
final String permission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||
? Manifest.permission.READ_MEDIA_IMAGES
|
||||
: Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||
// Already have permission
|
||||
mGetContent.launch("image/*");
|
||||
} else if (shouldShowRequestPermissionRationale(permission)) {
|
||||
// Explain to the user why the permission is needed before requesting
|
||||
showRationaleDialog(permission);
|
||||
} else {
|
||||
// Directly request the permission
|
||||
requestPermissionLauncher.launch(permission);
|
||||
}
|
||||
}
|
||||
|
||||
private void showRationaleDialog(final String permission) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.txt_permission_hint_title)
|
||||
.setMessage(R.string.txt_permission_hint_description)
|
||||
.setPositiveButton(R.string.txt_permission_hint_button_ok, (d, w) -> requestPermissionLauncher.launch(permission))
|
||||
.setNegativeButton(R.string.txt_permission_hint_button_cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes gesture detectors to handle pinch-to-zoom and pan gestures.
|
||||
*/
|
||||
@@ -436,7 +476,9 @@ public class AddPlayerActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the selected player from the database.
|
||||
* Deletes the currently loaded player from the database.
|
||||
* Shows confirmation toast and closes the activity upon successful deletion.
|
||||
* Runs database operation on background thread. Does nothing if no player is loaded.
|
||||
*/
|
||||
private void deletePlayer() {
|
||||
if (mExistingPlayer == null) return;
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.media.SoundPool;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.VibrationEffect;
|
||||
@@ -26,10 +25,8 @@ import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.aldo.apps.ochecompanion.database.AppDatabase;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Match;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Player;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Statistics;
|
||||
import com.aldo.apps.ochecompanion.utils.CheckoutConstants;
|
||||
import com.aldo.apps.ochecompanion.utils.CheckoutEngine;
|
||||
import com.aldo.apps.ochecompanion.utils.DartsConstants;
|
||||
import com.aldo.apps.ochecompanion.utils.SoundEngine;
|
||||
@@ -169,7 +166,7 @@ public class GameActivity extends AppCompatActivity {
|
||||
/**
|
||||
* Array of three TextViews showing darts thrown in current turn.
|
||||
*/
|
||||
private TextView[] tvDartPills = new TextView[3];
|
||||
private final TextView[] tvDartPills = new TextView[3];
|
||||
|
||||
/**
|
||||
* GridLayout container holding numeric keyboard buttons (1-20).
|
||||
@@ -220,9 +217,7 @@ public class GameActivity extends AppCompatActivity {
|
||||
new Thread(() -> {
|
||||
final List<Player> allAvailablePlayers = AppDatabase.getDatabase(GameActivity.this).playerDao().getAllPlayers();
|
||||
Log.d(TAG, "onCreate: allAvailablePlayers = [" + allAvailablePlayers + "]");
|
||||
runOnUiThread(() -> {
|
||||
setupGame(allAvailablePlayers);
|
||||
});
|
||||
runOnUiThread(() -> setupGame(allAvailablePlayers));
|
||||
}).start();
|
||||
}
|
||||
|
||||
@@ -395,6 +390,16 @@ public class GameActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates player statistics in the database after a turn.
|
||||
* Tracks darts thrown, points made, missed darts, and updates career average.
|
||||
* Runs on background thread to avoid blocking UI.
|
||||
*
|
||||
* @param active Current player's game state
|
||||
* @param dartsThrown Number of darts thrown this turn
|
||||
* @param pointsMade Total points scored this turn
|
||||
* @param wasBust Whether the turn resulted in a bust
|
||||
*/
|
||||
private void updatePlayerStats(GameActivity.X01State active, int dartsThrown, int pointsMade, boolean wasBust) {
|
||||
new Thread(() -> {
|
||||
final Player player = active.player;
|
||||
@@ -411,7 +416,7 @@ public class GameActivity extends AppCompatActivity {
|
||||
// Calculate career average: total points / total darts thrown
|
||||
final long totalDarts = playerStats.getDartsThrown();
|
||||
if (totalDarts > 0) {
|
||||
player.careerAverage = (double) playerStats.getOverallPointsMade() / totalDarts;
|
||||
player.careerAverage = (double) playerStats.getOverallPointsMade() / totalDarts * 3;
|
||||
} else {
|
||||
player.careerAverage = 0.0;
|
||||
}
|
||||
@@ -443,7 +448,15 @@ public class GameActivity extends AppCompatActivity {
|
||||
|
||||
final Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
|
||||
if (vibrator != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(300, VibrationEffect.DEFAULT_AMPLITUDE));
|
||||
Log.d(TAG, "submitTurn: Pattern vibration");
|
||||
// Pattern that should match the 180 shout.
|
||||
long[] pattern = {0, 150, 100, 1650, 50, 150, 10, 500, 300, 200};
|
||||
vibrator.vibrate(VibrationEffect.createWaveform(pattern, -1));
|
||||
} else if (vibrator != null) {
|
||||
Log.d(TAG, "submitTurn: Vibrating legacy mode");
|
||||
vibrator.vibrate(500);
|
||||
} else {
|
||||
Log.e(TAG, "submitTurn: Vibrator not available");
|
||||
}
|
||||
mSoundEngine.playOneHundredAndEightySound();
|
||||
}
|
||||
@@ -601,9 +614,11 @@ public class GameActivity extends AppCompatActivity {
|
||||
|
||||
/**
|
||||
* Handles win condition when a player finishes on zero with a double.
|
||||
* Displays win toast and finishes activity.
|
||||
* Updates statistics, displays win toast, and plays celebration animation.
|
||||
*
|
||||
* @param winner X01State of the winning player
|
||||
* @param dartsThrown Number of darts thrown in the winning turn
|
||||
* @param pointsMade Points scored in the winning turn
|
||||
*/
|
||||
private void handleWin(final X01State winner, final int dartsThrown, final int pointsMade) {
|
||||
updatePlayerStats(winner, dartsThrown, pointsMade, false);
|
||||
@@ -619,6 +634,12 @@ public class GameActivity extends AppCompatActivity {
|
||||
// - Offer rematch
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays confetti animation and displays winner's name overlay.
|
||||
* Shows full-screen dimmer with celebratory confetti effect.
|
||||
*
|
||||
* @param winnerName Name of the winning player to display
|
||||
*/
|
||||
private void playWinnerAnimation(final String winnerName) {
|
||||
final KonfettiView konfettiView = findViewById(R.id.konfetti_view);
|
||||
final View dimmerLayout = findViewById(R.id.dimmer);
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
@@ -20,6 +19,7 @@ import com.aldo.apps.ochecompanion.database.AppDatabase;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Player;
|
||||
import com.aldo.apps.ochecompanion.database.objects.Match;
|
||||
import com.aldo.apps.ochecompanion.ui.MatchRecapView;
|
||||
import com.aldo.apps.ochecompanion.ui.QuickStartButton;
|
||||
import com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter;
|
||||
import com.aldo.apps.ochecompanion.utils.DartsConstants;
|
||||
import com.aldo.apps.ochecompanion.utils.UIConstants;
|
||||
@@ -77,7 +77,11 @@ public class MainMenuActivity extends AppCompatActivity {
|
||||
return insets;
|
||||
});
|
||||
|
||||
findViewById(R.id.quick_start_btn).setOnClickListener(v -> quickStart());
|
||||
final QuickStartButton quickStartBtn = findViewById(R.id.quick_start_btn);
|
||||
final String defaultGameMode = mSettingsPref.getString(getString(R.string.pref_desc_standard_game_mode),
|
||||
getString(R.string.pref_game_mode_501_value));
|
||||
quickStartBtn.setSubText(defaultGameMode);
|
||||
quickStartBtn.setOnClickListener(v -> quickStart());
|
||||
findViewById(R.id.btnSettings).setOnClickListener(v -> launchSettings());
|
||||
|
||||
// Set up match recap view with test data functionality
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
package com.aldo.apps.ochecompanion.utils;
|
||||
|
||||
import static android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION;
|
||||
import static android.media.AudioAttributes.USAGE_GAME;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.SoundPool;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.aldo.apps.ochecompanion.R;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.SimpleTimeZone;
|
||||
|
||||
/**
|
||||
* Singleton sound engine for managing game audio effects.
|
||||
* Uses Android SoundPool API for low-latency sound playback during gameplay.
|
||||
* Supports winner celebration, bust notifications, and perfect score achievements.
|
||||
*/
|
||||
public final class SoundEngine {
|
||||
|
||||
/**
|
||||
@@ -16,30 +23,80 @@ public final class SoundEngine {
|
||||
*/
|
||||
private static final String TAG = "SoundEngine";
|
||||
|
||||
private Context mContext;
|
||||
/**
|
||||
* Application context used for audio operations.
|
||||
* On Android R+, uses attribution context for proper audio tracking.
|
||||
*/
|
||||
private final Context mContext;
|
||||
|
||||
/**
|
||||
* Singleton instance of the SoundEngine.
|
||||
*/
|
||||
private static SoundEngine sInstance;
|
||||
|
||||
/**
|
||||
* Android SoundPool for low-latency audio playback.
|
||||
* Configured with game audio attributes and max 5 concurrent streams.
|
||||
*/
|
||||
private final SoundPool mSoundPool;
|
||||
|
||||
/**
|
||||
* Flag indicating whether all sound resources have been loaded.
|
||||
* Set to true by the OnLoadCompleteListener once all sounds are ready.
|
||||
*/
|
||||
private boolean mIsReady;
|
||||
|
||||
private int mWinnerSoundId;
|
||||
/**
|
||||
* Sound ID for the winner celebration audio effect.
|
||||
* Loaded from R.raw.winner resource.
|
||||
*/
|
||||
private final int mWinnerSoundId;
|
||||
|
||||
private int mBustedSoundId;
|
||||
/**
|
||||
* Sound ID for the bust notification audio effect.
|
||||
* Loaded from R.raw.busted resource.
|
||||
*/
|
||||
private final int mBustedSoundId;
|
||||
|
||||
private int m180SoundId;
|
||||
/**
|
||||
* Sound ID for the perfect score (180) celebration audio effect.
|
||||
* Loaded from R.raw.onehundredandeighty resource.
|
||||
*/
|
||||
private final int m180SoundId;
|
||||
|
||||
/**
|
||||
* Private constructor for singleton pattern.
|
||||
* Initializes SoundPool with game audio attributes and preloads sound effects.
|
||||
*
|
||||
* @param context Application context for loading sound resources
|
||||
*/
|
||||
private SoundEngine(final Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
mContext = context.createAttributionContext("oche_gameplay");
|
||||
} else {
|
||||
mContext = context;
|
||||
mSoundPool = new SoundPool.Builder().setMaxStreams(5).build();
|
||||
}
|
||||
mSoundPool = new SoundPool.Builder()
|
||||
.setMaxStreams(5)
|
||||
.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setUsage(USAGE_GAME)
|
||||
.setContentType(CONTENT_TYPE_SONIFICATION)
|
||||
.build())
|
||||
.build();
|
||||
mSoundPool.setOnLoadCompleteListener((soundPool, sampleId, status) -> mIsReady = true);
|
||||
mWinnerSoundId = mSoundPool.load(context, R.raw.winner, 1);
|
||||
mBustedSoundId = mSoundPool.load(context, R.raw.busted, 1);
|
||||
m180SoundId = mSoundPool.load(context, R.raw.onehundredandeighty, 1);
|
||||
mWinnerSoundId = mSoundPool.load(context.getApplicationContext(), R.raw.winner, 1);
|
||||
mBustedSoundId = mSoundPool.load(context.getApplicationContext(), R.raw.busted, 1);
|
||||
m180SoundId = mSoundPool.load(context.getApplicationContext(), R.raw.onehundredandeighty, 1);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton SoundEngine instance.
|
||||
* Creates instance on first call using the provided context.
|
||||
*
|
||||
* @param context Application context for initialization
|
||||
* @return Singleton SoundEngine instance
|
||||
*/
|
||||
public static SoundEngine getInstance(final Context context) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new SoundEngine(context);
|
||||
@@ -47,6 +104,10 @@ public final class SoundEngine {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the winner celebration sound effect.
|
||||
* Only plays if sound engine has finished loading resources.
|
||||
*/
|
||||
public void playWinnerSound() {
|
||||
if (mIsReady) {
|
||||
mSoundPool.play(mWinnerSoundId, 1.0f, 1.0f, 0, 0, 1.0f);
|
||||
@@ -55,6 +116,10 @@ public final class SoundEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the bust notification sound effect.
|
||||
* Only plays if sound engine has finished loading resources.
|
||||
*/
|
||||
public void playBustedSound() {
|
||||
if (mIsReady) {
|
||||
mSoundPool.play(mBustedSoundId, 1.0f, 1.0f, 0, 0, 1.0f);
|
||||
@@ -63,6 +128,10 @@ public final class SoundEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the 180 score notification sound effect.
|
||||
* Only plays if sound engine has finished loading resources.
|
||||
*/
|
||||
public void playOneHundredAndEightySound() {
|
||||
if (mIsReady) {
|
||||
mSoundPool.play(m180SoundId, 1.0f, 1.0f, 0, 0, 1.0f);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="50"
|
||||
android:fromXDelta="0"
|
||||
android:toXDelta="15"
|
||||
android:repeatCount="8"
|
||||
android:duration="80"
|
||||
android:fromXDelta="-12"
|
||||
android:toXDelta="12"
|
||||
android:repeatCount="15"
|
||||
android:repeatMode="reverse"
|
||||
android:interpolator="@android:anim/linear_interpolator" />
|
||||
android:interpolator="@android:anim/cycle_interpolator" />
|
||||
@@ -21,6 +21,10 @@
|
||||
<string name="txt_update_profile_username_save">Update Squad</string>
|
||||
<string name="txt_cancel_crop">Cancel</string>
|
||||
<string name="txt_confirm_crop">Confirm Crop</string>
|
||||
<string name="txt_permission_hint_title">Image Permission required</string>
|
||||
<string name="txt_permission_hint_description">This app needs access to your images to set a custom profile picture</string>
|
||||
<string name="txt_permission_hint_button_ok">OK</string>
|
||||
<string name="txt_permission_hint_button_cancel">Cancel</string>
|
||||
|
||||
<!-- GameActivity -->
|
||||
<string name="txt_game_btn_single">Single</string>
|
||||
@@ -32,9 +36,9 @@
|
||||
<!-- Preference Strings -->
|
||||
<string name="pref_key_day_night_mode">day_night_mode</string>
|
||||
<string name="pref_key_standard_game_mode">standard_game_mode</string>
|
||||
<string name="pref_game_mode_701_value">701</string>
|
||||
<string name="pref_game_mode_501_value">501</string>
|
||||
<string name="pref_game_mode_301_value">301</string>
|
||||
<string name="pref_game_mode_701_value">Standard 701 - Double Out</string>
|
||||
<string name="pref_game_mode_501_value">Standard 501 - Double Out</string>
|
||||
<string name="pref_game_mode_301_value">Standard 301 - Double Out</string>
|
||||
<string name="pref_game_mode_cricket_value">Cricket</string>
|
||||
<string name="pref_desc_day_night_mode">Day/Night Mode</string>
|
||||
<string name="pref_title_standard_game_mode">Standard Game Mode</string>
|
||||
@@ -61,4 +65,7 @@
|
||||
<string name="attachment_summary_on">Automatically download attachments for incoming emails
|
||||
</string>
|
||||
<string name="attachment_summary_off">Only download attachments when manually requested</string>
|
||||
|
||||
<!-- Other strings -->
|
||||
<string name="attribution_label">"Oche Game Effects"</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user