package org.telegram.ui.Components; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.os.SystemClock; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.graphics.ColorUtils; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.Emoji; import org.telegram.messenger.MessagesController; import org.telegram.messenger.MessagesStorage; import org.telegram.messenger.UserConfig; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.Theme; import java.util.ArrayList; public class ViewPagerFixed extends FrameLayout { int currentPosition; int nextPosition; private View viewPages[]; private int viewTypes[]; protected SparseArray viewsByType = new SparseArray<>(); private int startedTrackingPointerId; private int startedTrackingX; private int startedTrackingY; private VelocityTracker velocityTracker; private AnimatorSet tabsAnimation; private boolean tabsAnimationInProgress; private boolean animatingForward; private float additionalOffset; private boolean backAnimation; private int maximumVelocity; private boolean startedTracking; private boolean maybeStartTracking; private static final Interpolator interpolator = t -> { --t; return t * t * t * t * t + 1.0F; }; private final float touchSlop; private Adapter adapter; TabsView tabsView; ValueAnimator.AnimatorUpdateListener updateTabProgress = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { if (tabsAnimationInProgress) { float scrollProgress = Math.abs(viewPages[0].getTranslationX()) / (float) viewPages[0].getMeasuredWidth(); if (tabsView != null) { tabsView.selectTab(nextPosition, currentPosition, 1f - scrollProgress); } } } }; private Rect rect = new Rect(); public ViewPagerFixed(@NonNull Context context) { super(context); touchSlop = AndroidUtilities.getPixelsInCM(0.3f, true); maximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity(); viewTypes = new int[2]; viewPages = new View[2]; setClipChildren(true); } public void setAdapter(Adapter adapter) { this.adapter = adapter; viewTypes[0] = adapter.getItemViewType(currentPosition); viewPages[0] = adapter.createView(viewTypes[0]); adapter.bindView(viewPages[0], currentPosition, viewTypes[0]); addView(viewPages[0]); viewPages[0].setVisibility(View.VISIBLE); fillTabs(); } public TabsView createTabsView() { tabsView = new TabsView(getContext()); tabsView.setDelegate(new TabsView.TabsViewDelegate() { @Override public void onPageSelected(int page, boolean forward) { animatingForward = forward; nextPosition = page; updateViewForIndex(1); if (forward) { viewPages[1].setTranslationX(viewPages[0].getMeasuredWidth()); } else { viewPages[1].setTranslationX(-viewPages[0].getMeasuredWidth()); } } @Override public void onPageScrolled(float progress) { if (progress == 1f) { if (viewPages[1] != null) { swapViews(); viewsByType.put(viewTypes[1], viewPages[1]); removeView(viewPages[1]); viewPages[0].setTranslationX(0); viewPages[1] = null; } return; } if (viewPages[1] == null) { return; } if (animatingForward) { viewPages[1].setTranslationX(viewPages[0].getMeasuredWidth() * (1f - progress)); viewPages[0].setTranslationX(-viewPages[0].getMeasuredWidth() * progress); } else { viewPages[1].setTranslationX(-viewPages[0].getMeasuredWidth() * (1f - progress)); viewPages[0].setTranslationX(viewPages[0].getMeasuredWidth() * progress); } } @Override public void onSamePageSelected() { } @Override public boolean canPerformActions() { return !tabsAnimationInProgress && !startedTracking; } }); fillTabs(); return tabsView; } private void updateViewForIndex(int index) { int adapterPosition = index == 0 ? currentPosition : nextPosition; if (viewPages[index] == null) { viewTypes[index] = adapter.getItemViewType(adapterPosition); View v = viewsByType.get(viewTypes[index]); if (v == null) { v = adapter.createView(viewTypes[index]); } else { viewsByType.remove(viewTypes[index]); } if (v.getParent() != null) { ViewGroup parent = (ViewGroup) v.getParent(); parent.removeView(v); } addView(v); viewPages[index] = v; adapter.bindView(viewPages[index], adapterPosition, viewTypes[index]); viewPages[index].setVisibility(View.VISIBLE); } else { if (viewTypes[index] == adapter.getItemViewType(adapterPosition)) { adapter.bindView(viewPages[index], adapterPosition, viewTypes[index]); viewPages[index].setVisibility(View.VISIBLE); } else { viewsByType.put(viewTypes[index], viewPages[index]); viewPages[index].setVisibility(View.GONE); removeView(viewPages[index]); viewTypes[index] = adapter.getItemViewType(adapterPosition); View v = viewsByType.get(viewTypes[index]); if (v == null) { v = adapter.createView(viewTypes[index]); } else { viewsByType.remove(viewTypes[index]); } addView(v); viewPages[index] = v; viewPages[index].setVisibility(View.VISIBLE); adapter.bindView(viewPages[index], adapterPosition, adapter.getItemViewType(adapterPosition)); } } } private void fillTabs() { if (adapter != null && tabsView != null) { tabsView.removeTabs(); for (int i = 0; i < adapter.getItemCount(); i++) { tabsView.addTab(adapter.getItemId(i), adapter.getItemTitle(i)); } } } private boolean prepareForMoving(MotionEvent ev, boolean forward) { if ((!forward && currentPosition == 0) || (forward && currentPosition == adapter.getItemCount() - 1)) { return false; } getParent().requestDisallowInterceptTouchEvent(true); maybeStartTracking = false; startedTracking = true; startedTrackingX = (int) (ev.getX() + additionalOffset); if (tabsView != null) { tabsView.setEnabled(false); } animatingForward = forward; nextPosition = currentPosition + (forward ? 1 : -1); updateViewForIndex(1); if (forward) { viewPages[1].setTranslationX(viewPages[0].getMeasuredWidth()); } else { viewPages[1].setTranslationX(-viewPages[0].getMeasuredWidth()); } return true; } public boolean onInterceptTouchEvent(MotionEvent ev) { if (tabsView != null && tabsView.isAnimatingIndicator()) { return false; } if (checkTabsAnimationInProgress()) { return true; } onTouchEvent(ev); return startedTracking; } @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (maybeStartTracking && !startedTracking) { onTouchEvent(null); } super.requestDisallowInterceptTouchEvent(disallowIntercept); } @Override public boolean onTouchEvent(MotionEvent ev) { if (tabsView != null && tabsView.animatingIndicator) { return false; } if (ev != null) { if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } velocityTracker.addMovement(ev); } if (ev != null && ev.getAction() == MotionEvent.ACTION_DOWN && checkTabsAnimationInProgress()) { startedTracking = true; startedTrackingPointerId = ev.getPointerId(0); startedTrackingX = (int) ev.getX(); if (animatingForward) { if (startedTrackingX < viewPages[0].getMeasuredWidth() + viewPages[0].getTranslationX()) { additionalOffset = viewPages[0].getTranslationX(); } else { swapViews(); animatingForward = false; additionalOffset = viewPages[0].getTranslationX(); } } else { if (startedTrackingX < viewPages[1].getMeasuredWidth() + viewPages[1].getTranslationX()) { swapViews(); animatingForward = true; additionalOffset = viewPages[0].getTranslationX(); } else { additionalOffset = viewPages[0].getTranslationX(); } } tabsAnimation.removeAllListeners(); tabsAnimation.cancel(); tabsAnimationInProgress = false; } else if (ev != null && ev.getAction() == MotionEvent.ACTION_DOWN) { additionalOffset = 0; } if (!startedTracking && ev != null) { View child = findScrollingChild(this, ev.getX(), ev.getY()); if (child != null && (child.canScrollHorizontally(1) || child.canScrollHorizontally(-1))) { return false; } } if (ev != null && ev.getAction() == MotionEvent.ACTION_DOWN && !startedTracking && !maybeStartTracking) { startedTrackingPointerId = ev.getPointerId(0); maybeStartTracking = true; startedTrackingX = (int) ev.getX(); startedTrackingY = (int) ev.getY(); } else if (ev != null && ev.getAction() == MotionEvent.ACTION_MOVE && ev.getPointerId(0) == startedTrackingPointerId) { int dx = (int) (ev.getX() - startedTrackingX + additionalOffset); int dy = Math.abs((int) ev.getY() - startedTrackingY); if (startedTracking && (animatingForward && dx > 0 || !animatingForward && dx < 0)) { if (!prepareForMoving(ev, dx < 0)) { maybeStartTracking = true; startedTracking = false; viewPages[0].setTranslationX(0); viewPages[1].setTranslationX(animatingForward ? viewPages[0].getMeasuredWidth() : -viewPages[0].getMeasuredWidth()); if (tabsView != null) { tabsView.selectTab(currentPosition, 0, 0); } } } if (maybeStartTracking && !startedTracking) { int dxLocal = (int) (ev.getX() - startedTrackingX); if (Math.abs(dxLocal) >= touchSlop && Math.abs(dxLocal) > dy) { prepareForMoving(ev, dx < 0); } } else if (startedTracking) { viewPages[0].setTranslationX(dx); if (animatingForward) { viewPages[1].setTranslationX(viewPages[0].getMeasuredWidth() + dx); } else { viewPages[1].setTranslationX(dx - viewPages[0].getMeasuredWidth()); } float scrollProgress = Math.abs(dx) / (float) viewPages[0].getMeasuredWidth(); if (tabsView != null) { tabsView.selectTab(nextPosition, currentPosition, 1f - scrollProgress); } } } else if (ev == null || ev.getPointerId(0) == startedTrackingPointerId && (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_POINTER_UP)) { velocityTracker.computeCurrentVelocity(1000, maximumVelocity); float velX; float velY; if (ev != null && ev.getAction() != MotionEvent.ACTION_CANCEL) { velX = velocityTracker.getXVelocity(); velY = velocityTracker.getYVelocity(); if (!startedTracking) { if (Math.abs(velX) >= 3000 && Math.abs(velX) > Math.abs(velY)) { prepareForMoving(ev, velX < 0); } } } else { velX = 0; velY = 0; } if (startedTracking) { float x = viewPages[0].getX(); tabsAnimation = new AnimatorSet(); if (additionalOffset != 0) { if (Math.abs(velX) > 1500) { backAnimation = animatingForward ? velX > 0 : velX < 0; } else { if (animatingForward) { backAnimation = (viewPages[1].getX() > (viewPages[0].getMeasuredWidth() >> 1)); } else { backAnimation = (viewPages[0].getX() < (viewPages[0].getMeasuredWidth() >> 1)); } } } else { backAnimation = Math.abs(x) < viewPages[0].getMeasuredWidth() / 3.0f && (Math.abs(velX) < 3500 || Math.abs(velX) < Math.abs(velY)); } float distToMove; float dx; if (backAnimation) { dx = Math.abs(x); if (animatingForward) { tabsAnimation.playTogether( ObjectAnimator.ofFloat(viewPages[0], View.TRANSLATION_X, 0), ObjectAnimator.ofFloat(viewPages[1], View.TRANSLATION_X, viewPages[1].getMeasuredWidth()) ); } else { tabsAnimation.playTogether( ObjectAnimator.ofFloat(viewPages[0], View.TRANSLATION_X, 0), ObjectAnimator.ofFloat(viewPages[1], View.TRANSLATION_X, -viewPages[1].getMeasuredWidth()) ); } } else { dx = viewPages[0].getMeasuredWidth() - Math.abs(x); if (animatingForward) { tabsAnimation.playTogether( ObjectAnimator.ofFloat(viewPages[0], View.TRANSLATION_X, -viewPages[0].getMeasuredWidth()), ObjectAnimator.ofFloat(viewPages[1], View.TRANSLATION_X, 0) ); } else { tabsAnimation.playTogether( ObjectAnimator.ofFloat(viewPages[0], View.TRANSLATION_X, viewPages[0].getMeasuredWidth()), ObjectAnimator.ofFloat(viewPages[1], View.TRANSLATION_X, 0) ); } } ValueAnimator animator = ValueAnimator.ofFloat(0,1f); animator.addUpdateListener(updateTabProgress); tabsAnimation.playTogether(animator); tabsAnimation.setInterpolator(interpolator); int width = getMeasuredWidth(); int halfWidth = width / 2; float distanceRatio = Math.min(1.0f, 1.0f * dx / (float) width); float distance = (float) halfWidth + (float) halfWidth * distanceInfluenceForSnapDuration(distanceRatio); velX = Math.abs(velX); int duration; if (velX > 0) { duration = 4 * Math.round(1000.0f * Math.abs(distance / velX)); } else { float pageDelta = dx / getMeasuredWidth(); duration = (int) ((pageDelta + 1.0f) * 100.0f); } duration = Math.max(150, Math.min(duration, 600)); tabsAnimation.setDuration(duration); tabsAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { tabsAnimation = null; if (viewPages[1] != null) { if (!backAnimation) { swapViews(); } viewsByType.put(viewTypes[1], viewPages[1]); removeView(viewPages[1]); viewPages[1].setVisibility(View.GONE); viewPages[1] = null; } tabsAnimationInProgress = false; maybeStartTracking = false; if (tabsView != null) { tabsView.setEnabled(true); } } }); tabsAnimation.start(); tabsAnimationInProgress = true; startedTracking = false; } else { maybeStartTracking = false; if (tabsView != null) { tabsView.setEnabled(true); } } if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; } } return startedTracking || maybeStartTracking; } private void swapViews() { View page = viewPages[0]; viewPages[0] = viewPages[1]; viewPages[1] = page; int p = currentPosition; currentPosition = nextPosition; nextPosition = p; p = viewTypes[0]; viewTypes[0] = viewTypes[1]; viewTypes[1] = p; onItemSelected(viewPages[0], viewPages[1], currentPosition, nextPosition); } public boolean checkTabsAnimationInProgress() { if (tabsAnimationInProgress) { boolean cancel = false; if (backAnimation) { if (Math.abs(viewPages[0].getTranslationX()) < 1) { viewPages[0].setTranslationX(0); viewPages[1].setTranslationX(viewPages[0].getMeasuredWidth() * (animatingForward ? 1 : -1)); cancel = true; } } else if (Math.abs(viewPages[1].getTranslationX()) < 1) { viewPages[0].setTranslationX(viewPages[0].getMeasuredWidth() * (animatingForward ? -1 : 1)); viewPages[1].setTranslationX(0); cancel = true; } if (cancel) { //showScrollbars(true); if (tabsAnimation != null) { tabsAnimation.cancel(); tabsAnimation = null; } tabsAnimationInProgress = false; } return tabsAnimationInProgress; } return false; } public static float distanceInfluenceForSnapDuration(float f) { f -= 0.5F; f *= 0.47123894F; return (float) Math.sin(f); } public void setPosition(int position) { if (tabsAnimation != null) { tabsAnimation.cancel(); } if (viewPages[1] != null) { viewsByType.put(viewTypes[1], viewPages[1]); removeView(viewPages[1]); viewPages[1] = null; } if (currentPosition != position) { int oldPosition = currentPosition; currentPosition = position; View oldView = viewPages[0]; updateViewForIndex(0); onItemSelected(viewPages[0], oldView, currentPosition, oldPosition); viewPages[0].setTranslationX(0); if (tabsView != null) { tabsView.selectTab(position, 0, 1f); } } } protected void onItemSelected(View currentPage, View oldPage, int position, int oldPosition) { } public abstract static class Adapter { public abstract int getItemCount(); public abstract View createView(int viewType); public abstract void bindView(View view, int position, int viewType); public int getItemId(int position) { return position; } public String getItemTitle(int position) { return ""; } public int getItemViewType(int position) { return 0; } } @Override public boolean canScrollHorizontally(int direction) { if (direction == 0) { return false; } if (tabsAnimationInProgress || startedTracking) { return true; } boolean forward = direction > 0; if ((!forward && currentPosition == 0) || (forward && currentPosition == adapter.getItemCount() - 1)) { return false; } return true; } public View getCurrentView() { return viewPages[0]; } public int getCurrentPosition() { return currentPosition; } public static class TabsView extends FrameLayout { public interface TabsViewDelegate { void onPageSelected(int page, boolean forward); void onPageScrolled(float progress); void onSamePageSelected(); boolean canPerformActions(); } private static class Tab { public int id; public String title; public int titleWidth; public int counter; public Tab(int i, String t) { id = i; title = t; } public int getWidth(boolean store, TextPaint textPaint) { int width = titleWidth = (int) Math.ceil(textPaint.measureText(title)); return Math.max(AndroidUtilities.dp(40), width); } public boolean setTitle(String newTitle) { if (TextUtils.equals(title, newTitle)) { return false; } title = newTitle; return true; } } public class TabView extends View { private Tab currentTab; private int textHeight; private int tabWidth; private int currentPosition; private RectF rect = new RectF(); private String currentText; private StaticLayout textLayout; private int textOffsetX; public TabView(Context context) { super(context); } public void setTab(Tab tab, int position) { currentTab = tab; currentPosition = position; setContentDescription(tab.title); requestLayout(); } public int getId() { return currentTab.id; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int w = currentTab.getWidth(false, textPaint) + AndroidUtilities.dp(32) + additionalTabWidth; setMeasuredDimension(w, MeasureSpec.getSize(heightMeasureSpec)); } @SuppressLint("DrawAllocation") @Override protected void onDraw(Canvas canvas) { if (currentTab.id != Integer.MAX_VALUE && editingAnimationProgress != 0) { canvas.save(); float p = editingAnimationProgress * (currentPosition % 2 == 0 ? 1.0f : -1.0f); canvas.translate(AndroidUtilities.dp(0.66f) * p, 0); canvas.rotate(p, getMeasuredWidth() / 2, getMeasuredHeight() / 2); } String key; String animateToKey; String otherKey; String animateToOtherKey; String unreadKey; String unreadOtherKey; int id1; int id2; if (manualScrollingToId != -1) { id1 = manualScrollingToId; id2 = selectedTabId; } else { id1 = selectedTabId; id2 = previousId; } if (currentTab.id == id1) { key = activeTextColorKey; otherKey = unactiveTextColorKey; unreadKey = Theme.key_chats_tabUnreadActiveBackground; unreadOtherKey = Theme.key_chats_tabUnreadUnactiveBackground; } else { key = unactiveTextColorKey; otherKey = activeTextColorKey; unreadKey = Theme.key_chats_tabUnreadUnactiveBackground; unreadOtherKey = Theme.key_chats_tabUnreadActiveBackground; } if ((animatingIndicator || manualScrollingToId != -1) && (currentTab.id == id1 || currentTab.id == id2)) { textPaint.setColor(ColorUtils.blendARGB(Theme.getColor(otherKey), Theme.getColor(key), animatingIndicatorProgress)); } else { textPaint.setColor(Theme.getColor(key)); } int counterWidth; int countWidth; String counterText; if (currentTab.counter > 0) { counterText = String.format("%d", currentTab.counter); counterWidth = (int) Math.ceil(textCounterPaint.measureText(counterText)); countWidth = Math.max(AndroidUtilities.dp(10), counterWidth) + AndroidUtilities.dp(10); } else { counterText = null; counterWidth = 0; countWidth = 0; } if (currentTab.id != Integer.MAX_VALUE && (isEditing || editingStartAnimationProgress != 0)) { countWidth = (int) (countWidth + (AndroidUtilities.dp(20) - countWidth) * editingStartAnimationProgress); } tabWidth = currentTab.titleWidth + (countWidth != 0 ? countWidth + AndroidUtilities.dp(6 * (counterText != null ? 1.0f : editingStartAnimationProgress)) : 0); int textX = (getMeasuredWidth() - tabWidth) / 2; if (!TextUtils.equals(currentTab.title, currentText)) { currentText = currentTab.title; CharSequence text = Emoji.replaceEmoji(currentText, textPaint.getFontMetricsInt(), AndroidUtilities.dp(15), false); textLayout = new StaticLayout(text, textPaint, AndroidUtilities.dp(400), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0, false); textHeight = textLayout.getHeight(); textOffsetX = (int) -textLayout.getLineLeft(0); } if (textLayout != null) { canvas.save(); canvas.translate(textX + textOffsetX, (getMeasuredHeight() - textHeight) / 2 + 1); textLayout.draw(canvas); canvas.restore(); } if (counterText != null || currentTab.id != Integer.MAX_VALUE && (isEditing || editingStartAnimationProgress != 0)) { textCounterPaint.setColor(Theme.getColor(backgroundColorKey)); if (Theme.hasThemeKey(unreadKey) && Theme.hasThemeKey(unreadOtherKey)) { int color1 = Theme.getColor(unreadKey); if ((animatingIndicator || manualScrollingToPosition != -1) && (currentTab.id == id1 || currentTab.id == id2)) { int color3 = Theme.getColor(unreadOtherKey); counterPaint.setColor(ColorUtils.blendARGB(color3, color1, animatingIndicatorProgress)); } else { counterPaint.setColor(color1); } } else { counterPaint.setColor(textPaint.getColor()); } int x = textX + currentTab.titleWidth + AndroidUtilities.dp(6); int countTop = (getMeasuredHeight() - AndroidUtilities.dp(20)) / 2; if (currentTab.id != Integer.MAX_VALUE && (isEditing || editingStartAnimationProgress != 0) && counterText == null) { counterPaint.setAlpha((int) (editingStartAnimationProgress * 255)); } else { counterPaint.setAlpha(255); } rect.set(x, countTop, x + countWidth, countTop + AndroidUtilities.dp(20)); canvas.drawRoundRect(rect, 11.5f * AndroidUtilities.density, 11.5f * AndroidUtilities.density, counterPaint); if (counterText != null) { if (currentTab.id != Integer.MAX_VALUE) { textCounterPaint.setAlpha((int) (255 * (1.0f - editingStartAnimationProgress))); } canvas.drawText(counterText, rect.left + (rect.width() - counterWidth) / 2, countTop + AndroidUtilities.dp(14.5f), textCounterPaint); } if (currentTab.id != Integer.MAX_VALUE && (isEditing || editingStartAnimationProgress != 0)) { deletePaint.setColor(textCounterPaint.getColor()); deletePaint.setAlpha((int) (255 * editingStartAnimationProgress)); int side = AndroidUtilities.dp(3); canvas.drawLine(rect.centerX() - side, rect.centerY() - side, rect.centerX() + side, rect.centerY() + side, deletePaint); canvas.drawLine(rect.centerX() - side, rect.centerY() + side, rect.centerX() + side, rect.centerY() - side, deletePaint); } } if (currentTab.id != Integer.MAX_VALUE && editingAnimationProgress != 0) { canvas.restore(); } } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setSelected(currentTab != null && selectedTabId != -1 && currentTab.id == selectedTabId); } } private TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); private TextPaint textCounterPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); private Paint deletePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); private Paint counterPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private ArrayList tabs = new ArrayList<>(); private Bitmap crossfadeBitmap; private Paint crossfadePaint = new Paint(); private float crossfadeAlpha; private boolean commitCrossfade; private boolean isEditing; private long lastEditingAnimationTime; private boolean editingForwardAnimation; private float editingAnimationProgress; private float editingStartAnimationProgress; private boolean orderChanged; private boolean ignoreLayout; private RecyclerListView listView; private LinearLayoutManager layoutManager; private ListAdapter adapter; private TabsViewDelegate delegate; private int currentPosition; private int selectedTabId = -1; private int allTabsWidth; private int additionalTabWidth; private boolean animatingIndicator; private float animatingIndicatorProgress; private int manualScrollingToPosition = -1; private int manualScrollingToId = -1; private int scrollingToChild = -1; private GradientDrawable selectorDrawable; private String tabLineColorKey = Theme.key_profile_tabSelectedLine; private String activeTextColorKey = Theme.key_profile_tabSelectedText; private String unactiveTextColorKey = Theme.key_profile_tabText; private String selectorColorKey = Theme.key_profile_tabSelector; private String backgroundColorKey = Theme.key_actionBarDefault; private int prevLayoutWidth; private boolean invalidated; private boolean isInHiddenMode; private float hideProgress; private CubicBezierInterpolator interpolator = CubicBezierInterpolator.EASE_OUT_QUINT; private SparseIntArray positionToId = new SparseIntArray(5); private SparseIntArray idToPosition = new SparseIntArray(5); private SparseIntArray positionToWidth = new SparseIntArray(5); private SparseIntArray positionToX = new SparseIntArray(5); private boolean animationRunning; private long lastAnimationTime; private float animationTime; private int previousPosition; private int previousId; private Runnable animationRunnable = new Runnable() { @Override public void run() { if (!animatingIndicator) { return; } long newTime = SystemClock.elapsedRealtime(); long dt = (newTime - lastAnimationTime); if (dt > 17) { dt = 17; } animationTime += dt / 200.0f; setAnimationIdicatorProgress(interpolator.getInterpolation(animationTime)); if (animationTime > 1.0f) { animationTime = 1.0f; } if (animationTime < 1.0f) { AndroidUtilities.runOnUIThread(animationRunnable); } else { animatingIndicator = false; setEnabled(true); if (delegate != null) { delegate.onPageScrolled(1.0f); } } } }; ValueAnimator tabsAnimator; private float animationValue; public TabsView(Context context) { super(context); textCounterPaint.setTextSize(AndroidUtilities.dp(13)); textCounterPaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); textPaint.setTextSize(AndroidUtilities.dp(15)); textPaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); deletePaint.setStyle(Paint.Style.STROKE); deletePaint.setStrokeCap(Paint.Cap.ROUND); deletePaint.setStrokeWidth(AndroidUtilities.dp(1.5f)); selectorDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, null); float rad = AndroidUtilities.dpf2(3); selectorDrawable.setCornerRadii(new float[]{rad, rad, rad, rad, 0, 0, 0, 0}); selectorDrawable.setColor(Theme.getColor(tabLineColorKey)); setHorizontalScrollBarEnabled(false); listView = new RecyclerListView(context) { @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { super.addView(child, index, params); if (isInHiddenMode) { child.setScaleX(0.3f); child.setScaleY(0.3f); child.setAlpha(0); } else { child.setScaleX(1f); child.setScaleY(1f); child.setAlpha(1f); } } @Override public void setAlpha(float alpha) { super.setAlpha(alpha); TabsView.this.invalidate(); } @Override protected boolean canHighlightChildAt(View child, float x, float y) { if (isEditing) { TabView tabView = (TabView) child; int side = AndroidUtilities.dp(6); if (tabView.rect.left - side < x && tabView.rect.right + side > x) { return false; } } return super.canHighlightChildAt(child, x, y); } }; ((DefaultItemAnimator) listView.getItemAnimator()).setDelayAnimations(false); listView.setSelectorType(7); listView.setSelectorDrawableColor(Theme.getColor(selectorColorKey)); listView.setLayoutManager(layoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) { @Override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) { @Override protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference()); if (dx > 0 || dx == 0 && targetView.getLeft() - AndroidUtilities.dp(21) < 0) { dx += AndroidUtilities.dp(60); } else if (dx < 0 || dx == 0 && targetView.getRight() + AndroidUtilities.dp(21) > getMeasuredWidth()) { dx -= AndroidUtilities.dp(60); } final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference()); final int distance = (int) Math.sqrt(dx * dx + dy * dy); final int time = Math.max(180, calculateTimeForDeceleration(distance)); if (time > 0) { action.update(-dx, -dy, time, mDecelerateInterpolator); } } }; linearSmoothScroller.setTargetPosition(position); startSmoothScroll(linearSmoothScroller); } @Override public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { return super.scrollHorizontallyBy(dx, recycler, state); } @Override public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(recycler, state, info); if (isInHiddenMode) { info.setVisibleToUser(false); } } }); listView.setPadding(AndroidUtilities.dp(7), 0, AndroidUtilities.dp(7), 0); listView.setClipToPadding(false); listView.setDrawSelectorBehind(true); listView.setAdapter(adapter = new ListAdapter(context)); listView.setOnItemClickListener((view, position, x, y) -> { if (!delegate.canPerformActions()) { return; } TabView tabView = (TabView) view; if (position == currentPosition && delegate != null) { delegate.onSamePageSelected(); return; } scrollToTab(tabView.currentTab.id, position); }); listView.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { invalidate(); } }); addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); } public void setDelegate(TabsViewDelegate filterTabsViewDelegate) { delegate = filterTabsViewDelegate; } public boolean isAnimatingIndicator() { return animatingIndicator; } public void scrollToTab(int id, int position) { boolean scrollingForward = currentPosition < position; scrollingToChild = -1; previousPosition = currentPosition; previousId = selectedTabId; currentPosition = position; selectedTabId = id; if (tabsAnimator != null) { tabsAnimator.cancel(); } if (animatingIndicator) { animatingIndicator = false; } animationTime = 0; animatingIndicatorProgress = 0; animatingIndicator = true; setEnabled(false); if (delegate != null) { delegate.onPageSelected(id, scrollingForward); } scrollToChild(position); tabsAnimator = ValueAnimator.ofFloat(0,1f); tabsAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float progress = (float) valueAnimator.getAnimatedValue(); setAnimationIdicatorProgress(progress); if (delegate != null) { delegate.onPageScrolled(progress); } } }); tabsAnimator.setDuration(250); tabsAnimator.setInterpolator(CubicBezierInterpolator.DEFAULT); tabsAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { animatingIndicator = false; setEnabled(true); if (delegate != null) { delegate.onPageScrolled(1.0f); } invalidate(); } }); tabsAnimator.start(); } public void setAnimationIdicatorProgress(float value) { animatingIndicatorProgress = value; listView.invalidateViews(); invalidate(); if (delegate != null) { delegate.onPageScrolled(value); } } public Drawable getSelectorDrawable() { return selectorDrawable; } public RecyclerListView getTabsContainer() { return listView; } public int getNextPageId(boolean forward) { return positionToId.get(currentPosition + (forward ? 1 : -1), -1); } public void addTab(int id, String text) { int position = tabs.size(); if (position == 0 && selectedTabId == -1) { selectedTabId = id; } positionToId.put(position, id); idToPosition.put(id, position); if (selectedTabId != -1 && selectedTabId == id) { currentPosition = position; } Tab tab = new Tab(id, text); allTabsWidth += tab.getWidth(true, textPaint) + AndroidUtilities.dp(32); tabs.add(tab); } public void removeTabs() { tabs.clear(); positionToId.clear(); idToPosition.clear(); positionToWidth.clear(); positionToX.clear(); allTabsWidth = 0; } public void finishAddingTabs() { adapter.notifyDataSetChanged(); } public int getCurrentTabId() { return selectedTabId; } public int getFirstTabId() { return positionToId.get(0, 0); } private void updateTabsWidths() { positionToX.clear(); positionToWidth.clear(); int xOffset = AndroidUtilities.dp(7); for (int a = 0, N = tabs.size(); a < N; a++) { int tabWidth = tabs.get(a).getWidth(false, textPaint); positionToWidth.put(a, tabWidth); positionToX.put(a, xOffset + additionalTabWidth / 2); xOffset += tabWidth + AndroidUtilities.dp(32) + additionalTabWidth; } } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean result = super.drawChild(canvas, child, drawingTime); if (child == listView) { final int height = getMeasuredHeight(); boolean invalidate = false; if (isInHiddenMode && hideProgress != 1f) { hideProgress += 0.1f; if (hideProgress > 1f) { hideProgress = 1f; } invalidate(); } else if (!isInHiddenMode && hideProgress != 0) { hideProgress -= 0.12f; if (hideProgress < 0) { hideProgress = 0; } invalidate(); } selectorDrawable.setAlpha((int) (255 * listView.getAlpha())); int indicatorX = 0; int indicatorWidth = 0; if (animatingIndicator || manualScrollingToPosition != -1) { int position = layoutManager.findFirstVisibleItemPosition(); if (position != RecyclerListView.NO_POSITION) { RecyclerListView.ViewHolder holder = listView.findViewHolderForAdapterPosition(position); if (holder != null) { int idx1; int idx2; if (animatingIndicator) { idx1 = previousPosition; idx2 = currentPosition; } else { idx1 = currentPosition; idx2 = manualScrollingToPosition; } int prevX = positionToX.get(idx1); int newX = positionToX.get(idx2); int prevW = positionToWidth.get(idx1); int newW = positionToWidth.get(idx2); if (additionalTabWidth != 0) { indicatorX = (int) (prevX + (newX - prevX) * animatingIndicatorProgress) + AndroidUtilities.dp(16); } else { int x = positionToX.get(position); indicatorX = (int) (prevX + (newX - prevX) * animatingIndicatorProgress) - (x - holder.itemView.getLeft()) + AndroidUtilities.dp(16); } indicatorWidth = (int) (prevW + (newW - prevW) * animatingIndicatorProgress); } } } else { RecyclerListView.ViewHolder holder = listView.findViewHolderForAdapterPosition(currentPosition); if (holder != null) { TabView tabView = (TabView) holder.itemView; indicatorWidth = Math.max(AndroidUtilities.dp(40), tabView.tabWidth); indicatorX = (int) (tabView.getX() + (tabView.getMeasuredWidth() - indicatorWidth) / 2); } } if (indicatorWidth != 0) { selectorDrawable.setBounds(indicatorX, (int) (height - AndroidUtilities.dpr(4) + hideProgress * AndroidUtilities.dpr(4)), indicatorX + indicatorWidth, (int) (height + hideProgress * AndroidUtilities.dpr(4))); selectorDrawable.draw(canvas); } if (crossfadeBitmap != null) { crossfadePaint.setAlpha((int) (crossfadeAlpha * 255)); canvas.drawBitmap(crossfadeBitmap, 0, 0, crossfadePaint); } } return result; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (!tabs.isEmpty()) { int width = MeasureSpec.getSize(widthMeasureSpec) - AndroidUtilities.dp(7) - AndroidUtilities.dp(7); int prevWidth = additionalTabWidth; additionalTabWidth = allTabsWidth < width ? (width - allTabsWidth) / tabs.size() : 0; if (prevWidth != additionalTabWidth) { ignoreLayout = true; adapter.notifyDataSetChanged(); ignoreLayout = false; } updateTabsWidths(); invalidated = false; } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } public void updateColors() { selectorDrawable.setColor(Theme.getColor(tabLineColorKey)); listView.invalidateViews(); listView.invalidate(); invalidate(); } @Override public void requestLayout() { if (ignoreLayout) { return; } super.requestLayout(); } private void scrollToChild(int position) { if (tabs.isEmpty() || scrollingToChild == position || position < 0 || position >= tabs.size()) { return; } scrollingToChild = position; listView.smoothScrollToPosition(position); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (prevLayoutWidth != r - l) { prevLayoutWidth = r - l; scrollingToChild = -1; if (animatingIndicator) { AndroidUtilities.cancelRunOnUIThread(animationRunnable); animatingIndicator = false; setEnabled(true); if (delegate != null) { delegate.onPageScrolled(1.0f); } } } } public void selectTab(int currentPosition, int nextPosition, float progress) { if (progress < 0) { progress = 0; } else if (progress > 1.0f) { progress = 1.0f; } this.currentPosition = currentPosition; selectedTabId = positionToId.get(currentPosition); if (progress > 0) { manualScrollingToPosition = nextPosition; manualScrollingToId = positionToId.get(nextPosition); } else { manualScrollingToPosition = -1; manualScrollingToId = -1; } animatingIndicatorProgress = progress; listView.invalidateViews(); invalidate(); scrollToChild(currentPosition); if (progress >= 1.0f) { manualScrollingToPosition = -1; manualScrollingToId = -1; this.currentPosition = nextPosition; selectedTabId = positionToId.get(nextPosition); } } public void selectTabWithId(int id, float progress) { int position = idToPosition.get(id, -1); if (position < 0) { return; } if (progress < 0) { progress = 0; } else if (progress > 1.0f) { progress = 1.0f; } if (progress > 0) { manualScrollingToPosition = position; manualScrollingToId = id; } else { manualScrollingToPosition = -1; manualScrollingToId = -1; } animatingIndicatorProgress = progress; listView.invalidateViews(); invalidate(); scrollToChild(position); if (progress >= 1.0f) { manualScrollingToPosition = -1; manualScrollingToId = -1; currentPosition = position; selectedTabId = id; } } private int getChildWidth(TextView child) { Layout layout = child.getLayout(); if (layout != null) { int w = (int) Math.ceil(layout.getLineWidth(0)) + AndroidUtilities.dp(2); if (child.getCompoundDrawables()[2] != null) { w += child.getCompoundDrawables()[2].getIntrinsicWidth() + AndroidUtilities.dp(6); } return w; } else { return child.getMeasuredWidth(); } } public void onPageScrolled(int position, int first) { if (currentPosition == position) { return; } currentPosition = position; if (position >= tabs.size()) { return; } if (first == position && position > 1) { scrollToChild(position - 1); } else { scrollToChild(position); } invalidate(); } public boolean isEditing() { return isEditing; } public void setIsEditing(boolean value) { isEditing = value; editingForwardAnimation = true; listView.invalidateViews(); invalidate(); if (!isEditing && orderChanged) { MessagesStorage.getInstance(UserConfig.selectedAccount).saveDialogFiltersOrder(); TLRPC.TL_messages_updateDialogFiltersOrder req = new TLRPC.TL_messages_updateDialogFiltersOrder(); ArrayList filters = MessagesController.getInstance(UserConfig.selectedAccount).dialogFilters; for (int a = 0, N = filters.size(); a < N; a++) { MessagesController.DialogFilter filter = filters.get(a); req.order.add(filters.get(a).id); } ConnectionsManager.getInstance(UserConfig.selectedAccount).sendRequest(req, (response, error) -> { }); orderChanged = false; } } private class ListAdapter extends RecyclerListView.SelectionAdapter { private Context mContext; public ListAdapter(Context context) { mContext = context; } @Override public int getItemCount() { return tabs.size(); } @Override public long getItemId(int i) { return i; } @Override public boolean isEnabled(RecyclerView.ViewHolder holder) { return true; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new RecyclerListView.Holder(new TabView(mContext)); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { TabView tabView = (TabView) holder.itemView; tabView.setTab(tabs.get(position), position); } @Override public int getItemViewType(int i) { return 0; } } public void hide(boolean hide, boolean animated) { isInHiddenMode = hide; if (animated) { for (int i = 0; i < listView.getChildCount(); i++) { listView.getChildAt(i).animate().alpha(hide ? 0 : 1f).scaleX(hide ? 0 : 1f).scaleY(hide ? 0 : 1f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(220).start(); } } else { for (int i = 0; i < listView.getChildCount(); i++) { View v = listView.getChildAt(i); v.setScaleX(hide ? 0 : 1f); v.setScaleY(hide ? 0 : 1f); v.setAlpha(hide ? 0 : 1f); } hideProgress = hide ? 1 : 0; } invalidate(); } } private View findScrollingChild(ViewGroup parent, float x, float y) { int n = parent.getChildCount(); for (int i = 0; i < n; i++) { View child = parent.getChildAt(i); if (child.getVisibility() != View.VISIBLE) { continue; } child.getHitRect(rect); if (rect.contains((int) x, (int) y)) { if (child.canScrollHorizontally(-1)) { return child; } else if (child instanceof ViewGroup) { View v = findScrollingChild((ViewGroup) child, x - rect.left, y - rect.top); if (v != null) { return v; } } } } return null; } }