Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ android {
applicationId "com.nicue.onetwo"
minSdkVersion 21
targetSdkVersion 35
versionCode 40
versionName "2.1.3"
versionCode 41
versionName "2.1.4"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
buildTypes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public class SettingsPrefsDataSource {
private static final String KEY_DARK_MODE = "dark_mode";
private static final String KEY_CHOOSER_ORDER = "chooser_order";
private static final String KEY_LIFE_COUNTER_HAPTIC_FEEDBACK = "life_counter_haptic_feedback";
private static final String KEY_MTG_SETUP_COACH_MARK_DISMISSED =
"mtg_setup_coach_mark_dismissed";
private final SharedPreferences sharedPreferences;

public SettingsPrefsDataSource(Context context) {
Expand Down Expand Up @@ -46,4 +48,12 @@ public boolean isLifeCounterHapticFeedbackEnabled() {
public void setLifeCounterHapticFeedbackEnabled(boolean enabled) {
sharedPreferences.edit().putBoolean(KEY_LIFE_COUNTER_HAPTIC_FEEDBACK, enabled).apply();
}

public boolean isMtgSetupCoachMarkDismissed() {
return sharedPreferences.getBoolean(KEY_MTG_SETUP_COACH_MARK_DISMISSED, false);
}

public void setMtgSetupCoachMarkDismissed(boolean dismissed) {
sharedPreferences.edit().putBoolean(KEY_MTG_SETUP_COACH_MARK_DISMISSED, dismissed).apply();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ public void setLifeCounterHapticFeedbackEnabled(boolean enabled) {
dataSource.setLifeCounterHapticFeedbackEnabled(enabled);
}

public boolean isMtgSetupCoachMarkDismissed() {
return dataSource.isMtgSetupCoachMarkDismissed();
}

public void setMtgSetupCoachMarkDismissed(boolean dismissed) {
dataSource.setMtgSetupCoachMarkDismissed(dismissed);
}

public void applyNightMode() {
boolean darkModeEnabled = isDarkModeEnabled();
AppCompatDelegate.setDefaultNightMode(
Expand Down
142 changes: 142 additions & 0 deletions app/src/main/java/com/nicue/onetwo/ui/life/MtgLifeFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,21 @@
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.view.MenuHost;
import androidx.core.view.MenuItemCompat;
import androidx.core.view.MenuProvider;
import androidx.core.view.ViewCompat;
import androidx.core.widget.TextViewCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.nicue.onetwo.OneTwoApplication;
import com.nicue.onetwo.R;
Expand Down Expand Up @@ -68,6 +72,7 @@ public class MtgLifeFragment extends Fragment implements MenuProvider {
private final Map<Integer, TextView> activeDialogDamageTextViews = new HashMap<>();
private final Handler holdRepeatHandler = new Handler(Looper.getMainLooper());
private final Map<View, Runnable> activeLifeHoldRunnables = new HashMap<>();
private PopupWindow coachMarkPopup;

private static final class CommanderGridLayout {
private final int rows;
Expand Down Expand Up @@ -210,6 +215,138 @@ public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater)
newGameItem.setVisible(!currentUiState.isShowingSetup());
}
}
showCoachMarkIfNecessary();
}

private void showCoachMarkIfNecessary() {
if (viewModel == null || viewModel.isSetupCoachMarkDismissed()) {
return;
}
MtgLifeUiState state = viewModel.getUiState().getValue();
if (state != null && state.isShowingSetup()) {
return;
}

View view = getView();
if (view == null) return;
view.post(
() -> {
if (getActivity() == null) return;
View anchor = findCoachMarkAnchor();
if (anchor != null
&& anchor.isAttachedToWindow()
&& anchor.getVisibility() == View.VISIBLE) {
if (viewModel.isSetupCoachMarkDismissed()) return;
showCoachMark(anchor);
}
});
}

private View findCoachMarkAnchor() {
MaterialToolbar toolbar = requireActivity().findViewById(R.id.app_toolbar);
if (toolbar == null) {
toolbar = requireActivity().findViewById(R.id.toolbar);
}
if (toolbar == null) {
return null;
}

View directMatch = toolbar.findViewById(R.id.action_new_game);
if (directMatch != null) {
return directMatch;
}

MenuItem newGameItem = toolbar.getMenu().findItem(R.id.action_new_game);
if (newGameItem == null) {
return toolbar;
}

View contentDescriptionMatch =
findViewWithContentDescription(
toolbar, MenuItemCompat.getContentDescription(newGameItem));
if (contentDescriptionMatch != null) {
return contentDescriptionMatch;
}

View titleMatch = findViewWithContentDescription(toolbar, newGameItem.getTitle());
return titleMatch != null ? titleMatch : toolbar;
}

private View findViewWithContentDescription(View root, CharSequence contentDescription) {
if (root == null || contentDescription == null) {
return null;
}
CharSequence rootDescription = root.getContentDescription();
if (rootDescription != null
&& rootDescription.toString().contentEquals(contentDescription)) {
return root;
}
if (root instanceof ViewGroup rootGroup) {
for (int i = 0; i < rootGroup.getChildCount(); i++) {
View match =
findViewWithContentDescription(rootGroup.getChildAt(i), contentDescription);
if (match != null) {
return match;
}
}
}
return null;
}

private void showCoachMark(View anchor) {
if (coachMarkPopup != null) {
coachMarkPopup.setOnDismissListener(null);
coachMarkPopup.dismiss();
}
View popupView =
LayoutInflater.from(requireContext()).inflate(R.layout.mtg_coach_mark, null);
coachMarkPopup =
new PopupWindow(
popupView,
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
true);
coachMarkPopup.setOutsideTouchable(true);
coachMarkPopup.setElevation(dpToPx(8));
coachMarkPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

popupView.setOnClickListener(
v -> {
if (coachMarkPopup != null) {
coachMarkPopup.dismiss();
}
anchor.performClick();
});

coachMarkPopup.setOnDismissListener(
() -> {
viewModel.markSetupCoachMarkDismissed();
coachMarkPopup = null;
});

popupView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
popupView.layout(0, 0, popupView.getMeasuredWidth(), popupView.getMeasuredHeight());
View pointer = popupView.findViewById(R.id.coach_mark_pointer);
int pointerCenterX = popupView.getMeasuredWidth() / 2;
if (pointer != null) {
pointerCenterX = pointer.getLeft() + (pointer.getMeasuredWidth() / 2);
}
int anchorCenterX = getCoachMarkAnchorCenterX(anchor);
int xOffset = anchorCenterX - pointerCenterX;
coachMarkPopup.showAsDropDown(anchor, xOffset + dpToPx(14), dpToPx(10));
}

private int getCoachMarkAnchorCenterX(View anchor) {
if (anchor.getId() != R.id.toolbar || !(anchor instanceof MaterialToolbar toolbar)) {
return Math.round(anchor.getWidth() / 2f);
}

int actionCenterOffset = dpToPx(24);
boolean isRtl = ViewCompat.getLayoutDirection(toolbar) == ViewCompat.LAYOUT_DIRECTION_RTL;
if (isRtl) {
return toolbar.getContentInsetStart() + actionCenterOffset;
}
return toolbar.getWidth() - toolbar.getContentInsetEnd() - actionCenterOffset;
}

@Override
Expand Down Expand Up @@ -1302,6 +1439,11 @@ private void vibrate(long milliseconds) {
@Override
public void onDestroyView() {
super.onDestroyView();
if (coachMarkPopup != null) {
coachMarkPopup.setOnDismissListener(null);
coachMarkPopup.dismiss();
coachMarkPopup = null;
}
for (Runnable repeatRunnable : activeLifeHoldRunnables.values()) {
holdRepeatHandler.removeCallbacks(repeatRunnable);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,14 @@ private void handleTick(long nowMs) {
updateUiState();
}

public boolean isSetupCoachMarkDismissed() {
return settingsRepository.isMtgSetupCoachMarkDismissed();
}

public void markSetupCoachMarkDismissed() {
settingsRepository.setMtgSetupCoachMarkDismissed(true);
}

public void resetToSetup() {
pauseTimer();
savedStateHandle.set(KEY_SHOWING_SETUP, true);
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/drawable/bg_mtg_coach_mark_pointer.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/colorSurfaceContainerHigh" />
<corners android:radius="2dp" />
</shape>
14 changes: 14 additions & 0 deletions app/src/main/res/drawable/ic_add_circle_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<group
android:translateX="-1.5"
android:translateY="1.5">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/>
</group>
</vector>
7 changes: 7 additions & 0 deletions app/src/main/res/drawable/ic_add_circle_inset.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_add_circle_24"
android:insetTop="2dp"
android:insetRight="4dp"
android:insetBottom="-2dp"
android:insetLeft="-4dp" />
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_add_circle_outline_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM16,11h-3L13,8h-2v3L8,11v2h3v3h2v-3h3v-2z"/>
</vector>
59 changes: 59 additions & 0 deletions app/src/main/res/layout/mtg_coach_mark.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="6dp"
android:paddingTop="6dp"
android:paddingEnd="6dp"
android:paddingBottom="2dp">

<View
android:id="@+id/coach_mark_pointer"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="top|end"
android:layout_marginTop="1dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_mtg_coach_mark_pointer"
android:elevation="2dp"
android:rotation="45" />

<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginTop="7dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerHigh"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
app:strokeWidth="0dp">

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingTop="10dp"
android:paddingEnd="12dp"
android:paddingBottom="10dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/mtg_setup_title"
android:textColor="?attr/colorOnSurface"
android:textSize="13sp"
android:textStyle="bold" />

<TextView
android:layout_width="160dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:lineSpacingExtra="1dp"
android:text="@string/mtg_setup_coach_mark"
android:textColor="?attr/colorOnSurfaceVariant"
android:textSize="12sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
4 changes: 3 additions & 1 deletion app/src/main/res/menu/life_actions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
<item
android:id="@+id/action_new_game"
android:title="@string/mtg_setup_title"
app:showAsAction="always|withText" />
android:icon="@drawable/ic_add_circle_24"
android:contentDescription="@string/mtg_setup_title"
app:showAsAction="always" />
</menu>
1 change: 1 addition & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,5 @@
<string name="mtg_start_timer_desc">Iniciar temporizador para el jugador %1$d</string>
<string name="mtg_player_timer_desc">Tiempo restante del jugador %1$d: %2$s</string>
<string name="mtg_setup_choose_and_start">Elegir 1º y empezar</string>
<string name="mtg_setup_coach_mark">Configuración de partida, temporizador y selector de dedos</string>
</resources>
1 change: 1 addition & 0 deletions app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,6 @@
<string name="mtg_pass_turn_desc">Passer le tour du joueur %1$d au joueur suivant</string>
<string name="mtg_start_timer_desc">Démarrer le minuteur pour le joueur %1$d</string>
<string name="mtg_player_timer_desc">Temps restant du joueur %1$d : %2$s</string>
<string name="mtg_setup_coach_mark">Paramètres de partie, minuteur et sélecteur tactile</string>

</resources>
1 change: 1 addition & 0 deletions app/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,6 @@
<string name="mtg_pass_turn_desc">プレイヤー%1$dのターンを次のプレイヤーに渡す</string>
<string name="mtg_start_timer_desc">プレイヤー%1$dのタイマーを開始する</string>
<string name="mtg_player_timer_desc">プレイヤー%1$dの残り時間: %2$s</string>
<string name="mtg_setup_coach_mark">ゲーム設定、タイマー、指で選ぶ機能</string>

</resources>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,5 @@
<string name="mtg_start_timer_desc">Start timer for player %1$d</string>
<string name="mtg_player_timer_desc">Player %1$d remaining time: %2$s</string>
<string name="mtg_setup_choose_and_start">Choose 1st &amp; Start</string>
<string name="mtg_setup_coach_mark">Game settings, timer and finger chooser</string>
</resources>
Loading
Loading