NekoX/TMessagesProj/src/main/java/org/telegram/ui/Components/PopupSwipeBackLayout.java

439 lines
15 KiB
Java

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.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.SparseIntArray;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.core.view.GestureDetectorCompat;
import org.telegram.messenger.AndroidUtilities;
import org.telegram.messenger.NotificationCenter;
import org.telegram.messenger.UserConfig;
import org.telegram.ui.ActionBar.ActionBarPopupWindow;
import org.telegram.ui.ActionBar.Theme;
public class PopupSwipeBackLayout extends FrameLayout {
private final static int DURATION = 300;
SparseIntArray overrideHeightIndex = new SparseIntArray();
private float transitionProgress;
private float toProgress = -1;
private GestureDetectorCompat detector;
private boolean isProcessingSwipe;
private boolean isAnimationInProgress;
private boolean isSwipeDisallowed;
private Paint overlayPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint foregroundPaint = new Paint();
private Path mPath = new Path();
private RectF mRect = new RectF();
private OnSwipeBackProgressListener onSwipeBackProgressListener;
private boolean isSwipeBackDisallowed;
private float overrideForegroundHeight;
private ValueAnimator foregroundAnimator;
private int currentForegroundIndex = -1;
private int notificationIndex;
Theme.ResourcesProvider resourcesProvider;
private Rect hitRect = new Rect();
public PopupSwipeBackLayout(@NonNull Context context, Theme.ResourcesProvider resourcesProvider) {
super(context);
this.resourcesProvider = resourcesProvider;
int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
detector = new GestureDetectorCompat(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (!isProcessingSwipe && !isSwipeDisallowed) {
if (!isSwipeBackDisallowed && transitionProgress == 1 && distanceX <= -touchSlop && Math.abs(distanceX) >= Math.abs(distanceY * 1.5f) && !isDisallowedView(e2, getChildAt(transitionProgress > 0.5f ? 1 : 0))) {
isProcessingSwipe = true;
MotionEvent c = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
for (int i = 0; i < getChildCount(); i++)
getChildAt(i).dispatchTouchEvent(c);
c.recycle();
} else isSwipeDisallowed = true;
}
if (isProcessingSwipe) {
toProgress = -1;
transitionProgress = 1f - Math.max(0, Math.min(1, (e2.getX() - e1.getX()) / getWidth()));
invalidateTransforms();
}
return isProcessingSwipe;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (isAnimationInProgress || isSwipeDisallowed)
return false;
if (velocityX >= 600) {
clearFlags();
animateToState(0, velocityX / 6000f);
}
return false;
}
});
overlayPaint.setColor(Color.BLACK);
}
/**
* Sets if swipeback action should be disallowed
*
* @param swipeBackDisallowed If swipe should be disallowed
*/
public void setSwipeBackDisallowed(boolean swipeBackDisallowed) {
isSwipeBackDisallowed = swipeBackDisallowed;
}
/**
* Sets new swipeback listener
*
* @param onSwipeBackProgressListener New progress listener
*/
public void setOnSwipeBackProgressListener(OnSwipeBackProgressListener onSwipeBackProgressListener) {
this.onSwipeBackProgressListener = onSwipeBackProgressListener;
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
int i = indexOfChild(child);
int s = canvas.save();
if (i != 0) {
foregroundPaint.setColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuBackground, resourcesProvider));
canvas.drawRect(child.getX(), 0, child.getX() + child.getMeasuredWidth(), getMeasuredHeight(), foregroundPaint);
}
boolean b = super.drawChild(canvas, child, drawingTime);
if (i == 0) {
overlayPaint.setAlpha((int) (transitionProgress * 0x40));
canvas.drawRect(0, 0, getWidth(), getHeight(), overlayPaint);
}
canvas.restoreToCount(s);
return b;
}
/**
* Invalidates transformations
*/
private void invalidateTransforms() {
if (onSwipeBackProgressListener != null) {
onSwipeBackProgressListener.onSwipeBackProgress(this, toProgress, transitionProgress);
}
View bg = getChildAt(0);
View fg = null;
if (currentForegroundIndex >= 0 && currentForegroundIndex < getChildCount()) {
fg = getChildAt(currentForegroundIndex);
}
bg.setTranslationX(-transitionProgress * getWidth() * 0.5f);
float bSc = 0.95f + (1f - transitionProgress) * 0.05f;
bg.setScaleX(bSc);
bg.setScaleY(bSc);
if (fg != null) {
fg.setTranslationX((1f - transitionProgress) * getWidth());
}
invalidateVisibility();
float fW = bg.getMeasuredWidth(), fH = bg.getMeasuredHeight();
float tW = 0;
float tH = 0;
if (fg != null) {
tW = fg.getMeasuredWidth();
tH = overrideForegroundHeight != 0 ? overrideForegroundHeight : fg.getMeasuredHeight();
}
if (bg.getMeasuredWidth() == 0 || bg.getMeasuredHeight() == 0) {
return;
}
ActionBarPopupWindow.ActionBarPopupWindowLayout p = (ActionBarPopupWindow.ActionBarPopupWindowLayout) getParent();
float w = fW + (tW - fW) * transitionProgress;
float h = fH + (tH - fH) * transitionProgress;
w += p.getPaddingLeft() + p.getPaddingRight();
h += p.getPaddingTop() + p.getPaddingBottom();
p.setBackScaleX(w / p.getMeasuredWidth());
p.setBackScaleY(h / p.getMeasuredHeight());
for (int i = 0; i < getChildCount(); i++) {
View ch = getChildAt(i);
ch.setPivotX(0);
ch.setPivotY(0);
}
invalidate();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (processTouchEvent(ev))
return true;
int act = ev.getActionMasked();
if (act == MotionEvent.ACTION_DOWN && !mRect.contains(ev.getX(), ev.getY())) {
callOnClick();
return true;
}
if (currentForegroundIndex < 0 || currentForegroundIndex >= getChildCount()) {
return super.dispatchTouchEvent(ev);
}
View bv = getChildAt(0);
View fv = getChildAt(currentForegroundIndex);
boolean b = (transitionProgress > 0.5f ? fv : bv).dispatchTouchEvent(ev);
if (!b && act == MotionEvent.ACTION_DOWN) {
return true;
}
return b || onTouchEvent(ev);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
invalidateTransforms();
}
/**
* Processes touch event and return true if processed
*
* @param ev Event to process
* @return If event is processed
*/
private boolean processTouchEvent(MotionEvent ev) {
int act = ev.getAction() & MotionEvent.ACTION_MASK;
if (isAnimationInProgress)
return true;
if (!detector.onTouchEvent(ev)) {
switch (act) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (isProcessingSwipe) {
clearFlags();
animateToState(transitionProgress >= 0.5f ? 1 : 0, 0);
} else if (isSwipeDisallowed) clearFlags();
return false;
}
}
return isProcessingSwipe;
}
/**
* Animates transition value
*
* @param f End value
* @param flingVal Fling value(If from fling, zero otherwise)
*/
private void animateToState(float f, float flingVal) {
ValueAnimator val = ValueAnimator.ofFloat(transitionProgress, f).setDuration((long) (DURATION * Math.max(0.5f, Math.abs(transitionProgress - f) - Math.min(0.2f, flingVal))));
val.setInterpolator(CubicBezierInterpolator.DEFAULT);
int selectedAccount = UserConfig.selectedAccount;
notificationIndex = NotificationCenter.getInstance(selectedAccount).setAnimationInProgress(notificationIndex, null);
val.addUpdateListener(animation -> {
transitionProgress = (float) animation.getAnimatedValue();
invalidateTransforms();
});
val.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
isAnimationInProgress = true;
toProgress = f;
}
@Override
public void onAnimationEnd(Animator animation) {
NotificationCenter.getInstance(selectedAccount).onAnimationFinish(notificationIndex);
transitionProgress = f;
invalidateTransforms();
isAnimationInProgress = false;
}
});
val.start();
}
/**
* Clears touch flags
*/
private void clearFlags() {
isProcessingSwipe = false;
isSwipeDisallowed = false;
}
/**
* Opens up foreground
*/
public void openForeground(int viewIndex) {
if (isAnimationInProgress) {
return;
}
currentForegroundIndex = viewIndex;
overrideForegroundHeight = overrideHeightIndex.get(viewIndex);
animateToState(1, 0);
}
/**
* Closes foreground view
*/
public void closeForeground() {
if (isAnimationInProgress) return;
animateToState(0, 0);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
for (int i = 0; i < getChildCount(); i++) {
View ch = getChildAt(i);
ch.layout(0, 0, ch.getMeasuredWidth(), ch.getMeasuredHeight());
}
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
invalidateTransforms();
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (getChildCount() == 0) {
return;
}
View backgroundView = getChildAt(0);
float fW = backgroundView.getMeasuredWidth(), fH = backgroundView.getMeasuredHeight();
float w, h;
if (currentForegroundIndex == -1 || currentForegroundIndex >= getChildCount()) {
w = fW;
h = fH;
} else {
View foregroundView = getChildAt(currentForegroundIndex);
float tW = foregroundView.getMeasuredWidth(), tH = overrideForegroundHeight != 0 ? overrideForegroundHeight : foregroundView.getMeasuredHeight();
if (backgroundView.getMeasuredWidth() == 0 || backgroundView.getMeasuredHeight() == 0 || foregroundView.getMeasuredWidth() == 0 || foregroundView.getMeasuredHeight() == 0) {
w = fW;
h = fH;
} else {
w = fW + (tW - fW) * transitionProgress;
h = fH + (tH - fH) * transitionProgress;
}
}
int s = canvas.save();
mPath.rewind();
int rad = AndroidUtilities.dp(6);
mRect.set(0, 0, w, h);
mPath.addRoundRect(mRect, rad, rad, Path.Direction.CW);
canvas.clipPath(mPath);
super.dispatchDraw(canvas);
canvas.restoreToCount(s);
}
/**
* @param e Motion event to check
* @param v View to check
* @return If we should ignore view
*/
private boolean isDisallowedView(MotionEvent e, View v) {
v.getHitRect(hitRect);
if (hitRect.contains((int) e.getX(), (int) e.getY()) && v.canScrollHorizontally(-1))
return true;
if (v instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) v;
for (int i = 0; i < vg.getChildCount(); i++)
if (isDisallowedView(e, vg.getChildAt(i)))
return true;
}
return false;
}
/**
* Invalidates view transforms
*/
private void invalidateVisibility() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (i == 0) {
if (transitionProgress == 1 && child.getVisibility() != INVISIBLE)
child.setVisibility(INVISIBLE);
if (transitionProgress != 1 && child.getVisibility() != VISIBLE)
child.setVisibility(VISIBLE);
} else if (i == currentForegroundIndex) {
if (transitionProgress == 0 && child.getVisibility() != INVISIBLE)
child.setVisibility(INVISIBLE);
if (transitionProgress != 0 && child.getVisibility() != VISIBLE)
child.setVisibility(VISIBLE);
} else {
child.setVisibility(INVISIBLE);
}
}
}
public void setNewForegroundHeight(int index, int height) {
overrideHeightIndex.put(index, height);
if (index != currentForegroundIndex) {
return;
}
if (currentForegroundIndex < 0 || currentForegroundIndex >= getChildCount()) {
return;
}
if (foregroundAnimator != null) {
foregroundAnimator.cancel();
}
View fg = getChildAt(currentForegroundIndex);
float fromH = overrideForegroundHeight != 0 ? overrideForegroundHeight : fg.getMeasuredHeight();
float toH = height;
ValueAnimator animator = ValueAnimator.ofFloat(fromH, toH).setDuration(240);
animator.setInterpolator(Easings.easeInOutQuad);
animator.addUpdateListener(animation -> {
overrideForegroundHeight = (float) animation.getAnimatedValue();
invalidateTransforms();
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isAnimationInProgress = false;
}
@Override
public void onAnimationStart(Animator animation) {
isAnimationInProgress = true;
}
});
animator.start();
foregroundAnimator = animator;
}
public interface OnSwipeBackProgressListener {
void onSwipeBackProgress(PopupSwipeBackLayout layout, float toProgress, float progress);
}
}