package org.telegram.ui.Components; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.text.Layout; import android.text.SpannableStringBuilder; import android.text.StaticLayout; import android.text.TextPaint; import android.view.Gravity; import android.view.View; import android.view.animation.OvershootInterpolator; import org.telegram.messenger.AndroidUtilities; import org.telegram.ui.ActionBar.Theme; public class CounterView extends View { public CounterDrawable counterDrawable; private final Theme.ResourcesProvider resourcesProvider; public CounterView(Context context, Theme.ResourcesProvider resourcesProvider) { super(context); this.resourcesProvider = resourcesProvider; setVisibility(View.GONE); counterDrawable = new CounterDrawable(this, true, resourcesProvider); counterDrawable.updateVisibility = true; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); counterDrawable.setSize(getMeasuredHeight(), getMeasuredWidth()); } @Override protected void onDraw(Canvas canvas) { counterDrawable.draw(canvas); } public void setColors(String textKey, String circleKey){ counterDrawable.textColorKey = textKey; counterDrawable.circleColorKey = circleKey; } public void setGravity(int gravity) { counterDrawable.gravity = gravity; } public void setReverse(boolean b) { counterDrawable.reverseAnimation = b; } public void setCount(int count, boolean animated) { counterDrawable.setCount(count, animated); } private int getThemedColor(String key) { Integer color = resourcesProvider != null ? resourcesProvider.getColor(key) : null; return color != null ? color : Theme.getColor(key); } public static class CounterDrawable { private final static int ANIMATION_TYPE_IN = 0; private final static int ANIMATION_TYPE_OUT = 1; private final static int ANIMATION_TYPE_REPLACE = 2; public boolean shortFormat; int animationType = -1; public Paint circlePaint; public TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); public RectF rectF = new RectF(); public boolean addServiceGradient; int currentCount; private boolean countAnimationIncrement; private ValueAnimator countAnimator; public float countChangeProgress = 1f; private StaticLayout countLayout; private StaticLayout countOldLayout; private StaticLayout countAnimationStableLayout; private StaticLayout countAnimationInLayout; private int countWidthOld; private int countWidth; private int circleColor; private int textColor; private String textColorKey = Theme.key_chat_goDownButtonCounter; private String circleColorKey = Theme.key_chat_goDownButtonCounterBackground; int lastH; int width; public int gravity = Gravity.CENTER; float countLeft; float x; private boolean reverseAnimation; public float horizontalPadding; private boolean drawBackground = true; public boolean updateVisibility; private View parent; public final static int TYPE_DEFAULT = 0; public final static int TYPE_CHAT_PULLING_DOWN = 1; public final static int TYPE_CHAT_REACTIONS = 2; int type = TYPE_DEFAULT; private final Theme.ResourcesProvider resourcesProvider; public CounterDrawable(View parent, boolean drawBackground, Theme.ResourcesProvider resourcesProvider) { this.parent = parent; this.resourcesProvider = resourcesProvider; this.drawBackground = drawBackground; if (drawBackground) { circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); circlePaint.setColor(Color.BLACK); } textPaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); textPaint.setTextSize(AndroidUtilities.dp(13)); } public void setSize(int h, int w) { if (h != lastH) { int count = currentCount; currentCount = -1; setCount(count, animationType == ANIMATION_TYPE_IN); lastH = h; } width = w; } private void drawInternal(Canvas canvas) { float countTop = (lastH - AndroidUtilities.dp(23)) / 2f; updateX(countWidth); rectF.set(x, countTop, x + countWidth + AndroidUtilities.dp(11), countTop + AndroidUtilities.dp(23)); if (circlePaint != null && drawBackground) { canvas.drawRoundRect(rectF, 11.5f * AndroidUtilities.density, 11.5f * AndroidUtilities.density, circlePaint); if (addServiceGradient && Theme.hasGradientService()) { canvas.drawRoundRect(rectF, 11.5f * AndroidUtilities.density, 11.5f * AndroidUtilities.density, Theme.chat_actionBackgroundGradientDarkenPaint); } } if (countLayout != null) { canvas.save(); canvas.translate(countLeft, countTop + AndroidUtilities.dp(4)); countLayout.draw(canvas); canvas.restore(); } } public void setCount(int count, boolean animated) { if (count == currentCount) { return; } if (countAnimator != null) { countAnimator.cancel(); } if (count > 0 && updateVisibility && parent != null) { parent.setVisibility(View.VISIBLE); } if (Math.abs(count - currentCount) > 99) { animated = false; } if (!animated) { currentCount = count; if (count == 0) { if (updateVisibility && parent != null) { parent.setVisibility(View.GONE); } return; } String newStr = getStringOfCCount(count); countWidth = Math.max(AndroidUtilities.dp(12), (int) Math.ceil(textPaint.measureText(newStr))); countLayout = new StaticLayout(newStr, textPaint, countWidth, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); if (parent != null) { parent.invalidate(); } } String newStr = getStringOfCCount(count); if (animated) { if (countAnimator != null) { countAnimator.cancel(); } countChangeProgress = 0f; countAnimator = ValueAnimator.ofFloat(0, 1f); countAnimator.addUpdateListener(valueAnimator -> { countChangeProgress = (float) valueAnimator.getAnimatedValue(); if (parent != null) { parent.invalidate(); } }); countAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { animationType = -1; countChangeProgress = 1f; countOldLayout = null; countAnimationStableLayout = null; countAnimationInLayout = null; if (parent != null) { if (currentCount == 0 && updateVisibility) { parent.setVisibility(View.GONE); } parent.invalidate(); } } }); if (currentCount <= 0) { animationType = ANIMATION_TYPE_IN; countAnimator.setDuration(220); countAnimator.setInterpolator(new OvershootInterpolator()); } else if (count == 0) { animationType = ANIMATION_TYPE_OUT; countAnimator.setDuration(150); countAnimator.setInterpolator(CubicBezierInterpolator.DEFAULT); } else { animationType = ANIMATION_TYPE_REPLACE; countAnimator.setDuration(430); countAnimator.setInterpolator(CubicBezierInterpolator.DEFAULT); } if (countLayout != null) { String oldStr = getStringOfCCount(currentCount); if (oldStr.length() == newStr.length()) { SpannableStringBuilder oldSpannableStr = new SpannableStringBuilder(oldStr); SpannableStringBuilder newSpannableStr = new SpannableStringBuilder(newStr); SpannableStringBuilder stableStr = new SpannableStringBuilder(newStr); for (int i = 0; i < oldStr.length(); i++) { if (oldStr.charAt(i) == newStr.charAt(i)) { oldSpannableStr.setSpan(new EmptyStubSpan(), i, i + 1, 0); newSpannableStr.setSpan(new EmptyStubSpan(), i, i + 1, 0); } else { stableStr.setSpan(new EmptyStubSpan(), i, i + 1, 0); } } int countOldWidth = Math.max(AndroidUtilities.dp(12), (int) Math.ceil(textPaint.measureText(oldStr))); countOldLayout = new StaticLayout(oldSpannableStr, textPaint, countOldWidth, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); countAnimationStableLayout = new StaticLayout(stableStr, textPaint, countOldWidth, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); countAnimationInLayout = new StaticLayout(newSpannableStr, textPaint, countOldWidth, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); } else { countOldLayout = countLayout; } } countWidthOld = countWidth; countAnimationIncrement = count > currentCount; countAnimator.start(); } if (count > 0) { countWidth = Math.max(AndroidUtilities.dp(12), (int) Math.ceil(textPaint.measureText(newStr))); countLayout = new StaticLayout(newStr, textPaint, countWidth, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); } currentCount = count; if (parent != null) { parent.invalidate(); } } private String getStringOfCCount(int count) { if (shortFormat) { return AndroidUtilities.formatWholeNumber(count, 0); } return String.valueOf(count); } public void draw(Canvas canvas) { if (type != TYPE_CHAT_PULLING_DOWN && type != TYPE_CHAT_REACTIONS) { int textColor = getThemedColor(textColorKey); int circleColor = getThemedColor(circleColorKey); if (this.textColor != textColor) { this.textColor = textColor; textPaint.setColor(textColor); } if (circlePaint != null && this.circleColor != circleColor) { this.circleColor = circleColor; circlePaint.setColor(circleColor); } } if (countChangeProgress != 1f) { if (animationType == ANIMATION_TYPE_IN || animationType == ANIMATION_TYPE_OUT) { updateX(countWidth); float cx = countLeft + countWidth / 2f; float cy = lastH / 2f; canvas.save(); float progress = animationType == ANIMATION_TYPE_IN ? countChangeProgress : (1f - countChangeProgress); canvas.scale(progress, progress, cx, cy); drawInternal(canvas); canvas.restore(); } else { float progressHalf = countChangeProgress * 2; if (progressHalf > 1f) { progressHalf = 1f; } float countTop = (lastH - AndroidUtilities.dp(23)) / 2f; float countWidth; if (this.countWidth == this.countWidthOld) { countWidth = this.countWidth; } else { countWidth = this.countWidth * progressHalf + this.countWidthOld * (1f - progressHalf); } updateX(countWidth); float scale = 1f; if (countAnimationIncrement) { if (countChangeProgress <= 0.5f) { scale += 0.1f * CubicBezierInterpolator.EASE_OUT.getInterpolation(countChangeProgress * 2); } else { scale += 0.1f * CubicBezierInterpolator.EASE_IN.getInterpolation((1f - (countChangeProgress - 0.5f) * 2)); } } rectF.set(x, countTop, x + countWidth + AndroidUtilities.dp(11), countTop + AndroidUtilities.dp(23)); canvas.save(); canvas.scale(scale, scale, rectF.centerX(), rectF.centerY()); if (drawBackground && circlePaint != null) { canvas.drawRoundRect(rectF, 11.5f * AndroidUtilities.density, 11.5f * AndroidUtilities.density, circlePaint); if (addServiceGradient && Theme.hasGradientService()) { canvas.drawRoundRect(rectF, 11.5f * AndroidUtilities.density, 11.5f * AndroidUtilities.density, Theme.chat_actionBackgroundGradientDarkenPaint); } } canvas.clipRect(rectF); boolean increment = reverseAnimation != countAnimationIncrement; if (countAnimationInLayout != null) { canvas.save(); canvas.translate(countLeft, countTop + AndroidUtilities.dp(4) + (increment ? AndroidUtilities.dp(13) : -AndroidUtilities.dp(13)) * (1f - progressHalf)); textPaint.setAlpha((int) (255 * progressHalf)); countAnimationInLayout.draw(canvas); canvas.restore(); } else if (countLayout != null) { canvas.save(); canvas.translate(countLeft, countTop + AndroidUtilities.dp(4) + (increment ? AndroidUtilities.dp(13) : -AndroidUtilities.dp(13)) * (1f - progressHalf)); textPaint.setAlpha((int) (255 * progressHalf)); countLayout.draw(canvas); canvas.restore(); } if (countOldLayout != null) { canvas.save(); canvas.translate(countLeft, countTop + AndroidUtilities.dp(4) + (increment ? -AndroidUtilities.dp(13) : AndroidUtilities.dp(13)) * (progressHalf)); textPaint.setAlpha((int) (255 * (1f - progressHalf))); countOldLayout.draw(canvas); canvas.restore(); } if (countAnimationStableLayout != null) { canvas.save(); canvas.translate(countLeft, countTop + AndroidUtilities.dp(4)); textPaint.setAlpha(255); countAnimationStableLayout.draw(canvas); canvas.restore(); } textPaint.setAlpha(255); canvas.restore(); } } else { drawInternal(canvas); } } public void updateBackgroundRect() { if (countChangeProgress != 1f) { if (animationType == ANIMATION_TYPE_IN || animationType == ANIMATION_TYPE_OUT) { updateX(countWidth); float countTop = (lastH - AndroidUtilities.dp(23)) / 2f; rectF.set(x, countTop, x + countWidth + AndroidUtilities.dp(11), countTop + AndroidUtilities.dp(23)); } else { float progressHalf = countChangeProgress * 2; if (progressHalf > 1f) { progressHalf = 1f; } float countTop = (lastH - AndroidUtilities.dp(23)) / 2f; float countWidth; if (this.countWidth == this.countWidthOld) { countWidth = this.countWidth; } else { countWidth = this.countWidth * progressHalf + this.countWidthOld * (1f - progressHalf); } updateX(countWidth); rectF.set(x, countTop, x + countWidth + AndroidUtilities.dp(11), countTop + AndroidUtilities.dp(23)); } } else { updateX(countWidth); float countTop = (lastH - AndroidUtilities.dp(23)) / 2f; rectF.set(x, countTop, x + countWidth + AndroidUtilities.dp(11), countTop + AndroidUtilities.dp(23)); } } private void updateX(float countWidth) { float padding = drawBackground ? AndroidUtilities.dp(5.5f) : 0f; if (gravity == Gravity.RIGHT) { countLeft = width - padding; if (horizontalPadding != 0) { countLeft -= Math.max(horizontalPadding + countWidth / 2f, countWidth); } else { countLeft -= countWidth; } } else if (gravity == Gravity.LEFT) { countLeft = padding; } else { countLeft = (int) ((width - countWidth) / 2f); } x = countLeft - padding; } public float getCenterX() { updateX(countWidth); return countLeft + countWidth / 2f; } public void setType(int type) { this.type = type; } public void setParent(View parent) { this.parent = parent; } private int getThemedColor(String key) { Integer color = resourcesProvider != null ? resourcesProvider.getColor(key) : null; return color != null ? color : Theme.getColor(key); } } }