Add tiles view mode

Minor UI improvements
Fix animations
Fix typo
Improvements made after PR review
PR improvements

Co-authored-by: Alexander Bakkker <ab@alexbakker.me>
This commit is contained in:
Michael Schättgen 2023-02-08 02:51:03 +01:00
parent 94a38e82e4
commit d90303cf0e
16 changed files with 461 additions and 107 deletions

View file

@ -5,7 +5,8 @@ import androidx.annotation.LayoutRes;
public enum ViewMode {
NORMAL,
COMPACT,
SMALL;
SMALL,
TILES;
private static ViewMode[] _values;
@ -26,6 +27,8 @@ public enum ViewMode {
return R.layout.card_entry_compact;
case SMALL:
return R.layout.card_entry_small;
case TILES:
return R.layout.card_entry_tile;
default:
return R.layout.card_entry;
}
@ -37,8 +40,34 @@ public enum ViewMode {
public float getDividerHeight() {
if (this == ViewMode.COMPACT) {
return 0;
} else if (this == ViewMode.TILES) {
return 4;
}
return 20;
}
public int getColumnSpan() {
if (this == ViewMode.TILES) {
return 2;
}
return 1;
}
public float getDividerWidth() {
if (this == ViewMode.TILES) {
return 4;
}
return 0;
}
public String getFormattedAccountName(String accountName) {
if (this == ViewMode.TILES) {
return accountName;
}
return String.format("(%s)", accountName);
}
}

View file

@ -16,6 +16,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final EntryAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
private int _dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
public SimpleItemTouchHelperCallback(EntryAdapter adapter) {
_adapter = adapter;
@ -46,6 +47,10 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
return false;
}
public void setDragFlags(int dragFlags) {
_dragFlags = dragFlags;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
// It's not clear when this can happen, but sometimes the ViewHolder
@ -57,16 +62,15 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
}
int swipeFlags = 0;
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
EntryAdapter adapter = (EntryAdapter) recyclerView.getAdapter();
if (adapter.isPositionFooter(position)
|| adapter.getEntryAt(position) != _selectedEntry
|| !isLongPressDragEnabled()) {
dragFlags = 0;
return makeMovementFlags(0, swipeFlags);
}
return makeMovementFlags(dragFlags, swipeFlags);
return makeMovementFlags(_dragFlags, swipeFlags);
}
@Override
@ -75,7 +79,11 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
if (target.getAdapterPosition() < _adapter.getShownFavoritesCount()){
return false;
}
_adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
int firstPosition = viewHolder.getLayoutPosition();
int secondPosition = target.getAdapterPosition();
_adapter.onItemMove(firstPosition, secondPosition);
_positionChanged = true;
return true;
}
@ -92,6 +100,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
if (_positionChanged) {
_adapter.onItemDrop(viewHolder.getAdapterPosition());
_positionChanged = false;
_adapter.refresh(false);
}
}
}

View file

@ -952,7 +952,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onEntryMove(VaultEntry entry1, VaultEntry entry2) {
_vaultManager.getVault().swapEntries(entry1, entry2);
_vaultManager.getVault().moveEntry(entry1, entry2);
}
@Override

View file

@ -17,6 +17,7 @@ import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
public class AppearancePreferencesFragment extends PreferencesFragment {
private Preference _groupsPreference;
private Preference _resetUsageCountPreference;
private Preference _currentAccountNamePositionPreference;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@ -89,6 +90,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
_prefs.setCurrentViewMode(ViewMode.fromInteger(i));
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i]));
getResult().putExtra("needsRefresh", true);
overrideAccountNamePosition(ViewMode.fromInteger(i) == ViewMode.TILES);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
@ -110,9 +112,9 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
});
int currentAccountNamePosition = _prefs.getAccountNamePosition().ordinal();
Preference currentAccountNamePositionPreference = requirePreference("pref_account_name_position");
currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[currentAccountNamePosition]));
currentAccountNamePositionPreference.setOnPreferenceClickListener(preference -> {
_currentAccountNamePositionPreference = requirePreference("pref_account_name_position");
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[currentAccountNamePosition]));
_currentAccountNamePositionPreference.setOnPreferenceClickListener(preference -> {
int currentAccountNamePosition1 = _prefs.getAccountNamePosition().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(requireContext())
@ -120,7 +122,7 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
.setSingleChoiceItems(R.array.account_name_position_titles, currentAccountNamePosition1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_prefs.setAccountNamePosition(AccountNamePosition.fromInteger(i));
currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[i]));
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[i]));
getResult().putExtra("needsRefresh", true);
dialog.dismiss();
})
@ -135,5 +137,17 @@ public class AppearancePreferencesFragment extends PreferencesFragment {
getResult().putExtra("needsRefresh", true);
return true;
});
overrideAccountNamePosition(_prefs.getCurrentViewMode() == ViewMode.TILES);
}
private void overrideAccountNamePosition(boolean override) {
if (override) {
_currentAccountNamePositionPreference.setEnabled(false);
_currentAccountNamePositionPreference.setSummary(getString(R.string.pref_account_name_position_summary_override));
} else {
_currentAccountNamePositionPreference.setEnabled(true);
_currentAccountNamePositionPreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.account_name_position_titles)[_prefs.getAccountNamePosition().ordinal()]));
}
}
}

View file

@ -29,6 +29,7 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.CollectionUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.ArrayList;
@ -382,9 +383,10 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
// notify the vault first
_view.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
// update our side of things
Collections.swap(_entries, firstPosition, secondPosition);
Collections.swap(_shownEntries, firstPosition, secondPosition);
// then update our end
CollectionUtils.move(_entries, firstPosition, secondPosition);
CollectionUtils.move(_shownEntries, firstPosition, secondPosition);
notifyItemMoved(firstPosition, secondPosition);
}
@ -438,7 +440,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
}
AccountNamePosition accountNamePosition = showAccountName ? _accountNamePosition : AccountNamePosition.HIDDEN;
entryHolder.setData(entry, _codeGroupSize, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed);
entryHolder.setData(entry, _codeGroupSize, _viewMode, accountNamePosition, _showIcon, showProgress, hidden, paused, dimmed);
entryHolder.setFocused(_selectedEntries.contains(entry));
entryHolder.setShowDragHandle(isEntryDraggable(entry));
@ -467,7 +469,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
case SINGLETAP:
if (!handled) {
_view.onEntryCopy(entry);
entryHolder.animateCopyText();
entryHolder.animateCopyText(_viewMode != ViewMode.TILES);
_clickedEntry = null;
}
break;
@ -476,7 +478,7 @@ public class EntryAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
if(entry == _clickedEntry) {
_view.onEntryCopy(entry);
entryHolder.animateCopyText();
entryHolder.animateCopyText(_viewMode != ViewMode.TILES);
_clickedEntry = null;
} else {
_clickedEntry = entry;

View file

@ -16,6 +16,7 @@ import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.AccountNamePosition;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
@ -46,6 +47,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private ImageView _buttonRefresh;
private RelativeLayout _description;
private ImageView _dragHandle;
private ViewMode _viewMode;
private final ImageView _selected;
private final Handler _selectedHandler;
@ -107,11 +109,12 @@ public class EntryHolder extends RecyclerView.ViewHolder {
});
}
public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, AccountNamePosition accountNamePosition, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed) {
public void setData(VaultEntry entry, Preferences.CodeGrouping groupSize, ViewMode viewMode, AccountNamePosition accountNamePosition, boolean showIcon, boolean showProgress, boolean hidden, boolean paused, boolean dimmed) {
_entry = entry;
_hidden = hidden;
_paused = paused;
_codeGrouping = groupSize;
_viewMode = viewMode;
_accountNamePosition = accountNamePosition;
_selected.clearAnimation();
@ -129,12 +132,12 @@ public class EntryHolder extends RecyclerView.ViewHolder {
String profileIssuer = entry.getIssuer();
String profileName = entry.getName();
if (!profileIssuer.isEmpty() && !profileName.isEmpty() && accountNamePosition == AccountNamePosition.END) {
profileName = String.format(" (%s)", profileName);
if (!profileIssuer.isEmpty() && !profileName.isEmpty() && _accountNamePosition == AccountNamePosition.END) {
profileName = _viewMode.getFormattedAccountName(profileName);
}
_profileIssuer.setText(profileIssuer);
_profileName.setText(profileName);
setAccountNameLayout(accountNamePosition);
setAccountNameLayout(_accountNamePosition);
if (_hidden) {
hideCode();
@ -148,6 +151,10 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
private void setAccountNameLayout(AccountNamePosition accountNamePosition) {
if (_viewMode == ViewMode.TILES) {
return;
}
RelativeLayout.LayoutParams profileNameLayoutParams;
RelativeLayout.LayoutParams copiedLayoutParams;
switch (accountNamePosition) {
@ -367,7 +374,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
animateAlphaTo(DEFAULT_ALPHA);
}
public void animateCopyText() {
public void animateCopyText(boolean includeSlideAnimation) {
_animationHandler.removeCallbacksAndMessages(null);
Animation slideDownFadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.slide_down_fade_in);
@ -375,16 +382,25 @@ public class EntryHolder extends RecyclerView.ViewHolder {
Animation fadeOut = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_out);
Animation fadeIn = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.fade_in);
_profileCopied.startAnimation(slideDownFadeIn);
View fadeOutView = (_accountNamePosition == AccountNamePosition.BELOW) ? _profileName : _description;
if (includeSlideAnimation) {
_profileCopied.startAnimation(slideDownFadeIn);
View fadeOutView = (_accountNamePosition == AccountNamePosition.BELOW) ? _profileName : _description;
fadeOutView.startAnimation(slideDownFadeOut);
_animationHandler.postDelayed(() -> {
_profileCopied.startAnimation(fadeOut);
fadeOutView.startAnimation(fadeIn);
}, 3000);
_animationHandler.postDelayed(() -> {
_profileCopied.startAnimation(fadeOut);
fadeOutView.startAnimation(fadeIn);
}, 3000);
} else {
_profileCopied.startAnimation(fadeIn);
_profileName.startAnimation(fadeOut);
_animationHandler.postDelayed(() -> {
_profileCopied.startAnimation(fadeOut);
_profileName.startAnimation(fadeIn);
}, 3000);
}
}
private void animateAlphaTo(float alpha) {

View file

@ -19,6 +19,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -64,7 +65,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private ItemTouchHelper _touchHelper;
private RecyclerView _recyclerView;
private RecyclerView.ItemDecoration _dividerDecoration;
private RecyclerView.ItemDecoration _verticalDividerDecoration;
private RecyclerView.ItemDecoration _horizontalDividerDecoration;
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
private TotpProgressBar _progressBar;
private boolean _showProgress;
@ -122,7 +124,17 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
RecyclerViewPreloader<VaultEntry> preloader = new RecyclerViewPreloader<>(Glide.with(this), modelProvider, _preloadSizeProvider, 10);
_recyclerView.addOnScrollListener(preloader);
LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
GridLayoutManager layoutManager = new GridLayoutManager(requireContext(), 1);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (_viewMode == ViewMode.TILES && position == _adapter.getEntriesCount()) {
return 2;
}
return 1;
}
});
_recyclerView.setLayoutManager(layoutManager);
_touchCallback = new SimpleItemTouchHelperCallback(_adapter);
_touchHelper = new ItemTouchHelper(_touchCallback);
@ -222,6 +234,13 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_viewMode = mode;
updateDividerDecoration();
_adapter.setViewMode(_viewMode);
if (_viewMode == ViewMode.TILES) {
_touchCallback.setDragFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
} else {
_touchCallback.setDragFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN);
}
((GridLayoutManager)_recyclerView.getLayoutManager()).setSpanCount(mode.getColumnSpan());
}
public void startDrag(RecyclerView.ViewHolder viewHolder) {
@ -536,18 +555,28 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
private void updateDividerDecoration() {
if (_dividerDecoration != null) {
_recyclerView.removeItemDecoration(_dividerDecoration);
if (_verticalDividerDecoration != null) {
_recyclerView.removeItemDecoration(_verticalDividerDecoration);
}
if(_horizontalDividerDecoration != null) {
_recyclerView.removeItemDecoration(_horizontalDividerDecoration);
}
float height = _viewMode.getDividerHeight();
float width = _viewMode.getDividerWidth();
if (_showProgress && height == 0) {
_dividerDecoration = new CompactDividerDecoration();
_verticalDividerDecoration = new CompactDividerDecoration();
} else {
_dividerDecoration = new VerticalSpaceItemDecoration(height);
_verticalDividerDecoration = new VerticalSpaceItemDecoration(height);
}
_recyclerView.addItemDecoration(_dividerDecoration);
if (width != 0) {
_horizontalDividerDecoration = new TileSpaceItemDecoration(width, height);
_recyclerView.addItemDecoration(_horizontalDividerDecoration);
} else {
_recyclerView.addItemDecoration(_verticalDividerDecoration);
}
}
private void updateEmptyState() {
@ -653,6 +682,30 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
}
private class TileSpaceItemDecoration extends RecyclerView.ItemDecoration {
private final int _width;
private final int _height;
private TileSpaceItemDecoration(float width, float height) {
// convert dp to pixels
_width = MetricsHelper.convertDpToPixels(requireContext(), width);
_height = MetricsHelper.convertDpToPixels(requireContext(), height);
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
int adapterPosition = parent.getChildAdapterPosition(view);
if (adapterPosition == NO_POSITION) {
return;
}
outRect.left = _width;
outRect.right = _width;
outRect.top = _height;
outRect.bottom = _height;
}
}
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<VaultEntry> {
@NonNull
@Override

View file

@ -0,0 +1,15 @@
package com.beemdevelopment.aegis.util;
import java.util.List;
public class CollectionUtils {
public static <T> void move(List<T> list, int fromIndex, int toIndex) {
if (fromIndex == toIndex) {
return;
}
T item = list.remove(fromIndex);
list.add(toIndex, item);
}
}

View file

@ -64,34 +64,31 @@ public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializa
}
/**
* Swaps the position of value1 and value2 in the internal map. This operation is
* quite expensive because it has to reallocate the entire underlying LinkedHashMap.
* @throws AssertionError if no map value exists with the UUID of the given entries.
*/
public void swap(T value1, T value2) {
boolean found1 = false;
boolean found2 = false;
List<T> values = new ArrayList<>();
* Moves value1 to the position of value2.
*/
public void move(T value1, T value2) {
List<T> values = new ArrayList<>(_map.values());
for (T value : _map.values()) {
int vi1 = -1, vi2 = -1;
for (int i = 0; i < values.size(); i++) {
T value = values.get(i);
if (value.getUUID().equals(value1.getUUID())) {
values.add(value2);
found1 = true;
} else if (value.getUUID().equals(value2.getUUID())) {
values.add(value1);
found2 = true;
} else {
values.add(value);
vi1 = i;
}
if (value.getUUID().equals(value2.getUUID())) {
vi2 = i;
}
}
if (!found1) {
if (vi1 < 0) {
throw new AssertionError(String.format("No value found for value1 with UUID: %s", value1.getUUID()));
}
if (!found2) {
if (vi2 < 0) {
throw new AssertionError(String.format("No value found for value2 with UUID: %s", value2.getUUID()));
}
CollectionUtils.move(values, vi1, vi2);
_map.clear();
for (T value : values) {
_map.put(value.getUUID(), value);

View file

@ -239,8 +239,11 @@ public class VaultRepository {
return _vault.getEntries().replace(entry);
}
public void swapEntries(VaultEntry entry1, VaultEntry entry2) {
_vault.getEntries().swap(entry1, entry2);
/**
* Moves entry1 to the position of entry2.
*/
public void moveEntry(VaultEntry entry1, VaultEntry entry2) {
_vault.getEntries().move(entry1, entry2);
}
public boolean isEntryDuplicate(VaultEntry entry) {