mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 10:51:27 +01:00
Add initial Mentions UI/UX for picker and compose edit.
This commit is contained in:
committed by
Greyson Parrelli
parent
8e45a546c9
commit
1ab61beeb9
@@ -4,7 +4,9 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
public final class DrawableUtil {
|
||||
|
||||
@@ -19,4 +21,13 @@ public final class DrawableUtil {
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Drawable} that safely wraps and tints the provided drawable.
|
||||
*/
|
||||
public static @NonNull Drawable tint(@NonNull Drawable drawable, @ColorInt int tint) {
|
||||
Drawable tinted = DrawableCompat.wrap(drawable).mutate();
|
||||
DrawableCompat.setTint(tinted, tint);
|
||||
return tinted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ public final class FeatureFlags {
|
||||
private static final String CDS = "android.cds";
|
||||
private static final String RECIPIENT_TRUST = "android.recipientTrust";
|
||||
private static final String INTERNAL_USER = "android.internalUser";
|
||||
private static final String MENTIONS = "android.mentions";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -71,7 +72,8 @@ public final class FeatureFlags {
|
||||
GROUPS_V2_CREATE,
|
||||
GROUPS_V2_CAPACITY,
|
||||
RECIPIENT_TRUST,
|
||||
INTERNAL_USER
|
||||
INTERNAL_USER,
|
||||
MENTIONS
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -221,6 +223,11 @@ public final class FeatureFlags {
|
||||
return getBoolean(RECIPIENT_TRUST, false);
|
||||
}
|
||||
|
||||
/** Whether or not we allow mentions send support in groups. */
|
||||
public static boolean mentions() {
|
||||
return getBoolean(MENTIONS, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.text.Layout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Utility functions for dealing with {@link Layout}.
|
||||
*
|
||||
* Ported and modified from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin
|
||||
*/
|
||||
public class LayoutUtil {
|
||||
private static final float DEFAULT_LINE_SPACING_EXTRA = 0f;
|
||||
|
||||
private static final float DEFAULT_LINE_SPACING_MULTIPLIER = 1f;
|
||||
|
||||
public static int getLineHeight(@NonNull Layout layout, int line) {
|
||||
return layout.getLineTop(line + 1) - layout.getLineTop(line);
|
||||
}
|
||||
|
||||
public static int getLineTopWithoutPadding(@NonNull Layout layout, int line) {
|
||||
int lineTop = layout.getLineTop(line);
|
||||
if (line == 0) {
|
||||
lineTop -= layout.getTopPadding();
|
||||
}
|
||||
return lineTop;
|
||||
}
|
||||
|
||||
public static int getLineBottomWithoutPadding(@NonNull Layout layout, int line) {
|
||||
int lineBottom = getLineBottomWithoutSpacing(layout, line);
|
||||
if (line == layout.getLineCount() - 1) {
|
||||
lineBottom -= layout.getBottomPadding();
|
||||
}
|
||||
return lineBottom;
|
||||
}
|
||||
|
||||
public static int getLineBottomWithoutSpacing(@NonNull Layout layout, int line) {
|
||||
int lineBottom = layout.getLineBottom(line);
|
||||
boolean isLastLine = line == layout.getLineCount() - 1;
|
||||
float lineSpacingExtra = layout.getSpacingAdd();
|
||||
float lineSpacingMultiplier = layout.getSpacingMultiplier();
|
||||
boolean hasLineSpacing = lineSpacingExtra != DEFAULT_LINE_SPACING_EXTRA || lineSpacingMultiplier != DEFAULT_LINE_SPACING_MULTIPLIER;
|
||||
|
||||
int lineBottomWithoutSpacing;
|
||||
if (!hasLineSpacing || isLastLine) {
|
||||
lineBottomWithoutSpacing = lineBottom;
|
||||
} else {
|
||||
float extra;
|
||||
if (Float.compare(lineSpacingMultiplier, DEFAULT_LINE_SPACING_MULTIPLIER) != 0) {
|
||||
int lineHeight = getLineHeight(layout, line);
|
||||
extra = lineHeight - (lineHeight - lineSpacingExtra) / lineSpacingMultiplier;
|
||||
} else {
|
||||
extra = lineSpacingExtra;
|
||||
}
|
||||
|
||||
lineBottomWithoutSpacing = (int) (lineBottom - extra);
|
||||
}
|
||||
|
||||
return lineBottomWithoutSpacing;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A reusable and composable {@link androidx.recyclerview.widget.RecyclerView.Adapter} built on-top of {@link ListAdapter} to
|
||||
* provide async item diffing support.
|
||||
* <p></p>
|
||||
* The adapter makes use of mapping a model class to view holder factory at runtime via one of the {@link #registerFactory(Class, Factory)}
|
||||
* methods. The factory creates a view holder specifically designed to handle the paired model type. This allows the view holder concretely
|
||||
* deal with the model type it cares about. Due to the enforcement of matching generics during factory registration we can safely ignore or
|
||||
* override compiler typing recommendations when binding and diffing.
|
||||
* <p></p>
|
||||
* General pattern for implementation:
|
||||
* <ol>
|
||||
* <li>Create {@link MappingModel}s for the items in the list. These encapsulate data massaging methods for views to use and the diff logic.</li>
|
||||
* <li>Create {@link MappingViewHolder}s for each item type in the list and their corresponding {@link Factory}.</li>
|
||||
* <li>Create an instance or subclass of {@link MappingAdapter} and register the mapping of model type to view holder factory for that model type.</li>
|
||||
* </ol>
|
||||
* Event listeners, click or otherwise, are handled at the view holder level and should be passed into the appropriate view holder factories. This
|
||||
* pattern mimics how we pass data into view models via factories.
|
||||
* <p></p>
|
||||
* NOTE: There can only be on factory registered per model type. Registering two for the same type will result in the last one being used. However, the
|
||||
* same factory can be registered multiple times for multiple model types (if the model type class hierarchy supports it).
|
||||
*/
|
||||
public class MappingAdapter extends ListAdapter<MappingModel<?>, MappingViewHolder<?>> {
|
||||
|
||||
private final Map<Integer, Factory<?>> factories;
|
||||
private final Map<Class<?>, Integer> itemTypes;
|
||||
private int typeCount;
|
||||
|
||||
public MappingAdapter() {
|
||||
super(new MappingDiffCallback());
|
||||
|
||||
factories = new HashMap<>();
|
||||
itemTypes = new HashMap<>();
|
||||
typeCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@NonNull MappingViewHolder<?> holder) {
|
||||
super.onViewAttachedToWindow(holder);
|
||||
holder.onAttachedToWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@NonNull MappingViewHolder<?> holder) {
|
||||
super.onViewDetachedFromWindow(holder);
|
||||
holder.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
public <T extends MappingModel<T>> void registerFactory(Class<T> clazz, Factory<T> factory) {
|
||||
int type = typeCount++;
|
||||
factories.put(type, factory);
|
||||
itemTypes.put(clazz, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
Integer type = itemTypes.get(getItem(position).getClass());
|
||||
if (type != null) {
|
||||
return type;
|
||||
}
|
||||
throw new AssertionError("No view holder factory for type: " + getItem(position).getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MappingViewHolder<?> onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return Objects.requireNonNull(factories.get(viewType)).createViewHolder(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull MappingViewHolder holder, int position) {
|
||||
//noinspection unchecked
|
||||
holder.bind(getItem(position));
|
||||
}
|
||||
|
||||
private static class MappingDiffCallback extends DiffUtil.ItemCallback<MappingModel<?>> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) {
|
||||
if (oldItem.getClass() == newItem.getClass()) {
|
||||
//noinspection unchecked
|
||||
return oldItem.areItemsTheSame(newItem);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressLint("DiffUtilEquals")
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) {
|
||||
if (oldItem.getClass() == newItem.getClass()) {
|
||||
//noinspection unchecked
|
||||
return oldItem.areContentsTheSame(newItem);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Factory<T extends MappingModel<T>> {
|
||||
@NonNull MappingViewHolder<T> createViewHolder(ViewGroup parent);
|
||||
}
|
||||
|
||||
public static class LayoutFactory<T extends MappingModel<T>> implements Factory<T> {
|
||||
private Function<View, MappingViewHolder<T>> creator;
|
||||
private final int layout;
|
||||
|
||||
public LayoutFactory(Function<View, MappingViewHolder<T>> creator, @LayoutRes int layout) {
|
||||
this.creator = creator;
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MappingViewHolder<T> createViewHolder(ViewGroup parent) {
|
||||
return creator.apply(LayoutInflater.from(parent.getContext()).inflate(layout, parent, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface MappingModel<T> {
|
||||
boolean areItemsTheSame(@NonNull T newItem);
|
||||
boolean areContentsTheSame(@NonNull T newItem);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LifecycleRegistry;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public abstract class MappingViewHolder<Model extends MappingModel<Model>> extends LifecycleViewHolder implements LifecycleOwner {
|
||||
|
||||
protected final Context context;
|
||||
|
||||
public MappingViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
context = itemView.getContext();
|
||||
}
|
||||
|
||||
public <T extends View> T findViewById(@IdRes int id) {
|
||||
return itemView.findViewById(id);
|
||||
}
|
||||
|
||||
public abstract void bind(@NonNull Model model);
|
||||
}
|
||||
Reference in New Issue
Block a user