package org.telegram.ui.Components; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.TextUtils; import android.util.Property; import android.util.TypedValue; import android.view.GestureDetector; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.CallSuper; import androidx.annotation.ColorInt; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Consumer; import androidx.core.view.ViewCompat; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.FloatPropertyCompat; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessagesController; import org.telegram.messenger.R; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ChatActivity; import org.telegram.ui.DialogsActivity; import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import static java.lang.annotation.RetentionPolicy.SOURCE; public final class Bulletin { public static final int DURATION_SHORT = 1500; public static final int DURATION_LONG = 2750; public static final int TYPE_STICKER = 0; public static final int TYPE_ERROR = 1; public static final int TYPE_BIO_CHANGED = 2; public static final int TYPE_NAME_CHANGED = 3; public static Bulletin make(@NonNull FrameLayout containerLayout, @NonNull Layout contentLayout, int duration) { return new Bulletin(containerLayout, contentLayout, duration); } @SuppressLint("RtlHardcoded") public static Bulletin make(@NonNull BaseFragment fragment, @NonNull Layout contentLayout, int duration) { if (fragment instanceof ChatActivity) { contentLayout.setWideScreenParams(ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.RIGHT); } else if (fragment instanceof DialogsActivity) { contentLayout.setWideScreenParams(ViewGroup.LayoutParams.MATCH_PARENT, Gravity.NO_GRAVITY); } return new Bulletin(fragment.getLayoutContainer(), contentLayout, duration); } public static Bulletin find(@NonNull FrameLayout containerLayout) { for (int i = 0, size = containerLayout.getChildCount(); i < size; i++) { final View view = containerLayout.getChildAt(i); if (view instanceof Layout) { return ((Layout) view).bulletin; } } return null; } public static void hide(@NonNull FrameLayout containerLayout) { hide(containerLayout, true); } public static void hide(@NonNull FrameLayout containerLayout, boolean animated) { final Bulletin bulletin = find(containerLayout); if (bulletin != null) { bulletin.hide(animated && isTransitionsEnabled(), 0); } } private static final HashMap delegates = new HashMap<>(); @SuppressLint("StaticFieldLeak") private static Bulletin visibleBulletin; private final Layout layout; private final ParentLayout parentLayout; private final FrameLayout containerLayout; private final Runnable hideRunnable = this::hide; private final int duration; private boolean showing; private boolean canHide; public int currentBottomOffset; private Delegate currentDelegate; private Layout.Transition layoutTransition; private Bulletin(@NonNull FrameLayout containerLayout, @NonNull Layout layout, int duration) { this.layout = layout; this.parentLayout = new ParentLayout(layout) { @Override protected void onPressedStateChanged(boolean pressed) { setCanHide(!pressed); if (containerLayout.getParent() != null) { containerLayout.getParent().requestDisallowInterceptTouchEvent(pressed); } } @Override protected void onHide() { hide(); } }; this.containerLayout = containerLayout; this.duration = duration; } public static Bulletin getVisibleBulletin() { return visibleBulletin; } public Bulletin show() { if (!showing && containerLayout != null) { showing = true; if (layout.getParent() != parentLayout) { throw new IllegalStateException("Layout has incorrect parent"); } if (visibleBulletin != null) { visibleBulletin.hide(); } visibleBulletin = this; layout.onAttach(this); layout.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { layout.removeOnLayoutChangeListener(this); if (showing) { layout.onShow(); currentDelegate = delegates.get(containerLayout); currentBottomOffset = currentDelegate != null ? currentDelegate.getBottomOffset() : 0; if (currentDelegate != null) { currentDelegate.onShow(Bulletin.this); } if (isTransitionsEnabled()) { ensureLayoutTransitionCreated(); layout.transitionRunning = true; layout.delegate = currentDelegate; layout.invalidate(); layoutTransition.animateEnter(layout, layout::onEnterTransitionStart, () -> { layout.transitionRunning = false; layout.onEnterTransitionEnd(); setCanHide(true); }, offset -> { if (currentDelegate != null) { currentDelegate.onOffsetChange(layout.getHeight() - offset); } }, currentBottomOffset); } else { if (currentDelegate != null) { currentDelegate.onOffsetChange(layout.getHeight() - currentBottomOffset); } updatePosition(); layout.onEnterTransitionStart(); layout.onEnterTransitionEnd(); setCanHide(true); } } } }); layout.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { layout.removeOnAttachStateChangeListener(this); hide(false, 0); } }); containerLayout.addView(parentLayout); } return this; } private void setCanHide(boolean canHide) { if (this.canHide != canHide) { this.canHide = canHide; if (canHide) { layout.postDelayed(hideRunnable, duration); } else { layout.removeCallbacks(hideRunnable); } } } private void ensureLayoutTransitionCreated() { if (layoutTransition == null) { layoutTransition = layout.createTransition(); } } public void hide() { hide(isTransitionsEnabled(), 0); } public void hide(long duration) { hide(isTransitionsEnabled(), duration); } public void hide(boolean animated, long duration) { if (showing) { showing = false; if (visibleBulletin == this) { visibleBulletin = null; } int bottomOffset = currentBottomOffset; currentBottomOffset = 0; if (ViewCompat.isLaidOut(layout)) { layout.removeCallbacks(hideRunnable); if (animated) { layout.transitionRunning = true; layout.delegate = currentDelegate; layout.invalidate(); if (duration >= 0) { Layout.DefaultTransition transition = new Layout.DefaultTransition(); transition.duration = duration; layoutTransition = transition; } else { ensureLayoutTransitionCreated(); } layoutTransition.animateExit(layout, layout::onExitTransitionStart, () -> { if (currentDelegate != null) { currentDelegate.onOffsetChange(0); currentDelegate.onHide(this); } layout.transitionRunning = false; layout.onExitTransitionEnd(); layout.onHide(); containerLayout.removeView(parentLayout); layout.onDetach(); }, offset -> { if (currentDelegate != null) { currentDelegate.onOffsetChange(layout.getHeight() - offset); } }, bottomOffset); return; } } if (currentDelegate != null) { currentDelegate.onOffsetChange(0); currentDelegate.onHide(this); } layout.onExitTransitionStart(); layout.onExitTransitionEnd(); layout.onHide(); if (containerLayout != null) { containerLayout.removeView(parentLayout); } layout.onDetach(); } } public boolean isShowing() { return showing; } public Layout getLayout() { return layout; } private static boolean isTransitionsEnabled() { return MessagesController.getGlobalMainSettings().getBoolean("view_animations", true) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; } public void updatePosition() { layout.updatePosition(); } @Retention(SOURCE) @IntDef(value = {ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT}) private @interface WidthDef { } @Retention(SOURCE) @SuppressLint("RtlHardcoded") @IntDef(value = {Gravity.LEFT, Gravity.RIGHT, Gravity.CENTER_HORIZONTAL, Gravity.NO_GRAVITY}) private @interface GravityDef { } private static abstract class ParentLayout extends FrameLayout { private final Layout layout; private final Rect rect = new Rect(); private final GestureDetector gestureDetector; private boolean pressed; private float translationX; private boolean hideAnimationRunning; private boolean needLeftAlphaAnimation; private boolean needRightAlphaAnimation; public ParentLayout(Layout layout) { super(layout.getContext()); this.layout = layout; gestureDetector = new GestureDetector(layout.getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { if (!hideAnimationRunning) { needLeftAlphaAnimation = layout.isNeedSwipeAlphaAnimation(true); needRightAlphaAnimation = layout.isNeedSwipeAlphaAnimation(false); return true; } return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { layout.setTranslationX(translationX -= distanceX); if (translationX == 0 || (translationX < 0f && needLeftAlphaAnimation) || (translationX > 0f && needRightAlphaAnimation)) { layout.setAlpha(1f - Math.abs(translationX) / layout.getWidth()); } return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (Math.abs(velocityX) > 2000f) { final boolean needAlphaAnimation = (velocityX < 0f && needLeftAlphaAnimation) || (velocityX > 0f && needRightAlphaAnimation); final SpringAnimation springAnimation = new SpringAnimation(layout, DynamicAnimation.TRANSLATION_X, Math.signum(velocityX) * layout.getWidth() * 2f); if (!needAlphaAnimation) { springAnimation.addEndListener((animation, canceled, value, velocity) -> onHide()); springAnimation.addUpdateListener(((animation, value, velocity) -> { if (Math.abs(value) > layout.getWidth()) { animation.cancel(); } })); } springAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY); springAnimation.getSpring().setStiffness(100f); springAnimation.setStartVelocity(velocityX); springAnimation.start(); if (needAlphaAnimation) { final SpringAnimation springAnimation2 = new SpringAnimation(layout, DynamicAnimation.ALPHA, 0f); springAnimation2.addEndListener((animation, canceled, value, velocity) -> onHide()); springAnimation2.addUpdateListener(((animation, value, velocity) -> { if (value <= 0f) { animation.cancel(); } })); springAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY); springAnimation.getSpring().setStiffness(10f); springAnimation.setStartVelocity(velocityX); springAnimation2.start(); } hideAnimationRunning = true; return true; } return false; } }); gestureDetector.setIsLongpressEnabled(false); addView(layout); } @Override public boolean onTouchEvent(MotionEvent event) { if (pressed || inLayoutHitRect(event.getX(), event.getY())) { gestureDetector.onTouchEvent(event); final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { if (!pressed && !hideAnimationRunning) { layout.animate().cancel(); translationX = layout.getTranslationX(); onPressedStateChanged(pressed = true); } } else if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL) { if (pressed) { if (!hideAnimationRunning) { if (Math.abs(translationX) > layout.getWidth() / 3f) { final float tx = Math.signum(translationX) * layout.getWidth(); final boolean needAlphaAnimation = (translationX < 0f && needLeftAlphaAnimation) || (translationX > 0f && needRightAlphaAnimation); layout.animate().translationX(tx).alpha(needAlphaAnimation ? 0f : 1f).setDuration(200).setInterpolator(AndroidUtilities.accelerateInterpolator).withEndAction(() -> { if (layout.getTranslationX() == tx) { onHide(); } }).start(); } else { layout.animate().translationX(0).alpha(1f).setDuration(200).start(); } } onPressedStateChanged(pressed = false); } } return true; } return false; } private boolean inLayoutHitRect(float x, float y) { layout.getHitRect(rect); return rect.contains((int) x, (int) y); } protected abstract void onPressedStateChanged(boolean pressed); protected abstract void onHide(); } //region Offset Providers public static void addDelegate(@NonNull BaseFragment fragment, @NonNull Delegate delegate) { final FrameLayout containerLayout = fragment.getLayoutContainer(); if (containerLayout != null) { addDelegate(containerLayout, delegate); } } public static void addDelegate(@NonNull FrameLayout containerLayout, @NonNull Delegate delegate) { delegates.put(containerLayout, delegate); } public static void removeDelegate(@NonNull BaseFragment fragment) { final FrameLayout containerLayout = fragment.getLayoutContainer(); if (containerLayout != null) { removeDelegate(containerLayout); } } public static void removeDelegate(@NonNull FrameLayout containerLayout) { delegates.remove(containerLayout); } public interface Delegate { default int getBottomOffset() { return 0; } default void onOffsetChange(float offset) { } default void onShow(Bulletin bulletin) { } default void onHide(Bulletin bulletin) { } } //endregion //region Layouts public abstract static class Layout extends FrameLayout { private final List callbacks = new ArrayList<>(); public boolean transitionRunning; Delegate delegate; public float inOutOffset; protected Bulletin bulletin; @WidthDef private int wideScreenWidth = ViewGroup.LayoutParams.WRAP_CONTENT; @GravityDef private int wideScreenGravity = Gravity.CENTER_HORIZONTAL; public Layout(@NonNull Context context) { this(context, Theme.getColor(Theme.key_undo_background)); } Drawable background; public Layout(@NonNull Context context, @ColorInt int backgroundColor) { super(context); setMinimumHeight(AndroidUtilities.dp(48)); background = Theme.createRoundRectDrawable(AndroidUtilities.dp(6), backgroundColor); updateSize(); setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(8), AndroidUtilities.dp(8), AndroidUtilities.dp(8)); setWillNotDraw(false); } public final static FloatPropertyCompat IN_OUT_OFFSET_Y = new FloatPropertyCompat("offsetY") { @Override public float getValue(Layout object) { return object.inOutOffset; } @Override public void setValue(Layout object, float value) { object.setInOutOffset(value); } }; public final static Property IN_OUT_OFFSET_Y2 = new AnimationProperties.FloatProperty("offsetY") { @Override public Float get(Layout layout) { return layout.inOutOffset; } @Override public void setValue(Layout object, float value) { object.setInOutOffset(value); } }; @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateSize(); } private void updateSize() { final boolean isWideScreen = isWideScreen(); setLayoutParams(LayoutHelper.createFrame(isWideScreen ? wideScreenWidth : LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, isWideScreen ? Gravity.BOTTOM | wideScreenGravity : Gravity.BOTTOM)); } private boolean isWideScreen() { return AndroidUtilities.isTablet() || AndroidUtilities.displaySize.x >= AndroidUtilities.displaySize.y; } private void setWideScreenParams(@WidthDef int width, @GravityDef int gravity) { boolean changed = false; if (wideScreenWidth != width) { wideScreenWidth = width; changed = true; } if (wideScreenGravity != gravity) { wideScreenGravity = gravity; changed = true; } if (isWideScreen() && changed) { updateSize(); } } @SuppressLint("RtlHardcoded") private boolean isNeedSwipeAlphaAnimation(boolean swipeLeft) { if (!isWideScreen() || wideScreenWidth == ViewGroup.LayoutParams.MATCH_PARENT) { return false; } if (wideScreenGravity == Gravity.CENTER_HORIZONTAL) { return true; } if (swipeLeft) { return wideScreenGravity == Gravity.RIGHT; } else { return wideScreenGravity != Gravity.RIGHT; } } public Bulletin getBulletin() { return bulletin; } public boolean isAttachedToBulletin() { return bulletin != null; } @CallSuper protected void onAttach(@NonNull Bulletin bulletin) { this.bulletin = bulletin; for (int i = 0, size = callbacks.size(); i < size; i++) { callbacks.get(i).onAttach(this, bulletin); } } @CallSuper protected void onDetach() { this.bulletin = null; for (int i = 0, size = callbacks.size(); i < size; i++) { callbacks.get(i).onDetach(this); } } @CallSuper protected void onShow() { for (int i = 0, size = callbacks.size(); i < size; i++) { callbacks.get(i).onShow(this); } } @CallSuper protected void onHide() { for (int i = 0, size = callbacks.size(); i < size; i++) { callbacks.get(i).onHide(this); } } @CallSuper protected void onEnterTransitionStart() { for (int i = 0, size = callbacks.size(); i < size; i++) { callbacks.get(i).onEnterTransitionStart(this); } } @CallSuper protected void onEnterTransitionEnd() { for (int i = 0, size = callbacks.size(); i < size; i++) { callbacks.get(i).onEnterTransitionEnd(this); } } @CallSuper protected void onExitTransitionStart() { for (int i = 0, size = callbacks.size(); i < size; i++) { callbacks.get(i).onExitTransitionStart(this); } } @CallSuper protected void onExitTransitionEnd() { for (int i = 0, size = callbacks.size(); i < size; i++) { callbacks.get(i).onExitTransitionEnd(this); } } //region Callbacks public void addCallback(@NonNull Callback callback) { callbacks.add(callback); } public void removeCallback(@NonNull Callback callback) { callbacks.remove(callback); } public void updatePosition() { float tranlsation = 0; if (delegate != null) { tranlsation += delegate.getBottomOffset() ; } setTranslationY(-tranlsation + inOutOffset); } public interface Callback { void onAttach(@NonNull Layout layout, @NonNull Bulletin bulletin); void onDetach(@NonNull Layout layout); void onShow(@NonNull Layout layout); void onHide(@NonNull Layout layout); void onEnterTransitionStart(@NonNull Layout layout); void onEnterTransitionEnd(@NonNull Layout layout); void onExitTransitionStart(@NonNull Layout layout); void onExitTransitionEnd(@NonNull Layout layout); } //endregion //region Transitions @NonNull public Transition createTransition() { return new SpringTransition(); } public interface Transition { void animateEnter(@NonNull Layout layout, @Nullable Runnable startAction, @Nullable Runnable endAction, @Nullable Consumer onUpdate, int bottomOffset); void animateExit(@NonNull Layout layout, @Nullable Runnable startAction, @Nullable Runnable endAction, @Nullable Consumer onUpdate, int bottomOffset); } public static class DefaultTransition implements Transition { long duration = 255; @Override public void animateEnter(@NonNull Layout layout, @Nullable Runnable startAction, @Nullable Runnable endAction, @Nullable Consumer onUpdate, int bottomOffset) { layout.setInOutOffset(layout.getMeasuredHeight()); if (onUpdate != null) { onUpdate.accept(layout.getTranslationY()); } final ObjectAnimator animator = ObjectAnimator.ofFloat(layout, IN_OUT_OFFSET_Y2, 0); animator.setDuration(duration); animator.setInterpolator(Easings.easeOutQuad); if (startAction != null || endAction != null) { animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { if (startAction != null) { startAction.run(); } } @Override public void onAnimationEnd(Animator animation) { if (endAction != null) { endAction.run(); } } }); } if (onUpdate != null) { animator.addUpdateListener(a -> onUpdate.accept(layout.getTranslationY())); } animator.start(); } @Override public void animateExit(@NonNull Layout layout, @Nullable Runnable startAction, @Nullable Runnable endAction, @Nullable Consumer onUpdate, int bottomOffset) { final ObjectAnimator animator = ObjectAnimator.ofFloat(layout, IN_OUT_OFFSET_Y2, layout.getHeight()); animator.setDuration(175); animator.setInterpolator(Easings.easeInQuad); if (startAction != null || endAction != null) { animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { if (startAction != null) { startAction.run(); } } @Override public void onAnimationEnd(Animator animation) { if (endAction != null) { endAction.run(); } } }); } if (onUpdate != null) { animator.addUpdateListener(a -> onUpdate.accept(layout.getTranslationY())); } animator.start(); } } public static class SpringTransition implements Transition { private static final float DAMPING_RATIO = 0.8f; private static final float STIFFNESS = 400f; @Override public void animateEnter(@NonNull Layout layout, @Nullable Runnable startAction, @Nullable Runnable endAction, @Nullable Consumer onUpdate, int bottomOffset) { layout.setInOutOffset(layout.getMeasuredHeight()); if (onUpdate != null) { onUpdate.accept(layout.getTranslationY()); } final SpringAnimation springAnimation = new SpringAnimation(layout, IN_OUT_OFFSET_Y, 0); springAnimation.getSpring().setDampingRatio(DAMPING_RATIO); springAnimation.getSpring().setStiffness(STIFFNESS); if (endAction != null) { springAnimation.addEndListener((animation, canceled, value, velocity) -> { layout.setInOutOffset(0); if (!canceled) { endAction.run(); } }); } if (onUpdate != null) { springAnimation.addUpdateListener((animation, value, velocity) -> onUpdate.accept(layout.getTranslationY())); } springAnimation.start(); if (startAction != null) { startAction.run(); } } @Override public void animateExit(@NonNull Layout layout, @Nullable Runnable startAction, @Nullable Runnable endAction, @Nullable Consumer onUpdate,int bottomOffset) { final SpringAnimation springAnimation = new SpringAnimation(layout, IN_OUT_OFFSET_Y, layout.getHeight()); springAnimation.getSpring().setDampingRatio(DAMPING_RATIO); springAnimation.getSpring().setStiffness(STIFFNESS); if (endAction != null) { springAnimation.addEndListener((animation, canceled, value, velocity) -> { if (!canceled) { endAction.run(); } }); } if (onUpdate != null) { springAnimation.addUpdateListener((animation, value, velocity) -> onUpdate.accept(layout.getTranslationY())); } springAnimation.start(); if (startAction != null) { startAction.run(); } } } private void setInOutOffset(float offset) { inOutOffset = offset; updatePosition(); } @Override protected void dispatchDraw(Canvas canvas) { background.setBounds(AndroidUtilities.dp(8), AndroidUtilities.dp(8), getMeasuredWidth() - AndroidUtilities.dp(8), getMeasuredHeight() - AndroidUtilities.dp(8)); if (transitionRunning && delegate != null) { int clipBottom = ((View)getParent()).getMeasuredHeight() - delegate.getBottomOffset(); int viewBottom = (int) (getY() + getMeasuredHeight()); canvas.save(); canvas.clipRect(0, 0, getMeasuredWidth(), getMeasuredHeight() - (viewBottom - clipBottom)); background.draw(canvas); super.dispatchDraw(canvas); canvas.restore(); invalidate(); } else { background.draw(canvas); super.dispatchDraw(canvas); } } //endregion } @SuppressLint("ViewConstructor") public static class ButtonLayout extends Layout { private Button button; private int childrenMeasuredWidth; public ButtonLayout(@NonNull Context context) { super(context); } public ButtonLayout(@NonNull Context context, int backgroundColor) { super(context, backgroundColor); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { childrenMeasuredWidth = 0; super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (button != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) { setMeasuredDimension(childrenMeasuredWidth + button.getMeasuredWidth(), getMeasuredHeight()); } } @Override protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { if (button != null && child != button) { widthUsed += button.getMeasuredWidth() - AndroidUtilities.dp(12); } super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed); if (child != button) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); childrenMeasuredWidth = Math.max(childrenMeasuredWidth, lp.leftMargin + lp.rightMargin + child.getMeasuredWidth()); } } public Button getButton() { return button; } public void setButton(Button button) { if (this.button != null) { removeCallback(this.button); removeView(this.button); } this.button = button; if (button != null) { addCallback(button); addView(button, 0, LayoutHelper.createFrameRelatively(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.END | Gravity.CENTER_VERTICAL)); } } } public static class SimpleLayout extends ButtonLayout { public final ImageView imageView; public final TextView textView; public SimpleLayout(@NonNull Context context) { super(context); final int undoInfoColor = Theme.getColor(Theme.key_undo_infoColor); imageView = new ImageView(context); imageView.setColorFilter(new PorterDuffColorFilter(undoInfoColor, PorterDuff.Mode.MULTIPLY)); addView(imageView, LayoutHelper.createFrameRelatively(24, 24, Gravity.START | Gravity.CENTER_VERTICAL, 16, 12, 16, 12)); textView = new TextView(context); textView.setSingleLine(); textView.setTextColor(undoInfoColor); textView.setTypeface(Typeface.SANS_SERIF); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); addView(textView, LayoutHelper.createFrameRelatively(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.START | Gravity.CENTER_VERTICAL, 56, 0, 16, 0)); } } @SuppressLint("ViewConstructor") public static class TwoLineLayout extends ButtonLayout { public final BackupImageView imageView; public final TextView titleTextView; public final TextView subtitleTextView; public TwoLineLayout(@NonNull Context context) { super(context); final int undoInfoColor = Theme.getColor(Theme.key_undo_infoColor); addView(imageView = new BackupImageView(context), LayoutHelper.createFrameRelatively(29, 29, Gravity.START | Gravity.CENTER_VERTICAL, 12, 12, 12, 12)); final LinearLayout linearLayout = new LinearLayout(context); linearLayout.setOrientation(LinearLayout.VERTICAL); addView(linearLayout, LayoutHelper.createFrameRelatively(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.START | Gravity.CENTER_VERTICAL, 54, 8, 12, 8)); titleTextView = new TextView(context); titleTextView.setSingleLine(); titleTextView.setTextColor(undoInfoColor); titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); titleTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); linearLayout.addView(titleTextView); subtitleTextView = new TextView(context); subtitleTextView.setMaxLines(2); subtitleTextView.setTextColor(undoInfoColor); subtitleTextView.setTypeface(Typeface.SANS_SERIF); subtitleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); linearLayout.addView(subtitleTextView); } } public static class TwoLineLottieLayout extends ButtonLayout { public final RLottieImageView imageView; public final TextView titleTextView; public final TextView subtitleTextView; private final int textColor; public TwoLineLottieLayout(@NonNull Context context) { this(context, Theme.getColor(Theme.key_undo_background), Theme.getColor(Theme.key_undo_infoColor)); } public TwoLineLottieLayout(@NonNull Context context, @ColorInt int backgroundColor, @ColorInt int textColor) { super(context, backgroundColor); this.textColor = textColor; imageView = new RLottieImageView(context); imageView.setScaleType(ImageView.ScaleType.CENTER); addView(imageView, LayoutHelper.createFrameRelatively(56, 48, Gravity.START | Gravity.CENTER_VERTICAL)); final int undoInfoColor = Theme.getColor(Theme.key_undo_infoColor); final LinearLayout linearLayout = new LinearLayout(context); linearLayout.setOrientation(LinearLayout.VERTICAL); addView(linearLayout, LayoutHelper.createFrameRelatively(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.START | Gravity.CENTER_VERTICAL, 56, 8, 12, 8)); titleTextView = new TextView(context); titleTextView.setSingleLine(); titleTextView.setTextColor(undoInfoColor); titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); titleTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); linearLayout.addView(titleTextView); subtitleTextView = new TextView(context); subtitleTextView.setTextColor(undoInfoColor); subtitleTextView.setTypeface(Typeface.SANS_SERIF); subtitleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); linearLayout.addView(subtitleTextView); } @Override protected void onShow() { super.onShow(); imageView.playAnimation(); } public void setAnimation(int resId, String... layers) { setAnimation(resId, 32, 32, layers); } public void setAnimation(int resId, int w, int h, String... layers) { imageView.setAnimation(resId, w, h); for (String layer : layers) { imageView.setLayerColor(layer + ".**", textColor); } } } public static class LottieLayout extends ButtonLayout { public final RLottieImageView imageView; public final TextView textView; private final int textColor; public LottieLayout(@NonNull Context context) { this(context, Theme.getColor(Theme.key_undo_background), Theme.getColor(Theme.key_undo_infoColor)); } public LottieLayout(@NonNull Context context, @ColorInt int backgroundColor, @ColorInt int textColor) { super(context, backgroundColor); this.textColor = textColor; imageView = new RLottieImageView(context); imageView.setScaleType(ImageView.ScaleType.CENTER); addView(imageView, LayoutHelper.createFrameRelatively(56, 48, Gravity.START | Gravity.CENTER_VERTICAL)); textView = new TextView(context); textView.setSingleLine(); textView.setTextColor(textColor); textView.setTypeface(Typeface.SANS_SERIF); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); textView.setEllipsize(TextUtils.TruncateAt.END); textView.setPadding(0, AndroidUtilities.dp(8), 0, AndroidUtilities.dp(8)); addView(textView, LayoutHelper.createFrameRelatively(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.START | Gravity.CENTER_VERTICAL, 56, 0, 16, 0)); } @Override protected void onShow() { super.onShow(); imageView.playAnimation(); } public void setAnimation(int resId, String... layers) { setAnimation(resId, 32, 32, layers); } public void setAnimation(int resId, int w, int h, String... layers) { imageView.setAnimation(resId, w, h); for (String layer : layers) { imageView.setLayerColor(layer + ".**", textColor); } } public void setIconPaddingBottom(int paddingBottom) { imageView.setLayoutParams(LayoutHelper.createFrameRelatively(56, 48 - paddingBottom, Gravity.START | Gravity.CENTER_VERTICAL, 0, 0, 0, paddingBottom)); } } //endregion //region Buttons @SuppressLint("ViewConstructor") public abstract static class Button extends FrameLayout implements Layout.Callback { public Button(@NonNull Context context) { super(context); } @Override public void onAttach(@NonNull Layout layout, @NonNull Bulletin bulletin) { } @Override public void onDetach(@NonNull Layout layout) { } @Override public void onShow(@NonNull Layout layout) { } @Override public void onHide(@NonNull Layout layout) { } @Override public void onEnterTransitionStart(@NonNull Layout layout) { } @Override public void onEnterTransitionEnd(@NonNull Layout layout) { } @Override public void onExitTransitionStart(@NonNull Layout layout) { } @Override public void onExitTransitionEnd(@NonNull Layout layout) { } } @SuppressLint("ViewConstructor") public static final class UndoButton extends Button { private Runnable undoAction; private Runnable delayedAction; private Bulletin bulletin; private boolean isUndone; public UndoButton(@NonNull Context context, boolean text) { super(context); final int undoCancelColor = Theme.getColor(Theme.key_undo_cancelColor); if (text) { TextView undoTextView = new TextView(context); undoTextView.setOnClickListener(v -> undo()); final int leftInset = LocaleController.isRTL ? AndroidUtilities.dp(16) : 0; final int rightInset = LocaleController.isRTL ? 0 : AndroidUtilities.dp(16); undoTextView.setBackground(Theme.createCircleSelectorDrawable((undoCancelColor & 0x00ffffff) | 0x19000000, leftInset, rightInset)); undoTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); undoTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); undoTextView.setTextColor(undoCancelColor); undoTextView.setText(LocaleController.getString("Undo", R.string.Undo)); undoTextView.setGravity(Gravity.CENTER_VERTICAL); ViewHelper.setPaddingRelative(undoTextView, 16, 0, 16, 0); addView(undoTextView, LayoutHelper.createFrameRelatively(LayoutHelper.WRAP_CONTENT, 48, Gravity.CENTER_VERTICAL, 8, 0, 0, 0)); } else { final ImageView undoImageView = new ImageView(getContext()); undoImageView.setOnClickListener(v -> undo()); undoImageView.setImageResource(R.drawable.chats_undo); undoImageView.setColorFilter(new PorterDuffColorFilter(undoCancelColor, PorterDuff.Mode.MULTIPLY)); undoImageView.setBackground(Theme.createSelectorDrawable((undoCancelColor & 0x00ffffff) | 0x19000000)); ViewHelper.setPaddingRelative(undoImageView, 0, 12, 0, 12); addView(undoImageView, LayoutHelper.createFrameRelatively(56, 48, Gravity.CENTER_VERTICAL)); } } public void undo() { if (bulletin != null) { isUndone = true; if (undoAction != null) { undoAction.run(); } bulletin.hide(); } } @Override public void onAttach(@NonNull Layout layout, @NonNull Bulletin bulletin) { this.bulletin = bulletin; } @Override public void onDetach(@NonNull Layout layout) { this.bulletin = null; if (delayedAction != null && !isUndone) { delayedAction.run(); } } public UndoButton setUndoAction(Runnable undoAction) { this.undoAction = undoAction; return this; } public UndoButton setDelayedAction(Runnable delayedAction) { this.delayedAction = delayedAction; return this; } } //endregion }