From 903c3989b951e7f53a94080e2d18f384115eb3f1 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 27 May 2020 12:19:20 -0300 Subject: [PATCH] Fix chip jank and other groups v2 ux issues. --- .../securesms/ClearProfileAvatarActivity.java | 38 ++++++--- .../ContactSelectionListFragment.java | 79 +++++++++++++------ .../securesms/contacts/ContactChip.java | 13 ++- .../securesms/groups/ui/GroupMemberEntry.java | 11 +-- .../groups/ui/GroupMemberListAdapter.java | 3 - .../details/AddGroupDetailsFragment.java | 66 +++------------- .../details/AddGroupDetailsViewModel.java | 42 ++-------- .../contact_selection_list_fragment.xml | 9 ++- ...dd_group_details_fragment_context_menu.xml | 9 --- app/src/main/res/values/strings.xml | 1 + 10 files changed, 120 insertions(+), 151 deletions(-) delete mode 100644 app/src/main/res/menu/add_group_details_fragment_context_menu.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/ClearProfileAvatarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ClearProfileAvatarActivity.java index ee783c08fb..11d2d3c62d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ClearProfileAvatarActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ClearProfileAvatarActivity.java @@ -3,12 +3,21 @@ package org.thoughtcrime.securesms; import android.app.Activity; import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; +import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.ThemeUtil; + public class ClearProfileAvatarActivity extends Activity { private static final String ARG_TITLE = "arg_title"; + private final DynamicTheme theme = new DynamicNoActionBarTheme(); + public static Intent createForUserProfilePhoto() { return new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO"); } @@ -19,23 +28,32 @@ public class ClearProfileAvatarActivity extends Activity { return intent; } + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + theme.onCreate(this); + } + @Override public void onResume() { super.onResume(); + theme.onResume(this); + int titleId = getIntent().getIntExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_profile_photo); new AlertDialog.Builder(this) - .setTitle(titleId) - .setNegativeButton(android.R.string.cancel, (dialog, which) -> finish()) - .setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> { - Intent result = new Intent(); - result.putExtra("delete", true); - setResult(Activity.RESULT_OK, result); - finish(); - }) - .setOnCancelListener(dialog -> finish()) - .show(); + .setMessage(titleId) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> finish()) + .setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> { + Intent result = new Intent(); + result.putExtra("delete", true); + setResult(Activity.RESULT_OK, result); + finish(); + }) + .setOnCancelListener(dialog -> finish()) + .show(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 47c4c4ccb3..6f521ebd99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -18,6 +18,7 @@ package org.thoughtcrime.securesms; import android.Manifest; +import android.animation.LayoutTransition; import android.annotation.SuppressLint; import android.content.Context; import android.database.Cursor; @@ -36,6 +37,8 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintSet; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.loader.app.LoaderManager; @@ -43,6 +46,8 @@ import androidx.loader.content.Loader; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.transition.AutoTransition; +import androidx.transition.TransitionManager; import com.google.android.material.chip.ChipGroup; import com.pnikosis.materialishprogress.ProgressWheel; @@ -89,13 +94,15 @@ public final class ContactSelectionListFragment extends Fragment @SuppressWarnings("unused") private static final String TAG = Log.tag(ContactSelectionListFragment.class); + private static final int CHIP_GROUP_EMPTY_COUNT = 1; + private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150; + public static final String DISPLAY_MODE = "display_mode"; public static final String MULTI_SELECT = "multi_select"; public static final String REFRESHABLE = "refreshable"; public static final String RECENTS = "recents"; - private final Debouncer scrollDebounce = new Debouncer(100); - + private ConstraintLayout constraintLayout; private TextView emptyText; private OnContactSelectedListener onContactSelectedListener; private SwipeRefreshLayout swipeRefresh; @@ -178,13 +185,12 @@ public final class ContactSelectionListFragment extends Fragment showContactsProgress = view.findViewById(R.id.progress); chipGroup = view.findViewById(R.id.chipGroup); chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer); + constraintLayout = view.findViewById(R.id.container); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true)); - autoScrollOnNewItem(); - return view; } @@ -445,7 +451,7 @@ public final class ContactSelectionListFragment extends Fragment cursorRecyclerViewAdapter.addSelectedContact(selectedContact); listItem.setChecked(true); if (isMulti() && FeatureFlags.newGroupUI()) { - chipGroup.addView(newChipForContact(listItem, selectedContact)); + addChipForContact(listItem, selectedContact); } } @@ -462,28 +468,65 @@ public final class ContactSelectionListFragment extends Fragment chipGroup.removeView(v); } } + + if (chipGroup.getChildCount() == CHIP_GROUP_EMPTY_COUNT) { + setChipGroupVisibility(ConstraintSet.GONE); + } } - private View newChipForContact(@NonNull ContactSelectionListItem contact, @NonNull SelectedContact selectedContact) { + private void addChipForContact(@NonNull ContactSelectionListItem contact, @NonNull SelectedContact selectedContact) { final ContactChip chip = new ContactChip(requireContext()); + + if (chipGroup.getChildCount() == CHIP_GROUP_EMPTY_COUNT) { + setChipGroupVisibility(ConstraintSet.VISIBLE); + } + chip.setText(contact.getChipName()); chip.setContact(selectedContact); + chip.setCloseIconVisible(true); + chip.setOnCloseIconClickListener(view -> markContactUnselected(selectedContact, contact)); + + chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() { + @Override + public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { + } + + @Override + public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { + if (view == chip && transitionType == LayoutTransition.APPEARING) { + chipGroup.getLayoutTransition().removeTransitionListener(this); + registerChipRecipientObserver(chip, contact.getRecipient()); + chipGroup.post(ContactSelectionListFragment.this::smoothScrollChipsToEnd); + } + } + }); LiveRecipient recipient = contact.getRecipient(); + if (recipient != null) { + chip.setAvatar(glideRequests, recipient.get(), () -> chipGroup.addView(chip)); + } else { + chipGroup.addView(chip); + } + } + + private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) { if (recipient != null) { recipient.observe(getViewLifecycleOwner(), resolved -> { - chip.setAvatar(glideRequests, resolved); + if (chip.isAttachedToWindow()) { + chip.setAvatar(glideRequests, resolved, null); chip.setText(resolved.getShortDisplayName(chip.getContext())); } - ); + }); } + } - chip.setCloseIconVisible(true); - chip.setOnCloseIconClickListener(view -> { - markContactUnselected(selectedContact, contact); - chipGroup.removeView(chip); - }); - return chip; + private void setChipGroupVisibility(int visibility) { + TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition().setDuration(CHIP_GROUP_REVEAL_DURATION_MS)); + + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.clone(constraintLayout); + constraintSet.setVisibility(R.id.chipGroupScrollContainer, visibility); + constraintSet.applyTo(constraintLayout); } public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) { @@ -494,14 +537,6 @@ public final class ContactSelectionListFragment extends Fragment this.swipeRefresh.setOnRefreshListener(onRefreshListener); } - private void autoScrollOnNewItem() { - chipGroup.addOnLayoutChangeListener((view1, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - if (right > oldRight) { - scrollDebounce.publish(this::smoothScrollChipsToEnd); - } - }); - } - private void smoothScrollChipsToEnd() { int x = chipGroupScrollContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? chipGroup.getWidth() : 0; chipGroupScrollContainer.smoothScrollTo(x, 0); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChip.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChip.java index d7708fa05b..7a5b36d274 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChip.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactChip.java @@ -44,17 +44,21 @@ public final class ContactChip extends Chip { return contact; } - public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient) { + public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, @Nullable Runnable onAvatarSet) { if (recipient != null) { requestManager.clear(this); - Drawable fallbackContactPhotoDrawable = recipient.getFallbackContactPhotoDrawable(getContext(), false); + Drawable fallbackContactPhotoDrawable = new HalfScaleDrawable(recipient.getFallbackContactPhotoDrawable(getContext(), false)); ContactPhoto contactPhoto = recipient.getContactPhoto(); if (contactPhoto == null) { - setChipIcon(new HalfScaleDrawable(fallbackContactPhotoDrawable)); + setChipIcon(fallbackContactPhotoDrawable); + if (onAvatarSet != null) { + onAvatarSet.run(); + } } else { requestManager.load(contactPhoto) + .placeholder(fallbackContactPhotoDrawable) .fallback(fallbackContactPhotoDrawable) .error(fallbackContactPhotoDrawable) .diskCacheStrategy(DiskCacheStrategy.ALL) @@ -63,6 +67,9 @@ public final class ContactChip extends Chip { @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { setChipIcon(resource); + if (onAvatarSet != null) { + onAvatarSet.run(); + } } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java index 24df4655a5..0c72a591a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java @@ -35,8 +35,7 @@ public abstract class GroupMemberEntry { public final static class NewGroupCandidate extends GroupMemberEntry { - private final DefaultValueLiveData isSelected = new DefaultValueLiveData<>(false); - private final Recipient member; + private final Recipient member; public NewGroupCandidate(@NonNull Recipient member) { this.member = member; @@ -46,14 +45,6 @@ public abstract class GroupMemberEntry { return member; } - public @NonNull LiveData isSelected() { - return isSelected; - } - - public void setSelected(boolean isSelected) { - this.isSelected.postValue(isSelected); - } - @Override boolean sameId(@NonNull GroupMemberEntry newItem) { if (getClass() != newItem.getClass()) return false; diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java index 3a0fa53504..94b31a5775 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java @@ -246,9 +246,6 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter AvatarSelectionBottomSheetDialogFragment.create(viewModel.hasAvatar(), true, REQUEST_CODE_AVATAR, true) .show(getChildFragmentManager(), "BOTTOM")); - members.setRecipientLongClickListener(this::handleRecipientLongClick); members.setRecipientClickListener(this::handleRecipientClick); name.addTextChangedListener(new AfterTextChanged(editable -> viewModel.setName(editable.toString()))); toolbar.setNavigationOnClickListener(unused -> callback.onNavigationButtonPressed()); @@ -219,29 +187,15 @@ public class AddGroupDetailsFragment extends Fragment { } private void handleRecipientClick(@NonNull Recipient recipient) { - if (actionMode == null) { - return; - } - - int size = viewModel.toggleSelected(recipient); - if (size == 0) { - actionMode.finish(); - } - } - - private boolean handleRecipientLongClick(@NonNull Recipient recipient) { - if (actionMode != null) { - return false; - } - - actionMode = toolbar.startActionMode(recipientActionModeCallback); - - if (actionMode != null) { - viewModel.toggleSelected(recipient); - return true; - } - - return false; + new AlertDialog.Builder(requireContext()) + .setMessage(getString(R.string.AddGroupDetailsFragment__remove_s_from_this_group, recipient.getDisplayName(requireContext()))) + .setCancelable(true) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()) + .setPositiveButton(R.string.AddGroupDetailsFragment__remove, (dialog, which) -> { + viewModel.delete(recipient.getId()); + dialog.dismiss(); + }) + .show(); } private void handleGroupCreateResult(@NonNull GroupCreateResult groupCreateResult) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java index 0a97ea33f1..2413641512 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.DefaultValueLiveData; import org.thoughtcrime.securesms.util.SingleLiveEvent; +import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import java.util.HashSet; @@ -28,7 +29,6 @@ import java.util.Set; public final class AddGroupDetailsViewModel extends ViewModel { private final LiveData> members; - private final DefaultValueLiveData> selected = new DefaultValueLiveData<>(new HashSet<>()); private final DefaultValueLiveData> deleted = new DefaultValueLiveData<>(new HashSet<>()); private final MutableLiveData name = new MutableLiveData<>(""); private final MutableLiveData avatar = new MutableLiveData<>(); @@ -37,17 +37,14 @@ public final class AddGroupDetailsViewModel extends ViewModel { private final LiveData canSubmitForm = Transformations.map(name, name -> !TextUtils.isEmpty(name)); private final AddGroupDetailsRepository repository; - AddGroupDetailsViewModel(@NonNull RecipientId[] recipientIds, - @NonNull AddGroupDetailsRepository repository) + private AddGroupDetailsViewModel(@NonNull RecipientId[] recipientIds, + @NonNull AddGroupDetailsRepository repository) { this.repository = repository; - MutableLiveData> initialMembers = new MutableLiveData<>(); - LiveData> membersWithoutDeleted = LiveDataUtil.combineLatest(initialMembers, - deleted, - AddGroupDetailsViewModel::filterDeletedMembers); + MutableLiveData> initialMembers = new MutableLiveData<>(); - members = LiveDataUtil.combineLatest(membersWithoutDeleted, selected, AddGroupDetailsViewModel::updateSelectedMembers); + members = LiveDataUtil.combineLatest(initialMembers, deleted, AddGroupDetailsViewModel::filterDeletedMembers); isMms = Transformations.map(members, this::isAnyForcedSms); repository.resolveMembers(recipientIds, initialMembers::postValue); @@ -85,27 +82,10 @@ public final class AddGroupDetailsViewModel extends ViewModel { this.name.setValue(name); } - int toggleSelected(@NonNull Recipient recipient) { - Set selected = this.selected.getValue(); - - if (!selected.add(recipient.getId())) { - selected.remove(recipient.getId()); - } - - this.selected.setValue(selected); - - return selected.size(); - } - - void clearSelected() { - this.selected.setValue(new HashSet<>()); - } - - void deleteSelected() { - Set selected = this.selected.getValue(); + void delete(@NonNull RecipientId recipientId) { Set deleted = this.deleted.getValue(); - deleted.addAll(selected); + deleted.add(recipientId); this.deleted.setValue(deleted); } @@ -139,14 +119,6 @@ public final class AddGroupDetailsViewModel extends ViewModel { .toList(); } - private static @NonNull List updateSelectedMembers(@NonNull List members, @NonNull Set selected) { - for (GroupMemberEntry.NewGroupCandidate member : members) { - member.setSelected(selected.contains(member.getMember().getId())); - } - - return members; - } - private boolean isAnyForcedSms(@NonNull List members) { return Stream.of(members) .anyMatch(member -> !member.getMember().isRegistered()); diff --git a/app/src/main/res/layout/contact_selection_list_fragment.xml b/app/src/main/res/layout/contact_selection_list_fragment.xml index e7d6bbbb0f..8809ec68b9 100644 --- a/app/src/main/res/layout/contact_selection_list_fragment.xml +++ b/app/src/main/res/layout/contact_selection_list_fragment.xml @@ -2,6 +2,7 @@ @@ -112,20 +113,22 @@ + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible"> diff --git a/app/src/main/res/menu/add_group_details_fragment_context_menu.xml b/app/src/main/res/menu/add_group_details_fragment_context_menu.xml deleted file mode 100644 index 8156129fc0..0000000000 --- a/app/src/main/res/menu/add_group_details_fragment_context_menu.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4dd6fa6030..1967494029 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -500,6 +500,7 @@ You\'ve selected a contact that doesn\'t support Signal groups, so this group will be MMS. Remove SMS contact + Remove %1$s from this group? Disappearing messages