Добавить доступный интерфейс этапа 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 ? "Вкл." : "Выкл.";
|
if (russian()) return enabled ? "Вкл." : "Выкл.";
|
||||||
return enabled ? "On" : "Off";
|
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 back() { return russian() ? "Назад" : "Back"; }
|
||||||
|
|
||||||
public String hudStats(int deaths, long elapsedMilliseconds) {
|
public String hudStats(int deaths, long elapsedMilliseconds) {
|
||||||
@@ -224,6 +234,79 @@ public final class GameStrings {
|
|||||||
return step + " / " + total;
|
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) {
|
public String levelTitle(int oneBasedLevelNumber) {
|
||||||
return (russian() ? RU_TITLES : EN_TITLES)[levelIndex(oneBasedLevelNumber)];
|
return (russian() ? RU_TITLES : EN_TITLES)[levelIndex(oneBasedLevelNumber)];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,16 @@ import android.util.SparseArray;
|
|||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.accessibility.AccessibilityEvent;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A dependency-free 2D puzzle-platformer renderer and deterministic physics loop.
|
* 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 ControlSettingsStore controlSettingsStore;
|
||||||
private final OnboardingStore onboardingStore;
|
private final OnboardingStore onboardingStore;
|
||||||
private final GameLanguageStore languageStore;
|
private final GameLanguageStore languageStore;
|
||||||
|
private final AccessibilitySettingsStore accessibilitySettingsStore;
|
||||||
private final SparseArray<PointF> pointers = new SparseArray<>();
|
private final SparseArray<PointF> pointers = new SparseArray<>();
|
||||||
private final Player player = new Player();
|
private final Player player = new Player();
|
||||||
private final ArrayList<Corpse> corpses = new ArrayList<>();
|
private final ArrayList<Corpse> corpses = new ArrayList<>();
|
||||||
@@ -194,6 +198,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
private CompletionResult completionResult;
|
private CompletionResult completionResult;
|
||||||
private GameLanguage language;
|
private GameLanguage language;
|
||||||
private GameStrings strings;
|
private GameStrings strings;
|
||||||
|
private AccessibilitySettings accessibilitySettings;
|
||||||
|
private String lastAccessibilityStateKey = "";
|
||||||
|
|
||||||
public GameView(Context context) {
|
public GameView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
@@ -205,12 +211,14 @@ public final class GameView extends View implements Runnable {
|
|||||||
languageStore = new GameLanguageStore(context);
|
languageStore = new GameLanguageStore(context);
|
||||||
language = languageStore.load();
|
language = languageStore.load();
|
||||||
strings = GameStrings.forLanguage(language);
|
strings = GameStrings.forLanguage(language);
|
||||||
|
accessibilitySettingsStore = new AccessibilitySettingsStore(context);
|
||||||
|
accessibilitySettings = accessibilitySettingsStore.load();
|
||||||
paint.setTypeface(android.graphics.Typeface.create("sans", android.graphics.Typeface.NORMAL));
|
paint.setTypeface(android.graphics.Typeface.create("sans", android.graphics.Typeface.NORMAL));
|
||||||
stroke.setStyle(Paint.Style.STROKE);
|
stroke.setStyle(Paint.Style.STROKE);
|
||||||
stroke.setStrokeCap(Paint.Cap.ROUND);
|
stroke.setStrokeCap(Paint.Cap.ROUND);
|
||||||
setFocusable(true);
|
setFocusable(true);
|
||||||
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
|
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||||
setContentDescription(strings.appContentDescription());
|
refreshAccessibilityDescription(false);
|
||||||
running = true;
|
running = true;
|
||||||
postOnAnimation(this);
|
postOnAnimation(this);
|
||||||
}
|
}
|
||||||
@@ -239,12 +247,14 @@ public final class GameView extends View implements Runnable {
|
|||||||
pointers.clear();
|
pointers.clear();
|
||||||
deriveControls();
|
deriveControls();
|
||||||
lastFrameNanos = System.nanoTime();
|
lastFrameNanos = System.nanoTime();
|
||||||
|
refreshAccessibilityDescription(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleBack() {
|
public boolean handleBack() {
|
||||||
if (screen == Screen.LEVELS) return false;
|
if (screen == Screen.LEVELS) return false;
|
||||||
if (screen == Screen.SETTINGS) {
|
if (screen == Screen.SETTINGS) {
|
||||||
screen = Screen.LEVELS;
|
screen = Screen.LEVELS;
|
||||||
|
refreshAccessibilityDescription(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (screen == Screen.COMPLETE) {
|
if (screen == Screen.COMPLETE) {
|
||||||
@@ -258,6 +268,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
}
|
}
|
||||||
pointers.clear();
|
pointers.clear();
|
||||||
deriveControls();
|
deriveControls();
|
||||||
|
refreshAccessibilityDescription(true);
|
||||||
return 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));
|
for (LevelDefinition.Platform item : level.platforms) platformStates.add(new PlatformState(item));
|
||||||
spawnPlayer();
|
spawnPlayer();
|
||||||
lastFrameNanos = System.nanoTime();
|
lastFrameNanos = System.nanoTime();
|
||||||
|
refreshAccessibilityDescription(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restartLevel() {
|
private void restartLevel() {
|
||||||
@@ -320,6 +332,8 @@ public final class GameView extends View implements Runnable {
|
|||||||
checkGoal();
|
checkGoal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshAccessibilityDescriptionIfStateChanged();
|
||||||
|
|
||||||
float targetCamera = player.alive ? player.x - 610f + player.facing * 110f : cameraX;
|
float targetCamera = player.alive ? player.x - 610f + player.facing * 110f : cameraX;
|
||||||
targetCamera = GameMath.clamp(targetCamera, 0f, Math.max(0f, level.worldWidth - VIEW_WIDTH));
|
targetCamera = GameMath.clamp(targetCamera, 0f, Math.max(0f, level.worldWidth - VIEW_WIDTH));
|
||||||
cameraX += (targetCamera - cameraX) * Math.min(1f, dt * 4.5f);
|
cameraX += (targetCamera - cameraX) * Math.min(1f, dt * 4.5f);
|
||||||
@@ -656,6 +670,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
pointers.clear();
|
pointers.clear();
|
||||||
deriveControls();
|
deriveControls();
|
||||||
emitHaptic(HapticFeedbackConstants.LONG_PRESS);
|
emitHaptic(HapticFeedbackConstants.LONG_PRESS);
|
||||||
|
refreshAccessibilityDescription(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RectF playerRect() {
|
private RectF playerRect() {
|
||||||
@@ -673,9 +688,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
@Override
|
@Override
|
||||||
protected void onDraw(Canvas canvas) {
|
protected void onDraw(Canvas canvas) {
|
||||||
super.onDraw(canvas);
|
super.onDraw(canvas);
|
||||||
canvasScale = Math.min(getWidth() / VIEW_WIDTH, getHeight() / VIEW_HEIGHT);
|
updateViewport(getWidth(), getHeight());
|
||||||
canvasOffsetX = (getWidth() - VIEW_WIDTH * canvasScale) * 0.5f;
|
|
||||||
canvasOffsetY = (getHeight() - VIEW_HEIGHT * canvasScale) * 0.5f;
|
|
||||||
|
|
||||||
paint.setShader(outsideGradient);
|
paint.setShader(outsideGradient);
|
||||||
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
|
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
|
||||||
@@ -714,6 +727,12 @@ public final class GameView extends View implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void drawSky(Canvas canvas) {
|
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);
|
paint.setShader(skyGradient);
|
||||||
canvas.drawRect(0, 0, VIEW_WIDTH, VIEW_HEIGHT, paint);
|
canvas.drawRect(0, 0, VIEW_WIDTH, VIEW_HEIGHT, paint);
|
||||||
paint.setShader(null);
|
paint.setShader(null);
|
||||||
@@ -745,10 +764,19 @@ public final class GameView extends View implements Runnable {
|
|||||||
for (PlatformState state : platformStates) {
|
for (PlatformState state : platformStates) {
|
||||||
RectF box = state.rect();
|
RectF box = state.rect();
|
||||||
boolean moving = state.definition.isMoving();
|
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);
|
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);
|
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) {
|
if (moving) {
|
||||||
stroke.setColor(Color.argb(120, 244, 214, 137));
|
stroke.setColor(Color.argb(120, 244, 214, 137));
|
||||||
stroke.setStrokeWidth(5f);
|
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);
|
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));
|
paint.setColor(active ? Color.rgb(250, 194, 76) : Color.rgb(174, 126, 55));
|
||||||
canvas.drawRoundRect(box, 7, 7, paint);
|
canvas.drawRoundRect(box, 7, 7, paint);
|
||||||
|
drawWorldLabel(canvas, active ? "DOWN" : "UP", box.centerX(), box.top - 18);
|
||||||
} else {
|
} else {
|
||||||
paint.setColor(Color.rgb(50, 56, 70));
|
paint.setColor(Color.rgb(50, 56, 70));
|
||||||
canvas.drawRoundRect(box, 12, 12, paint);
|
canvas.drawRoundRect(box, 12, 12, paint);
|
||||||
@@ -785,6 +814,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
Float remaining = timedSignals.get(item.signalId);
|
Float remaining = timedSignals.get(item.signalId);
|
||||||
drawWorldLabel(canvas, String.format(Locale.US, "%.1f", remaining == null ? 0f : remaining), box.centerX(), box.top - 52);
|
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));
|
paint.setColor(Color.rgb(239, 195, 92));
|
||||||
canvas.drawCircle(box.centerX(), box.centerY(), 15, paint);
|
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) {
|
private void drawHud(Canvas canvas) {
|
||||||
paint.setColor(Color.argb(178, 15, 20, 34));
|
paint.setColor(Color.argb(178, 15, 20, 34));
|
||||||
canvas.drawRoundRect(new RectF(28, 26, 940, 138), 24, 24, paint);
|
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,
|
drawText(canvas, String.format(Locale.US, "%02d %s", level.number,
|
||||||
strings.levelTitle(level.number)), 56, 72, 34,
|
strings.levelTitle(level.number)), 56, 72, 34,
|
||||||
Color.rgb(248, 220, 151), false);
|
Color.rgb(248, 220, 151), false);
|
||||||
drawFittedText(canvas, strings.levelHint(level.number), 56, 116, 25, 16,
|
drawFittedText(canvas, strings.levelHint(level.number), 56, 116, 25, 16,
|
||||||
850, Color.rgb(231, 231, 235), false);
|
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)),
|
drawText(canvas, strings.hudStats(deaths, (long) (levelElapsed * 1000)),
|
||||||
1010, 82, 31, Color.WHITE, false);
|
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(1608, 26, 1770, 112), strings.restart(), false);
|
||||||
drawHudButton(canvas, new RectF(1790, 26, 1892, 112), "II", 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) {
|
private void drawControlCircle(Canvas canvas, float x, float y, float radius, String label, boolean active) {
|
||||||
int inactiveFillAlpha = Math.round(190f * controlSettings.controlOpacity());
|
int inactiveFillAlpha = Math.round(190f * controlSettings.controlOpacity());
|
||||||
int inactiveStrokeAlpha = Math.round(230f * controlSettings.controlOpacity());
|
int inactiveStrokeAlpha = Math.round(230f * controlSettings.controlOpacity());
|
||||||
paint.setColor(active ? Color.argb(220, 245, 190, 74)
|
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||||
: Color.argb(inactiveFillAlpha, 22, 28, 43));
|
? (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);
|
canvas.drawCircle(x, y, radius, paint);
|
||||||
stroke.setColor(active ? Color.rgb(255, 232, 169)
|
stroke.setColor(accessibilitySettings.highContrastEnabled()
|
||||||
: Color.argb(inactiveStrokeAlpha, 240, 240, 245));
|
? Color.WHITE
|
||||||
|
: (active ? Color.rgb(255, 232, 169)
|
||||||
|
: Color.argb(inactiveStrokeAlpha, 240, 240, 245)));
|
||||||
stroke.setStrokeWidth(5f);
|
stroke.setStrokeWidth(5f);
|
||||||
canvas.drawCircle(x, y, radius - 5, stroke);
|
canvas.drawCircle(x, y, radius - 5, stroke);
|
||||||
drawCenteredText(canvas, label, x, y + 21, 68 * controlSettings.controlScale(),
|
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);
|
RectF card = new RectF(left, top, left + cardW, top + cardH);
|
||||||
boolean enabled = i < unlocked;
|
boolean enabled = i < unlocked;
|
||||||
boolean complete = progress.isCompleted(i + 1);
|
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);
|
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.setColor(complete ? Color.rgb(109, 222, 143) : (enabled ? Color.rgb(205, 164, 83) : Color.rgb(73, 77, 91)));
|
||||||
stroke.setStrokeWidth(5f);
|
stroke.setStrokeWidth(5f);
|
||||||
canvas.drawRoundRect(card, 22, 22, stroke);
|
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));
|
enabled ? Color.rgb(248, 210, 118) : Color.rgb(120, 123, 135));
|
||||||
LevelDefinition item = LevelCatalog.get(i);
|
LevelDefinition item = LevelCatalog.get(i);
|
||||||
drawCenteredFittedText(canvas, strings.levelTitle(item.number), card.centerX(),
|
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,
|
drawCenteredText(canvas, strings.tapToEnter(), card.centerX(), top + 190,
|
||||||
22, Color.rgb(202, 204, 214));
|
22, Color.rgb(202, 204, 214));
|
||||||
}
|
}
|
||||||
|
canvas.restore();
|
||||||
}
|
}
|
||||||
drawHudButton(canvas, new RectF(1560, 70, 1850, 160), strings.settingsButton(), false);
|
drawHudButton(canvas, new RectF(1560, 70, 1850, 160), strings.settingsButton(), false);
|
||||||
drawCenteredText(canvas, strings.levelSelectFooter(), VIEW_WIDTH * 0.5f, 980, 24,
|
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()));
|
strings.controlOpacityValue(controlSettings.controlOpacityIndex()));
|
||||||
drawSettingRow(canvas, new RectF(420, 620, 1500, 750), strings.vibrationSetting(),
|
drawSettingRow(canvas, new RectF(420, 620, 1500, 750), strings.vibrationSetting(),
|
||||||
strings.hapticsValue(controlSettings.hapticsEnabled()));
|
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);
|
drawHudButton(canvas, new RectF(710, 850, 1210, 960), strings.back(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawSettingRow(Canvas canvas, RectF box, String label, String value) {
|
private void drawCompactSettingRow(Canvas canvas, RectF box, String label, String value) {
|
||||||
paint.setColor(Color.argb(220, 38, 45, 65));
|
paint.setColor(accessibilitySettings.highContrastEnabled()
|
||||||
|
? Color.BLACK : Color.argb(220, 38, 45, 65));
|
||||||
canvas.drawRoundRect(box, 22, 22, paint);
|
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);
|
stroke.setStrokeWidth(4f);
|
||||||
canvas.drawRoundRect(box, 22, 22, stroke);
|
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);
|
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),
|
drawHudButton(canvas, new RectF(1_090f, box.top + 24f, 1_445f, box.bottom - 24f),
|
||||||
value, false);
|
value, false);
|
||||||
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawComplete(Canvas canvas) {
|
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) {
|
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);
|
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);
|
stroke.setStrokeWidth(4f);
|
||||||
canvas.drawRoundRect(box, 22, 22, stroke);
|
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);
|
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) {
|
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) {
|
private void drawText(Canvas canvas, String text, float x, float baseline, float size, int color, boolean bold) {
|
||||||
paint.setShader(null);
|
paint.setShader(null);
|
||||||
paint.setStyle(Paint.Style.FILL);
|
paint.setStyle(Paint.Style.FILL);
|
||||||
paint.setTextSize(size);
|
paint.setTextSize(accessibilitySettings.canvasTextSize(size));
|
||||||
paint.setColor(color);
|
paint.setColor(accessibleTextColor(color));
|
||||||
paint.setTextAlign(Paint.Align.LEFT);
|
paint.setTextAlign(Paint.Align.LEFT);
|
||||||
paint.setTypeface(android.graphics.Typeface.create("sans", bold ? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL));
|
paint.setTypeface(android.graphics.Typeface.create("sans", bold ? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL));
|
||||||
canvas.drawText(text, x, baseline, paint);
|
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) {
|
private void drawCenteredText(Canvas canvas, String text, float x, float baseline, float size, int color) {
|
||||||
paint.setShader(null);
|
paint.setShader(null);
|
||||||
paint.setStyle(Paint.Style.FILL);
|
paint.setStyle(Paint.Style.FILL);
|
||||||
paint.setTextSize(size);
|
paint.setTextSize(accessibilitySettings.canvasTextSize(size));
|
||||||
paint.setColor(color);
|
paint.setColor(accessibleTextColor(color));
|
||||||
paint.setTextAlign(Paint.Align.CENTER);
|
paint.setTextAlign(Paint.Align.CENTER);
|
||||||
paint.setTypeface(android.graphics.Typeface.create("sans", android.graphics.Typeface.BOLD));
|
paint.setTypeface(android.graphics.Typeface.create("sans", android.graphics.Typeface.BOLD));
|
||||||
canvas.drawText(text, x, baseline, paint);
|
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));
|
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
|
@Override
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
PointF point = toCanvasPoint(event.getX(event.getActionIndex()), event.getY(event.getActionIndex()));
|
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;
|
userPaused = true;
|
||||||
pointers.clear();
|
pointers.clear();
|
||||||
deriveControls();
|
deriveControls();
|
||||||
|
refreshAccessibilityDescription(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1334,6 +1433,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
if (contains(1560, 70, 1850, 160, x, y)) {
|
if (contains(1560, 70, 1850, 160, x, y)) {
|
||||||
screen = Screen.SETTINGS;
|
screen = Screen.SETTINGS;
|
||||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||||
|
refreshAccessibilityDescription(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
float startX = 110f;
|
float startX = 110f;
|
||||||
@@ -1384,6 +1484,7 @@ public final class GameView extends View implements Runnable {
|
|||||||
screen = Screen.LEVELS;
|
screen = Screen.LEVELS;
|
||||||
pointers.clear();
|
pointers.clear();
|
||||||
deriveControls();
|
deriveControls();
|
||||||
|
refreshAccessibilityDescription(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void replayFromResult() {
|
private void replayFromResult() {
|
||||||
@@ -1402,10 +1503,17 @@ public final class GameView extends View implements Runnable {
|
|||||||
userPaused = false;
|
userPaused = false;
|
||||||
screen = Screen.LEVELS;
|
screen = Screen.LEVELS;
|
||||||
}
|
}
|
||||||
|
refreshAccessibilityDescription(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSettingsTap(float x, float y) {
|
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());
|
updateLanguage(language.next());
|
||||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||||
} else if (contains(420, 280, 1500, 410, x, y)) {
|
} 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)) {
|
} else if (contains(710, 850, 1210, 960, x, y)) {
|
||||||
screen = Screen.LEVELS;
|
screen = Screen.LEVELS;
|
||||||
emitHaptic(HapticFeedbackConstants.KEYBOARD_TAP);
|
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));
|
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) {
|
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;
|
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.controlOpacityValue(controlSettings.controlOpacityIndex()));
|
||||||
visible.add(strings.vibrationSetting());
|
visible.add(strings.vibrationSetting());
|
||||||
visible.add(strings.hapticsValue(controlSettings.hapticsEnabled()));
|
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());
|
visible.add(strings.back());
|
||||||
} else if (level != null) {
|
} else if (level != null) {
|
||||||
visible.add(String.format(Locale.US, "%02d %s", level.number,
|
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;
|
language = updated == null ? GameLanguage.EN : updated;
|
||||||
strings = GameStrings.forLanguage(language);
|
strings = GameStrings.forLanguage(language);
|
||||||
languageStore.save(language);
|
languageStore.save(language);
|
||||||
setContentDescription(strings.appContentDescription());
|
refreshAccessibilityDescription(false);
|
||||||
invalidate();
|
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) {
|
private void updateControlSettings(ControlSettings updated) {
|
||||||
controlSettings = updated;
|
controlSettings = updated;
|
||||||
controlSettingsStore.save(updated);
|
controlSettingsStore.save(updated);
|
||||||
pointers.clear();
|
pointers.clear();
|
||||||
deriveControls();
|
deriveControls();
|
||||||
|
refreshAccessibilityDescription(false);
|
||||||
invalidate();
|
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) {
|
public void setAutomationInput(boolean moveLeft, boolean moveRight, boolean jump) {
|
||||||
boolean jumpWasPressed = jumpPressed;
|
boolean jumpWasPressed = jumpPressed;
|
||||||
leftPressed = moveLeft;
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Stage 3 accessibility QA
|
||||||
|
|
||||||
|
Дата: 18 августа 2026 года. Slice: масштаб текста, high contrast, non-audio state
|
||||||
|
и семантика Canvas-экранов.
|
||||||
|
|
||||||
|
## Реализованный контракт
|
||||||
|
|
||||||
|
- Settings содержит сохраняемые пресеты `Text size: Standard/Large`
|
||||||
|
(`1.0×/1.25×`) и `High contrast: Off/On`;
|
||||||
|
- настройки хранятся отдельно от control settings и progress, поэтому миграция не
|
||||||
|
меняет существующие сохранения;
|
||||||
|
- оба общих Canvas text helper применяют выбранный масштаб ко всему тексту;
|
||||||
|
viewport и hit mapping остаются неизменными, а фиксированные HUD/card/button
|
||||||
|
области обрезают переполнение внутри своих границ;
|
||||||
|
- high contrast использует чёрный фон, непрозрачные кнопки, белые контуры и
|
||||||
|
бело-жёлтый текст; активные механизмы не различаются только цветом;
|
||||||
|
- `ON/OFF`, `UP/DOWN`, `OPEN/LOCKED` и постоянная строка
|
||||||
|
`Signals · Doors · Volunteer` дают видимое представление критического состояния
|
||||||
|
при выключенной вибрации;
|
||||||
|
- content description меняется для level select, settings, playing, pause и
|
||||||
|
complete; системный Back сохраняет прежний детерминированный контракт;
|
||||||
|
- read-only automation surface: `textScaleForAutomation()`,
|
||||||
|
`highContrastForAutomation()`, `screenDescriptionForAutomation()`,
|
||||||
|
`criticalVisibleStatusForAutomation()` и `safeViewportForAutomation()`.
|
||||||
|
|
||||||
|
## TDD evidence
|
||||||
|
|
||||||
|
- RED: `AccessibilitySettings` отсутствовал, focused JVM-test завершался шестью
|
||||||
|
`cannot find symbol`;
|
||||||
|
- GREEN: `Standard/Large`, `Off/On` и безопасное восстановление persisted index;
|
||||||
|
- RED → GREEN отдельными циклами: `highContrastEnabled`, `restore`,
|
||||||
|
`canvasTextSize`, `AccessibilityStatus.describe` и
|
||||||
|
`AccessibilityScreenDescription`;
|
||||||
|
- RED device compile: отсутствовали persisted/public Settings surface,
|
||||||
|
screen transition и safe-viewport hooks;
|
||||||
|
- GREEN device compile: пять focused Android acceptance методов компилируются и
|
||||||
|
проверяют recreation, реальные Settings taps, screen/Back semantics,
|
||||||
|
неизменный safe viewport и textual state при `Vibration: Off`.
|
||||||
|
|
||||||
|
## Проверки этого isolated slice
|
||||||
|
|
||||||
|
- focused accessibility JVM tests — GREEN;
|
||||||
|
- все JVM tests — обязательный gate перед коммитом;
|
||||||
|
- `lintDebug`, `assembleDebug`, `compileDebugAndroidTestJavaWithJavac` — обязательный
|
||||||
|
gate перед коммитом;
|
||||||
|
- device execution намеренно оставлен финальной интеграционной ветке: этот stream
|
||||||
|
не запускает и не меняет общий emulator.
|
||||||
|
|
||||||
|
## Интеграционные замечания
|
||||||
|
|
||||||
|
- `GameView` одновременно изменяется onboarding/result и localization slices;
|
||||||
|
при merge нужно сохранить единые text helpers и вызывать refresh semantics после
|
||||||
|
всех новых screen transitions;
|
||||||
|
- localized строки должны заменить английский текст и в Canvas, и в content
|
||||||
|
descriptions одним источником, не удаляя read-only acceptance hooks;
|
||||||
|
- minSdk 23, control settings, progress format и Stage 2 physics/levels не менялись.
|
||||||
Reference in New Issue
Block a user