Skip to content
Closed

Dev #82

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
6 changes: 6 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ android {
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
buildTypes {
debug {
// Distinct applicationId so debug builds install alongside a
// release install of OneTwo rather than replacing it.
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
Expand Down
4 changes: 4 additions & 0 deletions app/src/debug/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">OneTwo Debug</string>
</resources>
166 changes: 138 additions & 28 deletions app/src/main/java/com/nicue/onetwo/ui/dice/DiceAdapter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.nicue.onetwo.ui.dice;

import android.content.res.ColorStateList;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
Expand All @@ -17,6 +18,8 @@ public interface Listener {
void onRollDie(int position);

void onRemoveDie(int position);

void onToggleLock(int position);
}

private final Listener listener;
Expand Down Expand Up @@ -70,7 +73,8 @@ public boolean areContentsTheSame(
DieUiModel oldItem = dice.get(oldItemPosition);
DieUiModel newItem = newDice.get(newItemPosition);
return oldItem.getFaces() == newItem.getFaces()
&& oldItem.getValue() == newItem.getValue();
&& oldItem.getValue() == newItem.getValue()
&& oldItem.isLocked() == newItem.isLocked();
}
});
this.dice = newDice == null ? new ArrayList<DieUiModel>() : new ArrayList<>(newDice);
Expand All @@ -82,7 +86,7 @@ public void animateAllVisibleItems(RecyclerView recyclerView, final Runnable end
for (int i = 0; i < recyclerView.getChildCount(); i++) {
RecyclerView.ViewHolder holder =
recyclerView.getChildViewHolder(recyclerView.getChildAt(i));
if (holder instanceof DiceViewHolder) {
if (isRollable(holder)) {
animatedItemCount++;
}
}
Expand All @@ -95,7 +99,7 @@ public void animateAllVisibleItems(RecyclerView recyclerView, final Runnable end
for (int i = 0; i < recyclerView.getChildCount(); i++) {
RecyclerView.ViewHolder holder =
recyclerView.getChildViewHolder(recyclerView.getChildAt(i));
if (holder instanceof DiceViewHolder) {
if (isRollable(holder)) {
((DiceViewHolder) holder)
.animateRoll(
new Runnable() {
Expand All @@ -111,8 +115,17 @@ public void run() {
}
}

private boolean isRollable(RecyclerView.ViewHolder holder) {
return holder instanceof DiceViewHolder && !((DiceViewHolder) holder).isLocked();
}

class DiceViewHolder extends RecyclerView.ViewHolder {
private final DiceItemBinding binding;
private final ColorStateList defaultStrokeColor;
private final int defaultStrokeWidth;
private final int lockedStrokeWidth;
private final float nudgeTranslation;
private boolean locked;
private final int[] diceColors = {
R.color.diceColor0, R.color.diceColor1, R.color.diceColor2,
R.color.diceColor3, R.color.diceColor4, R.color.diceColor5,
Expand All @@ -123,23 +136,33 @@ class DiceViewHolder extends RecyclerView.ViewHolder {
DiceViewHolder(DiceItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
this.defaultStrokeColor = binding.diceCv.getStrokeColorStateList();
this.defaultStrokeWidth = binding.diceCv.getStrokeWidth();
float density = binding.getRoot().getResources().getDisplayMetrics().density;
this.lockedStrokeWidth = Math.round(2 * density);
this.nudgeTranslation = 6 * density;
binding.getRoot()
.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
animateRoll(
new Runnable() {
@Override
public void run() {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onRollDie(position);
}
}
});
if (getAdapterPosition() == RecyclerView.NO_POSITION) {
return;
}
if (locked) {
animateLockedNudge();
return;
}
animateRoll(
new Runnable() {
@Override
public void run() {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onRollDie(position);
}
}
});
}
});
binding.getRoot()
Expand All @@ -154,14 +177,37 @@ public boolean onLongClick(View v) {
return true;
}
});
binding.btnLock.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onToggleLock(position);
}
}
});
// The button consumes touches, so without this a long press on the
// lock corner would be swallowed instead of removing the die.
binding.btnLock.setOnLongClickListener(
new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onRemoveDie(position);
}
return true;
}
});
}

boolean isLocked() {
return locked;
}

public void animateRoll(final Runnable endAction) {
binding.getRoot().animate().cancel();
binding.getRoot().setRotation(0f);
binding.getRoot().setScaleX(1f);
binding.getRoot().setScaleY(1f);
binding.getRoot().setTranslationZ(0f);
cancelAndResetTile();

binding.getRoot()
.animate()
Expand All @@ -184,10 +230,7 @@ public void run() {
new Runnable() {
@Override
public void run() {
binding.getRoot().setRotation(0f);
binding.getRoot().setScaleX(1f);
binding.getRoot().setScaleY(1f);
binding.getRoot().setTranslationZ(0f);
resetTileProperties();
if (endAction != null) {
endAction.run();
}
Expand All @@ -199,17 +242,56 @@ public void run() {
.start();
}

void bind(DieUiModel dieUiModel) {
int faces = dieUiModel.getFaces();
int position = getAdapterPosition();
int colorRes = diceColors[position % diceColors.length];
int color = binding.getRoot().getContext().getResources().getColor(colorRes);
void animateLockedNudge() {
final View root = binding.getRoot();
cancelAndResetTile();

root.animate()
.translationX(nudgeTranslation)
.setDuration(45)
.withEndAction(
new Runnable() {
@Override
public void run() {
root.animate()
.translationX(-nudgeTranslation)
.setDuration(45)
.withEndAction(
new Runnable() {
@Override
public void run() {
root.animate()
.translationX(0f)
.setDuration(45)
.start();
}
})
.start();
}
})
.start();
}

private void cancelAndResetTile() {
binding.getRoot().animate().cancel();
resetTileProperties();
}

private void resetTileProperties() {
binding.getRoot().setRotation(0f);
binding.getRoot().setScaleX(1f);
binding.getRoot().setScaleY(1f);
binding.getRoot().setTranslationX(0f);
binding.getRoot().setTranslationZ(0f);
}

void bind(DieUiModel dieUiModel) {
int faces = dieUiModel.getFaces();
int position = getAdapterPosition();
int colorRes = diceColors[position % diceColors.length];
int color = binding.getRoot().getContext().getResources().getColor(colorRes);

cancelAndResetTile();

binding.diceCv.setCardBackgroundColor(color);

Expand All @@ -225,6 +307,34 @@ void bind(DieUiModel dieUiModel) {
binding.tvDice.setText(String.valueOf(dieUiModel.getValue()));
binding.tvDieType.setText(
binding.getRoot().getContext().getString(R.string.dice_type_label, faces));

bindLockState(dieUiModel.isLocked(), textColor);
}

private void bindLockState(boolean isLocked, int contrastColor) {
this.locked = isLocked;

binding.btnLock.setImageResource(
isLocked ? R.drawable.ic_lock_24 : R.drawable.ic_lock_open_24);
binding.btnLock.setColorFilter(contrastColor);
binding.btnLock.setAlpha(isLocked ? 1f : 0.5f);
binding.btnLock.setContentDescription(
binding.getRoot()
.getContext()
.getString(
isLocked
? R.string.content_desc_unlock_die
: R.string.content_desc_lock_die));

if (isLocked) {
binding.diceCv.setStrokeColor(contrastColor);
binding.diceCv.setStrokeWidth(lockedStrokeWidth);
} else {
if (defaultStrokeColor != null) {
binding.diceCv.setStrokeColor(defaultStrokeColor);
}
binding.diceCv.setStrokeWidth(defaultStrokeWidth);
}
}

@DrawableRes
Expand Down
48 changes: 48 additions & 0 deletions app/src/main/java/com/nicue/onetwo/ui/dice/DiceFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public class DiceFragment extends Fragment implements DiceAdapter.Listener, Menu
private DiceLayoutBinding binding;
private DiceAdapter adapter;
private DiceViewModel viewModel;
private boolean hasLockedDice;
private boolean hasRollableDice;

@Nullable @Override
public View onCreateView(
Expand Down Expand Up @@ -97,6 +99,7 @@ public void onClick(View v) {
public void onChanged(DiceUiState state) {
adapter.submitList(state.getDice());
renderResultSummary(state);
updateLockActionState(state);
}
});

Expand All @@ -120,9 +123,28 @@ public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater)
menuInflater.inflate(R.menu.dice_actions, menu);
}

@Override
public void onPrepareMenu(@NonNull Menu menu) {
MenuItem unlockAll = menu.findItem(R.id.action_unlock_all);
if (unlockAll != null) {
unlockAll.setVisible(hasLockedDice);
}
MenuItem rollAll = menu.findItem(R.id.action_roll_all);
if (rollAll != null) {
rollAll.setEnabled(hasRollableDice);
}
}

@Override
public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
if (menuItem.getItemId() == R.id.action_unlock_all) {
viewModel.unlockAllDice();
return true;
}
if (menuItem.getItemId() == R.id.action_roll_all) {
if (!hasRollableDice) {
return true;
}
vibrate(new long[] {0, 15, 10, 15, 10, 15, 10, 15});
animateSummaryCard();
adapter.animateAllVisibleItems(
Expand Down Expand Up @@ -180,6 +202,25 @@ public void onRemoveDie(int position) {
viewModel.removeDie(position);
}

@Override
public void onToggleLock(int position) {
if (binding == null) {
return;
}
vibrate(30L);
viewModel.toggleLock(position);
}

private void updateLockActionState(DiceUiState state) {
boolean lockedChanged = hasLockedDice != state.hasLockedDice();
boolean rollableChanged = hasRollableDice != state.hasRollableDice();
hasLockedDice = state.hasLockedDice();
hasRollableDice = state.hasRollableDice();
if (lockedChanged || rollableChanged) {
requireActivity().invalidateMenu();
}
}

public static String normalizeFacesInput(String input) {
if (input == null || input.trim().isEmpty()) {
return "6";
Expand Down Expand Up @@ -277,6 +318,13 @@ private void renderResultSummary(DiceUiState state) {
}
}

private void vibrate(long milliseconds) {
Vibrator vibrator = (Vibrator) requireContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null) {
vibrator.vibrate(milliseconds);
}
}

private void vibrate(long[] pattern) {
Vibrator vibrator = (Vibrator) requireContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null) {
Expand Down
Loading