Добавить доступный интерфейс этапа 3
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class AccessibilityPersistenceTest {
|
||||
@Test
|
||||
public void accessibilityAndControlChoicesSurviveActivityRecreationTogether() {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
activity.onActivity(current -> {
|
||||
GameView view = current.gameViewForAutomation();
|
||||
view.resetControlSettingsForAutomation();
|
||||
view.resetAccessibilitySettingsForAutomation();
|
||||
view.selectNextControlSizeForAutomation();
|
||||
view.selectNextTextSizeForAutomation();
|
||||
view.toggleHighContrastForAutomation();
|
||||
});
|
||||
|
||||
activity.recreate();
|
||||
|
||||
activity.onActivity(current -> {
|
||||
GameView view = current.gameViewForAutomation();
|
||||
assertEquals("Large", view.controlSettingsForAutomation().controlSizeLabel());
|
||||
assertEquals("Large", view.accessibilitySettingsForAutomation().textSizeLabel());
|
||||
assertTrue(view.accessibilitySettingsForAutomation().highContrastEnabled());
|
||||
view.resetControlSettingsForAutomation();
|
||||
view.resetAccessibilitySettingsForAutomation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void descriptionsFollowScreensWhileBackAlwaysHasOneDeterministicResult() {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
activity.onActivity(current -> assertContains(
|
||||
current.gameViewForAutomation(), "Level select"));
|
||||
|
||||
activity.onActivity(current -> current.gameViewForAutomation()
|
||||
.openSettingsForAutomation());
|
||||
activity.onActivity(current -> {
|
||||
assertContains(current.gameViewForAutomation(), "Settings");
|
||||
assertContains(current.gameViewForAutomation(), "Text size");
|
||||
current.onBackPressed();
|
||||
assertContains(current.gameViewForAutomation(), "Level select");
|
||||
});
|
||||
|
||||
activity.onActivity(current -> {
|
||||
GameView view = current.gameViewForAutomation();
|
||||
view.startLevelForAutomation(1);
|
||||
assertContains(view, "Playing level 1");
|
||||
view.pauseForAutomation();
|
||||
assertContains(view, "Paused on level 1");
|
||||
current.onBackPressed();
|
||||
assertContains(view, "Playing level 1");
|
||||
assertFalse(view.automationSnapshot().visiblyPaused());
|
||||
view.showCompleteForAutomation();
|
||||
assertContains(view, "Level complete");
|
||||
current.onBackPressed();
|
||||
assertContains(view, "Level select");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void textAndContrastPresetsDoNotMoveContentOutsideTheSafeViewport() {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
activity.onActivity(current -> {
|
||||
GameView view = current.gameViewForAutomation();
|
||||
float[] before = view.safeViewportForAutomation();
|
||||
view.selectNextTextSizeForAutomation();
|
||||
view.toggleHighContrastForAutomation();
|
||||
float[] after = view.safeViewportForAutomation();
|
||||
|
||||
assertTrue(before[0] >= 0f);
|
||||
assertTrue(before[1] >= 0f);
|
||||
assertTrue(before[2] <= view.getWidth());
|
||||
assertTrue(before[3] <= view.getHeight());
|
||||
assertEquals(before[0], after[0], 0f);
|
||||
assertEquals(before[1], after[1], 0f);
|
||||
assertEquals(before[2], after[2], 0f);
|
||||
assertEquals(before[3], after[3], 0f);
|
||||
view.resetAccessibilitySettingsForAutomation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void accessibilityRowsChangeThePersistedSettingsThroughRealTouchInput() {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
activity.onActivity(current -> {
|
||||
GameView view = current.gameViewForAutomation();
|
||||
view.resetAccessibilitySettingsForAutomation();
|
||||
view.openSettingsForAutomation();
|
||||
});
|
||||
|
||||
tap(activity, 220f, 345f);
|
||||
tap(activity, 220f, 515f);
|
||||
|
||||
activity.onActivity(current -> {
|
||||
GameView view = current.gameViewForAutomation();
|
||||
assertEquals(1.25f, view.textScaleForAutomation(), 0f);
|
||||
assertTrue(view.highContrastForAutomation());
|
||||
assertTrue(view.screenDescriptionForAutomation().contains("Text size Large"));
|
||||
assertTrue(view.screenDescriptionForAutomation().contains("High contrast On"));
|
||||
view.resetAccessibilitySettingsForAutomation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void criticalSignalAndDoorStateRemainsTextualWhenVibrationIsOff() {
|
||||
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
|
||||
activity.onActivity(current -> {
|
||||
GameView view = current.gameViewForAutomation();
|
||||
view.resetControlSettingsForAutomation();
|
||||
view.toggleHapticsForAutomation();
|
||||
view.startLevelForAutomation(3);
|
||||
|
||||
assertEquals("Signals 0/1 · Doors 0/1 · Volunteer active",
|
||||
view.criticalVisibleStatusForAutomation());
|
||||
assertTrue(view.screenDescriptionForAutomation().contains(
|
||||
view.criticalVisibleStatusForAutomation()));
|
||||
view.resetControlSettingsForAutomation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void tap(ActivityScenario<MainActivity> activity, float canvasX, float canvasY) {
|
||||
touch(activity, MotionEvent.ACTION_DOWN, canvasX, canvasY);
|
||||
touch(activity, MotionEvent.ACTION_UP, canvasX, canvasY);
|
||||
}
|
||||
|
||||
private static void touch(ActivityScenario<MainActivity> activity, int action,
|
||||
float canvasX, float canvasY) {
|
||||
activity.onActivity(current -> {
|
||||
GameView view = current.gameViewForAutomation();
|
||||
float scale = Math.min(view.getWidth() / 1_920f, view.getHeight() / 1_080f);
|
||||
float x = (view.getWidth() - 1_920f * scale) * 0.5f + canvasX * scale;
|
||||
float y = (view.getHeight() - 1_080f * scale) * 0.5f + canvasY * scale;
|
||||
long now = SystemClock.uptimeMillis();
|
||||
MotionEvent event = MotionEvent.obtain(now, now, action, x, y, 0);
|
||||
view.dispatchTouchEvent(event);
|
||||
event.recycle();
|
||||
});
|
||||
}
|
||||
|
||||
private static void assertContains(GameView view, String expected) {
|
||||
String description = String.valueOf(view.getContentDescription());
|
||||
assertTrue("Expected '" + expected + "' in '" + description + "'",
|
||||
description.contains(expected));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
final class AccessibilityScreenDescription {
|
||||
private AccessibilityScreenDescription() {
|
||||
}
|
||||
|
||||
static String levelSelect(int unlocked, int total) {
|
||||
return "Level select. " + unlocked + " of " + total
|
||||
+ " unlocked. Choose an unlocked level or open Settings.";
|
||||
}
|
||||
|
||||
static String settings(ControlSettings controls, AccessibilitySettings accessibility) {
|
||||
return "Settings. Control size " + controls.controlSizeLabel()
|
||||
+ ". Control opacity " + controls.controlOpacityLabel()
|
||||
+ ". Vibration " + controls.hapticsLabel()
|
||||
+ ". Text size " + accessibility.textSizeLabel()
|
||||
+ ". High contrast " + accessibility.highContrastLabel()
|
||||
+ ". Back.";
|
||||
}
|
||||
|
||||
static String playing(LevelDefinition level, int deaths, String elapsed,
|
||||
String criticalState, boolean paused) {
|
||||
if (paused) {
|
||||
return "Paused on level " + level.number + ", " + level.title
|
||||
+ ". Resume or return to level select.";
|
||||
}
|
||||
return "Playing level " + level.number + ", " + level.title
|
||||
+ ". " + deaths + " volunteers. Time " + elapsed
|
||||
+ ". " + criticalState
|
||||
+ ". Move left, move right, jump, restart, pause.";
|
||||
}
|
||||
|
||||
static String complete(LevelDefinition level, int deaths, String elapsed,
|
||||
boolean hasNextLevel) {
|
||||
return "Level complete. " + level.title + ". " + deaths
|
||||
+ " volunteers. Time " + elapsed
|
||||
+ ". Level select. Replay."
|
||||
+ (hasNextLevel ? " Next level." : "");
|
||||
}
|
||||
|
||||
static String levelSelect(GameStrings strings, int unlocked, int total) {
|
||||
return strings.levelSelectDescription(unlocked, total);
|
||||
}
|
||||
|
||||
static String settings(GameStrings strings, ControlSettings controls,
|
||||
AccessibilitySettings accessibility) {
|
||||
return strings.settingsDescription(controls, accessibility);
|
||||
}
|
||||
|
||||
static String playing(GameStrings strings, LevelDefinition level, int deaths,
|
||||
String elapsed, String criticalState, boolean paused) {
|
||||
return strings.playingDescription(level.number, deaths, elapsed, criticalState, paused);
|
||||
}
|
||||
|
||||
static String complete(GameStrings strings, LevelDefinition level, int deaths,
|
||||
String elapsed, boolean hasNextLevel) {
|
||||
return strings.completeDescription(level.number, deaths, elapsed, hasNextLevel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
public final class AccessibilitySettings {
|
||||
private enum TextSize {
|
||||
STANDARD("Standard", 1f),
|
||||
LARGE("Large", 1.25f);
|
||||
|
||||
final String label;
|
||||
final float scale;
|
||||
|
||||
TextSize(String label, float scale) {
|
||||
this.label = label;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
|
||||
private final TextSize textSize;
|
||||
private final boolean highContrastEnabled;
|
||||
|
||||
private AccessibilitySettings(TextSize textSize, boolean highContrastEnabled) {
|
||||
this.textSize = textSize;
|
||||
this.highContrastEnabled = highContrastEnabled;
|
||||
}
|
||||
|
||||
public static AccessibilitySettings defaults() {
|
||||
return new AccessibilitySettings(TextSize.STANDARD, false);
|
||||
}
|
||||
|
||||
static AccessibilitySettings restore(int textSizeIndex, boolean highContrastEnabled) {
|
||||
TextSize[] values = TextSize.values();
|
||||
TextSize restored = textSizeIndex >= 0 && textSizeIndex < values.length
|
||||
? values[textSizeIndex] : TextSize.STANDARD;
|
||||
return new AccessibilitySettings(restored, highContrastEnabled);
|
||||
}
|
||||
|
||||
public String textSizeLabel() {
|
||||
return textSize.label;
|
||||
}
|
||||
|
||||
public float textScale() {
|
||||
return textSize.scale;
|
||||
}
|
||||
|
||||
public float canvasTextSize(float baseSize) {
|
||||
return baseSize * textSize.scale;
|
||||
}
|
||||
|
||||
public AccessibilitySettings nextTextSize() {
|
||||
TextSize[] values = TextSize.values();
|
||||
return new AccessibilitySettings(values[(textSize.ordinal() + 1) % values.length],
|
||||
highContrastEnabled);
|
||||
}
|
||||
|
||||
public boolean highContrastEnabled() {
|
||||
return highContrastEnabled;
|
||||
}
|
||||
|
||||
public String highContrastLabel() {
|
||||
return highContrastEnabled ? "On" : "Off";
|
||||
}
|
||||
|
||||
public AccessibilitySettings toggleHighContrast() {
|
||||
return new AccessibilitySettings(textSize, !highContrastEnabled);
|
||||
}
|
||||
|
||||
int textSizeIndex() {
|
||||
return textSize.ordinal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
final class AccessibilitySettingsStore {
|
||||
private static final String FILE = "project_sacrifice_accessibility_settings_v1";
|
||||
private final SharedPreferences preferences;
|
||||
|
||||
AccessibilitySettingsStore(Context context) {
|
||||
preferences = context.getSharedPreferences(FILE, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
AccessibilitySettings load() {
|
||||
AccessibilitySettings defaults = AccessibilitySettings.defaults();
|
||||
return AccessibilitySettings.restore(
|
||||
preferences.getInt("text_size", defaults.textSizeIndex()),
|
||||
preferences.getBoolean("high_contrast", defaults.highContrastEnabled())
|
||||
);
|
||||
}
|
||||
|
||||
void save(AccessibilitySettings settings) {
|
||||
preferences.edit()
|
||||
.putInt("text_size", settings.textSizeIndex())
|
||||
.putBoolean("high_contrast", settings.highContrastEnabled())
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
final class AccessibilityStatus {
|
||||
private AccessibilityStatus() {
|
||||
}
|
||||
|
||||
static String describe(int activeSignals, int totalSignals, int openDoors, int totalDoors,
|
||||
boolean playerAlive) {
|
||||
return "Signals " + activeSignals + "/" + totalSignals
|
||||
+ " · Doors " + openDoors + "/" + totalDoors
|
||||
+ " · Volunteer " + (playerAlive ? "active" : "respawning");
|
||||
}
|
||||
|
||||
static String describe(GameStrings strings, int activeSignals, int totalSignals,
|
||||
int openDoors, int totalDoors, boolean playerAlive) {
|
||||
return strings.accessibilityStatus(activeSignals, totalSignals, openDoors, totalDoors,
|
||||
playerAlive);
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,16 @@ public final class GameStrings {
|
||||
if (russian()) return enabled ? "Вкл." : "Выкл.";
|
||||
return enabled ? "On" : "Off";
|
||||
}
|
||||
public String accessibilityTitle() { return russian() ? "ДОСТУПНОСТЬ" : "ACCESSIBILITY"; }
|
||||
public String textSizeSetting() { return russian() ? "Размер текста" : "Text size"; }
|
||||
public String textSizeValue(float scale) {
|
||||
return scale > 1f ? (russian() ? "Крупный" : "Large")
|
||||
: (russian() ? "Обычный" : "Standard");
|
||||
}
|
||||
public String highContrastSetting() { return russian() ? "Контраст" : "High contrast"; }
|
||||
public String highContrastValue(boolean enabled) {
|
||||
return hapticsValue(enabled);
|
||||
}
|
||||
public String back() { return russian() ? "Назад" : "Back"; }
|
||||
|
||||
public String hudStats(int deaths, long elapsedMilliseconds) {
|
||||
@@ -224,6 +234,79 @@ public final class GameStrings {
|
||||
return step + " / " + total;
|
||||
}
|
||||
|
||||
public String accessibilityStatus(int activeSignals, int totalSignals,
|
||||
int openDoors, int totalDoors, boolean playerAlive) {
|
||||
if (russian()) {
|
||||
return "Сигналы " + activeSignals + "/" + totalSignals
|
||||
+ " · Двери " + openDoors + "/" + totalDoors
|
||||
+ " · Доброволец " + (playerAlive ? "активен" : "возрождается");
|
||||
}
|
||||
return "Signals " + activeSignals + "/" + totalSignals
|
||||
+ " · Doors " + openDoors + "/" + totalDoors
|
||||
+ " · Volunteer " + (playerAlive ? "active" : "respawning");
|
||||
}
|
||||
|
||||
public String levelSelectDescription(int unlocked, int total) {
|
||||
if (russian()) {
|
||||
return "Выбор уровня. Открыто " + unlocked + " из " + total
|
||||
+ ". Выберите открытый уровень или Настройки.";
|
||||
}
|
||||
return "Level select. " + unlocked + " of " + total
|
||||
+ " unlocked. Choose an unlocked level or open Settings.";
|
||||
}
|
||||
|
||||
public String settingsDescription(ControlSettings controls,
|
||||
AccessibilitySettings accessibility) {
|
||||
if (russian()) {
|
||||
return "Настройки. Размер управления "
|
||||
+ controlSizeValue(controls.controlSizeIndex())
|
||||
+ ". Прозрачность управления "
|
||||
+ controlOpacityValue(controls.controlOpacityIndex())
|
||||
+ ". Вибрация " + hapticsValue(controls.hapticsEnabled())
|
||||
+ ". Размер текста " + textSizeValue(accessibility.textScale())
|
||||
+ ". Контраст " + highContrastValue(accessibility.highContrastEnabled())
|
||||
+ ". Назад.";
|
||||
}
|
||||
return "Settings. Control size " + controls.controlSizeLabel()
|
||||
+ ". Control opacity " + controls.controlOpacityLabel()
|
||||
+ ". Vibration " + controls.hapticsLabel()
|
||||
+ ". Text size " + accessibility.textSizeLabel()
|
||||
+ ". High contrast " + accessibility.highContrastLabel()
|
||||
+ ". Back.";
|
||||
}
|
||||
|
||||
public String playingDescription(int levelNumber, int deaths, String elapsed,
|
||||
String criticalState, boolean paused) {
|
||||
if (paused) {
|
||||
return russian()
|
||||
? "Пауза на уровне " + levelNumber + ", " + levelTitle(levelNumber)
|
||||
+ ". Продолжить или вернуться к выбору уровня."
|
||||
: "Paused on level " + levelNumber + ", " + levelTitle(levelNumber)
|
||||
+ ". Resume or return to level select.";
|
||||
}
|
||||
if (russian()) {
|
||||
return "Идёт уровень " + levelNumber + ", " + levelTitle(levelNumber)
|
||||
+ ". Добровольцы " + deaths + ". Время " + elapsed + ". "
|
||||
+ criticalState + ". Движение влево и вправо, прыжок, заново, пауза.";
|
||||
}
|
||||
return "Playing level " + levelNumber + ", " + levelTitle(levelNumber)
|
||||
+ ". " + deaths + " volunteers. Time " + elapsed + ". " + criticalState
|
||||
+ ". Move left, move right, jump, restart, pause.";
|
||||
}
|
||||
|
||||
public String completeDescription(int levelNumber, int deaths, String elapsed,
|
||||
boolean hasNextLevel) {
|
||||
if (russian()) {
|
||||
return "Уровень завершён. " + levelTitle(levelNumber) + ". Добровольцы "
|
||||
+ deaths + ". Время " + elapsed
|
||||
+ ". Выбор уровня. Повторить."
|
||||
+ (hasNextLevel ? " Следующий уровень." : "");
|
||||
}
|
||||
return "Level complete. " + levelTitle(levelNumber) + ". " + deaths
|
||||
+ " volunteers. Time " + elapsed + ". Level select. Replay."
|
||||
+ (hasNextLevel ? " Next level." : "");
|
||||
}
|
||||
|
||||
public String levelTitle(int oneBasedLevelNumber) {
|
||||
return (russian() ? RU_TITLES : EN_TITLES)[levelIndex(oneBasedLevelNumber)];
|
||||
}
|
||||
|
||||
@@ -14,13 +14,16 @@ import android.util.SparseArray;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A dependency-free 2D puzzle-platformer renderer and deterministic physics loop.
|
||||
@@ -164,6 +167,7 @@ public final class GameView extends View implements Runnable {
|
||||
private final ControlSettingsStore controlSettingsStore;
|
||||
private final OnboardingStore onboardingStore;
|
||||
private final GameLanguageStore languageStore;
|
||||
private final AccessibilitySettingsStore accessibilitySettingsStore;
|
||||
private final SparseArray<PointF> pointers = new SparseArray<>();
|
||||
private final Player player = new Player();
|
||||
private final ArrayList<Corpse> corpses = new ArrayList<>();
|
||||
@@ -194,6 +198,8 @@ public final class GameView extends View implements Runnable {
|
||||
private CompletionResult completionResult;
|
||||
private GameLanguage language;
|
||||
private GameStrings strings;
|
||||
private AccessibilitySettings accessibilitySettings;
|
||||
private String lastAccessibilityStateKey = "";
|
||||
|
||||
public GameView(Context context) {
|
||||
super(context);
|
||||
@@ -205,12 +211,14 @@ public final class GameView extends View implements Runnable {
|
||||
languageStore = new GameLanguageStore(context);
|
||||
language = languageStore.load();
|
||||
strings = GameStrings.forLanguage(language);
|
||||
accessibilitySettingsStore = new AccessibilitySettingsStore(context);
|
||||
accessibilitySettings = accessibilitySettingsStore.load();
|
||||
paint.setTypeface(android.graphics.Typeface.create("sans", android.graphics.Typeface.NORMAL));
|
||||
stroke.setStyle(Paint.Style.STROKE);
|
||||
stroke.setStrokeCap(Paint.Cap.ROUND);
|
||||
setFocusable(true);
|
||||
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
setContentDescription(strings.appContentDescription());
|
||||
refreshAccessibilityDescription(false);
|
||||
running = true;
|
||||
postOnAnimation(this);
|
||||
}
|
||||
@@ -239,12 +247,14 @@ public final class GameView extends View implements Runnable {
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
lastFrameNanos = System.nanoTime();
|
||||
refreshAccessibilityDescription(false);
|
||||
}
|
||||
|
||||
public boolean handleBack() {
|
||||
if (screen == Screen.LEVELS) return false;
|
||||
if (screen == Screen.SETTINGS) {
|
||||
screen = Screen.LEVELS;
|
||||
refreshAccessibilityDescription(true);
|
||||
return true;
|
||||
}
|
||||
if (screen == Screen.COMPLETE) {
|
||||
@@ -258,6 +268,7 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
refreshAccessibilityDescription(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -281,6 +292,7 @@ public final class GameView extends View implements Runnable {
|
||||
for (LevelDefinition.Platform item : level.platforms) platformStates.add(new PlatformState(item));
|
||||
spawnPlayer();
|
||||
lastFrameNanos = System.nanoTime();
|
||||
refreshAccessibilityDescription(true);
|
||||
}
|
||||
|
||||
private void restartLevel() {
|
||||
@@ -320,6 +332,8 @@ public final class GameView extends View implements Runnable {
|
||||
checkGoal();
|
||||
}
|
||||
|
||||
refreshAccessibilityDescriptionIfStateChanged();
|
||||
|
||||
float targetCamera = player.alive ? player.x - 610f + player.facing * 110f : cameraX;
|
||||
targetCamera = GameMath.clamp(targetCamera, 0f, Math.max(0f, level.worldWidth - VIEW_WIDTH));
|
||||
cameraX += (targetCamera - cameraX) * Math.min(1f, dt * 4.5f);
|
||||
@@ -656,6 +670,7 @@ public final class GameView extends View implements Runnable {
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
emitHaptic(HapticFeedbackConstants.LONG_PRESS);
|
||||
refreshAccessibilityDescription(true);
|
||||
}
|
||||
|
||||
private RectF playerRect() {
|
||||
@@ -673,9 +688,7 @@ public final class GameView extends View implements Runnable {
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
canvasScale = Math.min(getWidth() / VIEW_WIDTH, getHeight() / VIEW_HEIGHT);
|
||||
canvasOffsetX = (getWidth() - VIEW_WIDTH * canvasScale) * 0.5f;
|
||||
canvasOffsetY = (getHeight() - VIEW_HEIGHT * canvasScale) * 0.5f;
|
||||
updateViewport(getWidth(), getHeight());
|
||||
|
||||
paint.setShader(outsideGradient);
|
||||
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
|
||||
@@ -714,6 +727,12 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
|
||||
private void drawSky(Canvas canvas) {
|
||||
if (accessibilitySettings.highContrastEnabled()) {
|
||||
canvas.drawColor(Color.BLACK);
|
||||
paint.setColor(Color.rgb(255, 214, 77));
|
||||
canvas.drawCircle(1560, 170, 88, paint);
|
||||
return;
|
||||
}
|
||||
paint.setShader(skyGradient);
|
||||
canvas.drawRect(0, 0, VIEW_WIDTH, VIEW_HEIGHT, paint);
|
||||
paint.setShader(null);
|
||||
@@ -745,10 +764,19 @@ public final class GameView extends View implements Runnable {
|
||||
for (PlatformState state : platformStates) {
|
||||
RectF box = state.rect();
|
||||
boolean moving = state.definition.isMoving();
|
||||
paint.setColor(moving ? Color.rgb(172, 126, 55) : Color.rgb(69, 74, 91));
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? (moving ? Color.rgb(255, 191, 0) : Color.rgb(56, 56, 56))
|
||||
: (moving ? Color.rgb(172, 126, 55) : Color.rgb(69, 74, 91)));
|
||||
canvas.drawRoundRect(box, 10, 10, paint);
|
||||
paint.setColor(moving ? Color.rgb(242, 195, 92) : Color.rgb(117, 121, 137));
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE
|
||||
: (moving ? Color.rgb(242, 195, 92) : Color.rgb(117, 121, 137)));
|
||||
canvas.drawRoundRect(new RectF(box.left, box.top, box.right, box.top + Math.min(14f, box.height())), 8, 8, paint);
|
||||
if (accessibilitySettings.highContrastEnabled()) {
|
||||
stroke.setColor(Color.WHITE);
|
||||
stroke.setStrokeWidth(5f);
|
||||
canvas.drawRoundRect(box, 10, 10, stroke);
|
||||
}
|
||||
if (moving) {
|
||||
stroke.setColor(Color.argb(120, 244, 214, 137));
|
||||
stroke.setStrokeWidth(5f);
|
||||
@@ -771,6 +799,7 @@ public final class GameView extends View implements Runnable {
|
||||
canvas.drawRoundRect(new RectF(box.left - 8, box.top + 6, box.right + 8, box.bottom + 12), 6, 6, paint);
|
||||
paint.setColor(active ? Color.rgb(250, 194, 76) : Color.rgb(174, 126, 55));
|
||||
canvas.drawRoundRect(box, 7, 7, paint);
|
||||
drawWorldLabel(canvas, active ? "DOWN" : "UP", box.centerX(), box.top - 18);
|
||||
} else {
|
||||
paint.setColor(Color.rgb(50, 56, 70));
|
||||
canvas.drawRoundRect(box, 12, 12, paint);
|
||||
@@ -785,6 +814,7 @@ public final class GameView extends View implements Runnable {
|
||||
Float remaining = timedSignals.get(item.signalId);
|
||||
drawWorldLabel(canvas, String.format(Locale.US, "%.1f", remaining == null ? 0f : remaining), box.centerX(), box.top - 52);
|
||||
}
|
||||
drawWorldLabel(canvas, active ? "ON" : "OFF", box.centerX(), box.bottom + 32);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -877,6 +907,7 @@ public final class GameView extends View implements Runnable {
|
||||
paint.setColor(Color.rgb(239, 195, 92));
|
||||
canvas.drawCircle(box.centerX(), box.centerY(), 15, paint);
|
||||
}
|
||||
drawWorldLabel(canvas, open ? "OPEN" : "LOCKED", box.centerX(), box.top - 20);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,13 +980,24 @@ public final class GameView extends View implements Runnable {
|
||||
private void drawHud(Canvas canvas) {
|
||||
paint.setColor(Color.argb(178, 15, 20, 34));
|
||||
canvas.drawRoundRect(new RectF(28, 26, 940, 138), 24, 24, paint);
|
||||
canvas.save();
|
||||
canvas.clipRect(40, 30, 925, 135);
|
||||
drawText(canvas, String.format(Locale.US, "%02d %s", level.number,
|
||||
strings.levelTitle(level.number)), 56, 72, 34,
|
||||
Color.rgb(248, 220, 151), false);
|
||||
drawFittedText(canvas, strings.levelHint(level.number), 56, 116, 25, 16,
|
||||
850, Color.rgb(231, 231, 235), false);
|
||||
canvas.restore();
|
||||
canvas.save();
|
||||
canvas.clipRect(990, 28, 1590, 130);
|
||||
drawText(canvas, strings.hudStats(deaths, (long) (levelElapsed * 1000)),
|
||||
1010, 82, 31, Color.WHITE, false);
|
||||
canvas.restore();
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.BLACK : Color.argb(178, 15, 20, 34));
|
||||
canvas.drawRoundRect(new RectF(590, 139, 1330, 185), 18, 18, paint);
|
||||
drawCenteredText(canvas, criticalStatusText(), VIEW_WIDTH * 0.5f, 170, 22,
|
||||
accessibilitySettings.highContrastEnabled() ? Color.WHITE : Color.rgb(218, 221, 230));
|
||||
|
||||
drawHudButton(canvas, new RectF(1608, 26, 1770, 112), strings.restart(), false);
|
||||
drawHudButton(canvas, new RectF(1790, 26, 1892, 112), "II", false);
|
||||
@@ -973,11 +1015,15 @@ public final class GameView extends View implements Runnable {
|
||||
private void drawControlCircle(Canvas canvas, float x, float y, float radius, String label, boolean active) {
|
||||
int inactiveFillAlpha = Math.round(190f * controlSettings.controlOpacity());
|
||||
int inactiveStrokeAlpha = Math.round(230f * controlSettings.controlOpacity());
|
||||
paint.setColor(active ? Color.argb(220, 245, 190, 74)
|
||||
: Color.argb(inactiveFillAlpha, 22, 28, 43));
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? (active ? Color.rgb(255, 214, 77) : Color.BLACK)
|
||||
: (active ? Color.argb(220, 245, 190, 74)
|
||||
: Color.argb(inactiveFillAlpha, 22, 28, 43)));
|
||||
canvas.drawCircle(x, y, radius, paint);
|
||||
stroke.setColor(active ? Color.rgb(255, 232, 169)
|
||||
: Color.argb(inactiveStrokeAlpha, 240, 240, 245));
|
||||
stroke.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE
|
||||
: (active ? Color.rgb(255, 232, 169)
|
||||
: Color.argb(inactiveStrokeAlpha, 240, 240, 245)));
|
||||
stroke.setStrokeWidth(5f);
|
||||
canvas.drawCircle(x, y, radius - 5, stroke);
|
||||
drawCenteredText(canvas, label, x, y + 21, 68 * controlSettings.controlScale(),
|
||||
@@ -1003,12 +1049,17 @@ public final class GameView extends View implements Runnable {
|
||||
RectF card = new RectF(left, top, left + cardW, top + cardH);
|
||||
boolean enabled = i < unlocked;
|
||||
boolean complete = progress.isCompleted(i + 1);
|
||||
paint.setColor(enabled ? Color.argb(220, 38, 45, 65) : Color.argb(185, 25, 29, 42));
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.BLACK
|
||||
: (enabled ? Color.argb(220, 38, 45, 65) : Color.argb(185, 25, 29, 42)));
|
||||
canvas.drawRoundRect(card, 22, 22, paint);
|
||||
stroke.setColor(complete ? Color.rgb(109, 222, 143) : (enabled ? Color.rgb(205, 164, 83) : Color.rgb(73, 77, 91)));
|
||||
stroke.setStrokeWidth(5f);
|
||||
canvas.drawRoundRect(card, 22, 22, stroke);
|
||||
drawCenteredText(canvas, enabled ? String.format(Locale.US, "%02d", i + 1) : strings.locked(), card.centerX(), top + 72, enabled ? 48 : 27,
|
||||
canvas.save();
|
||||
canvas.clipRect(card.left + 10, card.top + 8, card.right - 10, card.bottom - 8);
|
||||
drawCenteredText(canvas, enabled ? String.format(Locale.US, "%02d", i + 1)
|
||||
: strings.locked(), card.centerX(), top + 72, enabled ? 48 : 27,
|
||||
enabled ? Color.rgb(248, 210, 118) : Color.rgb(120, 123, 135));
|
||||
LevelDefinition item = LevelCatalog.get(i);
|
||||
drawCenteredFittedText(canvas, strings.levelTitle(item.number), card.centerX(),
|
||||
@@ -1024,6 +1075,7 @@ public final class GameView extends View implements Runnable {
|
||||
drawCenteredText(canvas, strings.tapToEnter(), card.centerX(), top + 190,
|
||||
22, Color.rgb(202, 204, 214));
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
drawHudButton(canvas, new RectF(1560, 70, 1850, 160), strings.settingsButton(), false);
|
||||
drawCenteredText(canvas, strings.levelSelectFooter(), VIEW_WIDTH * 0.5f, 980, 24,
|
||||
@@ -1044,18 +1096,47 @@ public final class GameView extends View implements Runnable {
|
||||
strings.controlOpacityValue(controlSettings.controlOpacityIndex()));
|
||||
drawSettingRow(canvas, new RectF(420, 620, 1500, 750), strings.vibrationSetting(),
|
||||
strings.hapticsValue(controlSettings.hapticsEnabled()));
|
||||
drawCenteredText(canvas, strings.accessibilityTitle(), 220, 240, 25,
|
||||
Color.rgb(248, 212, 126));
|
||||
drawCompactSettingRow(canvas, new RectF(55, 280, 385, 410),
|
||||
strings.textSizeSetting(),
|
||||
strings.textSizeValue(accessibilitySettings.textScale()));
|
||||
drawCompactSettingRow(canvas, new RectF(55, 450, 385, 580),
|
||||
strings.highContrastSetting(),
|
||||
strings.highContrastValue(accessibilitySettings.highContrastEnabled()));
|
||||
drawHudButton(canvas, new RectF(710, 850, 1210, 960), strings.back(), true);
|
||||
}
|
||||
|
||||
private void drawSettingRow(Canvas canvas, RectF box, String label, String value) {
|
||||
paint.setColor(Color.argb(220, 38, 45, 65));
|
||||
private void drawCompactSettingRow(Canvas canvas, RectF box, String label, String value) {
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.BLACK : Color.argb(220, 38, 45, 65));
|
||||
canvas.drawRoundRect(box, 22, 22, paint);
|
||||
stroke.setColor(Color.rgb(139, 145, 160));
|
||||
stroke.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE : Color.rgb(139, 145, 160));
|
||||
stroke.setStrokeWidth(4f);
|
||||
canvas.drawRoundRect(box, 22, 22, stroke);
|
||||
canvas.save();
|
||||
canvas.clipRect(box.left + 8f, box.top + 8f, box.right - 8f, box.bottom - 8f);
|
||||
drawCenteredText(canvas, label, box.centerX(), box.top + 47f, 24f, Color.WHITE);
|
||||
drawCenteredText(canvas, value, box.centerX(), box.top + 98f, 28f,
|
||||
Color.rgb(248, 212, 126));
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
private void drawSettingRow(Canvas canvas, RectF box, String label, String value) {
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.BLACK : Color.argb(220, 38, 45, 65));
|
||||
canvas.drawRoundRect(box, 22, 22, paint);
|
||||
stroke.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE : Color.rgb(139, 145, 160));
|
||||
stroke.setStrokeWidth(4f);
|
||||
canvas.drawRoundRect(box, 22, 22, stroke);
|
||||
canvas.save();
|
||||
canvas.clipRect(box.left + 10f, box.top + 8f, box.right - 10f, box.bottom - 8f);
|
||||
drawText(canvas, label, box.left + 55f, box.centerY() + 11f, 32f, Color.WHITE, true);
|
||||
drawHudButton(canvas, new RectF(1_090f, box.top + 24f, 1_445f, box.bottom - 24f),
|
||||
value, false);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
private void drawComplete(Canvas canvas) {
|
||||
@@ -1176,12 +1257,19 @@ public final class GameView extends View implements Runnable {
|
||||
}
|
||||
|
||||
private void drawHudButton(Canvas canvas, RectF box, String label, boolean primary) {
|
||||
paint.setColor(primary ? Color.rgb(226, 169, 63) : Color.argb(210, 45, 52, 71));
|
||||
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? (primary ? Color.rgb(255, 214, 77) : Color.BLACK)
|
||||
: (primary ? Color.rgb(226, 169, 63) : Color.argb(210, 45, 52, 71)));
|
||||
canvas.drawRoundRect(box, 22, 22, paint);
|
||||
stroke.setColor(primary ? Color.rgb(255, 224, 146) : Color.rgb(139, 145, 160));
|
||||
stroke.setColor(accessibilitySettings.highContrastEnabled()
|
||||
? Color.WHITE
|
||||
: (primary ? Color.rgb(255, 224, 146) : Color.rgb(139, 145, 160)));
|
||||
stroke.setStrokeWidth(4f);
|
||||
canvas.drawRoundRect(box, 22, 22, stroke);
|
||||
canvas.save();
|
||||
canvas.clipRect(box.left + 8f, box.top + 5f, box.right - 8f, box.bottom - 5f);
|
||||
drawCenteredText(canvas, label, box.centerX(), box.centerY() + 10, Math.min(31, box.height() * 0.34f), primary ? Color.rgb(35, 36, 43) : Color.WHITE);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
private void drawWorldLabel(Canvas canvas, String label, float x, float y) {
|
||||
@@ -1191,8 +1279,8 @@ public final class GameView extends View implements Runnable {
|
||||
private void drawText(Canvas canvas, String text, float x, float baseline, float size, int color, boolean bold) {
|
||||
paint.setShader(null);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setTextSize(size);
|
||||
paint.setColor(color);
|
||||
paint.setTextSize(accessibilitySettings.canvasTextSize(size));
|
||||
paint.setColor(accessibleTextColor(color));
|
||||
paint.setTextAlign(Paint.Align.LEFT);
|
||||
paint.setTypeface(android.graphics.Typeface.create("sans", bold ? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL));
|
||||
canvas.drawText(text, x, baseline, paint);
|
||||
@@ -1225,8 +1313,8 @@ public final class GameView extends View implements Runnable {
|
||||
private void drawCenteredText(Canvas canvas, String text, float x, float baseline, float size, int color) {
|
||||
paint.setShader(null);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setTextSize(size);
|
||||
paint.setColor(color);
|
||||
paint.setTextSize(accessibilitySettings.canvasTextSize(size));
|
||||
paint.setColor(accessibleTextColor(color));
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setTypeface(android.graphics.Typeface.create("sans", android.graphics.Typeface.BOLD));
|
||||
canvas.drawText(text, x, baseline, paint);
|
||||
@@ -1236,6 +1324,16 @@ public final class GameView extends View implements Runnable {
|
||||
return Color.argb((int) (255 * alpha), Color.red(color), Color.green(color), Color.blue(color));
|
||||
}
|
||||
|
||||
private int accessibleTextColor(int color) {
|
||||
if (!accessibilitySettings.highContrastEnabled()) return color;
|
||||
int red = Color.red(color);
|
||||
int green = Color.green(color);
|
||||
int blue = Color.blue(color);
|
||||
if (red < 90 && green < 90 && blue < 90) return Color.BLACK;
|
||||
if (red > 180 && green > 140 && blue < 170) return Color.rgb(255, 214, 77);
|
||||
return Color.WHITE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
PointF point = toCanvasPoint(event.getX(event.getActionIndex()), event.getY(event.getActionIndex()));
|
||||
@@ -1287,6 +1385,7 @@ public final class GameView extends View implements Runnable {
|
||||
userPaused = true;
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
refreshAccessibilityDescription(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1334,6 +1433,7 @@ public final class GameView extends View implements Runnable {
|
||||
if (contains(1560, 70, 1850, 160, x, y)) {
|
||||
screen = Screen.SETTINGS;
|
||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
refreshAccessibilityDescription(true);
|
||||
return;
|
||||
}
|
||||
float startX = 110f;
|
||||
@@ -1384,6 +1484,7 @@ public final class GameView extends View implements Runnable {
|
||||
screen = Screen.LEVELS;
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
refreshAccessibilityDescription(true);
|
||||
}
|
||||
|
||||
private void replayFromResult() {
|
||||
@@ -1402,10 +1503,17 @@ public final class GameView extends View implements Runnable {
|
||||
userPaused = false;
|
||||
screen = Screen.LEVELS;
|
||||
}
|
||||
refreshAccessibilityDescription(true);
|
||||
}
|
||||
|
||||
private void handleSettingsTap(float x, float y) {
|
||||
if (contains(1580, 70, 1850, 160, x, y)) {
|
||||
if (contains(55, 280, 385, 410, x, y)) {
|
||||
updateAccessibilitySettings(accessibilitySettings.nextTextSize());
|
||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
} else if (contains(55, 450, 385, 580, x, y)) {
|
||||
updateAccessibilitySettings(accessibilitySettings.toggleHighContrast());
|
||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
} else if (contains(1580, 70, 1850, 160, x, y)) {
|
||||
updateLanguage(language.next());
|
||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
} else if (contains(420, 280, 1500, 410, x, y)) {
|
||||
@@ -1420,6 +1528,7 @@ public final class GameView extends View implements Runnable {
|
||||
} else if (contains(710, 850, 1210, 960, x, y)) {
|
||||
screen = Screen.LEVELS;
|
||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
refreshAccessibilityDescription(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1431,6 +1540,12 @@ public final class GameView extends View implements Runnable {
|
||||
return new PointF((x - canvasOffsetX) / Math.max(0.001f, canvasScale), (y - canvasOffsetY) / Math.max(0.001f, canvasScale));
|
||||
}
|
||||
|
||||
private void updateViewport(float width, float height) {
|
||||
canvasScale = Math.min(width / VIEW_WIDTH, height / VIEW_HEIGHT);
|
||||
canvasOffsetX = (width - VIEW_WIDTH * canvasScale) * 0.5f;
|
||||
canvasOffsetY = (height - VIEW_HEIGHT * canvasScale) * 0.5f;
|
||||
}
|
||||
|
||||
private static boolean contains(float left, float top, float right, float bottom, float x, float y) {
|
||||
return x >= left && x <= right && y >= top && y <= bottom;
|
||||
}
|
||||
@@ -1581,6 +1696,11 @@ public final class GameView extends View implements Runnable {
|
||||
visible.add(strings.controlOpacityValue(controlSettings.controlOpacityIndex()));
|
||||
visible.add(strings.vibrationSetting());
|
||||
visible.add(strings.hapticsValue(controlSettings.hapticsEnabled()));
|
||||
visible.add(strings.accessibilityTitle());
|
||||
visible.add(strings.textSizeSetting());
|
||||
visible.add(strings.textSizeValue(accessibilitySettings.textScale()));
|
||||
visible.add(strings.highContrastSetting());
|
||||
visible.add(strings.highContrastValue(accessibilitySettings.highContrastEnabled()));
|
||||
visible.add(strings.back());
|
||||
} else if (level != null) {
|
||||
visible.add(String.format(Locale.US, "%02d %s", level.number,
|
||||
@@ -1623,18 +1743,145 @@ public final class GameView extends View implements Runnable {
|
||||
language = updated == null ? GameLanguage.EN : updated;
|
||||
strings = GameStrings.forLanguage(language);
|
||||
languageStore.save(language);
|
||||
setContentDescription(strings.appContentDescription());
|
||||
refreshAccessibilityDescription(false);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public AccessibilitySettings accessibilitySettingsForAutomation() {
|
||||
return accessibilitySettings;
|
||||
}
|
||||
|
||||
public void resetAccessibilitySettingsForAutomation() {
|
||||
updateAccessibilitySettings(AccessibilitySettings.defaults());
|
||||
}
|
||||
|
||||
public void selectNextTextSizeForAutomation() {
|
||||
updateAccessibilitySettings(accessibilitySettings.nextTextSize());
|
||||
}
|
||||
|
||||
public void toggleHighContrastForAutomation() {
|
||||
updateAccessibilitySettings(accessibilitySettings.toggleHighContrast());
|
||||
}
|
||||
|
||||
public float textScaleForAutomation() {
|
||||
return accessibilitySettings.textScale();
|
||||
}
|
||||
|
||||
public boolean highContrastForAutomation() {
|
||||
return accessibilitySettings.highContrastEnabled();
|
||||
}
|
||||
|
||||
public String screenDescriptionForAutomation() {
|
||||
return currentAccessibilityDescription();
|
||||
}
|
||||
|
||||
public String criticalVisibleStatusForAutomation() {
|
||||
return criticalStatusText();
|
||||
}
|
||||
|
||||
public void openSettingsForAutomation() {
|
||||
screen = Screen.SETTINGS;
|
||||
refreshAccessibilityDescription(false);
|
||||
}
|
||||
|
||||
public void pauseForAutomation() {
|
||||
if (screen == Screen.PLAYING) {
|
||||
userPaused = true;
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
refreshAccessibilityDescription(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void showCompleteForAutomation() {
|
||||
if (level != null) {
|
||||
screen = Screen.COMPLETE;
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
refreshAccessibilityDescription(false);
|
||||
}
|
||||
}
|
||||
|
||||
public float[] safeViewportForAutomation() {
|
||||
float width = getWidth();
|
||||
float height = getHeight();
|
||||
if (width <= 0f || height <= 0f) return new float[] {0f, 0f, 0f, 0f};
|
||||
float scale = Math.min(width / VIEW_WIDTH, height / VIEW_HEIGHT);
|
||||
float left = (width - VIEW_WIDTH * scale) * 0.5f;
|
||||
float top = (height - VIEW_HEIGHT * scale) * 0.5f;
|
||||
return new float[] {left, top, left + VIEW_WIDTH * scale, top + VIEW_HEIGHT * scale};
|
||||
}
|
||||
|
||||
private void updateControlSettings(ControlSettings updated) {
|
||||
controlSettings = updated;
|
||||
controlSettingsStore.save(updated);
|
||||
pointers.clear();
|
||||
deriveControls();
|
||||
refreshAccessibilityDescription(false);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void updateAccessibilitySettings(AccessibilitySettings updated) {
|
||||
accessibilitySettings = updated;
|
||||
accessibilitySettingsStore.save(updated);
|
||||
refreshAccessibilityDescription(false);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private String criticalStatusText() {
|
||||
if (level == null) return AccessibilityStatus.describe(strings, 0, 0, 0, 0, true);
|
||||
Set<String> signalIds = new HashSet<>();
|
||||
int activeSignals = 0;
|
||||
for (LevelDefinition.Switch item : level.switches) signalIds.add(item.signalId);
|
||||
for (String signalId : signalIds) if (signalActive(signalId)) activeSignals++;
|
||||
int openDoors = 0;
|
||||
for (LevelDefinition.Door door : level.doors) if (doorOpen(door)) openDoors++;
|
||||
return AccessibilityStatus.describe(strings, activeSignals, signalIds.size(), openDoors,
|
||||
level.doors.size(), player.alive);
|
||||
}
|
||||
|
||||
private String currentAccessibilityDescription() {
|
||||
if (screen == Screen.LEVELS) {
|
||||
return AccessibilityScreenDescription.levelSelect(strings, progress.unlockedCount(),
|
||||
LevelCatalog.all().size());
|
||||
}
|
||||
if (screen == Screen.SETTINGS) {
|
||||
return AccessibilityScreenDescription.settings(strings, controlSettings,
|
||||
accessibilitySettings);
|
||||
}
|
||||
if (screen == Screen.COMPLETE) {
|
||||
return AccessibilityScreenDescription.complete(strings, level, deaths,
|
||||
GameMath.formatTime((long) (levelElapsed * 1000f)),
|
||||
levelIndex + 1 < LevelCatalog.all().size());
|
||||
}
|
||||
return AccessibilityScreenDescription.playing(strings, level, deaths,
|
||||
GameMath.formatTime((long) (levelElapsed * 1000f)), criticalStatusText(),
|
||||
userPaused);
|
||||
}
|
||||
|
||||
private String accessibilityStateKey() {
|
||||
return screen + "|" + userPaused + "|" + deaths + "|" + criticalStatusText()
|
||||
+ "|" + controlSettings.controlSizeLabel()
|
||||
+ "|" + controlSettings.controlOpacityLabel()
|
||||
+ "|" + controlSettings.hapticsEnabled()
|
||||
+ "|" + accessibilitySettings.textSizeLabel()
|
||||
+ "|" + accessibilitySettings.highContrastEnabled()
|
||||
+ "|" + language;
|
||||
}
|
||||
|
||||
private void refreshAccessibilityDescriptionIfStateChanged() {
|
||||
String stateKey = accessibilityStateKey();
|
||||
if (!stateKey.equals(lastAccessibilityStateKey)) refreshAccessibilityDescription(false);
|
||||
}
|
||||
|
||||
private void refreshAccessibilityDescription(boolean announce) {
|
||||
lastAccessibilityStateKey = accessibilityStateKey();
|
||||
setContentDescription(currentAccessibilityDescription());
|
||||
if (announce && isAttachedToWindow()) {
|
||||
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutomationInput(boolean moveLeft, boolean moveRight, boolean jump) {
|
||||
boolean jumpWasPressed = jumpPressed;
|
||||
leftPressed = moveLeft;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public final class AccessibilityScreenDescriptionTest {
|
||||
@Test
|
||||
public void everyPlayerFacingScreenNamesItsStateAndAvailableActions() {
|
||||
String levels = AccessibilityScreenDescription.levelSelect(3, 10);
|
||||
assertTrue(levels.contains("Level select"));
|
||||
assertTrue(levels.contains("3 of 10 unlocked"));
|
||||
assertTrue(levels.contains("Settings"));
|
||||
|
||||
String settings = AccessibilityScreenDescription.settings(
|
||||
ControlSettings.defaults(), AccessibilitySettings.defaults());
|
||||
assertTrue(settings.contains("Settings"));
|
||||
assertTrue(settings.contains("Text size Standard"));
|
||||
assertTrue(settings.contains("High contrast Off"));
|
||||
assertTrue(settings.contains("Back"));
|
||||
|
||||
String playing = AccessibilityScreenDescription.playing(
|
||||
LevelCatalog.get(0), 2, "01:23", "Signals 1/1", false);
|
||||
assertTrue(playing.contains("Playing level 1"));
|
||||
assertTrue(playing.contains("2 volunteers"));
|
||||
assertTrue(playing.contains("Signals 1/1"));
|
||||
assertTrue(playing.contains("Move left, move right, jump, restart, pause"));
|
||||
|
||||
String complete = AccessibilityScreenDescription.complete(
|
||||
LevelCatalog.get(0), 2, "01:23", true);
|
||||
assertTrue(complete.contains("Level complete"));
|
||||
assertTrue(complete.contains("Replay"));
|
||||
assertTrue(complete.contains("Next level"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import org.junit.Test;
|
||||
|
||||
public final class AccessibilitySettingsTest {
|
||||
@Test
|
||||
public void textSizeCyclesBetweenReadablePresetsAndWraps() {
|
||||
AccessibilitySettings settings = AccessibilitySettings.defaults();
|
||||
|
||||
assertEquals("Standard", settings.textSizeLabel());
|
||||
assertEquals(1f, settings.textScale(), 0f);
|
||||
|
||||
settings = settings.nextTextSize();
|
||||
assertEquals("Large", settings.textSizeLabel());
|
||||
assertEquals(1.25f, settings.textScale(), 0f);
|
||||
|
||||
assertEquals("Standard", settings.nextTextSize().textSizeLabel());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void highContrastIsExplicitAndReversible() {
|
||||
AccessibilitySettings settings = AccessibilitySettings.defaults();
|
||||
|
||||
assertFalse(settings.highContrastEnabled());
|
||||
assertEquals("Off", settings.highContrastLabel());
|
||||
|
||||
settings = settings.toggleHighContrast();
|
||||
assertTrue(settings.highContrastEnabled());
|
||||
assertEquals("On", settings.highContrastLabel());
|
||||
assertFalse(settings.toggleHighContrast().highContrastEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidPersistedPresetFallsBackWithoutChangingContrastChoice() {
|
||||
AccessibilitySettings settings = AccessibilitySettings.restore(99, true);
|
||||
|
||||
assertEquals("Standard", settings.textSizeLabel());
|
||||
assertTrue(settings.highContrastEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void largePresetScalesEveryCanvasTextRequestWithoutMovingLayout() {
|
||||
AccessibilitySettings large = AccessibilitySettings.defaults().nextTextSize();
|
||||
|
||||
assertEquals(30f, large.canvasTextSize(24f), 0f);
|
||||
assertEquals(80f, large.canvasTextSize(64f), 0f);
|
||||
assertEquals(24f, AccessibilitySettings.defaults().canvasTextSize(24f), 0f);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.projectsacrifice.game;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public final class AccessibilityStatusTest {
|
||||
@Test
|
||||
public void criticalGameplayStateIsRepresentedByTextNotOnlyColorOrVibration() {
|
||||
assertEquals(
|
||||
"Signals 1/2 · Doors 0/1 · Volunteer respawning",
|
||||
AccessibilityStatus.describe(1, 2, 0, 1, false)
|
||||
);
|
||||
assertEquals(
|
||||
"Signals 0/0 · Doors 0/0 · Volunteer active",
|
||||
AccessibilityStatus.describe(0, 0, 0, 0, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user