Add sticker support.

No sticker packs are available for use yet, but we now have the
latent ability to send and receive.
This commit is contained in:
Greyson Parrelli
2019-04-17 10:21:30 -04:00
parent d5fffb0132
commit 2a644437fb
447 changed files with 8782 additions and 1132 deletions

View File

@@ -3,9 +3,6 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -32,6 +29,7 @@ public class ConversationItemThumbnail extends FrameLayout {
private ConversationItemFooter footer;
private CornerMask cornerMask;
private Outliner outliner;
private boolean borderless;
public ConversationItemThumbnail(Context context) {
super(context);
@@ -75,10 +73,12 @@ public class ConversationItemThumbnail extends FrameLayout {
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
cornerMask.mask(canvas);
if (!borderless) {
cornerMask.mask(canvas);
if (album.getVisibility() != VISIBLE) {
outliner.draw(canvas);
if (album.getVisibility() != VISIBLE) {
outliner.draw(canvas);
}
}
}
@@ -110,6 +110,10 @@ public class ConversationItemThumbnail extends FrameLayout {
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
}
public void setBorderless(boolean borderless) {
this.borderless = borderless;
}
public ConversationItemFooter getFooter() {
return footer;
}

View File

@@ -9,6 +9,8 @@ import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
@@ -23,10 +25,14 @@ import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.SlideDeck;
@@ -39,21 +45,24 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class InputPanel extends LinearLayout
implements MicrophoneRecorderView.Listener,
KeyboardAwareLinearLayout.OnKeyboardShownListener,
EmojiDrawer.EmojiEventListener
EmojiKeyboardProvider.EmojiEventListener,
ConversationStickerSuggestionAdapter.EventListener
{
private static final String TAG = InputPanel.class.getSimpleName();
private static final int FADE_TIME = 150;
private RecyclerView stickerSuggestion;
private QuoteView quoteView;
private LinkPreviewView linkPreview;
private EmojiToggle emojiToggle;
private EmojiToggle mediaKeyboard;
private ComposeText composeText;
private View quickCameraToggle;
private View quickAudioToggle;
@@ -66,7 +75,9 @@ public class InputPanel extends LinearLayout
private RecordTime recordTime;
private @Nullable Listener listener;
private boolean emojiVisible;
private boolean emojiVisible;
private ConversationStickerSuggestionAdapter stickerSuggestionAdapter;
public InputPanel(Context context) {
super(context);
@@ -87,9 +98,10 @@ public class InputPanel extends LinearLayout
View quoteDismiss = findViewById(R.id.quote_dismiss);
this.stickerSuggestion = findViewById(R.id.input_panel_sticker_suggestion);
this.quoteView = findViewById(R.id.quote_view);
this.linkPreview = findViewById(R.id.link_preview);
this.emojiToggle = findViewById(R.id.emoji_toggle);
this.mediaKeyboard = findViewById(R.id.emoji_toggle);
this.composeText = findViewById(R.id.embedded_text_editor);
this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
this.quickAudioToggle = findViewById(R.id.quick_audio_toggle);
@@ -107,10 +119,10 @@ public class InputPanel extends LinearLayout
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction());
if (TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
emojiToggle.setVisibility(View.GONE);
mediaKeyboard.setVisibility(View.GONE);
emojiVisible = false;
} else {
emojiToggle.setVisibility(View.VISIBLE);
mediaKeyboard.setVisibility(View.VISIBLE);
emojiVisible = true;
}
@@ -121,12 +133,17 @@ public class InputPanel extends LinearLayout
listener.onLinkPreviewCanceled();
}
});
stickerSuggestionAdapter = new ConversationStickerSuggestionAdapter(GlideApp.with(this), this);
stickerSuggestion.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
stickerSuggestion.setAdapter(stickerSuggestionAdapter);
}
public void setListener(final @NonNull Listener listener) {
this.listener = listener;
emojiToggle.setOnClickListener(v -> listener.onEmojiToggle());
mediaKeyboard.setOnClickListener(v -> listener.onEmojiToggle());
}
public void setMediaListener(@NonNull MediaListener listener) {
@@ -179,8 +196,26 @@ public class InputPanel extends LinearLayout
this.linkPreview.setCorners(cornerRadius, cornerRadius);
}
public void setEmojiDrawer(@NonNull EmojiDrawer emojiDrawer) {
emojiToggle.attach(emojiDrawer);
public void setMediaKeyboard(@NonNull MediaKeyboard mediaKeyboard) {
this.mediaKeyboard.attach(mediaKeyboard);
}
public void setStickerSuggestions(@NonNull List<StickerRecord> stickers) {
stickerSuggestion.setVisibility(stickers.isEmpty() ? View.GONE : View.VISIBLE);
stickerSuggestionAdapter.setStickers(stickers);
}
public void showMediaKeyboardToggle(boolean show) {
emojiVisible = show;
mediaKeyboard.setVisibility(show ? View.VISIBLE : GONE);
}
public void setMediaKeyboardToggleMode(boolean isSticker) {
mediaKeyboard.setStickerMode(isSticker);
}
public View getMediaKeyboardToggleAnchorView() {
return mediaKeyboard;
}
@Override
@@ -194,7 +229,7 @@ public class InputPanel extends LinearLayout
recordTime.display();
slideToCancel.display();
if (emojiVisible) ViewUtil.fadeOut(emojiToggle, FADE_TIME, View.INVISIBLE);
if (emojiVisible) ViewUtil.fadeOut(mediaKeyboard, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(composeText, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickCameraToggle, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickAudioToggle, FADE_TIME, View.INVISIBLE);
@@ -250,7 +285,7 @@ public class InputPanel extends LinearLayout
public void setEnabled(boolean enabled) {
composeText.setEnabled(enabled);
emojiToggle.setEnabled(enabled);
mediaKeyboard.setEnabled(enabled);
quickAudioToggle.setEnabled(enabled);
quickCameraToggle.setEnabled(enabled);
}
@@ -264,7 +299,7 @@ public class InputPanel extends LinearLayout
future.addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void result) {
if (emojiVisible) ViewUtil.fadeIn(emojiToggle, FADE_TIME);
if (emojiVisible) ViewUtil.fadeIn(mediaKeyboard, FADE_TIME);
ViewUtil.fadeIn(composeText, FADE_TIME);
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
@@ -277,7 +312,7 @@ public class InputPanel extends LinearLayout
@Override
public void onKeyboardShown() {
emojiToggle.setToEmoji();
mediaKeyboard.setToMedia();
}
@Override
@@ -290,6 +325,13 @@ public class InputPanel extends LinearLayout
composeText.insertEmoji(emoji);
}
@Override
public void onStickerSuggestionClicked(@NonNull StickerRecord sticker) {
if (listener != null) {
listener.onStickerSuggestionSelected(sticker);
}
}
private int readDimen(@DimenRes int dimenRes) {
return getResources().getDimensionPixelSize(dimenRes);
}
@@ -310,6 +352,7 @@ public class InputPanel extends LinearLayout
void onRecorderPermissionRequired();
void onEmojiToggle();
void onLinkPreviewCanceled();
void onStickerSuggestionSelected(@NonNull StickerRecord sticker);
}
private static class SlideToCancel {

View File

@@ -209,6 +209,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
List<Slide> imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
List<Slide> videoSlides = Stream.of(attachments.getSlides()).filter(Slide::hasVideo).limit(1).toList();
List<Slide> stickerSlides = Stream.of(attachments.getSlides()).filter(Slide::hasSticker).limit(1).toList();
// Given that most types have images, we specifically check images last
if (!audioSlides.isEmpty()) {
@@ -217,13 +218,15 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
mediaDescriptionText.setVisibility(GONE);
} else if (!videoSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_video);
} else if (!stickerSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_sticker);
} else if (!imageSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_photo);
}
}
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
List<Slide> imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo()).limit(1).toList();
List<Slide> imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).limit(1).toList();
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
attachmentVideoOverlayView.setVisibility(GONE);

View File

@@ -0,0 +1,67 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
public class StickerView extends FrameLayout {
private ThumbnailView image;
private View missingShade;
public StickerView(@NonNull Context context) {
super(context);
init();
}
public StickerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
inflate(getContext(), R.layout.sticker_view, this);
this.image = findViewById(R.id.sticker_thumbnail);
this.missingShade = findViewById(R.id.sticker_missing_shade);
}
@Override
public void setFocusable(boolean focusable) {
image.setFocusable(focusable);
}
@Override
public void setClickable(boolean clickable) {
image.setClickable(clickable);
}
@Override
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
image.setOnLongClickListener(l);
}
public void setSticker(@NonNull GlideRequests glideRequests, @NonNull Slide stickerSlide) {
boolean showControls = stickerSlide.asAttachment().getDataUri() == null;
image.setImageResource(glideRequests, stickerSlide, showControls, false);
missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE);
}
public void setThumbnailClickListener(@NonNull SlideClickListener listener) {
image.setThumbnailClickListener(listener);
}
public void setDownloadClickListener(@NonNull SlidesClickedListener listener) {
image.setDownloadClickListener(listener);
}
}

View File

@@ -0,0 +1,224 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.PorterDuff;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.ThemeUtil;
/**
* Class for creating simple tooltips to show throughout the app. Utilizes a popup window so you
* don't have to worry about view hierarchies or anything.
*/
public class TooltipPopup extends PopupWindow {
public static final int POSITION_ABOVE = 0;
public static final int POSITION_BELOW = 1;
public static final int POSITION_START = 2;
public static final int POSITION_END = 3;
private static final int POSITION_LEFT = 4;
private static final int POSITION_RIGHT = 5;
private final View anchor;
private final ImageView arrow;
private final int position;
public static Builder forTarget(@NonNull View anchor) {
return new Builder(anchor);
}
private TooltipPopup(@NonNull View anchor,
int rawPosition,
@NonNull String text,
@ColorInt int backgroundTint,
@ColorInt int textColor,
@Nullable Object iconGlideModel,
@Nullable OnDismissListener dismissListener)
{
super(LayoutInflater.from(anchor.getContext()).inflate(R.layout.tooltip, null),
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
this.anchor = anchor;
this.position = getRtlPosition(anchor.getContext(), rawPosition);
switch (rawPosition) {
case POSITION_ABOVE: arrow = getContentView().findViewById(R.id.tooltip_arrow_bottom); break;
case POSITION_BELOW: arrow = getContentView().findViewById(R.id.tooltip_arrow_top); break;
case POSITION_START: arrow = getContentView().findViewById(R.id.tooltip_arrow_end); break;
case POSITION_END: arrow = getContentView().findViewById(R.id.tooltip_arrow_start); break;
default: throw new AssertionError("Invalid position!");
}
arrow.setVisibility(View.VISIBLE);
TextView textView = getContentView().findViewById(R.id.tooltip_text);
textView.setText(text);
if (textColor != 0) {
textView.setTextColor(textColor);
}
View bubble = getContentView().findViewById(R.id.tooltip_bubble);
if (backgroundTint == 0) {
bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(anchor.getContext(), R.attr.tooltip_default_color), PorterDuff.Mode.MULTIPLY);
arrow.setColorFilter(ThemeUtil.getThemedColor(anchor.getContext(), R.attr.tooltip_default_color), PorterDuff.Mode.MULTIPLY);
} else {
bubble.getBackground().setColorFilter(backgroundTint, PorterDuff.Mode.MULTIPLY);
arrow.setColorFilter(backgroundTint, PorterDuff.Mode.MULTIPLY);
}
if (iconGlideModel != null) {
ImageView iconView = getContentView().findViewById(R.id.tooltip_icon);
iconView.setVisibility(View.VISIBLE);
GlideApp.with(anchor.getContext()).load(iconGlideModel).into(iconView);
}
if (Build.VERSION.SDK_INT >= 21) {
setElevation(10);
}
getContentView().setOnClickListener(v -> dismiss());
setOnDismissListener(dismissListener);
setBackgroundDrawable(null);
setOutsideTouchable(true);
}
private void show() {
if (anchor.getWidth() == 0 && anchor.getHeight() == 0) {
anchor.post(this::show);
return;
}
getContentView().measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
int tooltipSpacing = anchor.getContext().getResources().getDimensionPixelOffset(R.dimen.tooltip_popup_margin);
int xoffset;
int yoffset;
switch (position) {
case POSITION_ABOVE:
xoffset = 0;
yoffset = -(2 * anchor.getWidth() + tooltipSpacing);
onLayout(() -> setArrowHorizontalPosition(arrow, anchor));
break;
case POSITION_BELOW:
xoffset = 0;
yoffset = tooltipSpacing;
onLayout(() -> setArrowHorizontalPosition(arrow, anchor));
break;
case POSITION_LEFT:
xoffset = -getContentView().getMeasuredWidth() - tooltipSpacing;
yoffset = -(getContentView().getMeasuredHeight()/2 + anchor.getHeight()/2);
break;
case POSITION_RIGHT:
xoffset = anchor.getWidth() + tooltipSpacing;
yoffset = -(getContentView().getMeasuredHeight()/2 + anchor.getHeight()/2);
break;
default:
throw new AssertionError("Invalid tooltip position!");
}
showAsDropDown(anchor, xoffset, yoffset);
}
private void onLayout(@NonNull Runnable runnable) {
getContentView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getContentView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
runnable.run();
}
});
}
private static void setArrowHorizontalPosition(@NonNull View arrow, @NonNull View anchor) {
int arrowCenterX = getAbsolutePosition(arrow)[0] + arrow.getWidth()/2;
int anchorCenterX = getAbsolutePosition(anchor)[0] + anchor.getWidth()/2;
arrow.setX(anchorCenterX - arrowCenterX);
}
private static int[] getAbsolutePosition(@NonNull View view) {
int[] position = new int[2];
view.getLocationOnScreen(position);
return position;
}
private static int getRtlPosition(@NonNull Context context, int position) {
if (position == POSITION_ABOVE || position == POSITION_BELOW) {
return position;
} else if (context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
return position == POSITION_START ? POSITION_RIGHT : POSITION_LEFT;
} else {
return position == POSITION_START ? POSITION_LEFT : POSITION_RIGHT;
}
}
public static class Builder {
private final View anchor;
private int backgroundTint;
private int textColor;
private int textResId;
private Object iconGlideModel;
private OnDismissListener dismissListener;
private Builder(@NonNull View anchor) {
this.anchor = anchor;
}
public Builder setBackgroundTint(int color) {
this.backgroundTint = color;
return this;
}
public Builder setTextColor(int color) {
this.textColor = color;
return this;
}
public Builder setText(@StringRes int stringResId) {
this.textResId = stringResId;
return this;
}
public Builder setIconGlideModel(Object model) {
this.iconGlideModel = model;
return this;
}
public Builder setOnDismissListener(OnDismissListener dismissListener) {
this.dismissListener = dismissListener;
return this;
}
public TooltipPopup show(int position) {
String text = anchor.getContext().getString(textResId);
TooltipPopup tooltip = new TooltipPopup(anchor, position, text, backgroundTint, textColor, iconGlideModel, dismissListener);
tooltip.show();
return tooltip;
}
}
}

View File

@@ -1,23 +1,16 @@
package org.thoughtcrime.securesms.components;
import android.animation.LayoutTransition;
import android.content.Context;
import android.graphics.PorterDuff.Mode;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.annimon.stream.Stream;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ValueAnimator;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.greenrobot.eventbus.EventBus;
@@ -36,7 +29,6 @@ import java.util.List;
import java.util.Map;
public class TransferControlView extends FrameLayout {
private static final int TRANSITION_MS = 300;
@Nullable private List<Slide> slides;
@Nullable private View current;
@@ -44,8 +36,6 @@ public class TransferControlView extends FrameLayout {
private final ProgressWheel progressWheel;
private final View downloadDetails;
private final TextView downloadDetailsText;
private final int contractedWidth;
private final int expandedWidth;
private final Map<Attachment, Float> downloadProgress;
@@ -61,20 +51,15 @@ public class TransferControlView extends FrameLayout {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.transfer_controls_view, this);
final Drawable background = ContextCompat.getDrawable(context, R.drawable.transfer_controls_background);
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
background.setColorFilter(0x66ffffff, Mode.MULTIPLY);
}
setLongClickable(false);
ViewUtil.setBackground(this, background);
ViewUtil.setBackground(this, ContextCompat.getDrawable(context, R.drawable.transfer_controls_background));
setVisibility(GONE);
setLayoutTransition(new LayoutTransition());
this.downloadProgress = new HashMap<>();
this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel);
this.downloadDetails = ViewUtil.findById(this, R.id.download_details);
this.downloadDetailsText = ViewUtil.findById(this, R.id.download_details_text);
this.contractedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_contracted_width);
this.expandedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_expanded_width);
}
@Override
@@ -208,42 +193,20 @@ public class TransferControlView extends FrameLayout {
}
private void display(@Nullable final View view) {
final int sourceWidth = (current == downloadDetails && downloadDetailsText.getVisibility() == VISIBLE) ? expandedWidth : contractedWidth;
final int targetWidth = (view == downloadDetails && downloadDetailsText.getVisibility() == VISIBLE) ? expandedWidth : contractedWidth;
if (current == view || current == null) {
ViewGroup.LayoutParams layoutParams = getLayoutParams();
layoutParams.width = targetWidth;
setLayoutParams(layoutParams);
} else {
ViewUtil.fadeOut(current, TRANSITION_MS);
Animator anim = getWidthAnimator(sourceWidth, targetWidth);
anim.start();
if (current != null) {
current.setVisibility(GONE);
}
if (view == null) {
ViewUtil.fadeOut(this, TRANSITION_MS);
if (view != null) {
view.setVisibility(VISIBLE);
setVisibility(VISIBLE);
} else {
ViewUtil.fadeIn(this, TRANSITION_MS);
ViewUtil.fadeIn(view, TRANSITION_MS);
setVisibility(GONE);
}
current = view;
}
private Animator getWidthAnimator(final int from, final int to) {
final ValueAnimator anim = ValueAnimator.ofInt(from, to);
anim.addUpdateListener(animation -> {
final int val = (Integer)animation.getAnimatedValue();
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
layoutParams.width = val;
setLayoutParams(layoutParams);
});
anim.setInterpolator(new FastOutSlowInInterpolator());
anim.setDuration(TRANSITION_MS);
return anim;
}
private float calculateProgress(@NonNull Map<Attachment, Float> downloadProgress) {
float totalProgress = 0;
for (float progress : downloadProgress.values()) {

View File

@@ -1,201 +0,0 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
import org.thoughtcrime.securesms.logging.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import com.astuetz.PagerSlidingTabStrip;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
import org.thoughtcrime.securesms.components.RepeatableImageKey;
import org.thoughtcrime.securesms.components.RepeatableImageKey.KeyEventListener;
import org.thoughtcrime.securesms.components.emoji.EmojiPageView.EmojiSelectionListener;
import org.thoughtcrime.securesms.util.ResUtil;
import java.util.LinkedList;
import java.util.List;
public class EmojiDrawer extends LinearLayout implements InputView, EmojiSelectionListener, VariationSelectorListener {
private static final String TAG = EmojiDrawer.class.getSimpleName();
private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
private ViewPager pager;
private List<EmojiPageModel> models;
private PagerSlidingTabStrip strip;
private RecentEmojiPageModel recentModel;
private EmojiEventListener listener;
private EmojiDrawerListener drawerListener;
public EmojiDrawer(Context context) {
this(context, null);
}
public EmojiDrawer(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(VERTICAL);
}
private void initView() {
final View v = LayoutInflater.from(getContext()).inflate(R.layout.emoji_drawer, this, true);
initializeResources(v);
initializePageModels();
initializeEmojiGrid();
}
public void setEmojiEventListener(EmojiEventListener listener) {
this.listener = listener;
}
public void setDrawerListener(EmojiDrawerListener listener) {
this.drawerListener = listener;
}
private void initializeResources(View v) {
Log.i(TAG, "initializeResources()");
this.pager = (ViewPager) v.findViewById(R.id.emoji_pager);
this.strip = (PagerSlidingTabStrip) v.findViewById(R.id.tabs);
RepeatableImageKey backspace = (RepeatableImageKey)v.findViewById(R.id.backspace);
backspace.setOnKeyEventListener(new KeyEventListener() {
@Override
public void onKeyEvent() {
if (listener != null) listener.onKeyEvent(DELETE_KEY_EVENT);
}
});
}
@Override
public boolean isShowing() {
return getVisibility() == VISIBLE;
}
@Override
public void show(int height, boolean immediate) {
if (this.pager == null) initView();
ViewGroup.LayoutParams params = getLayoutParams();
params.height = height;
Log.i(TAG, "showing emoji drawer with height " + params.height);
setLayoutParams(params);
setVisibility(VISIBLE);
if (drawerListener != null) drawerListener.onShown();
}
@Override
public void hide(boolean immediate) {
setVisibility(GONE);
if (drawerListener != null) drawerListener.onHidden();
Log.i(TAG, "hide()");
}
@Override
public void onEmojiSelected(String emoji) {
recentModel.onCodePointSelected(emoji);
if (listener != null) {
listener.onEmojiSelected(emoji);
}
}
@Override
public void onVariationSelectorStateChanged(boolean open) {
pager.setEnabled(!open);
}
private void initializeEmojiGrid() {
pager.setAdapter(new EmojiPagerAdapter(getContext(), models, this, this));
if (recentModel.getDisplayEmoji().size() == 0) {
pager.setCurrentItem(1);
}
strip.setViewPager(pager);
}
private void initializePageModels() {
this.models = new LinkedList<>();
this.recentModel = new RecentEmojiPageModel(getContext());
this.models.add(recentModel);
this.models.addAll(EmojiPages.DISPLAY_PAGES);
}
public static class EmojiPagerAdapter extends PagerAdapter
implements PagerSlidingTabStrip.CustomTabProvider
{
private Context context;
private List<EmojiPageModel> pages;
private EmojiSelectionListener emojiSelectionListener;
private VariationSelectorListener variationSelectorListener;
public EmojiPagerAdapter(@NonNull Context context,
@NonNull List<EmojiPageModel> pages,
@NonNull EmojiSelectionListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener)
{
super();
this.context = context;
this.pages = pages;
this.emojiSelectionListener = emojiSelectionListener;
this.variationSelectorListener = variationSelectorListener;
}
@Override
public int getCount() {
return pages.size();
}
@Override
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener);
page.setModel(pages.get(position));
container.addView(page);
return page;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View)object);
}
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
EmojiPageView current = (EmojiPageView) object;
current.onSelected();
super.setPrimaryItem(container, position, object);
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@Override
public View getCustomTabView(ViewGroup viewGroup, int i) {
ImageView image = new ImageView(context);
image.setScaleType(ScaleType.CENTER_INSIDE);
image.setImageResource(ResUtil.getDrawableRes(context, pages.get(i).getIconAttr()));
return image;
}
}
public interface EmojiEventListener extends EmojiSelectionListener {
void onKeyEvent(KeyEvent keyEvent);
}
public interface EmojiDrawerListener {
void onShown();
void onHidden();
}
}

View File

@@ -0,0 +1,164 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.ResUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.LinkedList;
import java.util.List;
/**
* A provider to select emoji in the {@link org.thoughtcrime.securesms.components.emoji.MediaKeyboard}.
*/
public class EmojiKeyboardProvider implements MediaKeyboardProvider,
MediaKeyboardProvider.TabIconProvider,
MediaKeyboardProvider.BackspaceObserver,
VariationSelectorListener
{
private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
private final Context context;
private final List<EmojiPageModel> models;
private final RecentEmojiPageModel recentModel;
private final EmojiPagerAdapter emojiPagerAdapter;
private final EmojiEventListener emojiEventListener;
private Controller controller;
public EmojiKeyboardProvider(@NonNull Context context, @Nullable EmojiEventListener emojiEventListener) {
this.context = context;
this.emojiEventListener = emojiEventListener;
this.models = new LinkedList<>();
this.recentModel = new RecentEmojiPageModel(context);
this.emojiPagerAdapter = new EmojiPagerAdapter(context, models, new EmojiEventListener() {
@Override
public void onEmojiSelected(String emoji) {
recentModel.onCodePointSelected(emoji);
if (emojiEventListener != null) {
emojiEventListener.onEmojiSelected(emoji);
}
}
@Override
public void onKeyEvent(KeyEvent keyEvent) {
if (emojiEventListener != null) {
emojiEventListener.onKeyEvent(keyEvent);
}
}
}, this);
models.add(recentModel);
models.addAll(EmojiPages.DISPLAY_PAGES);
}
@Override
public void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider) {
presenter.present(this, emojiPagerAdapter, this, this, null, null, recentModel.getEmoji().size() > 0 ? 0 : 1);
}
@Override
public void setController(@Nullable Controller controller) {
this.controller = controller;
}
@Override
public int getProviderIconView(boolean selected) {
if (selected) {
return ThemeUtil.isDarkTheme(context) ? R.layout.emoji_keyboard_icon_dark_selected : R.layout.emoji_keyboard_icon_light_selected;
} else {
return ThemeUtil.isDarkTheme(context) ? R.layout.emoji_keyboard_icon_dark : R.layout.emoji_keyboard_icon_light;
}
}
@Override
public void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index) {
Drawable drawable = ResUtil.getDrawable(context, models.get(index).getIconAttr());
imageView.setImageDrawable(drawable);
}
@Override
public void onBackspaceClicked() {
if (emojiEventListener != null) {
emojiEventListener.onKeyEvent(DELETE_KEY_EVENT);
}
}
@Override
public void onVariationSelectorStateChanged(boolean open) {
if (controller != null) {
controller.setViewPagerEnabled(!open);
}
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof EmojiKeyboardProvider;
}
private static class EmojiPagerAdapter extends PagerAdapter {
private Context context;
private List<EmojiPageModel> pages;
private EmojiEventListener emojiSelectionListener;
private VariationSelectorListener variationSelectorListener;
public EmojiPagerAdapter(@NonNull Context context,
@NonNull List<EmojiPageModel> pages,
@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener)
{
super();
this.context = context;
this.pages = pages;
this.emojiSelectionListener = emojiSelectionListener;
this.variationSelectorListener = variationSelectorListener;
}
@Override
public int getCount() {
return pages.size();
}
@Override
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener);
page.setModel(pages.get(position));
container.addView(page);
return page;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View)object);
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
EmojiPageView current = (EmojiPageView) object;
current.onSelected();
super.setPrimaryItem(container, position, object);
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
}
public interface EmojiEventListener {
void onEmojiSelected(String emoji);
void onKeyEvent(KeyEvent keyEvent);
}
}

View File

@@ -10,6 +10,7 @@ import android.view.View;
import android.widget.FrameLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
public class EmojiPageView extends FrameLayout implements VariationSelectorListener {
@@ -24,7 +25,7 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
private EmojiVariationSelectorPopup popup;
public EmojiPageView(@NonNull Context context,
@NonNull EmojiSelectionListener emojiSelectionListener,
@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener)
{
super(context);
@@ -37,9 +38,9 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
scrollDisabler = new ScrollDisabler();
popup = new EmojiVariationSelectorPopup(context, emojiSelectionListener);
adapter = new EmojiPageViewGridAdapter(EmojiProvider.getInstance(context),
popup,
emojiSelectionListener,
this);
popup,
emojiSelectionListener,
this);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
@@ -82,10 +83,6 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
}
}
public interface EmojiSelectionListener {
void onEmojiSelected(String emoji);
}
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {

View File

@@ -10,7 +10,7 @@ import android.widget.ImageView;
import android.widget.PopupWindow;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiPageView.EmojiSelectionListener;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
import java.util.ArrayList;
import java.util.List;
@@ -21,17 +21,17 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
private final EmojiProvider emojiProvider;
private final EmojiVariationSelectorPopup popup;
private final VariationSelectorListener variationSelectorListener;
private final EmojiSelectionListener emojiSelectionListener;
private final EmojiEventListener emojiEventListener;
public EmojiPageViewGridAdapter(@NonNull EmojiProvider emojiProvider,
@NonNull EmojiVariationSelectorPopup popup,
@NonNull EmojiSelectionListener emojiSelectionListener,
@NonNull EmojiEventListener emojiEventListener,
@NonNull VariationSelectorListener variationSelectorListener)
{
this.emojiList = new ArrayList<>();
this.emojiProvider = emojiProvider;
this.popup = popup;
this.emojiSelectionListener = emojiSelectionListener;
this.emojiEventListener = emojiEventListener;
this.variationSelectorListener = variationSelectorListener;
popup.setOnDismissListener(this);
@@ -62,7 +62,7 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
}
viewHolder.itemView.setOnClickListener(v -> {
emojiSelectionListener.onEmojiSelected(emoji.getValue());
emojiEventListener.onEmojiSelected(emoji.getValue());
});
if (emoji.getVariations().size() > 1) {
@@ -110,7 +110,7 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
}
}
interface VariationSelectorListener {
public interface VariationSelectorListener {
void onVariationSelectorStateChanged(boolean open);
}
}

View File

@@ -3,17 +3,23 @@ package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.v7.widget.AppCompatImageButton;
import android.util.AttributeSet;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer.EmojiDrawerListener;
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class EmojiToggle extends AppCompatImageButton implements EmojiDrawerListener {
public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.MediaKeyboardListener {
private Drawable emojiToggle;
private Drawable stickerToggle;
private Drawable mediaToggle;
private Drawable imeToggle;
public EmojiToggle(Context context) {
super(context);
initialize();
@@ -29,8 +35,8 @@ public class EmojiToggle extends AppCompatImageButton implements EmojiDrawerList
initialize();
}
public void setToEmoji() {
setImageDrawable(emojiToggle);
public void setToMedia() {
setImageDrawable(mediaToggle);
}
public void setToIme() {
@@ -39,18 +45,29 @@ public class EmojiToggle extends AppCompatImageButton implements EmojiDrawerList
private void initialize() {
int attributes[] = new int[] {R.attr.conversation_emoji_toggle,
R.attr.conversation_keyboard_toggle};
R.attr.conversation_sticker_toggle,
R.attr.conversation_keyboard_toggle};
TypedArray drawables = getContext().obtainStyledAttributes(attributes);
this.emojiToggle = drawables.getDrawable(0);
this.imeToggle = drawables.getDrawable(1);
this.stickerToggle = drawables.getDrawable(1);
this.imeToggle = drawables.getDrawable(2);
this.mediaToggle = emojiToggle;
drawables.recycle();
setToEmoji();
setToMedia();
}
public void attach(EmojiDrawer drawer) {
drawer.setDrawerListener(this);
public void attach(MediaKeyboard drawer) {
drawer.setKeyboardListener(this);
}
public void setStickerMode(boolean stickerMode) {
this.mediaToggle = stickerMode ? stickerToggle : emojiToggle;
if (getDrawable() != imeToggle) {
setToMedia();
}
}
@Override public void onShown() {
@@ -58,6 +75,13 @@ public class EmojiToggle extends AppCompatImageButton implements EmojiDrawerList
}
@Override public void onHidden() {
setToEmoji();
setToMedia();
}
@Override
public void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider) {
setStickerMode(provider instanceof StickerKeyboardProvider);
TextSecurePreferences.setMediaKeyboardMode(getContext(), (provider instanceof StickerKeyboardProvider) ? TextSecurePreferences.MediaKeyboardMode.STICKER
: TextSecurePreferences.MediaKeyboardMode.EMOJI);
}
}

View File

@@ -9,17 +9,17 @@ import android.widget.ImageView;
import android.widget.PopupWindow;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiPageView.EmojiSelectionListener;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
import java.util.List;
public class EmojiVariationSelectorPopup extends PopupWindow {
private final Context context;
private final ViewGroup list;
private final EmojiSelectionListener listener;
private final Context context;
private final ViewGroup list;
private final EmojiEventListener listener;
public EmojiVariationSelectorPopup(@NonNull Context context, @NonNull EmojiSelectionListener listener) {
public EmojiVariationSelectorPopup(@NonNull Context context, @NonNull EmojiEventListener listener) {
super(LayoutInflater.from(context).inflate(R.layout.emoji_variation_selector, null),
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);

View File

@@ -0,0 +1,274 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
import org.thoughtcrime.securesms.components.RepeatableImageKey;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import java.util.Arrays;
public class MediaKeyboard extends FrameLayout implements InputView,
MediaKeyboardProvider.Presenter,
MediaKeyboardProvider.Controller,
MediaKeyboardBottomTabAdapter.EventListener
{
private static final String TAG = Log.tag(MediaKeyboard.class);
private RecyclerView categoryTabs;
private ViewPager categoryPager;
private ViewGroup providerTabs;
private RepeatableImageKey backspaceButton;
private RepeatableImageKey backspaceButtonBackup;
private View searchButton;
private View addButton;
private MediaKeyboardListener keyboardListener;
private MediaKeyboardProvider[] providers;
private int providerIndex;
private MediaKeyboardBottomTabAdapter categoryTabAdapter;
public MediaKeyboard(Context context) {
this(context, null);
}
public MediaKeyboard(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setProviders(int startIndex, MediaKeyboardProvider... providers) {
if (!Arrays.equals(this.providers, providers)) {
this.providers = providers;
this.providerIndex = startIndex;
requestPresent(providers, providerIndex);
}
}
public void setKeyboardListener(MediaKeyboardListener listener) {
this.keyboardListener = listener;
}
@Override
public boolean isShowing() {
return getVisibility() == VISIBLE;
}
@Override
public void show(int height, boolean immediate) {
if (this.categoryPager == null) initView();
ViewGroup.LayoutParams params = getLayoutParams();
params.height = height;
Log.i(TAG, "showing emoji drawer with height " + params.height);
setLayoutParams(params);
setVisibility(VISIBLE);
if (keyboardListener != null) keyboardListener.onShown();
requestPresent(providers, providerIndex);
}
@Override
public void hide(boolean immediate) {
setVisibility(GONE);
if (keyboardListener != null) keyboardListener.onHidden();
Log.i(TAG, "hide()");
}
@Override
public void present(@NonNull MediaKeyboardProvider provider,
@NonNull PagerAdapter pagerAdapter,
@NonNull MediaKeyboardProvider.TabIconProvider tabIconProvider,
@Nullable MediaKeyboardProvider.BackspaceObserver backspaceObserver,
@Nullable MediaKeyboardProvider.AddObserver addObserver,
@Nullable MediaKeyboardProvider.SearchObserver searchObserver,
int startingIndex)
{
if (categoryPager == null) return;
if (!provider.equals(providers[providerIndex])) return;
if (keyboardListener != null) keyboardListener.onKeyboardProviderChanged(provider);
boolean isSolo = providers.length == 1;
presentProviderStrip(isSolo);
presentCategoryPager(pagerAdapter, tabIconProvider, startingIndex);
presentProviderTabs(providers, providerIndex);
presentSearchButton(searchObserver);
presentBackspaceButton(backspaceObserver, isSolo);
presentAddButton(addObserver);
}
@Override
public int getCurrentPosition() {
return categoryPager != null ? categoryPager.getCurrentItem() : 0;
}
@Override
public void requestDismissal() {
hide(true);
providerIndex = 0;
keyboardListener.onKeyboardProviderChanged(providers[providerIndex]);
}
@Override
public boolean isVisible() {
return getVisibility() == View.VISIBLE;
}
@Override
public void onTabSelected(int index) {
if (categoryPager != null) {
categoryPager.setCurrentItem(index);
categoryTabs.smoothScrollToPosition(index);
}
}
@Override
public void setViewPagerEnabled(boolean enabled) {
if (categoryPager != null) {
categoryPager.setEnabled(enabled);
}
}
private void initView() {
final View view = LayoutInflater.from(getContext()).inflate(R.layout.media_keyboard, this, true);
this.categoryTabs = view.findViewById(R.id.media_keyboard_tabs);
this.categoryPager = view.findViewById(R.id.media_keyboard_pager);
this.providerTabs = view.findViewById(R.id.media_keyboard_provider_tabs);
this.backspaceButton = view.findViewById(R.id.media_keyboard_backspace);
this.backspaceButtonBackup = view.findViewById(R.id.media_keyboard_backspace_backup);
this.searchButton = view.findViewById(R.id.media_keyboard_search);
this.addButton = view.findViewById(R.id.media_keyboard_add);
this.categoryTabAdapter = new MediaKeyboardBottomTabAdapter(GlideApp.with(this), this);
categoryTabs.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
categoryTabs.setAdapter(categoryTabAdapter);
}
private void requestPresent(@NonNull MediaKeyboardProvider[] providers, int newIndex) {
providers[providerIndex].setController(null);
providerIndex = newIndex;
providers[providerIndex].setController(this);
providers[providerIndex].requestPresentation(this, providers.length == 1);
}
private void presentCategoryPager(@NonNull PagerAdapter pagerAdapter,
@NonNull MediaKeyboardProvider.TabIconProvider iconProvider,
int startingIndex) {
if (categoryPager.getAdapter() != pagerAdapter) {
categoryPager.setAdapter(pagerAdapter);
}
categoryPager.setCurrentItem(startingIndex);
categoryPager.clearOnPageChangeListeners();
categoryPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int i, float v, int i1) {
}
@Override
public void onPageSelected(int i) {
categoryTabAdapter.setActivePosition(i);
categoryTabs.smoothScrollToPosition(i);
}
@Override
public void onPageScrollStateChanged(int i) {
}
});
categoryTabAdapter.setTabIconProvider(iconProvider, pagerAdapter.getCount());
categoryTabAdapter.setActivePosition(startingIndex);
}
private void presentProviderTabs(@NonNull MediaKeyboardProvider[] providers, int selected) {
providerTabs.removeAllViews();
LayoutInflater inflater = LayoutInflater.from(getContext());
for (int i = 0; i < providers.length; i++) {
MediaKeyboardProvider provider = providers[i];
View view = inflater.inflate(provider.getProviderIconView(i == selected), providerTabs, false);
view.setTag(provider);
final int index = i;
view.setOnClickListener(v -> {
requestPresent(providers, index);
});
providerTabs.addView(view);
}
}
private void presentBackspaceButton(@Nullable MediaKeyboardProvider.BackspaceObserver backspaceObserver,
boolean useBackupPosition)
{
if (backspaceObserver != null) {
if (useBackupPosition) {
backspaceButton.setVisibility(INVISIBLE);
backspaceButton.setOnKeyEventListener(null);
backspaceButtonBackup.setVisibility(VISIBLE);
backspaceButtonBackup.setOnKeyEventListener(backspaceObserver::onBackspaceClicked);
} else {
backspaceButton.setVisibility(VISIBLE);
backspaceButton.setOnKeyEventListener(backspaceObserver::onBackspaceClicked);
backspaceButtonBackup.setVisibility(GONE);
backspaceButtonBackup.setOnKeyEventListener(null);
}
} else {
backspaceButton.setVisibility(INVISIBLE);
backspaceButton.setOnKeyEventListener(null);
backspaceButtonBackup.setVisibility(GONE);
backspaceButton.setOnKeyEventListener(null);
}
}
private void presentAddButton(@Nullable MediaKeyboardProvider.AddObserver addObserver) {
if (addObserver != null) {
addButton.setVisibility(VISIBLE);
addButton.setOnClickListener(v -> addObserver.onAddClicked());
} else {
addButton.setVisibility(GONE);
addButton.setOnClickListener(null);
}
}
private void presentSearchButton(@Nullable MediaKeyboardProvider.SearchObserver searchObserver) {
searchButton.setVisibility(searchObserver != null ? VISIBLE : INVISIBLE);
}
private void presentProviderStrip(boolean isSolo) {
int visibility = isSolo ? View.GONE : View.VISIBLE;
searchButton.setVisibility(visibility);
backspaceButton.setVisibility(visibility);
providerTabs.setVisibility(visibility);
}
public interface MediaKeyboardListener {
void onShown();
void onHidden();
void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider);
}
}

View File

@@ -0,0 +1,95 @@
package org.thoughtcrime.securesms.components.emoji;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider.TabIconProvider;
import org.thoughtcrime.securesms.mms.GlideRequests;
public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKeyboardBottomTabAdapter.MediaKeyboardBottomTabViewHolder> {
private final GlideRequests glideRequests;
private final EventListener eventListener;
private TabIconProvider tabIconProvider;
private int activePosition;
private int count;
public MediaKeyboardBottomTabAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
this.glideRequests = glideRequests;
this.eventListener = eventListener;
}
@Override
public @NonNull MediaKeyboardBottomTabViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
return new MediaKeyboardBottomTabViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.media_keyboard_bottom_tab_item, viewGroup, false));
}
@Override
public void onBindViewHolder(@NonNull MediaKeyboardBottomTabViewHolder viewHolder, int i) {
viewHolder.bind(glideRequests, eventListener, tabIconProvider, i, i == activePosition);
}
@Override
public void onViewRecycled(@NonNull MediaKeyboardBottomTabViewHolder holder) {
holder.recycle();
}
@Override
public int getItemCount() {
return count;
}
public void setTabIconProvider(@NonNull TabIconProvider iconProvider, int count) {
this.tabIconProvider = iconProvider;
this.count = count;
notifyDataSetChanged();
}
public void setActivePosition(int position) {
this.activePosition = position;
notifyDataSetChanged();
}
static class MediaKeyboardBottomTabViewHolder extends RecyclerView.ViewHolder {
private final ImageView image;
private final View indicator;
public MediaKeyboardBottomTabViewHolder(@NonNull View itemView) {
super(itemView);
this.image = itemView.findViewById(R.id.media_keyboard_bottom_tab_image);
this.indicator = itemView.findViewById(R.id.media_keyboard_bottom_tab_indicator);
}
void bind(@NonNull GlideRequests glideRequests,
@NonNull EventListener eventListener,
@NonNull TabIconProvider tabIconProvider,
int index,
boolean selected)
{
tabIconProvider.loadCategoryTabIcon(glideRequests, image, index);
image.setAlpha(selected ? 1 : 0.5f);
image.setSelected(selected);
indicator.setVisibility(selected ? View.VISIBLE : View.INVISIBLE);
itemView.setOnClickListener(v -> eventListener.onTabSelected(index));
}
void recycle() {
itemView.setOnClickListener(null);
}
}
interface EventListener {
void onTabSelected(int index);
}
}

View File

@@ -0,0 +1,51 @@
package org.thoughtcrime.securesms.components.emoji;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.widget.ImageView;
import org.thoughtcrime.securesms.mms.GlideRequests;
public interface MediaKeyboardProvider {
@LayoutRes int getProviderIconView(boolean selected);
/** @return True if the click was handled with provider-specific logic, otherwise false */
void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider);
void setController(@Nullable Controller controller);
interface BackspaceObserver {
void onBackspaceClicked();
}
interface AddObserver {
void onAddClicked();
}
interface SearchObserver {
void onSearchOpened();
void onSearchClosed();
void onSearchChanged(@NonNull String query);
}
interface Controller {
void setViewPagerEnabled(boolean enabled);
}
interface Presenter {
void present(@NonNull MediaKeyboardProvider provider,
@NonNull PagerAdapter pagerAdapter,
@NonNull TabIconProvider iconProvider,
@Nullable BackspaceObserver backspaceObserver,
@Nullable AddObserver addObserver,
@Nullable SearchObserver searchObserver,
int startingIndex);
int getCurrentPosition();
void requestDismissal();
boolean isVisible();
}
interface TabIconProvider {
void loadCategoryTabIcon(@NonNull GlideRequests glideRequests, @NonNull ImageView imageView, int index);
}
}