Fix chip jank and other groups v2 ux issues.

fork-5.53.8
Alex Hart 2020-05-27 12:19:20 -03:00 zatwierdzone przez Greyson Parrelli
rodzic 00996f0d7a
commit 903c3989b9
10 zmienionych plików z 120 dodań i 151 usunięć

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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<? super Drawable> transition) {
setChipIcon(resource);
if (onAvatarSet != null) {
onAvatarSet.run();
}
}
@Override

Wyświetl plik

@ -35,8 +35,7 @@ public abstract class GroupMemberEntry {
public final static class NewGroupCandidate extends GroupMemberEntry {
private final DefaultValueLiveData<Boolean> 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<Boolean> isSelected() {
return isSelected;
}
public void setSelected(boolean isSelected) {
this.isSelected.postValue(isSelected);
}
@Override
boolean sameId(@NonNull GroupMemberEntry newItem) {
if (getClass() != newItem.getClass()) return false;

Wyświetl plik

@ -246,9 +246,6 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
bindRecipient(newGroupCandidate.getMember());
bindRecipientClick(newGroupCandidate.getMember());
itemView.setSelected(false);
newGroupCandidate.isSelected().observe(this, itemView::setSelected);
int smsWarningVisibility = newGroupCandidate.getMember().isRegistered() ? View.GONE : View.VISIBLE;
smsContact.setVisibility(smsWarningVisibility);

Wyświetl plik

@ -20,6 +20,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
@ -58,38 +59,6 @@ public class AddGroupDetailsFragment extends Fragment {
private Drawable avatarPlaceholder;
private EditText name;
private Toolbar toolbar;
private ActionMode actionMode;
private ActionMode.Callback recipientActionModeCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.add_group_details_fragment_context_menu, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == R.id.action_delete) {
viewModel.deleteSelected();
mode.finish();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
viewModel.clearSelected();
}
};
@Override
public void onAttach(@NonNull Context context) {
@ -142,7 +111,6 @@ public class AddGroupDetailsFragment extends Fragment {
avatar.setOnClickListener(v -> 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) {

Wyświetl plik

@ -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<List<GroupMemberEntry.NewGroupCandidate>> members;
private final DefaultValueLiveData<Set<RecipientId>> selected = new DefaultValueLiveData<>(new HashSet<>());
private final DefaultValueLiveData<Set<RecipientId>> deleted = new DefaultValueLiveData<>(new HashSet<>());
private final MutableLiveData<String> name = new MutableLiveData<>("");
private final MutableLiveData<byte[]> avatar = new MutableLiveData<>();
@ -37,17 +37,14 @@ public final class AddGroupDetailsViewModel extends ViewModel {
private final LiveData<Boolean> 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<List<GroupMemberEntry.NewGroupCandidate>> initialMembers = new MutableLiveData<>();
LiveData<List<GroupMemberEntry.NewGroupCandidate>> membersWithoutDeleted = LiveDataUtil.combineLatest(initialMembers,
deleted,
AddGroupDetailsViewModel::filterDeletedMembers);
MutableLiveData<List<GroupMemberEntry.NewGroupCandidate>> 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<RecipientId> 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<RecipientId> selected = this.selected.getValue();
void delete(@NonNull RecipientId recipientId) {
Set<RecipientId> 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<GroupMemberEntry.NewGroupCandidate> updateSelectedMembers(@NonNull List<GroupMemberEntry.NewGroupCandidate> members, @NonNull Set<RecipientId> selected) {
for (GroupMemberEntry.NewGroupCandidate member : members) {
member.setSelected(selected.contains(member.getMember().getId()));
}
return members;
}
private boolean isAnyForcedSms(@NonNull List<GroupMemberEntry.NewGroupCandidate> members) {
return Stream.of(members)
.anyMatch(member -> !member.getMember().isRegistered());

Wyświetl plik

@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -112,20 +113,22 @@
<HorizontalScrollView
android:id="@+id/chipGroupScrollContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="56dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingBottom="8dp"
android:scrollbars="none"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="56dp"
android:layout_marginEnd="96dp"
android:animateLayoutChanges="true"
app:singleLine="true">

Wyświetl plik

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_delete"
android:icon="?attr/menu_trash_icon"
android:title="@string/AddGroupDetailsFragment__remove"
app:showAsAction="always" />
</menu>

Wyświetl plik

@ -500,6 +500,7 @@
<string name="AddGroupDetailsFragment__youve_selected_a_contact_that_doesnt">You\'ve selected a contact that doesn\'t support Signal groups, so this group will be MMS.</string>
<string name="AddGroupDetailsFragment__remove">Remove</string>
<string name="AddGroupDetailsFragment__sms_contact">SMS contact</string>
<string name="AddGroupDetailsFragment__remove_s_from_this_group">Remove %1$s from this group?</string>
<!-- ManageGroupActivity -->
<string name="ManageGroupActivity_disappearing_messages">Disappearing messages</string>