package org.telegram.ui.Components.Reactions; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.text.TextPaint; import android.view.Gravity; import android.view.MotionEvent; import android.view.ViewConfiguration; import androidx.core.graphics.ColorUtils; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.DocumentObject; import org.telegram.messenger.ImageLocation; import org.telegram.messenger.ImageReceiver; import org.telegram.messenger.MediaDataController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.MessagesController; import org.telegram.messenger.SvgHelper; import org.telegram.messenger.UserConfig; import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Cells.ChatMessageCell; import org.telegram.ui.Components.AvatarsDarawable; import org.telegram.ui.Components.CounterView; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; public class ReactionsLayoutInBubble { private final static int ANIMATION_TYPE_IN = 1; private final static int ANIMATION_TYPE_OUT = 2; private final static int ANIMATION_TYPE_MOVE = 3; public boolean drawServiceShaderBackground; private static Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private static TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); public boolean isSmall; public int x; public int y; private float fromX; private float fromY; private float lastDrawnX; private float lastDrawnY; private boolean wasDrawn; private boolean animateMove; private boolean animateWidth; public int positionOffsetY; int currentAccount; public int height; public int totalHeight; public int width; public int fromWidth; public boolean isEmpty; private float touchSlop; public int lastLineX; ArrayList reactionButtons = new ArrayList<>(); ArrayList outButtons = new ArrayList<>(); HashMap lastDrawingReactionButtons = new HashMap<>(); HashMap lastDrawingReactionButtonsTmp = new HashMap<>(); ChatMessageCell parentView; MessageObject messageObject; Theme.ResourcesProvider resourcesProvider; int availableWidth; private int lastDrawnWidth; boolean attached; private final static ButtonsComparator comparator = new ButtonsComparator(); public ReactionsLayoutInBubble(ChatMessageCell parentView) { this.parentView = parentView; currentAccount = UserConfig.selectedAccount; paint.setColor(Theme.getColor(Theme.key_chat_inLoader)); textPaint.setColor(Theme.getColor(Theme.key_featuredStickers_buttonText)); textPaint.setTextSize(AndroidUtilities.dp(12)); textPaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); touchSlop = ViewConfiguration.get(ApplicationLoader.applicationContext).getScaledTouchSlop(); } public void setMessage(MessageObject messageObject, boolean isSmall, Theme.ResourcesProvider resourcesProvider) { this.resourcesProvider = resourcesProvider; this.isSmall = isSmall; this.messageObject = messageObject; for (int i = 0; i < reactionButtons.size(); i++) { reactionButtons.get(i).detach(); } reactionButtons.clear(); if (messageObject != null) { if (messageObject.messageOwner.reactions != null && messageObject.messageOwner.reactions.results != null) { int totalCount = 0; for (int i = 0; i < messageObject.messageOwner.reactions.results.size(); i++) { totalCount += messageObject.messageOwner.reactions.results.get(i).count; } for (int i = 0; i < messageObject.messageOwner.reactions.results.size(); i++) { TLRPC.TL_reactionCount reactionCount = messageObject.messageOwner.reactions.results.get(i); ReactionButton button = new ReactionButton(reactionCount); reactionButtons.add(button); if (!isSmall && messageObject.messageOwner.reactions.recent_reactons != null) { ArrayList users = null; if (reactionCount.count <= 3 && totalCount <= 3) { for (int j = 0; j < messageObject.messageOwner.reactions.recent_reactons.size(); j++) { TLRPC.TL_messageUserReaction reccent = messageObject.messageOwner.reactions.recent_reactons.get(j); if (reccent.reaction.equals(reactionCount.reaction) && MessagesController.getInstance(currentAccount).getUser(reccent.user_id) != null) { if (users == null) { users = new ArrayList<>(); } users.add(MessagesController.getInstance(currentAccount).getUser(reccent.user_id)); } } button.setUsers(users); if (users != null && !users.isEmpty()) { button.count = 0; button.counterDrawable.setCount(0, false); } } } if (isSmall && reactionCount.count > 1 && reactionCount.chosen) { reactionButtons.add(new ReactionButton(reactionCount)); reactionButtons.get(0).isSelected = false; reactionButtons.get(1).isSelected = true; break; } if (isSmall && i == 2) { break; } if (attached) { button.attach(); } } } comparator.currentAccount = currentAccount; Collections.sort(reactionButtons, comparator); } isEmpty = reactionButtons.isEmpty(); } public void measure(int availableWidth) { height = 0; width = 0; positionOffsetY = 0; if (isEmpty) { return; } this.availableWidth = availableWidth; int maxWidth = 0; int currentX = 0; int currentY = 0; for (int i = 0; i < reactionButtons.size(); i++) { ReactionButton button = reactionButtons.get(i); if (isSmall) { button.width = AndroidUtilities.dp(14); button.height = AndroidUtilities.dp(14); } else { button.width = (int) (AndroidUtilities.dp(8) + AndroidUtilities.dp(20) + AndroidUtilities.dp(4)); if (button.avatarsDarawable != null && button.users.size() > 0) { button.users.size(); int c1 = 1; int c2 = button.users.size() > 1 ? button.users.size() - 1 : 0; button.width += AndroidUtilities.dp(2) + c1 * AndroidUtilities.dp(20) + c2 * AndroidUtilities.dp(20) * 0.8f + AndroidUtilities.dp(1); button.avatarsDarawable.height = AndroidUtilities.dp(26); } else { button.width += button.counterDrawable.textPaint.measureText(button.countText) + AndroidUtilities.dp(8); } button.height = AndroidUtilities.dp(26); } if (currentX + button.width > availableWidth) { currentX = 0; currentY += button.height + AndroidUtilities.dp(4); } button.x = currentX; button.y = currentY; currentX += button.width + AndroidUtilities.dp(4); if (currentX > maxWidth) { maxWidth = currentX; } } lastLineX = currentX; width = maxWidth; height = currentY + (reactionButtons.size() == 0 ? 0 : AndroidUtilities.dp(26)); drawServiceShaderBackground = false; } public void draw(Canvas canvas, float animationProgress) { if (isEmpty && outButtons.isEmpty()) { return; } float totalX = this.x; float totalY = this.y; if (isEmpty) { totalX = lastDrawnX; totalY = lastDrawnY; } else if (animateMove) { totalX = totalX * (animationProgress) + fromX * (1f - animationProgress); totalY = totalY * (animationProgress) + fromY * (1f - animationProgress); } canvas.save(); canvas.translate(totalX, totalY); for (int i = 0; i < reactionButtons.size(); i++) { ReactionButton reactionButton = reactionButtons.get(i); canvas.save(); float x = reactionButton.x; float y = reactionButton.y; if (animationProgress != 1f && reactionButton.animationType == ANIMATION_TYPE_MOVE) { x = reactionButton.x * animationProgress + reactionButton.animateFromX * (1f - animationProgress); y = reactionButton.y * animationProgress + reactionButton.animateFromY * (1f - animationProgress); } canvas.translate(x, y); float alpha = 1f; if (animationProgress != 1f && reactionButton.animationType == ANIMATION_TYPE_IN) { float s = 0.5f + 0.5f * animationProgress; alpha = animationProgress; canvas.scale(s, s, reactionButton.width / 2f, reactionButton.height / 2f); } reactionButton.draw(canvas, reactionButton.animationType == ANIMATION_TYPE_IN ? 1f : animationProgress, alpha); canvas.restore(); } for (int i = 0; i < outButtons.size(); i++) { ReactionButton reactionButton = outButtons.get(i); canvas.save(); canvas.translate(reactionButton.x, reactionButton.y); float s = 0.5f + 0.5f * (1f - animationProgress); canvas.scale(s, s, reactionButton.width / 2f, reactionButton.height / 2f); outButtons.get(i).draw(canvas, 1f, (1f - animationProgress)); canvas.restore(); } canvas.restore(); } public void recordDrawingState() { lastDrawingReactionButtons.clear(); for (int i = 0; i < reactionButtons.size(); i++) { lastDrawingReactionButtons.put(reactionButtons.get(i).reaction, reactionButtons.get(i)); } wasDrawn = !isEmpty; lastDrawnX = x; lastDrawnY = y; lastDrawnWidth = width; } public boolean animateChange() { boolean changed = false; lastDrawingReactionButtonsTmp.clear(); for (int i = 0; i < outButtons.size(); i++) { outButtons.get(i).detach(); } outButtons.clear(); lastDrawingReactionButtonsTmp.putAll(lastDrawingReactionButtons); for (int i = 0; i < reactionButtons.size(); i++) { ReactionButton button = reactionButtons.get(i); ReactionButton lastButton = lastDrawingReactionButtonsTmp.remove(button.reaction); if (lastButton != null) { if (button.animateFromX != lastButton.x || button.animateFromY != lastButton.y || button.animateFromWidth != lastButton.width || button.count != lastButton.count || button.backgroundColor != lastButton.backgroundColor) { button.animateFromX = lastButton.x; button.animateFromY = lastButton.y; button.animateFromWidth = lastButton.width; button.fromTextColor = lastButton.lastDrawnTextColor; button.fromBackgroundColor = lastButton.lastDrawnBackgroundColor; button.animationType = ANIMATION_TYPE_MOVE; if (button.count != lastButton.count) { button.counterDrawable.setCount(lastButton.count, false); button.counterDrawable.setCount(button.count, true); } if (button.avatarsDarawable != null || lastButton.avatarsDarawable != null) { if (button.avatarsDarawable == null) { button.setUsers(new ArrayList<>()); } if (lastButton.avatarsDarawable == null) { lastButton.setUsers(new ArrayList<>()); } button.avatarsDarawable.animateFromState(lastButton.avatarsDarawable, currentAccount); } changed = true; } else { button.animationType = 0; } } else { changed = true; button.animationType = ANIMATION_TYPE_IN; } } if (!lastDrawingReactionButtonsTmp.isEmpty()) { changed = true; outButtons.addAll(lastDrawingReactionButtonsTmp.values()); for (int i = 0; i < outButtons.size(); i++) { outButtons.get(i).drawImage = outButtons.get(i).lastImageDrawn; outButtons.get(i).attach(); } } if (wasDrawn && (lastDrawnX != x || lastDrawnY != y)) { animateMove = true; fromX = lastDrawnX; fromY = lastDrawnY; changed = true; } if (lastDrawnWidth != width) { animateWidth = true; fromWidth = lastDrawnWidth; changed = true; } return changed; } public void resetAnimation() { for (int i = 0; i < outButtons.size(); i++) { outButtons.get(i).detach(); } outButtons.clear(); animateMove = false; animateWidth = false; for (int i = 0; i < reactionButtons.size(); i++) { reactionButtons.get(i).animationType = 0; } } public ReactionButton getReactionButton(String reaction) { return lastDrawingReactionButtons.get(reaction); } public class ReactionButton { private final TLRPC.TL_reactionCount reactionCount; public int animationType; public int animateFromX; public int animateFromY; public int animateFromWidth; public int fromBackgroundColor; public int fromTextColor; public int realCount; public boolean drawImage = true; public boolean lastImageDrawn; String countText; String reaction; int count; public int x; public int y; int width; int height; ImageReceiver imageReceiver = new ImageReceiver(); CounterView.CounterDrawable counterDrawable = new CounterView.CounterDrawable(parentView, false, null); int backgroundColor; int textColor; int serviceBackgroundColor; int serviceTextColor; int lastDrawnTextColor; int lastDrawnBackgroundColor; boolean isSelected; AvatarsDarawable avatarsDarawable; ArrayList users; public ReactionButton(TLRPC.TL_reactionCount reactionCount) { this.reactionCount = reactionCount; this.reaction = reactionCount.reaction; this.count = reactionCount.count; this.realCount = reactionCount.count; countText = Integer.toString(reactionCount.count); imageReceiver.setParentView(parentView); isSelected = reactionCount.chosen; counterDrawable.updateVisibility = false; if (reactionCount.chosen) { backgroundColor = Theme.getColor(messageObject.isOutOwner() ? Theme.key_chat_outReactionButtonBackground : Theme.key_chat_inReactionButtonBackground, resourcesProvider); textColor = Theme.getColor(messageObject.isOutOwner() ? Theme.key_chat_outReactionButtonTextSelected : Theme.key_chat_inReactionButtonTextSelected, resourcesProvider); serviceTextColor = Theme.getColor(messageObject.isOutOwner() ? Theme.key_chat_outReactionButtonBackground : Theme.key_chat_inReactionButtonBackground, resourcesProvider); serviceBackgroundColor = Theme.getColor(messageObject.isOutOwner() ? Theme.key_chat_outBubble : Theme.key_chat_inBubble); } else { textColor = Theme.getColor(messageObject.isOutOwner() ? Theme.key_chat_outReactionButtonText : Theme.key_chat_inReactionButtonText, resourcesProvider); backgroundColor = Theme.getColor(messageObject.isOutOwner() ? Theme.key_chat_outReactionButtonBackground : Theme.key_chat_inReactionButtonBackground, resourcesProvider); backgroundColor = ColorUtils.setAlphaComponent(backgroundColor, (int) (Color.alpha(backgroundColor) * 0.156f)); serviceTextColor = Theme.getColor(Theme.key_chat_serviceText, resourcesProvider); serviceBackgroundColor = Color.TRANSPARENT; } if (reaction != null) { TLRPC.TL_availableReaction r = MediaDataController.getInstance(currentAccount).getReactionsMap().get(reaction); if (r != null) { SvgHelper.SvgDrawable svgThumb = DocumentObject.getSvgThumb(r.static_icon, Theme.key_windowBackgroundGray, 1.0f); imageReceiver.setImage(ImageLocation.getForDocument(r.static_icon), "40_40", svgThumb, "webp", r, 1); } } counterDrawable.setSize(AndroidUtilities.dp(26), AndroidUtilities.dp(100)); counterDrawable.textPaint = textPaint; counterDrawable.setCount(count, false); counterDrawable.setType(CounterView.CounterDrawable.TYPE_CHAT_REACTIONS); counterDrawable.gravity = Gravity.LEFT; } public void draw(Canvas canvas, float progress, float alpha) { if (isSmall) { imageReceiver.setAlpha(alpha); imageReceiver.setImageCoords(0, 0, AndroidUtilities.dp(14), AndroidUtilities.dp(14)); drawImage(canvas); return; } if (drawServiceShaderBackground) { textPaint.setColor(lastDrawnTextColor = ColorUtils.blendARGB(fromTextColor, serviceTextColor, progress)); paint.setColor(lastDrawnBackgroundColor = ColorUtils.blendARGB(fromBackgroundColor, serviceBackgroundColor, progress)); } else { textPaint.setColor(lastDrawnTextColor = ColorUtils.blendARGB(fromTextColor, textColor, progress)); paint.setColor(lastDrawnBackgroundColor = ColorUtils.blendARGB(fromBackgroundColor, backgroundColor, progress)); } if (alpha != 1f) { textPaint.setAlpha((int) (textPaint.getAlpha() * alpha)); paint.setAlpha((int) (paint.getAlpha() * alpha)); } imageReceiver.setAlpha(alpha); int w = width; if (progress != 1f && animationType == ANIMATION_TYPE_MOVE) { w = (int) (width * progress + animateFromWidth * (1f - progress)); } AndroidUtilities.rectTmp.set(0, 0, w, height); float rad = height / 2f; if (drawServiceShaderBackground) { Paint paint1 = getThemedPaint(Theme.key_paint_chatActionBackground); Paint paint2 = Theme.chat_actionBackgroundGradientDarkenPaint; int oldAlpha = paint1.getAlpha(); int oldAlpha2 = paint2.getAlpha(); paint1.setAlpha((int) (oldAlpha * alpha)); paint2.setAlpha((int) (oldAlpha2 * alpha)); canvas.drawRoundRect(AndroidUtilities.rectTmp, rad, rad, paint1); if (hasGradientService()) { canvas.drawRoundRect(AndroidUtilities.rectTmp, rad, rad, paint2); } paint1.setAlpha(oldAlpha); paint2.setAlpha(oldAlpha2); } canvas.drawRoundRect(AndroidUtilities.rectTmp, rad, rad, paint); imageReceiver.setImageCoords(AndroidUtilities.dp(8), (height - AndroidUtilities.dp(20)) / 2f, AndroidUtilities.dp(20), AndroidUtilities.dp(20)); drawImage(canvas); if (count != 0 || counterDrawable.countChangeProgress != 1f) { canvas.save(); canvas.translate(AndroidUtilities.dp(8) + AndroidUtilities.dp(20) + AndroidUtilities.dp(2), 0); counterDrawable.draw(canvas); canvas.restore(); } if (avatarsDarawable != null) { canvas.save(); canvas.translate(AndroidUtilities.dp(10) + AndroidUtilities.dp(20) + AndroidUtilities.dp(2), 0); avatarsDarawable.setAlpha(alpha); avatarsDarawable.onDraw(canvas); canvas.restore(); } } private void drawImage(Canvas canvas) { if (drawImage && ((realCount > 1 || !ReactionsEffectOverlay.isPlaying(messageObject.getId(), messageObject.getGroupId(), reaction)) || !isSelected)) { imageReceiver.draw(canvas); lastImageDrawn = true; } else { imageReceiver.setAlpha(0); imageReceiver.draw(canvas); lastImageDrawn = false; } } public void setUsers(ArrayList users) { this.users = users; if (users != null) { if (avatarsDarawable == null) { avatarsDarawable = new AvatarsDarawable(parentView, false); avatarsDarawable.setSize(AndroidUtilities.dp(20)); avatarsDarawable.width = AndroidUtilities.dp(100); avatarsDarawable.height = height; if (attached) { avatarsDarawable.onAttachedToWindow(); } } for (int i = 0; i < users.size(); i++) { if (i == 3) { break; } avatarsDarawable.setObject(i, currentAccount, users.get(i)); } avatarsDarawable.commitTransition(false); } } public void attach() { if (imageReceiver != null) { imageReceiver.onAttachedToWindow(); } if (avatarsDarawable != null) { avatarsDarawable.onAttachedToWindow(); } } public void detach() { if (imageReceiver != null) { imageReceiver.onDetachedFromWindow(); } if (avatarsDarawable != null) { avatarsDarawable.onDetachedFromWindow(); } } } static int attachedCount; float lastX; float lastY; ReactionButton lastSelectedButton; boolean pressed; Runnable longPressRunnable; public boolean chekTouchEvent(MotionEvent event) { if (isEmpty || messageObject == null || messageObject.messageOwner == null || messageObject.messageOwner.reactions == null) { return false; } float x = event.getX() - this.x; float y = event.getY() - this.y; if (event.getAction() == MotionEvent.ACTION_DOWN) { for (int i = 0, n = reactionButtons.size(); i < n; i++) { if (x > reactionButtons.get(i).x && x < reactionButtons.get(i).x + reactionButtons.get(i).width && y > reactionButtons.get(i).y && y < reactionButtons.get(i).y + reactionButtons.get(i).height) { lastX = event.getX(); lastY = event.getY(); lastSelectedButton = reactionButtons.get(i); if (longPressRunnable != null && messageObject.messageOwner.reactions.can_see_list) { AndroidUtilities.cancelRunOnUIThread(longPressRunnable); longPressRunnable = null; } final ReactionButton selectedButtonFinal = lastSelectedButton; AndroidUtilities.runOnUIThread(longPressRunnable = () -> { parentView.getDelegate().didPressReaction(parentView, selectedButtonFinal.reactionCount, true); longPressRunnable = null; }, ViewConfiguration.getLongPressTimeout()); pressed = true; break; } } } else if (event.getAction() == MotionEvent.ACTION_MOVE) { if (pressed && Math.abs(event.getX() - lastX) > touchSlop || Math.abs(event.getY() - lastY) > touchSlop) { pressed = false; lastSelectedButton = null; if (longPressRunnable != null) { AndroidUtilities.cancelRunOnUIThread(longPressRunnable); longPressRunnable = null; } } } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { if (longPressRunnable != null) { AndroidUtilities.cancelRunOnUIThread(longPressRunnable); longPressRunnable = null; } if (pressed && lastSelectedButton != null && event.getAction() == MotionEvent.ACTION_UP) { if (parentView.getDelegate() != null) { parentView.getDelegate().didPressReaction(parentView, lastSelectedButton.reactionCount, false); } } pressed = false; lastSelectedButton = null; } return pressed; } private boolean hasGradientService() { return resourcesProvider != null ? resourcesProvider.hasGradientService() : Theme.hasGradientService(); } private Paint getThemedPaint(String paintKey) { Paint paint = resourcesProvider != null ? resourcesProvider.getPaint(paintKey) : null; return paint != null ? paint : Theme.getThemePaint(paintKey); } public float getCurrentWidth(float transitionProgress) { if (animateWidth) { return fromWidth * (1f - transitionProgress) + width * transitionProgress; } return width; } private static class ButtonsComparator implements Comparator { int currentAccount; @Override public int compare(ReactionButton o1, ReactionButton o2) { if (o1.realCount != o2.realCount) { return o2.realCount - o1.realCount; } TLRPC.TL_availableReaction availableReaction1 = MediaDataController.getInstance(currentAccount).getReactionsMap().get(o1.reaction); TLRPC.TL_availableReaction availableReaction2 = MediaDataController.getInstance(currentAccount).getReactionsMap().get(o2.reaction); if (availableReaction1 != null && availableReaction2 != null) { return availableReaction1.positionInList - availableReaction2.positionInList; } return 0; } } public void onAttachToWindow() { for (int i = 0; i < reactionButtons.size(); i++) { reactionButtons.get(i).attach(); } } public void onDetachFromWindow() { for (int i = 0; i < reactionButtons.size(); i++) { reactionButtons.get(i).detach(); } } }