2019-12-31 14:08:08 +01:00
|
|
|
package org.telegram.ui.Components;
|
|
|
|
|
|
|
|
import android.animation.Animator;
|
|
|
|
import android.animation.AnimatorListenerAdapter;
|
|
|
|
import android.animation.ValueAnimator;
|
2020-03-30 14:00:09 +02:00
|
|
|
import android.util.SparseArray;
|
2019-12-31 14:08:08 +01:00
|
|
|
import android.view.View;
|
|
|
|
|
|
|
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
|
|
|
|
|
|
import org.telegram.ui.Cells.ChatMessageCell;
|
|
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
|
|
|
public class RecyclerAnimationScrollHelper {
|
|
|
|
|
|
|
|
public final static int SCROLL_DIRECTION_UNSET = -1;
|
|
|
|
public final static int SCROLL_DIRECTION_DOWN = 0;
|
|
|
|
public final static int SCROLL_DIRECTION_UP = 1;
|
|
|
|
|
|
|
|
private RecyclerListView recyclerView;
|
|
|
|
private LinearLayoutManager layoutManager;
|
|
|
|
|
|
|
|
private int scrollDirection;
|
2020-02-13 19:26:53 +01:00
|
|
|
private ValueAnimator animator;
|
2019-12-31 14:08:08 +01:00
|
|
|
|
2020-02-13 19:26:53 +01:00
|
|
|
private ScrollListener scrollListener;
|
2019-12-31 14:08:08 +01:00
|
|
|
|
2020-02-13 19:26:53 +01:00
|
|
|
private AnimationCallback animationCallback;
|
2019-12-31 14:08:08 +01:00
|
|
|
|
2020-03-30 14:00:09 +02:00
|
|
|
public SparseArray<View> positionToOldView = new SparseArray<>();
|
|
|
|
|
2019-12-31 14:08:08 +01:00
|
|
|
public RecyclerAnimationScrollHelper(RecyclerListView recyclerView, LinearLayoutManager layoutManager) {
|
|
|
|
this.recyclerView = recyclerView;
|
|
|
|
this.layoutManager = layoutManager;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void scrollToPosition(int position, int offset) {
|
|
|
|
scrollToPosition(position, offset, layoutManager.getReverseLayout(), false);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void scrollToPosition(int position, int offset, boolean bottom) {
|
|
|
|
scrollToPosition(position, offset, bottom, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void scrollToPosition(int position, int offset, final boolean bottom, boolean smooth) {
|
|
|
|
if (recyclerView.animationRunning) return;
|
|
|
|
if (!smooth || scrollDirection == SCROLL_DIRECTION_UNSET) {
|
|
|
|
layoutManager.scrollToPositionWithOffset(position, offset, bottom);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
int n = recyclerView.getChildCount();
|
|
|
|
if (n == 0) {
|
|
|
|
layoutManager.scrollToPositionWithOffset(position, offset, bottom);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
boolean scrollDown = scrollDirection == SCROLL_DIRECTION_DOWN;
|
|
|
|
|
|
|
|
recyclerView.setScrollEnabled(false);
|
|
|
|
int h = 0;
|
|
|
|
int t = 0;
|
|
|
|
final ArrayList<View> oldViews = new ArrayList<>();
|
2020-03-30 14:00:09 +02:00
|
|
|
positionToOldView.clear();
|
2020-04-24 11:21:58 +02:00
|
|
|
final ArrayList<RecyclerView.ViewHolder> oldHolders = new ArrayList<>();
|
|
|
|
recyclerView.getRecycledViewPool().clear();
|
|
|
|
|
2019-12-31 14:08:08 +01:00
|
|
|
for (int i = 0; i < n; i++) {
|
|
|
|
View child = recyclerView.getChildAt(0);
|
|
|
|
oldViews.add(child);
|
2020-04-24 11:21:58 +02:00
|
|
|
RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(child);
|
|
|
|
if (holder != null) {
|
|
|
|
oldHolders.add(holder);
|
|
|
|
}
|
2020-03-30 14:00:09 +02:00
|
|
|
positionToOldView.put(layoutManager.getPosition(child), child);
|
2019-12-31 14:08:08 +01:00
|
|
|
|
|
|
|
int bot = child.getBottom();
|
|
|
|
int top = child.getTop();
|
|
|
|
if (bot > h) h = bot;
|
|
|
|
if (top < t) t = top;
|
|
|
|
|
|
|
|
if (child instanceof ChatMessageCell) {
|
2020-03-30 14:00:09 +02:00
|
|
|
((ChatMessageCell) child).setAnimationRunning(true, true);
|
2019-12-31 14:08:08 +01:00
|
|
|
}
|
|
|
|
recyclerView.removeView(child);
|
|
|
|
}
|
|
|
|
|
|
|
|
final int finalHeight = scrollDown ? h : recyclerView.getHeight() - t;
|
|
|
|
|
|
|
|
RecyclerView.Adapter adapter = recyclerView.getAdapter();
|
|
|
|
AnimatableAdapter animatableAdapter = null;
|
|
|
|
if (adapter instanceof AnimatableAdapter) {
|
|
|
|
animatableAdapter = (AnimatableAdapter) adapter;
|
|
|
|
}
|
|
|
|
|
|
|
|
layoutManager.scrollToPositionWithOffset(position, offset, bottom);
|
|
|
|
if (adapter != null) adapter.notifyDataSetChanged();
|
|
|
|
AnimatableAdapter finalAnimatableAdapter = animatableAdapter;
|
|
|
|
|
|
|
|
recyclerView.stopScroll();
|
|
|
|
recyclerView.setVerticalScrollBarEnabled(false);
|
|
|
|
if (animationCallback != null) animationCallback.onStartAnimation();
|
|
|
|
|
|
|
|
recyclerView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
|
|
|
|
@Override
|
|
|
|
public void onLayoutChange(View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) {
|
|
|
|
final ArrayList<View> incomingViews = new ArrayList<>();
|
|
|
|
|
|
|
|
recyclerView.stopScroll();
|
|
|
|
int n = recyclerView.getChildCount();
|
|
|
|
int top = 0;
|
|
|
|
int bottom = 0;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
|
|
View child = recyclerView.getChildAt(i);
|
|
|
|
incomingViews.add(child);
|
|
|
|
if (child.getTop() < top)
|
|
|
|
top = child.getTop();
|
|
|
|
if (child.getBottom() > bottom)
|
|
|
|
bottom = child.getBottom();
|
|
|
|
|
|
|
|
if (child instanceof ChatMessageCell) {
|
2020-03-30 14:00:09 +02:00
|
|
|
((ChatMessageCell) child).setAnimationRunning(true, false);
|
2019-12-31 14:08:08 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (View view : oldViews) {
|
2020-04-24 11:21:58 +02:00
|
|
|
if (view.getParent() == null) {
|
|
|
|
recyclerView.addView(view);
|
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
if (view instanceof ChatMessageCell) {
|
2020-03-30 14:00:09 +02:00
|
|
|
((ChatMessageCell) view).setAnimationRunning(true, true);
|
2019-12-31 14:08:08 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
recyclerView.animationRunning = true;
|
|
|
|
if (finalAnimatableAdapter != null) finalAnimatableAdapter.onAnimationStart();
|
|
|
|
|
|
|
|
final int scrollLength = finalHeight + (scrollDown ? -top : bottom - recyclerView.getHeight());
|
|
|
|
|
|
|
|
if (animator != null) {
|
|
|
|
animator.removeAllListeners();
|
|
|
|
animator.cancel();
|
|
|
|
}
|
|
|
|
animator = ValueAnimator.ofFloat(0, 1f);
|
|
|
|
animator.addUpdateListener(animation -> {
|
|
|
|
float value = ((float) animation.getAnimatedValue());
|
2020-03-30 14:00:09 +02:00
|
|
|
int size = oldViews.size();
|
|
|
|
for (int i = 0; i < size; i++) {
|
|
|
|
View view = oldViews.get(i);
|
|
|
|
float viewTop = view.getY();
|
|
|
|
float viewBottom = view.getY() + view.getMeasuredHeight();
|
|
|
|
if (viewBottom < 0 || viewTop > recyclerView.getMeasuredHeight()) {
|
|
|
|
continue;
|
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
if (scrollDown) {
|
|
|
|
view.setTranslationY(-scrollLength * value);
|
|
|
|
} else {
|
|
|
|
view.setTranslationY(scrollLength * value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-30 14:00:09 +02:00
|
|
|
size = incomingViews.size();
|
|
|
|
for (int i = 0; i < size; i++) {
|
|
|
|
View view = incomingViews.get(i);
|
2019-12-31 14:08:08 +01:00
|
|
|
if (scrollDown) {
|
|
|
|
view.setTranslationY((scrollLength) * (1f - value));
|
|
|
|
} else {
|
|
|
|
view.setTranslationY(-(scrollLength) * (1f - value));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
recyclerView.invalidate();
|
2020-03-30 14:00:09 +02:00
|
|
|
if (scrollListener != null) scrollListener.onScroll();
|
2019-12-31 14:08:08 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
animator.addListener(new AnimatorListenerAdapter() {
|
|
|
|
@Override
|
|
|
|
public void onAnimationEnd(Animator animation) {
|
|
|
|
recyclerView.animationRunning = false;
|
|
|
|
|
|
|
|
for (View view : oldViews) {
|
|
|
|
if (view instanceof ChatMessageCell) {
|
2020-03-30 14:00:09 +02:00
|
|
|
((ChatMessageCell) view).setAnimationRunning(false, true);
|
2019-12-31 14:08:08 +01:00
|
|
|
}
|
|
|
|
view.setTranslationY(0);
|
2020-03-30 14:00:09 +02:00
|
|
|
recyclerView.removeView(view);
|
2019-12-31 14:08:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
recyclerView.setVerticalScrollBarEnabled(true);
|
|
|
|
|
|
|
|
int n = recyclerView.getChildCount();
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
|
|
View child = recyclerView.getChildAt(i);
|
|
|
|
if (child instanceof ChatMessageCell) {
|
2020-03-30 14:00:09 +02:00
|
|
|
((ChatMessageCell) child).setAnimationRunning(false, false);
|
2019-12-31 14:08:08 +01:00
|
|
|
}
|
|
|
|
child.setTranslationY(0);
|
|
|
|
}
|
2020-01-23 13:58:50 +01:00
|
|
|
|
2019-12-31 14:08:08 +01:00
|
|
|
if (finalAnimatableAdapter != null) {
|
|
|
|
finalAnimatableAdapter.onAnimationEnd();
|
|
|
|
}
|
|
|
|
|
2020-01-23 13:58:50 +01:00
|
|
|
if (animationCallback != null) {
|
|
|
|
animationCallback.onEndAnimation();
|
|
|
|
}
|
|
|
|
|
2020-03-30 14:00:09 +02:00
|
|
|
positionToOldView.clear();
|
|
|
|
|
2019-12-31 14:08:08 +01:00
|
|
|
animator = null;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
recyclerView.removeOnLayoutChangeListener(this);
|
|
|
|
|
2020-04-24 11:21:58 +02:00
|
|
|
long duration = (long) (((scrollLength / (float) recyclerView.getMeasuredHeight()) + 1f) * 200L);
|
2019-12-31 14:08:08 +01:00
|
|
|
|
|
|
|
duration = Math.min(duration, 1300);
|
|
|
|
|
|
|
|
animator.setDuration(duration);
|
|
|
|
animator.setInterpolator(CubicBezierInterpolator.EASE_OUT_QUINT);
|
|
|
|
animator.start();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public void cancel() {
|
|
|
|
if (animator != null) animator.cancel();
|
|
|
|
clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void clear() {
|
|
|
|
recyclerView.setVerticalScrollBarEnabled(true);
|
|
|
|
recyclerView.animationRunning = false;
|
|
|
|
RecyclerView.Adapter adapter = recyclerView.getAdapter();
|
|
|
|
if (adapter instanceof AnimatableAdapter)
|
|
|
|
((AnimatableAdapter) adapter).onAnimationEnd();
|
|
|
|
animator = null;
|
|
|
|
|
|
|
|
int n = recyclerView.getChildCount();
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
|
|
View child = recyclerView.getChildAt(i);
|
|
|
|
child.setTranslationY(0f);
|
|
|
|
if (child instanceof ChatMessageCell) {
|
2020-03-30 14:00:09 +02:00
|
|
|
((ChatMessageCell) child).setAnimationRunning(false, false);
|
2019-12-31 14:08:08 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setScrollDirection(int scrollDirection) {
|
|
|
|
this.scrollDirection = scrollDirection;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setScrollListener(ScrollListener listener) {
|
|
|
|
scrollListener = listener;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setAnimationCallback(AnimationCallback animationCallback) {
|
|
|
|
this.animationCallback = animationCallback;
|
|
|
|
}
|
|
|
|
|
|
|
|
public int getScrollDirection() {
|
|
|
|
return scrollDirection;
|
|
|
|
}
|
|
|
|
|
|
|
|
public interface ScrollListener {
|
|
|
|
void onScroll();
|
|
|
|
}
|
|
|
|
|
|
|
|
public static class AnimationCallback {
|
|
|
|
public void onStartAnimation() {
|
|
|
|
}
|
|
|
|
|
|
|
|
public void onEndAnimation() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static abstract class AnimatableAdapter extends RecyclerListView.SelectionAdapter {
|
|
|
|
|
|
|
|
public boolean animationRunning;
|
|
|
|
private boolean shouldNotifyDataSetChanged;
|
|
|
|
private ArrayList<Integer> rangeInserted = new ArrayList<>();
|
|
|
|
private ArrayList<Integer> rangeRemoved = new ArrayList<>();
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void notifyDataSetChanged() {
|
|
|
|
if (!animationRunning) {
|
|
|
|
super.notifyDataSetChanged();
|
|
|
|
} else {
|
2020-01-23 13:58:50 +01:00
|
|
|
shouldNotifyDataSetChanged = true;
|
2019-12-31 14:08:08 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void notifyItemRangeInserted(int positionStart, int itemCount) {
|
|
|
|
if (!animationRunning) {
|
|
|
|
super.notifyItemRangeInserted(positionStart, itemCount);
|
|
|
|
} else {
|
|
|
|
rangeInserted.add(positionStart);
|
|
|
|
rangeInserted.add(itemCount);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void notifyItemRangeRemoved(int positionStart, int itemCount) {
|
|
|
|
if (!animationRunning) {
|
|
|
|
super.notifyItemRangeRemoved(positionStart, itemCount);
|
|
|
|
} else {
|
|
|
|
rangeRemoved.add(positionStart);
|
|
|
|
rangeRemoved.add(itemCount);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-24 11:21:58 +02:00
|
|
|
@Override
|
|
|
|
public void notifyItemRangeChanged(int positionStart, int itemCount) {
|
|
|
|
if (!animationRunning) {
|
|
|
|
super.notifyItemRangeChanged(positionStart, itemCount);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-31 14:08:08 +01:00
|
|
|
public void onAnimationStart() {
|
|
|
|
animationRunning = true;
|
|
|
|
shouldNotifyDataSetChanged = false;
|
|
|
|
rangeInserted.clear();
|
|
|
|
rangeRemoved.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void onAnimationEnd() {
|
|
|
|
animationRunning = false;
|
2020-01-23 13:58:50 +01:00
|
|
|
if (shouldNotifyDataSetChanged || !rangeInserted.isEmpty() || !rangeRemoved.isEmpty()) {
|
2019-12-31 14:08:08 +01:00
|
|
|
notifyDataSetChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|