From b697f9367fb0f98cce9480bc9d957336ff70315f Mon Sep 17 00:00:00 2001 From: chan Date: Fri, 13 Mar 2026 17:03:38 +0800 Subject: [PATCH 01/26] support fake bold --- .../me/chan/texas/TexasViewDemoActivity.java | 16 ++++++++++++---- .../renderer/selection/ParagraphSelection.java | 8 ++++++++ .../chan/texas/renderer/selection/Selection.java | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java b/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java index 2e74b5a0..7226ce2d 100644 --- a/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java +++ b/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java @@ -7,6 +7,7 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Bundle; import android.util.Log; import android.util.TypedValue; @@ -17,6 +18,7 @@ import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; @@ -385,6 +387,9 @@ protected Document onRead(TexasOption option, @Nullable Document previousDocumen } }); + Selection.Styles styles = Selection.Styles.create(Color.BLUE, Color.RED); + styles.enableFakeBold(); + findViewById(me.chan.texas.debug.R.id.anim).setOnClickListener(v -> { Selection selection = mTexasView.highlightParagraphs(new ParagraphPredicates() { @Override @@ -401,17 +406,20 @@ public boolean acceptParagraph(@Nullable Object paragraphTag) { return; } + int backgroundColor = styles.getBackgroundColor(); + int textColor = styles.getTextColor(); + float fakeBoldFactor = styles.getFakeBoldFactor(); ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1f); valueAnimator.setDuration(3000); valueAnimator.setRepeatCount(3); selection.startAnimator(valueAnimator, new Selection.SelectionAnimatorListener() { + @RequiresApi(api = Build.VERSION_CODES.O) @Override protected void onUpdate(ValueAnimator animation, Selection.Styles styles) { - int backgroundColor = (int) styles.getBackgroundColor(); - int textColor = styles.getTextColor(); float v = (float) animation.getAnimatedValue(); - styles.setTextColor(Color.argb((int) (255 * v), Color.red(textColor), Color.green(textColor), Color.blue(textColor))); - styles.setBackgroundColor(Color.argb((int) (255 * v), Color.red(backgroundColor), Color.green(backgroundColor), Color.blue(backgroundColor))); + styles.setTextColor(Color.argb((int) (255 * v) /* 按需设置透明度 */, Color.red(textColor), Color.green(textColor), Color.blue(textColor))); + styles.setBackgroundColor(Color.argb((int) (255 * v) /* 按需设置透明度 */, Color.red(backgroundColor), Color.green(backgroundColor), Color.blue(backgroundColor))); + styles.setFakeBoldFactor(fakeBoldFactor * v); } @Override diff --git a/library/src/main/java/me/chan/texas/renderer/selection/ParagraphSelection.java b/library/src/main/java/me/chan/texas/renderer/selection/ParagraphSelection.java index c1be8300..1cf30497 100644 --- a/library/src/main/java/me/chan/texas/renderer/selection/ParagraphSelection.java +++ b/library/src/main/java/me/chan/texas/renderer/selection/ParagraphSelection.java @@ -2,6 +2,8 @@ import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import android.graphics.Paint; + import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -342,6 +344,7 @@ private static class InternalSelectionStyle extends TextStyle { private int mTextColor = 0; private Selection.Styles mStyles; private int mBackgroundColor = 0; + private float mFakeBoldFactor = 0f; public void reset(Selection.Styles styles) { mStyles = styles; @@ -350,11 +353,16 @@ public void reset(Selection.Styles styles) { public void update() { mTextColor = mStyles.getTextColor(); mBackgroundColor = mStyles.getBackgroundColor(); + mFakeBoldFactor = mStyles.getFakeBoldFactor(); } @Override public void update(@NonNull TexasPaint textPaint, @Nullable Object tag) { textPaint.setColor(mTextColor); + if (mFakeBoldFactor > 0f) { + textPaint.setStyle(Paint.Style.FILL_AND_STROKE); + textPaint.setStrokeWidth(textPaint.getTextSize() * mFakeBoldFactor); + } } } } diff --git a/library/src/main/java/me/chan/texas/renderer/selection/Selection.java b/library/src/main/java/me/chan/texas/renderer/selection/Selection.java index cc9bb3a7..9fa6e426 100644 --- a/library/src/main/java/me/chan/texas/renderer/selection/Selection.java +++ b/library/src/main/java/me/chan/texas/renderer/selection/Selection.java @@ -363,6 +363,7 @@ public static Selection obtain(Type type, @Nullable TexasRecyclerView container, public static class Styles { private int mBackgroundColor; private int mTextColor; + private float mFakeBoldFactor = 0f; private Source mSource; @@ -424,6 +425,21 @@ public void setTextColor(int textColor) { mTextColor = textColor; } + public void enableFakeBold() { + setFakeBoldFactor(0.05f); + } + + public void setFakeBoldFactor(float fakeBoldFactor) { + if (mFakeBoldFactor != fakeBoldFactor) { + ++mVersion; + } + mFakeBoldFactor = fakeBoldFactor; + } + + public float getFakeBoldFactor() { + return mFakeBoldFactor; + } + @Override public String toString() { return "Styles{" + From 02d4b6fe99c1adcb614ab24c95eea7da2016b582 Mon Sep 17 00:00:00 2001 From: chan Date: Fri, 13 Mar 2026 18:06:07 +0800 Subject: [PATCH 02/26] disable internal anim --- .../renderer/ui/rv/TexasItemAnimator.java | 55 +++++++++++++++++++ .../renderer/ui/rv/TexasRecyclerViewImpl.java | 7 +-- 2 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java new file mode 100644 index 00000000..5ac7db99 --- /dev/null +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java @@ -0,0 +1,55 @@ +package me.chan.texas.renderer.ui.rv; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +class TexasItemAnimator extends RecyclerView.ItemAnimator { + private static final String TAG = "TexasItemAnim"; + + @Override + public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { + Log.d(TAG, "disappearance"); + return false; + } + + @Override + public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { + Log.d(TAG, "appearance"); + return false; + } + + @Override + public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { + Log.d(TAG, "persistence"); + return false; + } + + @Override + public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { + Log.d(TAG, "change"); + return false; + } + + @Override + public void runPendingAnimations() { + + } + + @Override + public void endAnimation(@NonNull RecyclerView.ViewHolder item) { + + } + + @Override + public void endAnimations() { + + } + + @Override + public boolean isRunning() { + return false; + } +} diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java index 04d0a141..3f1ea9ba 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java @@ -48,12 +48,7 @@ protected void onClicked(MotionEvent event) { } }; - ItemAnimator itemAnimator = getItemAnimator(); - if (itemAnimator instanceof SimpleItemAnimator) { - SimpleItemAnimator simpleItemAnimator = (SimpleItemAnimator) itemAnimator; - simpleItemAnimator.setSupportsChangeAnimations(false); - simpleItemAnimator.setChangeDuration(0); - } + setItemAnimator(new TexasItemAnimator()); } public void scrollToPosition(int position, boolean smooth, int offset) { From bdbd26784317cd5d4aa7b3136da0e83076b8864c Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 16 Mar 2026 14:15:25 +0800 Subject: [PATCH 03/26] disable internal anim --- .../renderer/ui/rv/TexasItemAnimator.java | 98 +++++++++++++++++-- 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java index 5ac7db99..5005fdb0 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java @@ -1,55 +1,133 @@ package me.chan.texas.renderer.ui.rv; -import android.util.Log; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.view.View; +import android.view.animation.DecelerateInterpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + class TexasItemAnimator extends RecyclerView.ItemAnimator { - private static final String TAG = "TexasItemAnim"; + private static final long APPEARANCE_DURATION = 250; + + private final List mPendingAppearances = new ArrayList<>(); + private final List mRunningAppearances = new ArrayList<>(); + private final Set mEndedHolders = new HashSet<>(); @Override public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { - Log.d(TAG, "disappearance"); return false; } @Override public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { - Log.d(TAG, "appearance"); - return false; + endAnimation(viewHolder); + View itemView = viewHolder.itemView; + int height = postLayoutInfo.bottom - postLayoutInfo.top; + + itemView.setAlpha(0f); + itemView.setTranslationY(-height); + + mPendingAppearances.add(viewHolder); + return true; } @Override public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { - Log.d(TAG, "persistence"); return false; } @Override public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { - Log.d(TAG, "change"); return false; } @Override public void runPendingAnimations() { + if (mPendingAppearances.isEmpty()) { + return; + } + List toAnimate = new ArrayList<>(mPendingAppearances); + mPendingAppearances.clear(); + for (RecyclerView.ViewHolder holder : toAnimate) { + View itemView = holder.itemView; + ViewCompat.postOnAnimation(itemView, () -> animateAppearanceImpl(holder)); + } + } + + private void animateAppearanceImpl(@NonNull RecyclerView.ViewHolder holder) { + if (mEndedHolders.remove(holder)) { + return; + } + View itemView = holder.itemView; + if (!itemView.isAttachedToWindow()) { + return; + } + + mRunningAppearances.add(holder); + dispatchAnimationStarted(holder); + + itemView.animate() + .alpha(1f) + .translationY(0f) + .setDuration(APPEARANCE_DURATION) + .setInterpolator(new DecelerateInterpolator()) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + itemView.animate().setListener(null); + mRunningAppearances.remove(holder); + dispatchAnimationFinished(holder); + } + + @Override + public void onAnimationCancel(Animator animation) { + itemView.setAlpha(1f); + itemView.setTranslationY(0f); + mRunningAppearances.remove(holder); + if (!mEndedHolders.remove(holder)) { + dispatchAnimationFinished(holder); + } + } + }) + .start(); } @Override public void endAnimation(@NonNull RecyclerView.ViewHolder item) { - + View itemView = item.itemView; + itemView.animate().cancel(); + itemView.setAlpha(1f); + itemView.setTranslationY(0f); + boolean wasPending = mPendingAppearances.remove(item); + boolean wasRunning = mRunningAppearances.remove(item); + if (wasPending || wasRunning) { + mEndedHolders.add(item); + } + dispatchAnimationFinished(item); } @Override public void endAnimations() { - + for (RecyclerView.ViewHolder holder : new ArrayList<>(mPendingAppearances)) { + endAnimation(holder); + } + for (RecyclerView.ViewHolder holder : new ArrayList<>(mRunningAppearances)) { + endAnimation(holder); + } } @Override public boolean isRunning() { - return false; + return !mPendingAppearances.isEmpty() || !mRunningAppearances.isEmpty(); } } From 82b4e69158cb466588c6bf4580a7f332b3ae3576 Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 16 Mar 2026 14:18:04 +0800 Subject: [PATCH 04/26] disable internal anim --- .../renderer/ui/rv/TexasItemAnimator.java | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java index 5005fdb0..c519b2ac 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java @@ -11,16 +11,13 @@ import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; class TexasItemAnimator extends RecyclerView.ItemAnimator { private static final long APPEARANCE_DURATION = 250; private final List mPendingAppearances = new ArrayList<>(); private final List mRunningAppearances = new ArrayList<>(); - private final Set mEndedHolders = new HashSet<>(); @Override public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { @@ -65,11 +62,9 @@ public void runPendingAnimations() { } private void animateAppearanceImpl(@NonNull RecyclerView.ViewHolder holder) { - if (mEndedHolders.remove(holder)) { - return; - } View itemView = holder.itemView; if (!itemView.isAttachedToWindow()) { + dispatchAnimationFinished(holder); return; } @@ -94,9 +89,7 @@ public void onAnimationCancel(Animator animation) { itemView.setAlpha(1f); itemView.setTranslationY(0f); mRunningAppearances.remove(holder); - if (!mEndedHolders.remove(holder)) { - dispatchAnimationFinished(holder); - } + dispatchAnimationFinished(holder); } }) .start(); @@ -108,11 +101,8 @@ public void endAnimation(@NonNull RecyclerView.ViewHolder item) { itemView.animate().cancel(); itemView.setAlpha(1f); itemView.setTranslationY(0f); - boolean wasPending = mPendingAppearances.remove(item); - boolean wasRunning = mRunningAppearances.remove(item); - if (wasPending || wasRunning) { - mEndedHolders.add(item); - } + mPendingAppearances.remove(item); + mRunningAppearances.remove(item); dispatchAnimationFinished(item); } From 439e4ba27123d78025f8dcfe8fb7521881246951 Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 16 Mar 2026 15:03:11 +0800 Subject: [PATCH 05/26] support more anim --- .../renderer/ui/rv/TexasItemAnimator.java | 124 +++++++++++++----- 1 file changed, 92 insertions(+), 32 deletions(-) diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java index c519b2ac..832cdf2a 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java @@ -3,6 +3,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.view.View; +import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import androidx.annotation.NonNull; @@ -15,13 +16,21 @@ class TexasItemAnimator extends RecyclerView.ItemAnimator { private static final long APPEARANCE_DURATION = 250; + private static final long DISAPPEARANCE_DURATION = 200; private final List mPendingAppearances = new ArrayList<>(); + private final List mPostponedAppearances = new ArrayList<>(); private final List mRunningAppearances = new ArrayList<>(); + private final List mPendingDisappearances = new ArrayList<>(); + private final List mPostponedDisappearances = new ArrayList<>(); + private final List mRunningDisappearances = new ArrayList<>(); + @Override public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { - return false; + endAnimation(viewHolder); + mPendingDisappearances.add(viewHolder); + return true; } @Override @@ -49,25 +58,39 @@ public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNul @Override public void runPendingAnimations() { - if (mPendingAppearances.isEmpty()) { + boolean hasDisappearances = !mPendingDisappearances.isEmpty(); + boolean hasAppearances = !mPendingAppearances.isEmpty(); + if (!hasDisappearances && !hasAppearances) { return; } - List toAnimate = new ArrayList<>(mPendingAppearances); - mPendingAppearances.clear(); - for (RecyclerView.ViewHolder holder : toAnimate) { - View itemView = holder.itemView; - ViewCompat.postOnAnimation(itemView, () -> animateAppearanceImpl(holder)); + if (hasDisappearances) { + mPostponedDisappearances.addAll(mPendingDisappearances); + mPendingDisappearances.clear(); + for (RecyclerView.ViewHolder holder : new ArrayList<>(mPostponedDisappearances)) { + ViewCompat.postOnAnimation(holder.itemView, () -> { + if (mPostponedDisappearances.remove(holder)) { + animateDisappearanceImpl(holder); + } + }); + } + } + + if (hasAppearances) { + mPostponedAppearances.addAll(mPendingAppearances); + mPendingAppearances.clear(); + for (RecyclerView.ViewHolder holder : new ArrayList<>(mPostponedAppearances)) { + ViewCompat.postOnAnimation(holder.itemView, () -> { + if (mPostponedAppearances.remove(holder)) { + animateAppearanceImpl(holder); + } + }); + } } } private void animateAppearanceImpl(@NonNull RecyclerView.ViewHolder holder) { View itemView = holder.itemView; - if (!itemView.isAttachedToWindow()) { - dispatchAnimationFinished(holder); - return; - } - mRunningAppearances.add(holder); dispatchAnimationStarted(holder); @@ -80,44 +103,81 @@ private void animateAppearanceImpl(@NonNull RecyclerView.ViewHolder holder) { @Override public void onAnimationEnd(Animator animation) { itemView.animate().setListener(null); - mRunningAppearances.remove(holder); - dispatchAnimationFinished(holder); + resetView(itemView); + if (mRunningAppearances.remove(holder)) { + dispatchAnimationFinished(holder); + } } + }) + .start(); + } + + private void animateDisappearanceImpl(@NonNull RecyclerView.ViewHolder holder) { + View itemView = holder.itemView; + mRunningDisappearances.add(holder); + dispatchAnimationStarted(holder); + int height = itemView.getHeight(); + itemView.animate() + .alpha(0f) + .translationY(-height) + .setDuration(DISAPPEARANCE_DURATION) + .setInterpolator(new AccelerateInterpolator()) + .setListener(new AnimatorListenerAdapter() { @Override - public void onAnimationCancel(Animator animation) { - itemView.setAlpha(1f); - itemView.setTranslationY(0f); - mRunningAppearances.remove(holder); - dispatchAnimationFinished(holder); + public void onAnimationEnd(Animator animation) { + itemView.animate().setListener(null); + resetView(itemView); + if (mRunningDisappearances.remove(holder)) { + dispatchAnimationFinished(holder); + } } }) .start(); } + private void resetView(View view) { + view.setAlpha(1f); + view.setTranslationY(0f); + } + @Override public void endAnimation(@NonNull RecyclerView.ViewHolder item) { - View itemView = item.itemView; - itemView.animate().cancel(); - itemView.setAlpha(1f); - itemView.setTranslationY(0f); - mPendingAppearances.remove(item); - mRunningAppearances.remove(item); - dispatchAnimationFinished(item); + item.itemView.animate().cancel(); + resetView(item.itemView); + + boolean needDispatch = mPendingAppearances.remove(item) + || mPostponedAppearances.remove(item) + || mPendingDisappearances.remove(item) + || mPostponedDisappearances.remove(item); + if (needDispatch) { + dispatchAnimationFinished(item); + } } @Override public void endAnimations() { - for (RecyclerView.ViewHolder holder : new ArrayList<>(mPendingAppearances)) { - endAnimation(holder); - } - for (RecyclerView.ViewHolder holder : new ArrayList<>(mRunningAppearances)) { - endAnimation(holder); + List> allLists = new ArrayList<>(); + allLists.add(mPendingAppearances); + allLists.add(mPostponedAppearances); + allLists.add(mRunningAppearances); + allLists.add(mPendingDisappearances); + allLists.add(mPostponedDisappearances); + allLists.add(mRunningDisappearances); + for (List list : allLists) { + for (RecyclerView.ViewHolder holder : new ArrayList<>(list)) { + endAnimation(holder); + } } } @Override public boolean isRunning() { - return !mPendingAppearances.isEmpty() || !mRunningAppearances.isEmpty(); + return !mPendingAppearances.isEmpty() + || !mPostponedAppearances.isEmpty() + || !mRunningAppearances.isEmpty() + || !mPendingDisappearances.isEmpty() + || !mPostponedDisappearances.isEmpty() + || !mRunningDisappearances.isEmpty(); } } From 902bfe68941bef770f99ca6dcb9e16a1714989e8 Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 16 Mar 2026 15:36:55 +0800 Subject: [PATCH 06/26] refactor item animator --- .../renderer/ui/rv/TexasItemAnimator.java | 213 ++++++++++-------- 1 file changed, 115 insertions(+), 98 deletions(-) diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java index 832cdf2a..1609586c 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java @@ -12,24 +12,19 @@ import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; class TexasItemAnimator extends RecyclerView.ItemAnimator { private static final long APPEARANCE_DURATION = 250; private static final long DISAPPEARANCE_DURATION = 200; - private final List mPendingAppearances = new ArrayList<>(); - private final List mPostponedAppearances = new ArrayList<>(); - private final List mRunningAppearances = new ArrayList<>(); - - private final List mPendingDisappearances = new ArrayList<>(); - private final List mPostponedDisappearances = new ArrayList<>(); - private final List mRunningDisappearances = new ArrayList<>(); + private final AnimTracker mTracker = new AnimTracker(); @Override public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { endAnimation(viewHolder); - mPendingDisappearances.add(viewHolder); + mTracker.add(viewHolder, AnimRecord.TYPE_DISAPPEARANCE); return true; } @@ -42,7 +37,7 @@ public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @N itemView.setAlpha(0f); itemView.setTranslationY(-height); - mPendingAppearances.add(viewHolder); + mTracker.add(viewHolder, AnimRecord.TYPE_APPEARANCE); return true; } @@ -58,82 +53,58 @@ public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNul @Override public void runPendingAnimations() { - boolean hasDisappearances = !mPendingDisappearances.isEmpty(); - boolean hasAppearances = !mPendingAppearances.isEmpty(); - if (!hasDisappearances && !hasAppearances) { + List pending = mTracker.holdersByPhase(AnimRecord.PHASE_PENDING); + if (pending.isEmpty()) { return; } - if (hasDisappearances) { - mPostponedDisappearances.addAll(mPendingDisappearances); - mPendingDisappearances.clear(); - for (RecyclerView.ViewHolder holder : new ArrayList<>(mPostponedDisappearances)) { - ViewCompat.postOnAnimation(holder.itemView, () -> { - if (mPostponedDisappearances.remove(holder)) { - animateDisappearanceImpl(holder); - } - }); - } - } - - if (hasAppearances) { - mPostponedAppearances.addAll(mPendingAppearances); - mPendingAppearances.clear(); - for (RecyclerView.ViewHolder holder : new ArrayList<>(mPostponedAppearances)) { - ViewCompat.postOnAnimation(holder.itemView, () -> { - if (mPostponedAppearances.remove(holder)) { - animateAppearanceImpl(holder); - } - }); - } + for (RecyclerView.ViewHolder holder : pending) { + mTracker.advanceTo(holder, AnimRecord.PHASE_POSTPONED); + ViewCompat.postOnAnimation(holder.itemView, () -> { + AnimRecord record = mTracker.get(holder); + if (record != null && record.phase == AnimRecord.PHASE_POSTPONED) { + mTracker.advanceTo(holder, AnimRecord.PHASE_RUNNING); + startAnimation(holder, record.type); + } + }); } } - private void animateAppearanceImpl(@NonNull RecyclerView.ViewHolder holder) { - View itemView = holder.itemView; - mRunningAppearances.add(holder); + private void startAnimation(@NonNull RecyclerView.ViewHolder holder, int type) { dispatchAnimationStarted(holder); + View itemView = holder.itemView; - itemView.animate() - .alpha(1f) - .translationY(0f) - .setDuration(APPEARANCE_DURATION) - .setInterpolator(new DecelerateInterpolator()) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - itemView.animate().setListener(null); - resetView(itemView); - if (mRunningAppearances.remove(holder)) { - dispatchAnimationFinished(holder); - } - } - }) - .start(); + if (type == AnimRecord.TYPE_APPEARANCE) { + itemView.animate() + .alpha(1f) + .translationY(0f) + .setDuration(APPEARANCE_DURATION) + .setInterpolator(new DecelerateInterpolator()) + .setListener(createEndListener(holder)) + .start(); + } else { + int height = itemView.getHeight(); + itemView.animate() + .alpha(0f) + .translationY(-height) + .setDuration(DISAPPEARANCE_DURATION) + .setInterpolator(new AccelerateInterpolator()) + .setListener(createEndListener(holder)) + .start(); + } } - private void animateDisappearanceImpl(@NonNull RecyclerView.ViewHolder holder) { - View itemView = holder.itemView; - mRunningDisappearances.add(holder); - dispatchAnimationStarted(holder); - - int height = itemView.getHeight(); - itemView.animate() - .alpha(0f) - .translationY(-height) - .setDuration(DISAPPEARANCE_DURATION) - .setInterpolator(new AccelerateInterpolator()) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - itemView.animate().setListener(null); - resetView(itemView); - if (mRunningDisappearances.remove(holder)) { - dispatchAnimationFinished(holder); - } - } - }) - .start(); + private AnimatorListenerAdapter createEndListener(@NonNull RecyclerView.ViewHolder holder) { + return new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + holder.itemView.animate().setListener(null); + resetView(holder.itemView); + if (mTracker.remove(holder)) { + dispatchAnimationFinished(holder); + } + } + }; } private void resetView(View view) { @@ -143,41 +114,87 @@ private void resetView(View view) { @Override public void endAnimation(@NonNull RecyclerView.ViewHolder item) { + AnimRecord record = mTracker.get(item); + if (record == null) { + return; + } + boolean isRunning = record.phase == AnimRecord.PHASE_RUNNING; item.itemView.animate().cancel(); resetView(item.itemView); - - boolean needDispatch = mPendingAppearances.remove(item) - || mPostponedAppearances.remove(item) - || mPendingDisappearances.remove(item) - || mPostponedDisappearances.remove(item); - if (needDispatch) { + // running: cancel() 已同步触发 onAnimationEnd → dispatch,不再重复 + if (!isRunning && mTracker.remove(item)) { dispatchAnimationFinished(item); } } @Override public void endAnimations() { - List> allLists = new ArrayList<>(); - allLists.add(mPendingAppearances); - allLists.add(mPostponedAppearances); - allLists.add(mRunningAppearances); - allLists.add(mPendingDisappearances); - allLists.add(mPostponedDisappearances); - allLists.add(mRunningDisappearances); - for (List list : allLists) { - for (RecyclerView.ViewHolder holder : new ArrayList<>(list)) { - endAnimation(holder); - } + for (RecyclerView.ViewHolder holder : mTracker.allHolders()) { + endAnimation(holder); } } @Override public boolean isRunning() { - return !mPendingAppearances.isEmpty() - || !mPostponedAppearances.isEmpty() - || !mRunningAppearances.isEmpty() - || !mPendingDisappearances.isEmpty() - || !mPostponedDisappearances.isEmpty() - || !mRunningDisappearances.isEmpty(); + return !mTracker.isEmpty(); + } + + static class AnimRecord { + static final int TYPE_APPEARANCE = 1; + static final int TYPE_DISAPPEARANCE = 2; + + static final int PHASE_PENDING = 0; + static final int PHASE_POSTPONED = 1; + static final int PHASE_RUNNING = 2; + + final int type; + int phase; + + AnimRecord(int type) { + this.type = type; + this.phase = PHASE_PENDING; + } + } + + static class AnimTracker { + private final HashMap mRecords = new HashMap<>(); + + void add(RecyclerView.ViewHolder holder, int type) { + mRecords.put(holder, new AnimRecord(type)); + } + + boolean remove(RecyclerView.ViewHolder holder) { + return mRecords.remove(holder) != null; + } + + @Nullable + AnimRecord get(RecyclerView.ViewHolder holder) { + return mRecords.get(holder); + } + + void advanceTo(RecyclerView.ViewHolder holder, int phase) { + AnimRecord record = mRecords.get(holder); + if (record != null) { + record.phase = phase; + } + } + + List holdersByPhase(int phase) { + List result = new ArrayList<>(); + for (HashMap.Entry entry : mRecords.entrySet()) { + if (entry.getValue().phase == phase) { + result.add(entry.getKey()); + } + } + return result; + } + + List allHolders() { + return new ArrayList<>(mRecords.keySet()); + } + + boolean isEmpty() { + return mRecords.isEmpty(); + } } } From 515055b49f43ceadbb70b358823ef5926995d5bb Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 16 Mar 2026 16:08:35 +0800 Subject: [PATCH 07/26] refactor item aninator --- .../me/chan/texas/renderer/ui/rv/SegmentItemDecoration.java | 2 ++ .../me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java | 6 ++---- .../DefaultTexasItemAnimator.java} | 6 ++++-- .../chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java | 4 ++++ 4 files changed, 12 insertions(+), 6 deletions(-) rename library/src/main/java/me/chan/texas/renderer/ui/rv/{TexasItemAnimator.java => anim/DefaultTexasItemAnimator.java} (96%) create mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/SegmentItemDecoration.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/SegmentItemDecoration.java index 9525f12d..2f2527bf 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/SegmentItemDecoration.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/SegmentItemDecoration.java @@ -9,8 +9,10 @@ import me.chan.texas.utils.TexasUtils; import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; import androidx.recyclerview.widget.RecyclerView; +@RestrictTo(RestrictTo.Scope.LIBRARY) public class SegmentItemDecoration extends RecyclerView.ItemDecoration { private final TexasRendererAdapter mAdapter; private final me.chan.texas.misc.Rect mRect = new me.chan.texas.misc.Rect(); diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java index 3f1ea9ba..8a3e77a5 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java @@ -8,19 +8,17 @@ import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.SimpleItemAnimator; import static androidx.annotation.RestrictTo.Scope.LIBRARY; -import me.chan.texas.R; import me.chan.texas.misc.Rect; import me.chan.texas.renderer.TouchEvent; import me.chan.texas.renderer.ui.TexasRendererAdapter; +import me.chan.texas.renderer.ui.rv.anim.DefaultTexasItemAnimator; import me.chan.texas.renderer.ui.text.ParagraphView; import me.chan.texas.text.Document; import me.chan.texas.text.Paragraph; import me.chan.texas.text.Segment; -import me.chan.texas.renderer.selection.SelectionProvider; import me.chan.texas.text.ViewSegment; import me.chan.texas.text.layout.Layout; @@ -48,7 +46,7 @@ protected void onClicked(MotionEvent event) { } }; - setItemAnimator(new TexasItemAnimator()); + setItemAnimator(new DefaultTexasItemAnimator()); } public void scrollToPosition(int position, boolean smooth, int offset) { diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java similarity index 96% rename from library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java rename to library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java index 1609586c..8a1a362f 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java @@ -1,4 +1,4 @@ -package me.chan.texas.renderer.ui.rv; +package me.chan.texas.renderer.ui.rv.anim; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView; @@ -15,7 +16,8 @@ import java.util.HashMap; import java.util.List; -class TexasItemAnimator extends RecyclerView.ItemAnimator { +@RestrictTo(RestrictTo.Scope.LIBRARY) +public class DefaultTexasItemAnimator extends RecyclerView.ItemAnimator { private static final long APPEARANCE_DURATION = 250; private static final long DISAPPEARANCE_DURATION = 200; diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java new file mode 100644 index 00000000..8917ec0f --- /dev/null +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java @@ -0,0 +1,4 @@ +package me.chan.texas.renderer.ui.rv.anim; + +public class SegmentItemAnimator { +} From 32ed355cd8a1b695f671df97c4c3f737a3b50444 Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 16 Mar 2026 16:38:28 +0800 Subject: [PATCH 08/26] refactor item aninator --- .../texas/renderer/ui/rv/anim/AnimRecord.java | 19 ++ .../renderer/ui/rv/anim/AnimTracker.java | 50 ++++++ .../ui/rv/anim/DefaultTexasItemAnimator.java | 164 ++++++------------ .../renderer/ui/rv/anim/ItemAnimType.java | 6 + .../ui/rv/anim/SegmentItemAnimator.java | 22 ++- 5 files changed, 145 insertions(+), 116 deletions(-) create mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java create mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimTracker.java create mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/ItemAnimType.java diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java new file mode 100644 index 00000000..5aff5aa9 --- /dev/null +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java @@ -0,0 +1,19 @@ +package me.chan.texas.renderer.ui.rv.anim; + +import android.animation.Animator; + +class AnimRecord { + public static final int PHASE_PENDING = 0; + public static final int PHASE_POSTPONED = 1; + public static final int PHASE_RUNNING = 2; + + public final ItemAnimType type; + public int phase; + public final Animator animator; + + AnimRecord(ItemAnimType type, Animator animator) { + this.type = type; + this.animator = animator; + this.phase = PHASE_PENDING; + } +} \ No newline at end of file diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimTracker.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimTracker.java new file mode 100644 index 00000000..5e7a540d --- /dev/null +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimTracker.java @@ -0,0 +1,50 @@ +package me.chan.texas.renderer.ui.rv.anim; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +class AnimTracker { + private final HashMap mRecords = new HashMap<>(); + + public void add(RecyclerView.ViewHolder holder, AnimRecord record) { + mRecords.put(holder, record); + } + + public boolean remove(RecyclerView.ViewHolder holder) { + return mRecords.remove(holder) != null; + } + + @Nullable + public AnimRecord get(RecyclerView.ViewHolder holder) { + return mRecords.get(holder); + } + + public void advanceTo(RecyclerView.ViewHolder holder, int phase) { + AnimRecord record = mRecords.get(holder); + if (record != null) { + record.phase = phase; + } + } + + public List holdersByPhase(int phase) { + List result = new ArrayList<>(); + for (HashMap.Entry entry : mRecords.entrySet()) { + if (entry.getValue().phase == phase) { + result.add(entry.getKey()); + } + } + return result; + } + + public List allHolders() { + return new ArrayList<>(mRecords.keySet()); + } + + public boolean isEmpty() { + return mRecords.isEmpty(); + } +} \ No newline at end of file diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java index 8a1a362f..827479c3 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java @@ -2,9 +2,6 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.view.View; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -12,45 +9,73 @@ import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; +import me.chan.texas.R; +import me.chan.texas.text.Segment; + @RestrictTo(RestrictTo.Scope.LIBRARY) public class DefaultTexasItemAnimator extends RecyclerView.ItemAnimator { - private static final long APPEARANCE_DURATION = 250; - private static final long DISAPPEARANCE_DURATION = 200; + @Nullable + private SegmentItemAnimator mSegmentItemAnimator; private final AnimTracker mTracker = new AnimTracker(); + public void setSegmentItemAnimator(@Nullable SegmentItemAnimator segmentItemAnimator) { + mSegmentItemAnimator = segmentItemAnimator; + } + @Override public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { endAnimation(viewHolder); - mTracker.add(viewHolder, AnimRecord.TYPE_DISAPPEARANCE); - return true; + return createAnimator(viewHolder, ItemAnimType.DISAPPEARANCE); } @Override public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { endAnimation(viewHolder); - View itemView = viewHolder.itemView; - int height = postLayoutInfo.bottom - postLayoutInfo.top; + return createAnimator(viewHolder, ItemAnimType.APPEARANCE); + } + + private boolean createAnimator(RecyclerView.ViewHolder holder, ItemAnimType type) { + if (mSegmentItemAnimator == null) { + return false; + } - itemView.setAlpha(0f); - itemView.setTranslationY(-height); + Segment segment = (Segment) holder.itemView.getTag(R.id.me_chan_texas_item_tag); + if (segment == null) { + return false; + } + + Animator animator = mSegmentItemAnimator.createAnimator(segment, holder.itemView, type); + if (animator == null) { + return false; + } - mTracker.add(viewHolder, AnimRecord.TYPE_APPEARANCE); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animation.removeListener(this); + if (mTracker.remove(holder)) { + dispatchAnimationFinished(holder); + } + } + }); + mTracker.add(holder, new AnimRecord(type, animator)); return true; } @Override public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { - return false; + endAnimation(viewHolder); + return createAnimator(viewHolder, ItemAnimType.PERSISTENCE); } @Override public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { - return false; + endAnimation(oldHolder); + endAnimation(newHolder); + return createAnimator(newHolder, ItemAnimType.CHANGE); } @Override @@ -66,52 +91,20 @@ public void runPendingAnimations() { AnimRecord record = mTracker.get(holder); if (record != null && record.phase == AnimRecord.PHASE_POSTPONED) { mTracker.advanceTo(holder, AnimRecord.PHASE_RUNNING); - startAnimation(holder, record.type); + startAnimation(holder); } }); } } - private void startAnimation(@NonNull RecyclerView.ViewHolder holder, int type) { + private void startAnimation(@NonNull RecyclerView.ViewHolder holder) { dispatchAnimationStarted(holder); - View itemView = holder.itemView; - - if (type == AnimRecord.TYPE_APPEARANCE) { - itemView.animate() - .alpha(1f) - .translationY(0f) - .setDuration(APPEARANCE_DURATION) - .setInterpolator(new DecelerateInterpolator()) - .setListener(createEndListener(holder)) - .start(); - } else { - int height = itemView.getHeight(); - itemView.animate() - .alpha(0f) - .translationY(-height) - .setDuration(DISAPPEARANCE_DURATION) - .setInterpolator(new AccelerateInterpolator()) - .setListener(createEndListener(holder)) - .start(); + AnimRecord record = mTracker.get(holder); + if (record == null) { + return; } - } - - private AnimatorListenerAdapter createEndListener(@NonNull RecyclerView.ViewHolder holder) { - return new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - holder.itemView.animate().setListener(null); - resetView(holder.itemView); - if (mTracker.remove(holder)) { - dispatchAnimationFinished(holder); - } - } - }; - } - private void resetView(View view) { - view.setAlpha(1f); - view.setTranslationY(0f); + record.animator.start(); } @Override @@ -120,9 +113,9 @@ public void endAnimation(@NonNull RecyclerView.ViewHolder item) { if (record == null) { return; } + boolean isRunning = record.phase == AnimRecord.PHASE_RUNNING; - item.itemView.animate().cancel(); - resetView(item.itemView); + record.animator.cancel(); // running: cancel() 已同步触发 onAnimationEnd → dispatch,不再重复 if (!isRunning && mTracker.remove(item)) { dispatchAnimationFinished(item); @@ -140,63 +133,4 @@ public void endAnimations() { public boolean isRunning() { return !mTracker.isEmpty(); } - - static class AnimRecord { - static final int TYPE_APPEARANCE = 1; - static final int TYPE_DISAPPEARANCE = 2; - - static final int PHASE_PENDING = 0; - static final int PHASE_POSTPONED = 1; - static final int PHASE_RUNNING = 2; - - final int type; - int phase; - - AnimRecord(int type) { - this.type = type; - this.phase = PHASE_PENDING; - } - } - - static class AnimTracker { - private final HashMap mRecords = new HashMap<>(); - - void add(RecyclerView.ViewHolder holder, int type) { - mRecords.put(holder, new AnimRecord(type)); - } - - boolean remove(RecyclerView.ViewHolder holder) { - return mRecords.remove(holder) != null; - } - - @Nullable - AnimRecord get(RecyclerView.ViewHolder holder) { - return mRecords.get(holder); - } - - void advanceTo(RecyclerView.ViewHolder holder, int phase) { - AnimRecord record = mRecords.get(holder); - if (record != null) { - record.phase = phase; - } - } - - List holdersByPhase(int phase) { - List result = new ArrayList<>(); - for (HashMap.Entry entry : mRecords.entrySet()) { - if (entry.getValue().phase == phase) { - result.add(entry.getKey()); - } - } - return result; - } - - List allHolders() { - return new ArrayList<>(mRecords.keySet()); - } - - boolean isEmpty() { - return mRecords.isEmpty(); - } - } } diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/ItemAnimType.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/ItemAnimType.java new file mode 100644 index 00000000..e56ebfc4 --- /dev/null +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/ItemAnimType.java @@ -0,0 +1,6 @@ +package me.chan.texas.renderer.ui.rv.anim; + +public enum ItemAnimType { + APPEARANCE, DISAPPEARANCE, + PERSISTENCE, CHANGE +} diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java index 8917ec0f..e29b45e2 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java @@ -1,4 +1,24 @@ package me.chan.texas.renderer.ui.rv.anim; -public class SegmentItemAnimator { +import android.animation.Animator; +import android.view.View; + +import androidx.annotation.Nullable; + +import me.chan.texas.text.Segment; + +public abstract class SegmentItemAnimator { + + public final Animator createAnimator(Segment segment, View itemView, ItemAnimType type) { + return onCreateAnimator(segment, itemView, type); + } + + /** + * @param segment segment + * @param itemView itemView + * @param type type + * @return Animator, 返回空则代表不显示动画 + */ + @Nullable + protected abstract Animator onCreateAnimator(Segment segment, View itemView, ItemAnimType type); } From 0e2f9977248654f7836321ce31918a1d38db7578 Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 16 Mar 2026 16:42:50 +0800 Subject: [PATCH 09/26] refactor item aninator --- .../renderer/ui/rv/anim/DefaultTexasItemAnimator.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java index 827479c3..28cf0cf5 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java @@ -75,7 +75,14 @@ public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder, @ public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { endAnimation(oldHolder); endAnimation(newHolder); - return createAnimator(newHolder, ItemAnimType.CHANGE); + boolean created = createAnimator(newHolder, ItemAnimType.CHANGE); + if (!created) { + return false; + } + if (oldHolder != newHolder) { + dispatchAnimationFinished(oldHolder); + } + return true; } @Override @@ -98,12 +105,12 @@ public void runPendingAnimations() { } private void startAnimation(@NonNull RecyclerView.ViewHolder holder) { - dispatchAnimationStarted(holder); AnimRecord record = mTracker.get(holder); if (record == null) { return; } + dispatchAnimationStarted(holder); record.animator.start(); } From e2075bc40b456013fdbb929ed8d13f45451c9343 Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 16 Mar 2026 16:49:33 +0800 Subject: [PATCH 10/26] support texas segment animator --- .../java/me/chan/texas/renderer/Renderer.java | 4 +++ .../me/chan/texas/renderer/TexasView.java | 28 +++++++++++++++++++ .../renderer/ui/rv/TexasRecyclerViewImpl.java | 8 +++++- .../texas/renderer/ui/rv/anim/AnimRecord.java | 4 +-- .../anim/{ItemAnimType.java => AnimType.java} | 2 +- .../ui/rv/anim/DefaultTexasItemAnimator.java | 15 +++++----- .../renderer/ui/rv/anim/SegmentAnimator.java | 8 ++++++ .../ui/rv/anim/SegmentItemAnimator.java | 24 ---------------- 8 files changed, 58 insertions(+), 35 deletions(-) rename library/src/main/java/me/chan/texas/renderer/ui/rv/anim/{ItemAnimType.java => AnimType.java} (77%) create mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimator.java delete mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java diff --git a/library/src/main/java/me/chan/texas/renderer/Renderer.java b/library/src/main/java/me/chan/texas/renderer/Renderer.java index 190c614e..f39bf386 100644 --- a/library/src/main/java/me/chan/texas/renderer/Renderer.java +++ b/library/src/main/java/me/chan/texas/renderer/Renderer.java @@ -546,4 +546,8 @@ public void smoothScrollBy(int dx, int dy) { public SelectionMethod getSelectionMethod() { return mSelectionMethod; } + + public void setSegmentAnimator(TexasView.SegmentAnimator segmentAnimator) { + mRecyclerView.setSegmentAnimator(segmentAnimator); + } } diff --git a/library/src/main/java/me/chan/texas/renderer/TexasView.java b/library/src/main/java/me/chan/texas/renderer/TexasView.java index 8281bb8a..3f47f6c4 100644 --- a/library/src/main/java/me/chan/texas/renderer/TexasView.java +++ b/library/src/main/java/me/chan/texas/renderer/TexasView.java @@ -1,5 +1,6 @@ package me.chan.texas.renderer; +import android.animation.Animator; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; @@ -15,6 +16,7 @@ import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; +import android.view.View; import android.widget.FrameLayout; import androidx.annotation.AnyThread; @@ -45,6 +47,7 @@ import me.chan.texas.misc.PaintSet; import me.chan.texas.renderer.core.worker.LoadingWorker; import me.chan.texas.renderer.selection.Selection; +import me.chan.texas.renderer.ui.rv.anim.AnimType; import me.chan.texas.source.Source; import me.chan.texas.text.BreakStrategy; import me.chan.texas.text.Document; @@ -342,6 +345,14 @@ public void setSegmentDecoration(@NonNull SegmentDecoration segmentDecoration) { mRenderer.setSegmentDecoration(segmentDecoration); } + public void setSegmentAnimator(@NonNull SegmentAnimator segmentAnimator) { + if (mRenderer == null) { + return; + } + + mRenderer.setSegmentAnimator(segmentAnimator); + } + private void load(String reason) { if (mRenderer == null) { return; @@ -1046,6 +1057,23 @@ public interface SegmentDecoration { void onDecorateSegment(int index, int count, Segment segment, Document document, Rect outRect); } + public abstract class SegmentAnimator { + + public final Animator createAnimator(Segment segment, View itemView, AnimType type) { + return onCreateAnimator(segment, itemView, type); + } + + /** + * @param segment segment + * @param itemView itemView + * @param type type + * @return Animator, 返回空则代表不显示动画 + */ + @Nullable + protected abstract Animator onCreateAnimator(Segment segment, View itemView, AnimType type); + } + + /** * Scroll state listener */ diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java index 8a3e77a5..ac707951 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java @@ -12,6 +12,7 @@ import static androidx.annotation.RestrictTo.Scope.LIBRARY; import me.chan.texas.misc.Rect; +import me.chan.texas.renderer.TexasView; import me.chan.texas.renderer.TouchEvent; import me.chan.texas.renderer.ui.TexasRendererAdapter; import me.chan.texas.renderer.ui.rv.anim.DefaultTexasItemAnimator; @@ -29,6 +30,7 @@ public class TexasRecyclerViewImpl extends RecyclerView implements TexasRecycler private OnClickedListener mOnClickedListener; private ScrollAction mScrollAction; private final TexasLinearLayoutManagerImpl mTexasLinearLayoutManager; + private final DefaultTexasItemAnimator mItemAnimator = new DefaultTexasItemAnimator(); public TexasRecyclerViewImpl(@NonNull Context context, TexasLinearLayoutManagerImpl texasLinearLayoutManager) { super(context); @@ -46,7 +48,7 @@ protected void onClicked(MotionEvent event) { } }; - setItemAnimator(new DefaultTexasItemAnimator()); + setItemAnimator(mItemAnimator); } public void scrollToPosition(int position, boolean smooth, int offset) { @@ -79,6 +81,10 @@ public void getChildLocations(View child, Rect locations) { locations.bottom = child.getBottom(); } + public void setSegmentAnimator(TexasView.SegmentAnimator segmentAnimator) { + mItemAnimator.setSegmentItemAnimator(segmentAnimator); + } + private class ScrollAction implements Runnable { public int position; public int offset; diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java index 5aff5aa9..d857e560 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java @@ -7,11 +7,11 @@ class AnimRecord { public static final int PHASE_POSTPONED = 1; public static final int PHASE_RUNNING = 2; - public final ItemAnimType type; + public final AnimType type; public int phase; public final Animator animator; - AnimRecord(ItemAnimType type, Animator animator) { + AnimRecord(AnimType type, Animator animator) { this.type = type; this.animator = animator; this.phase = PHASE_PENDING; diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/ItemAnimType.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimType.java similarity index 77% rename from library/src/main/java/me/chan/texas/renderer/ui/rv/anim/ItemAnimType.java rename to library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimType.java index e56ebfc4..4298ae99 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/ItemAnimType.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimType.java @@ -1,6 +1,6 @@ package me.chan.texas.renderer.ui.rv.anim; -public enum ItemAnimType { +public enum AnimType { APPEARANCE, DISAPPEARANCE, PERSISTENCE, CHANGE } diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java index 28cf0cf5..f4069c9d 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java @@ -12,32 +12,33 @@ import java.util.List; import me.chan.texas.R; +import me.chan.texas.renderer.TexasView; import me.chan.texas.text.Segment; @RestrictTo(RestrictTo.Scope.LIBRARY) public class DefaultTexasItemAnimator extends RecyclerView.ItemAnimator { @Nullable - private SegmentItemAnimator mSegmentItemAnimator; + private TexasView.SegmentAnimator mSegmentItemAnimator; private final AnimTracker mTracker = new AnimTracker(); - public void setSegmentItemAnimator(@Nullable SegmentItemAnimator segmentItemAnimator) { + public void setSegmentItemAnimator(@Nullable TexasView.SegmentAnimator segmentItemAnimator) { mSegmentItemAnimator = segmentItemAnimator; } @Override public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { endAnimation(viewHolder); - return createAnimator(viewHolder, ItemAnimType.DISAPPEARANCE); + return createAnimator(viewHolder, AnimType.DISAPPEARANCE); } @Override public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { endAnimation(viewHolder); - return createAnimator(viewHolder, ItemAnimType.APPEARANCE); + return createAnimator(viewHolder, AnimType.APPEARANCE); } - private boolean createAnimator(RecyclerView.ViewHolder holder, ItemAnimType type) { + private boolean createAnimator(RecyclerView.ViewHolder holder, AnimType type) { if (mSegmentItemAnimator == null) { return false; } @@ -68,14 +69,14 @@ public void onAnimationEnd(Animator animation) { @Override public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { endAnimation(viewHolder); - return createAnimator(viewHolder, ItemAnimType.PERSISTENCE); + return createAnimator(viewHolder, AnimType.PERSISTENCE); } @Override public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { endAnimation(oldHolder); endAnimation(newHolder); - boolean created = createAnimator(newHolder, ItemAnimType.CHANGE); + boolean created = createAnimator(newHolder, AnimType.CHANGE); if (!created) { return false; } diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimator.java new file mode 100644 index 00000000..ccf0258b --- /dev/null +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimator.java @@ -0,0 +1,8 @@ +package me.chan.texas.renderer.ui.rv.anim; + +import android.animation.Animator; +import android.view.View; + +import androidx.annotation.Nullable; + +import me.chan.texas.text.Segment; \ No newline at end of file diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java deleted file mode 100644 index e29b45e2..00000000 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentItemAnimator.java +++ /dev/null @@ -1,24 +0,0 @@ -package me.chan.texas.renderer.ui.rv.anim; - -import android.animation.Animator; -import android.view.View; - -import androidx.annotation.Nullable; - -import me.chan.texas.text.Segment; - -public abstract class SegmentItemAnimator { - - public final Animator createAnimator(Segment segment, View itemView, ItemAnimType type) { - return onCreateAnimator(segment, itemView, type); - } - - /** - * @param segment segment - * @param itemView itemView - * @param type type - * @return Animator, 返回空则代表不显示动画 - */ - @Nullable - protected abstract Animator onCreateAnimator(Segment segment, View itemView, ItemAnimType type); -} From 10e5b23bffce81aaab0b0bcfbdf77dcdd87055c4 Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 16 Mar 2026 16:57:21 +0800 Subject: [PATCH 11/26] support texas segment animator --- .../renderer/ui/rv/anim/DefaultTexasItemAnimator.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java index f4069c9d..5dd56804 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java @@ -88,6 +88,10 @@ public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNul @Override public void runPendingAnimations() { + if (mTracker.isEmpty()) { + return; + } + List pending = mTracker.holdersByPhase(AnimRecord.PHASE_PENDING); if (pending.isEmpty()) { return; @@ -132,6 +136,10 @@ public void endAnimation(@NonNull RecyclerView.ViewHolder item) { @Override public void endAnimations() { + if (mTracker.isEmpty()) { + return; + } + for (RecyclerView.ViewHolder holder : mTracker.allHolders()) { endAnimation(holder); } From 089e19910315f8e2cd619e54a7f1c0fb166d546b Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 16 Mar 2026 17:08:12 +0800 Subject: [PATCH 12/26] support texas segment animator --- .../me/chan/texas/TexasViewDemoActivity.java | 34 ++++++++++++++++++- .../main/res/layout/activity_paragraph.xml | 2 +- .../me/chan/texas/renderer/TexasView.java | 8 ++--- .../texas/renderer/ui/rv/anim/AnimRecord.java | 4 +-- .../ui/rv/anim/DefaultTexasItemAnimator.java | 10 +++--- .../{AnimType.java => SegmentAnimType.java} | 2 +- 6 files changed, 46 insertions(+), 14 deletions(-) rename library/src/main/java/me/chan/texas/renderer/ui/rv/anim/{AnimType.java => SegmentAnimType.java} (76%) diff --git a/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java b/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java index 7226ce2d..a54266f4 100644 --- a/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java +++ b/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java @@ -1,6 +1,9 @@ package me.chan.texas; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.graphics.Color; import android.graphics.Paint; @@ -29,9 +32,11 @@ import me.chan.texas.renderer.TexasView; import me.chan.texas.renderer.TouchEvent; import me.chan.texas.renderer.selection.Selection; +import me.chan.texas.renderer.ui.rv.anim.SegmentAnimType; import me.chan.texas.text.BreakStrategy; import me.chan.texas.text.Document; import me.chan.texas.text.Paragraph; +import me.chan.texas.text.Segment; import me.chan.texas.utils.TexasUtils; public class TexasViewDemoActivity extends AppCompatActivity { @@ -369,6 +374,31 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { renderOption.setTypeface(typeface); mTexasView.refresh(renderOption); + mTexasView.setSegmentAnimator(new TexasView.SegmentAnimator() { + + @Nullable + @Override + protected Animator onCreateAnimator(Segment segment, View itemView, SegmentAnimType type) { + if (segment.getTag() != SegmentAnimType.APPEARANCE && type != SegmentAnimType.APPEARANCE) { + return null; + } + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.play(ObjectAnimator.ofFloat(itemView, "alpha", 0, 1)) + .with(ObjectAnimator.ofFloat(itemView, "translationY", -itemView.getHeight(), 0)); + animatorSet.setDuration(500); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + itemView.setAlpha(1); + itemView.setTranslationY(0); + } + }); + return animatorSet; + } + }); + findViewById(me.chan.texas.debug.R.id.add_content).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -377,8 +407,10 @@ public void onClick(View v) { protected Document onRead(TexasOption option, @Nullable Document previousDocument) { return new Document.Builder(previousDocument) .addSegment( + 0, Paragraph.Builder.newBuilder(option) - .text("hello world") + .tag(SegmentAnimType.APPEARANCE) + .text("生活就像点菜,饥饿时菜会点得特别多,但吃一阵就会意识到浪费;如果慢条斯理地盘算怎么点菜,别人已经要吃完了。") .build() ) .build(); diff --git a/app/src/main/res/layout/activity_paragraph.xml b/app/src/main/res/layout/activity_paragraph.xml index 899ca2c9..ee474a23 100644 --- a/app/src/main/res/layout/activity_paragraph.xml +++ b/app/src/main/res/layout/activity_paragraph.xml @@ -74,7 +74,7 @@ android:id="@+id/anim" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="动画" /> + android:text="选中动画" /> Date: Mon, 16 Mar 2026 17:09:59 +0800 Subject: [PATCH 13/26] support texas segment animator --- app/src/main/java/me/chan/texas/TexasViewDemoActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java b/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java index a54266f4..0dc51380 100644 --- a/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java +++ b/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java @@ -379,10 +379,11 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @Nullable @Override protected Animator onCreateAnimator(Segment segment, View itemView, SegmentAnimType type) { - if (segment.getTag() != SegmentAnimType.APPEARANCE && type != SegmentAnimType.APPEARANCE) { + if (segment.getTag() != SegmentAnimType.APPEARANCE || type != SegmentAnimType.APPEARANCE) { return null; } + Log.d("chan_debug", "onCreateAnimator: " + type + " -> " + segment); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.play(ObjectAnimator.ofFloat(itemView, "alpha", 0, 1)) .with(ObjectAnimator.ofFloat(itemView, "translationY", -itemView.getHeight(), 0)); From 75ab1da19bb16f4500e82f1ea51fc0fdd5a8e865 Mon Sep 17 00:00:00 2001 From: chan Date: Mon, 16 Mar 2026 17:31:06 +0800 Subject: [PATCH 14/26] support texas segment animator --- .../chan/texas/renderer/ui/rv/anim/SegmentAnimator.java | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimator.java diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimator.java deleted file mode 100644 index ccf0258b..00000000 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimator.java +++ /dev/null @@ -1,8 +0,0 @@ -package me.chan.texas.renderer.ui.rv.anim; - -import android.animation.Animator; -import android.view.View; - -import androidx.annotation.Nullable; - -import me.chan.texas.text.Segment; \ No newline at end of file From ee3c7b2662421072819c31801429a8ea7ed4fa78 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 10:08:16 +0800 Subject: [PATCH 15/26] support texas segment animator --- .../java/me/chan/texas/text/Paragraph.java | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/library/src/main/java/me/chan/texas/text/Paragraph.java b/library/src/main/java/me/chan/texas/text/Paragraph.java index c4ef0e92..d6e672d0 100644 --- a/library/src/main/java/me/chan/texas/text/Paragraph.java +++ b/library/src/main/java/me/chan/texas/text/Paragraph.java @@ -41,19 +41,6 @@ * 段落 */ public final class Paragraph implements Segment { - - @NonNull - @RestrictTo(LIBRARY) - volatile Layout mLayout; - - @RestrictTo(LIBRARY) - final List mElements; - - @RestrictTo(RestrictTo.Scope.LIBRARY) - SparseArrayCompat mTagsKv; - - ParagraphDecor mDecor; - /** * 默认 */ @@ -71,6 +58,29 @@ public final class Paragraph implements Segment { */ public static final int TYPESET_POLICY_ACCEPT_CONTROL_CHAR = 4; + + @NonNull + @RestrictTo(LIBRARY) + volatile Layout mLayout; + + @RestrictTo(LIBRARY) + final List mElements; + + @RestrictTo(RestrictTo.Scope.LIBRARY) + SparseArrayCompat mTagsKv; + + ParagraphDecor mDecor; + + int mId; + + private ParagraphSelection mSelection; + + private ParagraphSelection mHighlight; + + private RecyclerView.ViewHolder mHolder; + + private RendererHost mHost; + @RestrictTo(LIBRARY) @Nullable public ParagraphDecor getDecor() { @@ -83,8 +93,6 @@ public ParagraphDecor getDecor() { public @interface TypesetPolicy { } - int mId; - @Nullable @Override public Object getTag() { @@ -116,10 +124,6 @@ public void setPadding(Rect rect) { mLayout.setPadding(rect); } - private ParagraphSelection mSelection; - - private ParagraphSelection mHighlight; - @RestrictTo(LIBRARY) @Nullable public ParagraphSelection getSelection(Selection.Type type) { @@ -192,9 +196,6 @@ public int getId() { return mId; } - private RecyclerView.ViewHolder mHolder; - private RendererHost mHost; - @Override public void bind(RendererHost host) { mHost = host; From c81fd8bf13ce15dafcfcc43e27bb83bf28818ea3 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 11:35:48 +0800 Subject: [PATCH 16/26] support paragraph split --- .../java/me/chan/texas/text/Paragraph.java | 53 +++++++++++++++ .../texas/text/ParagraphBuilderInternal.java | 2 - .../me/chan/texas/text/ParagraphUnitTest.java | 68 +++++++++++++++++++ 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/me/chan/texas/text/Paragraph.java b/library/src/main/java/me/chan/texas/text/Paragraph.java index d6e672d0..cd33b14b 100644 --- a/library/src/main/java/me/chan/texas/text/Paragraph.java +++ b/library/src/main/java/me/chan/texas/text/Paragraph.java @@ -24,6 +24,7 @@ import me.chan.texas.renderer.selection.Selection; import me.chan.texas.renderer.ui.RendererHost; import me.chan.texas.renderer.ui.decor.ParagraphDecor; +import me.chan.texas.text.layout.Box; import me.chan.texas.text.layout.Element; import me.chan.texas.text.layout.Glue; import me.chan.texas.text.layout.Layout; @@ -36,6 +37,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; /** * 段落 @@ -154,6 +156,8 @@ private Paragraph(Object tag) { } Texas.MemoryOption memoryOption = Texas.getMemoryOption(); mElements = new ArrayList<>(memoryOption.getParagraphElementInitialCapacity()); + mLayout = Layout.obtain(); + mId = Segment.nextId(); } @RestrictTo(LIBRARY) @@ -842,6 +846,55 @@ public void setTag(@IdRes int id, @Nullable Object tag) { mTagsKv.put(id, tag); } + @NonNull + public List split(Predicate predicate) { + List paragraphs = new ArrayList<>(); + int start = 0; + int end = 1; + for (; end < mElements.size(); ++end) { + Element element = mElements.get(end); + if (!(element instanceof Box)) { + continue; + } + + Box box = (Box) element; + if (predicate.test(box)) { + paragraphs.add(copy(start, end + 1)); + start = end + 1; + } + } + + if (start < end) { + paragraphs.add(copy(start, end)); + } + + return paragraphs; + } + + private Paragraph copy(int start, int end) { + Paragraph copy = new Paragraph(null); + for (int i = start; i < end; ++i) { + copy.mElements.add(mElements.get(i)); + } + copy.mTagsKv = mTagsKv; + copy.mDecor = mDecor; + copy.mSelection = mSelection; + copy.mHighlight = mHighlight; + copy.fillTail(); + + return copy; + } + + private void fillTail() { + int size = mElements.size(); + if (size > 2 && mElements.get(size - 2) == Glue.TERMINAL && mElements.get(size - 1) == Penalty.FORCE_BREAK) { + return; + } + + mElements.add(Glue.TERMINAL); + mElements.add(Penalty.FORCE_BREAK); + } + @NonNull @Override public String toString() { diff --git a/library/src/main/java/me/chan/texas/text/ParagraphBuilderInternal.java b/library/src/main/java/me/chan/texas/text/ParagraphBuilderInternal.java index bcf9a0f4..1b3a35f7 100644 --- a/library/src/main/java/me/chan/texas/text/ParagraphBuilderInternal.java +++ b/library/src/main/java/me/chan/texas/text/ParagraphBuilderInternal.java @@ -116,7 +116,6 @@ public Paragraph build(boolean brk) { } mParagraph.setTag(mTag); - mParagraph.mId = Segment.nextId(); return mParagraph; } @@ -137,7 +136,6 @@ public void reset(TexasOption texasOption) { mRenderOption = texasOption.getRenderOption(); mHyphenation = texasOption.getHyphenation(); mParagraph = Paragraph.obtain(); - mParagraph.mLayout = Layout.obtain(); mParagraph.mLayout.getAdvise().copy(mRenderOption); mCommonGlue = Glue.obtain(); mStretchOnlyGlue = Glue.obtain(Glue.FLAG_STRETCH); diff --git a/library/src/test/java/me/chan/texas/text/ParagraphUnitTest.java b/library/src/test/java/me/chan/texas/text/ParagraphUnitTest.java index 04aa0dd1..5869774b 100644 --- a/library/src/test/java/me/chan/texas/text/ParagraphUnitTest.java +++ b/library/src/test/java/me/chan/texas/text/ParagraphUnitTest.java @@ -1516,4 +1516,72 @@ protected void onVisitBox(Box box, RectF inner, RectF outer, @NonNull RendererCo }; visitor.visit(paragraph); } + + @Test + public void testSpilt() { + TexasOption texasOption = new TexasOption(mPaintSet, Hyphenation.getInstance(), mMeasurer, mTextAttribute, new RenderOption()); + Paragraph.Builder builder = Paragraph.Builder.newBuilder(texasOption); + + // 重点:Paragraph 的 element 末尾一定是 Glue.TERMINAL + Penalty.FORCE_BREAK + // split 后的每个子 paragraph 也必须满足此约束(由 copy -> fillTail 保证) + + // case 1: 无匹配时,整个 paragraph 保留,末尾应有 Glue.TERMINAL + Penalty.FORCE_BREAK + builder.text("hello world"); + Paragraph paragraph = builder.build(); + java.util.List result = paragraph.split(box -> false); + Assert.assertEquals(1, result.size()); + assertElementEndsWithTerminalAndForceBreak(result.get(0)); + + // case 2: 在中间 split,两个子 paragraph 都应以 Glue.TERMINAL + Penalty.FORCE_BREAK 结尾 + builder = Paragraph.Builder.newBuilder(texasOption); + builder.text("hello world foo"); + paragraph = builder.build(); + result = paragraph.split(box -> "world".equals(box.toString())); + Assert.assertEquals(2, result.size()); + assertElementEndsWithTerminalAndForceBreak(result.get(0)); + assertElementEndsWithTerminalAndForceBreak(result.get(1)); + + // case 3: 在第一个可匹配的 box 处 split(split 从 index 1 开始遍历,故首元素不会被当作 split 点) + builder = Paragraph.Builder.newBuilder(texasOption); + builder.text("a bb ccc"); + paragraph = builder.build(); + result = paragraph.split(box -> "bb".equals(box.toString())); + Assert.assertEquals(2, result.size()); + assertElementEndsWithTerminalAndForceBreak(result.get(0)); + assertElementEndsWithTerminalAndForceBreak(result.get(1)); + + // case 4: 在最后一个 box 处 split,第二个子 paragraph 仅含最后一个 box + tail + builder = Paragraph.Builder.newBuilder(texasOption); + builder.text("x y z"); + paragraph = builder.build(); + result = paragraph.split(box -> "z".equals(box.toString())); + Assert.assertEquals(2, result.size()); + assertElementEndsWithTerminalAndForceBreak(result.get(0)); + assertElementEndsWithTerminalAndForceBreak(result.get(1)); + + // case 5: 多处 split + builder = Paragraph.Builder.newBuilder(texasOption); + builder.text("a b c d"); + paragraph = builder.build(); + result = paragraph.split(box -> "b".equals(box.toString()) || "d".equals(box.toString())); + Assert.assertEquals(3, result.size()); + for (Paragraph p : result) { + assertElementEndsWithTerminalAndForceBreak(p); + } + } + + /** + * 断言 Paragraph 的 element 末尾一定是 Glue.TERMINAL + Penalty.FORCE_BREAK + */ + private static void assertElementEndsWithTerminalAndForceBreak(Paragraph paragraph) { + int count = paragraph.getElementCount(); + Assert.assertTrue("element 至少需要 2 个(Glue.TERMINAL + Penalty.FORCE_BREAK)", count >= 2); + Assert.assertSame("倒数第二个 element 必须是 Glue.TERMINAL", Glue.TERMINAL, paragraph.getElement(count - 2)); + Assert.assertSame("最后一个 element 必须是 Penalty.FORCE_BREAK", Penalty.FORCE_BREAK, paragraph.getElement(count - 1)); + + if (count > 4) { + Assert.assertNotSame("没有重复的 Glue.TERMINAL", Glue.TERMINAL, paragraph.getElement(count - 4)); + Assert.assertNotSame("没有重复的 Penalty.FORCE_BREAK", Penalty.FORCE_BREAK, paragraph.getElement(count - 3)); + } + } } From 8037378387941c78944eb3614a7404a3f62fc50a Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 11:39:43 +0800 Subject: [PATCH 17/26] support paragraph split --- library/src/main/java/me/chan/texas/text/Paragraph.java | 2 +- .../test/java/me/chan/texas/text/ParagraphUnitTest.java | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/me/chan/texas/text/Paragraph.java b/library/src/main/java/me/chan/texas/text/Paragraph.java index cd33b14b..cc7c13bf 100644 --- a/library/src/main/java/me/chan/texas/text/Paragraph.java +++ b/library/src/main/java/me/chan/texas/text/Paragraph.java @@ -887,7 +887,7 @@ private Paragraph copy(int start, int end) { private void fillTail() { int size = mElements.size(); - if (size > 2 && mElements.get(size - 2) == Glue.TERMINAL && mElements.get(size - 1) == Penalty.FORCE_BREAK) { + if (size >= 2 && mElements.get(size - 2) == Glue.TERMINAL && mElements.get(size - 1) == Penalty.FORCE_BREAK) { return; } diff --git a/library/src/test/java/me/chan/texas/text/ParagraphUnitTest.java b/library/src/test/java/me/chan/texas/text/ParagraphUnitTest.java index 5869774b..2a21f5fa 100644 --- a/library/src/test/java/me/chan/texas/text/ParagraphUnitTest.java +++ b/library/src/test/java/me/chan/texas/text/ParagraphUnitTest.java @@ -1568,6 +1568,13 @@ public void testSpilt() { for (Paragraph p : result) { assertElementEndsWithTerminalAndForceBreak(p); } + + builder = Paragraph.Builder.newBuilder(texasOption); + paragraph = builder.build(); + Assert.assertFalse(paragraph.hasContent()); + result = paragraph.split(b -> false); + Assert.assertEquals(1, result.size()); + assertElementEndsWithTerminalAndForceBreak(result.get(0)); } /** @@ -1579,7 +1586,7 @@ private static void assertElementEndsWithTerminalAndForceBreak(Paragraph paragra Assert.assertSame("倒数第二个 element 必须是 Glue.TERMINAL", Glue.TERMINAL, paragraph.getElement(count - 2)); Assert.assertSame("最后一个 element 必须是 Penalty.FORCE_BREAK", Penalty.FORCE_BREAK, paragraph.getElement(count - 1)); - if (count > 4) { + if (count >= 4) { Assert.assertNotSame("没有重复的 Glue.TERMINAL", Glue.TERMINAL, paragraph.getElement(count - 4)); Assert.assertNotSame("没有重复的 Penalty.FORCE_BREAK", Penalty.FORCE_BREAK, paragraph.getElement(count - 3)); } From 130bc6a7dd819fe3043b2c15c1064741df74b95e Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 11:41:23 +0800 Subject: [PATCH 18/26] support paragraph split --- library/src/main/java/me/chan/texas/text/Paragraph.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/me/chan/texas/text/Paragraph.java b/library/src/main/java/me/chan/texas/text/Paragraph.java index cc7c13bf..c87ce94b 100644 --- a/library/src/main/java/me/chan/texas/text/Paragraph.java +++ b/library/src/main/java/me/chan/texas/text/Paragraph.java @@ -859,19 +859,19 @@ public List split(Predicate predicate) { Box box = (Box) element; if (predicate.test(box)) { - paragraphs.add(copy(start, end + 1)); + paragraphs.add(fork(start, end + 1)); start = end + 1; } } if (start < end) { - paragraphs.add(copy(start, end)); + paragraphs.add(fork(start, end)); } return paragraphs; } - private Paragraph copy(int start, int end) { + private Paragraph fork(int start, int end) { Paragraph copy = new Paragraph(null); for (int i = start; i < end; ++i) { copy.mElements.add(mElements.get(i)); From d96c4f6ae399e699eaee80c207be335c76b832fc Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 11:49:17 +0800 Subject: [PATCH 19/26] support paragraph split --- library/src/main/java/me/chan/texas/text/Paragraph.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/me/chan/texas/text/Paragraph.java b/library/src/main/java/me/chan/texas/text/Paragraph.java index c87ce94b..d89c89e9 100644 --- a/library/src/main/java/me/chan/texas/text/Paragraph.java +++ b/library/src/main/java/me/chan/texas/text/Paragraph.java @@ -847,7 +847,7 @@ public void setTag(@IdRes int id, @Nullable Object tag) { } @NonNull - public List split(Predicate predicate) { + public List split(Predicate predicate) { List paragraphs = new ArrayList<>(); int start = 0; int end = 1; From a717a78068bceec8933aad28f5a724985a3e93e2 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 14:33:08 +0800 Subject: [PATCH 20/26] support 17x --- .../ui/rv/anim/DefaultItemAnimator.java | 654 ++++++++++++++++++ 1 file changed, 654 insertions(+) create mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java new file mode 100644 index 00000000..e2769484 --- /dev/null +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java @@ -0,0 +1,654 @@ +package me.chan.texas.renderer.ui.rv.anim; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.view.View; +import android.view.ViewPropertyAnimator; + +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; + +import java.util.ArrayList; +import java.util.List; + +/** + * This implementation of {@link RecyclerView.ItemAnimator} provides basic + * animations on remove, add, and move events that happen to the items in + * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default. + * + * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator) + */ +public class DefaultItemAnimator extends SimpleItemAnimator { + private static final boolean DEBUG = false; + + private static TimeInterpolator sDefaultInterpolator; + + private ArrayList mPendingRemovals = new ArrayList<>(); + private ArrayList mPendingAdditions = new ArrayList<>(); + private ArrayList mPendingMoves = new ArrayList<>(); + private ArrayList mPendingChanges = new ArrayList<>(); + + ArrayList> mAdditionsList = new ArrayList<>(); + ArrayList> mMovesList = new ArrayList<>(); + ArrayList> mChangesList = new ArrayList<>(); + + ArrayList mAddAnimations = new ArrayList<>(); + ArrayList mMoveAnimations = new ArrayList<>(); + ArrayList mRemoveAnimations = new ArrayList<>(); + ArrayList mChangeAnimations = new ArrayList<>(); + + private static class MoveInfo { + public RecyclerView.ViewHolder holder; + public int fromX, fromY, toX, toY; + + MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { + this.holder = holder; + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + } + + private static class ChangeInfo { + public RecyclerView.ViewHolder oldHolder, newHolder; + public int fromX, fromY, toX, toY; + private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) { + this.oldHolder = oldHolder; + this.newHolder = newHolder; + } + + ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + this(oldHolder, newHolder); + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + + @Override + public String toString() { + return "ChangeInfo{" + + "oldHolder=" + oldHolder + + ", newHolder=" + newHolder + + ", fromX=" + fromX + + ", fromY=" + fromY + + ", toX=" + toX + + ", toY=" + toY + + '}'; + } + } + + @Override + public void runPendingAnimations() { + boolean removalsPending = !mPendingRemovals.isEmpty(); + boolean movesPending = !mPendingMoves.isEmpty(); + boolean changesPending = !mPendingChanges.isEmpty(); + boolean additionsPending = !mPendingAdditions.isEmpty(); + if (!removalsPending && !movesPending && !additionsPending && !changesPending) { + // nothing to animate + return; + } + // First, remove stuff + for (RecyclerView.ViewHolder holder : mPendingRemovals) { + animateRemoveImpl(holder); + } + mPendingRemovals.clear(); + // Next, move stuff + if (movesPending) { + final ArrayList moves = new ArrayList<>(); + moves.addAll(mPendingMoves); + mMovesList.add(moves); + mPendingMoves.clear(); + Runnable mover = new Runnable() { + @Override + public void run() { + for (MoveInfo moveInfo : moves) { + animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, + moveInfo.toX, moveInfo.toY); + } + moves.clear(); + mMovesList.remove(moves); + } + }; + if (removalsPending) { + View view = moves.get(0).holder.itemView; + ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); + } else { + mover.run(); + } + } + // Next, change stuff, to run in parallel with move animations + if (changesPending) { + final ArrayList changes = new ArrayList<>(); + changes.addAll(mPendingChanges); + mChangesList.add(changes); + mPendingChanges.clear(); + Runnable changer = new Runnable() { + @Override + public void run() { + for (ChangeInfo change : changes) { + animateChangeImpl(change); + } + changes.clear(); + mChangesList.remove(changes); + } + }; + if (removalsPending) { + RecyclerView.ViewHolder holder = changes.get(0).oldHolder; + ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); + } else { + changer.run(); + } + } + // Next, add stuff + if (additionsPending) { + final ArrayList additions = new ArrayList<>(); + additions.addAll(mPendingAdditions); + mAdditionsList.add(additions); + mPendingAdditions.clear(); + Runnable adder = new Runnable() { + @Override + public void run() { + for (RecyclerView.ViewHolder holder : additions) { + animateAddImpl(holder); + } + additions.clear(); + mAdditionsList.remove(additions); + } + }; + if (removalsPending || movesPending || changesPending) { + long removeDuration = removalsPending ? getRemoveDuration() : 0; + long moveDuration = movesPending ? getMoveDuration() : 0; + long changeDuration = changesPending ? getChangeDuration() : 0; + long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); + View view = additions.get(0).itemView; + ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); + } else { + adder.run(); + } + } + } + + @Override + public boolean animateRemove(final RecyclerView.ViewHolder holder) { + resetAnimation(holder); + mPendingRemovals.add(holder); + return true; + } + + private void animateRemoveImpl(final RecyclerView.ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimator animation = view.animate(); + mRemoveAnimations.add(holder); + animation.setDuration(getRemoveDuration()).alpha(0).setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchRemoveStarting(holder); + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + view.setAlpha(1); + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateAdd(final RecyclerView.ViewHolder holder) { + resetAnimation(holder); + holder.itemView.setAlpha(0); + mPendingAdditions.add(holder); + return true; + } + + void animateAddImpl(final RecyclerView.ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimator animation = view.animate(); + mAddAnimations.add(holder); + animation.alpha(1).setDuration(getAddDuration()) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchAddStarting(holder); + } + + @Override + public void onAnimationCancel(Animator animator) { + view.setAlpha(1); + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + dispatchAddFinished(holder); + mAddAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, + int toX, int toY) { + final View view = holder.itemView; + fromX += (int) holder.itemView.getTranslationX(); + fromY += (int) holder.itemView.getTranslationY(); + resetAnimation(holder); + int deltaX = toX - fromX; + int deltaY = toY - fromY; + if (deltaX == 0 && deltaY == 0) { + dispatchMoveFinished(holder); + return false; + } + if (deltaX != 0) { + view.setTranslationX(-deltaX); + } + if (deltaY != 0) { + view.setTranslationY(-deltaY); + } + mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); + return true; + } + + void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { + final View view = holder.itemView; + final int deltaX = toX - fromX; + final int deltaY = toY - fromY; + if (deltaX != 0) { + view.animate().translationX(0); + } + if (deltaY != 0) { + view.animate().translationY(0); + } + // TODO: make EndActions end listeners instead, since end actions aren't called when + // vpas are canceled (and can't end them. why?) + // need listener functionality in VPACompat for this. Ick. + final ViewPropertyAnimator animation = view.animate(); + mMoveAnimations.add(holder); + animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchMoveStarting(holder); + } + + @Override + public void onAnimationCancel(Animator animator) { + if (deltaX != 0) { + view.setTranslationX(0); + } + if (deltaY != 0) { + view.setTranslationY(0); + } + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + dispatchMoveFinished(holder); + mMoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + if (oldHolder == newHolder) { + // Don't know how to run change animations when the same view holder is re-used. + // run a move animation to handle position changes. + return animateMove(oldHolder, fromX, fromY, toX, toY); + } + final float prevTranslationX = oldHolder.itemView.getTranslationX(); + final float prevTranslationY = oldHolder.itemView.getTranslationY(); + final float prevAlpha = oldHolder.itemView.getAlpha(); + resetAnimation(oldHolder); + int deltaX = (int) (toX - fromX - prevTranslationX); + int deltaY = (int) (toY - fromY - prevTranslationY); + // recover prev translation state after ending animation + oldHolder.itemView.setTranslationX(prevTranslationX); + oldHolder.itemView.setTranslationY(prevTranslationY); + oldHolder.itemView.setAlpha(prevAlpha); + if (newHolder != null) { + // carry over translation values + resetAnimation(newHolder); + newHolder.itemView.setTranslationX(-deltaX); + newHolder.itemView.setTranslationY(-deltaY); + newHolder.itemView.setAlpha(0); + } + mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); + return true; + } + + void animateChangeImpl(final ChangeInfo changeInfo) { + final RecyclerView.ViewHolder holder = changeInfo.oldHolder; + final View view = holder == null ? null : holder.itemView; + final RecyclerView.ViewHolder newHolder = changeInfo.newHolder; + final View newView = newHolder != null ? newHolder.itemView : null; + if (view != null) { + final ViewPropertyAnimator oldViewAnim = view.animate().setDuration( + getChangeDuration()); + mChangeAnimations.add(changeInfo.oldHolder); + oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); + oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); + oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchChangeStarting(changeInfo.oldHolder, true); + } + + @Override + public void onAnimationEnd(Animator animator) { + oldViewAnim.setListener(null); + view.setAlpha(1); + view.setTranslationX(0); + view.setTranslationY(0); + dispatchChangeFinished(changeInfo.oldHolder, true); + mChangeAnimations.remove(changeInfo.oldHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + if (newView != null) { + final ViewPropertyAnimator newViewAnimation = newView.animate(); + mChangeAnimations.add(changeInfo.newHolder); + newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()) + .alpha(1).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchChangeStarting(changeInfo.newHolder, false); + } + @Override + public void onAnimationEnd(Animator animator) { + newViewAnimation.setListener(null); + newView.setAlpha(1); + newView.setTranslationX(0); + newView.setTranslationY(0); + dispatchChangeFinished(changeInfo.newHolder, false); + mChangeAnimations.remove(changeInfo.newHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + } + + private void endChangeAnimation(List infoList, RecyclerView.ViewHolder item) { + for (int i = infoList.size() - 1; i >= 0; i--) { + ChangeInfo changeInfo = infoList.get(i); + if (endChangeAnimationIfNecessary(changeInfo, item)) { + if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { + infoList.remove(changeInfo); + } + } + } + } + + private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { + if (changeInfo.oldHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); + } + if (changeInfo.newHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); + } + } + private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) { + boolean oldItem = false; + if (changeInfo.newHolder == item) { + changeInfo.newHolder = null; + } else if (changeInfo.oldHolder == item) { + changeInfo.oldHolder = null; + oldItem = true; + } else { + return false; + } + item.itemView.setAlpha(1); + item.itemView.setTranslationX(0); + item.itemView.setTranslationY(0); + dispatchChangeFinished(item, oldItem); + return true; + } + + @Override + public void endAnimation(RecyclerView.ViewHolder item) { + final View view = item.itemView; + // this will trigger end callback which should set properties to their target values. + view.animate().cancel(); + // TODO if some other animations are chained to end, how do we cancel them as well? + for (int i = mPendingMoves.size() - 1; i >= 0; i--) { + MoveInfo moveInfo = mPendingMoves.get(i); + if (moveInfo.holder == item) { + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item); + mPendingMoves.remove(i); + } + } + endChangeAnimation(mPendingChanges, item); + if (mPendingRemovals.remove(item)) { + view.setAlpha(1); + dispatchRemoveFinished(item); + } + if (mPendingAdditions.remove(item)) { + view.setAlpha(1); + dispatchAddFinished(item); + } + + for (int i = mChangesList.size() - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + endChangeAnimation(changes, item); + if (changes.isEmpty()) { + mChangesList.remove(i); + } + } + for (int i = mMovesList.size() - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + for (int j = moves.size() - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + if (moveInfo.holder == item) { + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(i); + } + break; + } + } + } + for (int i = mAdditionsList.size() - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + if (additions.remove(item)) { + view.setAlpha(1); + dispatchAddFinished(item); + if (additions.isEmpty()) { + mAdditionsList.remove(i); + } + } + } + + // animations should be ended by the cancel above. + //noinspection PointlessBooleanExpression,ConstantConditions + if (mRemoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mRemoveAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mAddAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mAddAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mChangeAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mChangeAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mMoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mMoveAnimations list"); + } + dispatchFinishedWhenDone(); + } + + private void resetAnimation(RecyclerView.ViewHolder holder) { + if (sDefaultInterpolator == null) { + sDefaultInterpolator = new ValueAnimator().getInterpolator(); + } + holder.itemView.animate().setInterpolator(sDefaultInterpolator); + endAnimation(holder); + } + + @Override + public boolean isRunning() { + return (!mPendingAdditions.isEmpty() + || !mPendingChanges.isEmpty() + || !mPendingMoves.isEmpty() + || !mPendingRemovals.isEmpty() + || !mMoveAnimations.isEmpty() + || !mRemoveAnimations.isEmpty() + || !mAddAnimations.isEmpty() + || !mChangeAnimations.isEmpty() + || !mMovesList.isEmpty() + || !mAdditionsList.isEmpty() + || !mChangesList.isEmpty()); + } + + /** + * Check the state of currently pending and running animations. If there are none + * pending/running, call {@link #dispatchAnimationsFinished()} to notify any + * listeners. + */ + void dispatchFinishedWhenDone() { + if (!isRunning()) { + dispatchAnimationsFinished(); + } + } + + @Override + public void endAnimations() { + int count = mPendingMoves.size(); + for (int i = count - 1; i >= 0; i--) { + MoveInfo item = mPendingMoves.get(i); + View view = item.holder.itemView; + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item.holder); + mPendingMoves.remove(i); + } + count = mPendingRemovals.size(); + for (int i = count - 1; i >= 0; i--) { + RecyclerView.ViewHolder item = mPendingRemovals.get(i); + dispatchRemoveFinished(item); + mPendingRemovals.remove(i); + } + count = mPendingAdditions.size(); + for (int i = count - 1; i >= 0; i--) { + RecyclerView.ViewHolder item = mPendingAdditions.get(i); + item.itemView.setAlpha(1); + dispatchAddFinished(item); + mPendingAdditions.remove(i); + } + count = mPendingChanges.size(); + for (int i = count - 1; i >= 0; i--) { + endChangeAnimationIfNecessary(mPendingChanges.get(i)); + } + mPendingChanges.clear(); + if (!isRunning()) { + return; + } + + int listCount = mMovesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + count = moves.size(); + for (int j = count - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + RecyclerView.ViewHolder item = moveInfo.holder; + View view = item.itemView; + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(moveInfo.holder); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(moves); + } + } + } + listCount = mAdditionsList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + count = additions.size(); + for (int j = count - 1; j >= 0; j--) { + RecyclerView.ViewHolder item = additions.get(j); + View view = item.itemView; + view.setAlpha(1); + dispatchAddFinished(item); + additions.remove(j); + if (additions.isEmpty()) { + mAdditionsList.remove(additions); + } + } + } + listCount = mChangesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + count = changes.size(); + for (int j = count - 1; j >= 0; j--) { + endChangeAnimationIfNecessary(changes.get(j)); + if (changes.isEmpty()) { + mChangesList.remove(changes); + } + } + } + + cancelAll(mRemoveAnimations); + cancelAll(mMoveAnimations); + cancelAll(mAddAnimations); + cancelAll(mChangeAnimations); + + dispatchAnimationsFinished(); + } + + void cancelAll(List viewHolders) { + for (int i = viewHolders.size() - 1; i >= 0; i--) { + viewHolders.get(i).itemView.animate().cancel(); + } + } + + /** + * {@inheritDoc} + *

+ * If the payload list is not empty, DefaultItemAnimator returns true. + * When this is the case: + *

    + *
  • If you override {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, both + * ViewHolder arguments will be the same instance. + *
  • + *
  • + * If you are not overriding {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, + * then DefaultItemAnimator will call {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and + * run a move animation instead. + *
  • + *
+ */ + @Override + public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, + @NonNull List payloads) { + return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); + } +} From 16bbcb764dfb5358a68f2c1b52fe299c427ee78a Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 14:50:46 +0800 Subject: [PATCH 21/26] support default animator --- .../renderer/ui/rv/TexasRecyclerViewImpl.java | 3 +- .../ui/rv/anim/DefaultItemAnimator.java | 1259 +++++++++-------- 2 files changed, 633 insertions(+), 629 deletions(-) diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java index ac707951..f70d539a 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java @@ -15,6 +15,7 @@ import me.chan.texas.renderer.TexasView; import me.chan.texas.renderer.TouchEvent; import me.chan.texas.renderer.ui.TexasRendererAdapter; +import me.chan.texas.renderer.ui.rv.anim.DefaultItemAnimator; import me.chan.texas.renderer.ui.rv.anim.DefaultTexasItemAnimator; import me.chan.texas.renderer.ui.text.ParagraphView; import me.chan.texas.text.Document; @@ -48,7 +49,7 @@ protected void onClicked(MotionEvent event) { } }; - setItemAnimator(mItemAnimator); + setItemAnimator(new DefaultItemAnimator()); } public void scrollToPosition(int position, boolean smooth, int offset) { diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java index e2769484..ec441021 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java @@ -23,632 +23,635 @@ * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator) */ public class DefaultItemAnimator extends SimpleItemAnimator { - private static final boolean DEBUG = false; - - private static TimeInterpolator sDefaultInterpolator; - - private ArrayList mPendingRemovals = new ArrayList<>(); - private ArrayList mPendingAdditions = new ArrayList<>(); - private ArrayList mPendingMoves = new ArrayList<>(); - private ArrayList mPendingChanges = new ArrayList<>(); - - ArrayList> mAdditionsList = new ArrayList<>(); - ArrayList> mMovesList = new ArrayList<>(); - ArrayList> mChangesList = new ArrayList<>(); - - ArrayList mAddAnimations = new ArrayList<>(); - ArrayList mMoveAnimations = new ArrayList<>(); - ArrayList mRemoveAnimations = new ArrayList<>(); - ArrayList mChangeAnimations = new ArrayList<>(); - - private static class MoveInfo { - public RecyclerView.ViewHolder holder; - public int fromX, fromY, toX, toY; - - MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { - this.holder = holder; - this.fromX = fromX; - this.fromY = fromY; - this.toX = toX; - this.toY = toY; - } - } - - private static class ChangeInfo { - public RecyclerView.ViewHolder oldHolder, newHolder; - public int fromX, fromY, toX, toY; - private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) { - this.oldHolder = oldHolder; - this.newHolder = newHolder; - } - - ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, - int fromX, int fromY, int toX, int toY) { - this(oldHolder, newHolder); - this.fromX = fromX; - this.fromY = fromY; - this.toX = toX; - this.toY = toY; - } - - @Override - public String toString() { - return "ChangeInfo{" - + "oldHolder=" + oldHolder - + ", newHolder=" + newHolder - + ", fromX=" + fromX - + ", fromY=" + fromY - + ", toX=" + toX - + ", toY=" + toY - + '}'; - } - } - - @Override - public void runPendingAnimations() { - boolean removalsPending = !mPendingRemovals.isEmpty(); - boolean movesPending = !mPendingMoves.isEmpty(); - boolean changesPending = !mPendingChanges.isEmpty(); - boolean additionsPending = !mPendingAdditions.isEmpty(); - if (!removalsPending && !movesPending && !additionsPending && !changesPending) { - // nothing to animate - return; - } - // First, remove stuff - for (RecyclerView.ViewHolder holder : mPendingRemovals) { - animateRemoveImpl(holder); - } - mPendingRemovals.clear(); - // Next, move stuff - if (movesPending) { - final ArrayList moves = new ArrayList<>(); - moves.addAll(mPendingMoves); - mMovesList.add(moves); - mPendingMoves.clear(); - Runnable mover = new Runnable() { - @Override - public void run() { - for (MoveInfo moveInfo : moves) { - animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, - moveInfo.toX, moveInfo.toY); - } - moves.clear(); - mMovesList.remove(moves); - } - }; - if (removalsPending) { - View view = moves.get(0).holder.itemView; - ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); - } else { - mover.run(); - } - } - // Next, change stuff, to run in parallel with move animations - if (changesPending) { - final ArrayList changes = new ArrayList<>(); - changes.addAll(mPendingChanges); - mChangesList.add(changes); - mPendingChanges.clear(); - Runnable changer = new Runnable() { - @Override - public void run() { - for (ChangeInfo change : changes) { - animateChangeImpl(change); - } - changes.clear(); - mChangesList.remove(changes); - } - }; - if (removalsPending) { - RecyclerView.ViewHolder holder = changes.get(0).oldHolder; - ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); - } else { - changer.run(); - } - } - // Next, add stuff - if (additionsPending) { - final ArrayList additions = new ArrayList<>(); - additions.addAll(mPendingAdditions); - mAdditionsList.add(additions); - mPendingAdditions.clear(); - Runnable adder = new Runnable() { - @Override - public void run() { - for (RecyclerView.ViewHolder holder : additions) { - animateAddImpl(holder); - } - additions.clear(); - mAdditionsList.remove(additions); - } - }; - if (removalsPending || movesPending || changesPending) { - long removeDuration = removalsPending ? getRemoveDuration() : 0; - long moveDuration = movesPending ? getMoveDuration() : 0; - long changeDuration = changesPending ? getChangeDuration() : 0; - long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); - View view = additions.get(0).itemView; - ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); - } else { - adder.run(); - } - } - } - - @Override - public boolean animateRemove(final RecyclerView.ViewHolder holder) { - resetAnimation(holder); - mPendingRemovals.add(holder); - return true; - } - - private void animateRemoveImpl(final RecyclerView.ViewHolder holder) { - final View view = holder.itemView; - final ViewPropertyAnimator animation = view.animate(); - mRemoveAnimations.add(holder); - animation.setDuration(getRemoveDuration()).alpha(0).setListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - dispatchRemoveStarting(holder); - } - - @Override - public void onAnimationEnd(Animator animator) { - animation.setListener(null); - view.setAlpha(1); - dispatchRemoveFinished(holder); - mRemoveAnimations.remove(holder); - dispatchFinishedWhenDone(); - } - }).start(); - } - - @Override - public boolean animateAdd(final RecyclerView.ViewHolder holder) { - resetAnimation(holder); - holder.itemView.setAlpha(0); - mPendingAdditions.add(holder); - return true; - } - - void animateAddImpl(final RecyclerView.ViewHolder holder) { - final View view = holder.itemView; - final ViewPropertyAnimator animation = view.animate(); - mAddAnimations.add(holder); - animation.alpha(1).setDuration(getAddDuration()) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - dispatchAddStarting(holder); - } - - @Override - public void onAnimationCancel(Animator animator) { - view.setAlpha(1); - } - - @Override - public void onAnimationEnd(Animator animator) { - animation.setListener(null); - dispatchAddFinished(holder); - mAddAnimations.remove(holder); - dispatchFinishedWhenDone(); - } - }).start(); - } - - @Override - public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, - int toX, int toY) { - final View view = holder.itemView; - fromX += (int) holder.itemView.getTranslationX(); - fromY += (int) holder.itemView.getTranslationY(); - resetAnimation(holder); - int deltaX = toX - fromX; - int deltaY = toY - fromY; - if (deltaX == 0 && deltaY == 0) { - dispatchMoveFinished(holder); - return false; - } - if (deltaX != 0) { - view.setTranslationX(-deltaX); - } - if (deltaY != 0) { - view.setTranslationY(-deltaY); - } - mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); - return true; - } - - void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { - final View view = holder.itemView; - final int deltaX = toX - fromX; - final int deltaY = toY - fromY; - if (deltaX != 0) { - view.animate().translationX(0); - } - if (deltaY != 0) { - view.animate().translationY(0); - } - // TODO: make EndActions end listeners instead, since end actions aren't called when - // vpas are canceled (and can't end them. why?) - // need listener functionality in VPACompat for this. Ick. - final ViewPropertyAnimator animation = view.animate(); - mMoveAnimations.add(holder); - animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - dispatchMoveStarting(holder); - } - - @Override - public void onAnimationCancel(Animator animator) { - if (deltaX != 0) { - view.setTranslationX(0); - } - if (deltaY != 0) { - view.setTranslationY(0); - } - } - - @Override - public void onAnimationEnd(Animator animator) { - animation.setListener(null); - dispatchMoveFinished(holder); - mMoveAnimations.remove(holder); - dispatchFinishedWhenDone(); - } - }).start(); - } - - @Override - public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, - int fromX, int fromY, int toX, int toY) { - if (oldHolder == newHolder) { - // Don't know how to run change animations when the same view holder is re-used. - // run a move animation to handle position changes. - return animateMove(oldHolder, fromX, fromY, toX, toY); - } - final float prevTranslationX = oldHolder.itemView.getTranslationX(); - final float prevTranslationY = oldHolder.itemView.getTranslationY(); - final float prevAlpha = oldHolder.itemView.getAlpha(); - resetAnimation(oldHolder); - int deltaX = (int) (toX - fromX - prevTranslationX); - int deltaY = (int) (toY - fromY - prevTranslationY); - // recover prev translation state after ending animation - oldHolder.itemView.setTranslationX(prevTranslationX); - oldHolder.itemView.setTranslationY(prevTranslationY); - oldHolder.itemView.setAlpha(prevAlpha); - if (newHolder != null) { - // carry over translation values - resetAnimation(newHolder); - newHolder.itemView.setTranslationX(-deltaX); - newHolder.itemView.setTranslationY(-deltaY); - newHolder.itemView.setAlpha(0); - } - mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); - return true; - } - - void animateChangeImpl(final ChangeInfo changeInfo) { - final RecyclerView.ViewHolder holder = changeInfo.oldHolder; - final View view = holder == null ? null : holder.itemView; - final RecyclerView.ViewHolder newHolder = changeInfo.newHolder; - final View newView = newHolder != null ? newHolder.itemView : null; - if (view != null) { - final ViewPropertyAnimator oldViewAnim = view.animate().setDuration( - getChangeDuration()); - mChangeAnimations.add(changeInfo.oldHolder); - oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); - oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); - oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - dispatchChangeStarting(changeInfo.oldHolder, true); - } - - @Override - public void onAnimationEnd(Animator animator) { - oldViewAnim.setListener(null); - view.setAlpha(1); - view.setTranslationX(0); - view.setTranslationY(0); - dispatchChangeFinished(changeInfo.oldHolder, true); - mChangeAnimations.remove(changeInfo.oldHolder); - dispatchFinishedWhenDone(); - } - }).start(); - } - if (newView != null) { - final ViewPropertyAnimator newViewAnimation = newView.animate(); - mChangeAnimations.add(changeInfo.newHolder); - newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()) - .alpha(1).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - dispatchChangeStarting(changeInfo.newHolder, false); - } - @Override - public void onAnimationEnd(Animator animator) { - newViewAnimation.setListener(null); - newView.setAlpha(1); - newView.setTranslationX(0); - newView.setTranslationY(0); - dispatchChangeFinished(changeInfo.newHolder, false); - mChangeAnimations.remove(changeInfo.newHolder); - dispatchFinishedWhenDone(); - } - }).start(); - } - } - - private void endChangeAnimation(List infoList, RecyclerView.ViewHolder item) { - for (int i = infoList.size() - 1; i >= 0; i--) { - ChangeInfo changeInfo = infoList.get(i); - if (endChangeAnimationIfNecessary(changeInfo, item)) { - if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { - infoList.remove(changeInfo); - } - } - } - } - - private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { - if (changeInfo.oldHolder != null) { - endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); - } - if (changeInfo.newHolder != null) { - endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); - } - } - private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) { - boolean oldItem = false; - if (changeInfo.newHolder == item) { - changeInfo.newHolder = null; - } else if (changeInfo.oldHolder == item) { - changeInfo.oldHolder = null; - oldItem = true; - } else { - return false; - } - item.itemView.setAlpha(1); - item.itemView.setTranslationX(0); - item.itemView.setTranslationY(0); - dispatchChangeFinished(item, oldItem); - return true; - } - - @Override - public void endAnimation(RecyclerView.ViewHolder item) { - final View view = item.itemView; - // this will trigger end callback which should set properties to their target values. - view.animate().cancel(); - // TODO if some other animations are chained to end, how do we cancel them as well? - for (int i = mPendingMoves.size() - 1; i >= 0; i--) { - MoveInfo moveInfo = mPendingMoves.get(i); - if (moveInfo.holder == item) { - view.setTranslationY(0); - view.setTranslationX(0); - dispatchMoveFinished(item); - mPendingMoves.remove(i); - } - } - endChangeAnimation(mPendingChanges, item); - if (mPendingRemovals.remove(item)) { - view.setAlpha(1); - dispatchRemoveFinished(item); - } - if (mPendingAdditions.remove(item)) { - view.setAlpha(1); - dispatchAddFinished(item); - } - - for (int i = mChangesList.size() - 1; i >= 0; i--) { - ArrayList changes = mChangesList.get(i); - endChangeAnimation(changes, item); - if (changes.isEmpty()) { - mChangesList.remove(i); - } - } - for (int i = mMovesList.size() - 1; i >= 0; i--) { - ArrayList moves = mMovesList.get(i); - for (int j = moves.size() - 1; j >= 0; j--) { - MoveInfo moveInfo = moves.get(j); - if (moveInfo.holder == item) { - view.setTranslationY(0); - view.setTranslationX(0); - dispatchMoveFinished(item); - moves.remove(j); - if (moves.isEmpty()) { - mMovesList.remove(i); - } - break; - } - } - } - for (int i = mAdditionsList.size() - 1; i >= 0; i--) { - ArrayList additions = mAdditionsList.get(i); - if (additions.remove(item)) { - view.setAlpha(1); - dispatchAddFinished(item); - if (additions.isEmpty()) { - mAdditionsList.remove(i); - } - } - } - - // animations should be ended by the cancel above. - //noinspection PointlessBooleanExpression,ConstantConditions - if (mRemoveAnimations.remove(item) && DEBUG) { - throw new IllegalStateException("after animation is cancelled, item should not be in " - + "mRemoveAnimations list"); - } - - //noinspection PointlessBooleanExpression,ConstantConditions - if (mAddAnimations.remove(item) && DEBUG) { - throw new IllegalStateException("after animation is cancelled, item should not be in " - + "mAddAnimations list"); - } - - //noinspection PointlessBooleanExpression,ConstantConditions - if (mChangeAnimations.remove(item) && DEBUG) { - throw new IllegalStateException("after animation is cancelled, item should not be in " - + "mChangeAnimations list"); - } - - //noinspection PointlessBooleanExpression,ConstantConditions - if (mMoveAnimations.remove(item) && DEBUG) { - throw new IllegalStateException("after animation is cancelled, item should not be in " - + "mMoveAnimations list"); - } - dispatchFinishedWhenDone(); - } - - private void resetAnimation(RecyclerView.ViewHolder holder) { - if (sDefaultInterpolator == null) { - sDefaultInterpolator = new ValueAnimator().getInterpolator(); - } - holder.itemView.animate().setInterpolator(sDefaultInterpolator); - endAnimation(holder); - } - - @Override - public boolean isRunning() { - return (!mPendingAdditions.isEmpty() - || !mPendingChanges.isEmpty() - || !mPendingMoves.isEmpty() - || !mPendingRemovals.isEmpty() - || !mMoveAnimations.isEmpty() - || !mRemoveAnimations.isEmpty() - || !mAddAnimations.isEmpty() - || !mChangeAnimations.isEmpty() - || !mMovesList.isEmpty() - || !mAdditionsList.isEmpty() - || !mChangesList.isEmpty()); - } - - /** - * Check the state of currently pending and running animations. If there are none - * pending/running, call {@link #dispatchAnimationsFinished()} to notify any - * listeners. - */ - void dispatchFinishedWhenDone() { - if (!isRunning()) { - dispatchAnimationsFinished(); - } - } - - @Override - public void endAnimations() { - int count = mPendingMoves.size(); - for (int i = count - 1; i >= 0; i--) { - MoveInfo item = mPendingMoves.get(i); - View view = item.holder.itemView; - view.setTranslationY(0); - view.setTranslationX(0); - dispatchMoveFinished(item.holder); - mPendingMoves.remove(i); - } - count = mPendingRemovals.size(); - for (int i = count - 1; i >= 0; i--) { - RecyclerView.ViewHolder item = mPendingRemovals.get(i); - dispatchRemoveFinished(item); - mPendingRemovals.remove(i); - } - count = mPendingAdditions.size(); - for (int i = count - 1; i >= 0; i--) { - RecyclerView.ViewHolder item = mPendingAdditions.get(i); - item.itemView.setAlpha(1); - dispatchAddFinished(item); - mPendingAdditions.remove(i); - } - count = mPendingChanges.size(); - for (int i = count - 1; i >= 0; i--) { - endChangeAnimationIfNecessary(mPendingChanges.get(i)); - } - mPendingChanges.clear(); - if (!isRunning()) { - return; - } - - int listCount = mMovesList.size(); - for (int i = listCount - 1; i >= 0; i--) { - ArrayList moves = mMovesList.get(i); - count = moves.size(); - for (int j = count - 1; j >= 0; j--) { - MoveInfo moveInfo = moves.get(j); - RecyclerView.ViewHolder item = moveInfo.holder; - View view = item.itemView; - view.setTranslationY(0); - view.setTranslationX(0); - dispatchMoveFinished(moveInfo.holder); - moves.remove(j); - if (moves.isEmpty()) { - mMovesList.remove(moves); - } - } - } - listCount = mAdditionsList.size(); - for (int i = listCount - 1; i >= 0; i--) { - ArrayList additions = mAdditionsList.get(i); - count = additions.size(); - for (int j = count - 1; j >= 0; j--) { - RecyclerView.ViewHolder item = additions.get(j); - View view = item.itemView; - view.setAlpha(1); - dispatchAddFinished(item); - additions.remove(j); - if (additions.isEmpty()) { - mAdditionsList.remove(additions); - } - } - } - listCount = mChangesList.size(); - for (int i = listCount - 1; i >= 0; i--) { - ArrayList changes = mChangesList.get(i); - count = changes.size(); - for (int j = count - 1; j >= 0; j--) { - endChangeAnimationIfNecessary(changes.get(j)); - if (changes.isEmpty()) { - mChangesList.remove(changes); - } - } - } - - cancelAll(mRemoveAnimations); - cancelAll(mMoveAnimations); - cancelAll(mAddAnimations); - cancelAll(mChangeAnimations); - - dispatchAnimationsFinished(); - } - - void cancelAll(List viewHolders) { - for (int i = viewHolders.size() - 1; i >= 0; i--) { - viewHolders.get(i).itemView.animate().cancel(); - } - } - - /** - * {@inheritDoc} - *

- * If the payload list is not empty, DefaultItemAnimator returns true. - * When this is the case: - *

    - *
  • If you override {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, both - * ViewHolder arguments will be the same instance. - *
  • - *
  • - * If you are not overriding {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, - * then DefaultItemAnimator will call {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and - * run a move animation instead. - *
  • - *
- */ - @Override - public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, - @NonNull List payloads) { - return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); - } + private static final boolean DEBUG = false; + + private static TimeInterpolator sDefaultInterpolator; + + private ArrayList mPendingRemovals = new ArrayList<>(); + private ArrayList mPendingAdditions = new ArrayList<>(); + private ArrayList mPendingMoves = new ArrayList<>(); + private ArrayList mPendingChanges = new ArrayList<>(); + + ArrayList> mAdditionsList = new ArrayList<>(); + ArrayList> mMovesList = new ArrayList<>(); + ArrayList> mChangesList = new ArrayList<>(); + + ArrayList mAddAnimations = new ArrayList<>(); + ArrayList mMoveAnimations = new ArrayList<>(); + ArrayList mRemoveAnimations = new ArrayList<>(); + ArrayList mChangeAnimations = new ArrayList<>(); + + private static class MoveInfo { + public RecyclerView.ViewHolder holder; + public int fromX, fromY, toX, toY; + + MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { + this.holder = holder; + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + } + + private static class ChangeInfo { + public RecyclerView.ViewHolder oldHolder, newHolder; + public int fromX, fromY, toX, toY; + + private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) { + this.oldHolder = oldHolder; + this.newHolder = newHolder; + } + + ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + this(oldHolder, newHolder); + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + + @Override + public String toString() { + return "ChangeInfo{" + + "oldHolder=" + oldHolder + + ", newHolder=" + newHolder + + ", fromX=" + fromX + + ", fromY=" + fromY + + ", toX=" + toX + + ", toY=" + toY + + '}'; + } + } + + @Override + public void runPendingAnimations() { + boolean removalsPending = !mPendingRemovals.isEmpty(); + boolean movesPending = !mPendingMoves.isEmpty(); + boolean changesPending = !mPendingChanges.isEmpty(); + boolean additionsPending = !mPendingAdditions.isEmpty(); + if (!removalsPending && !movesPending && !additionsPending && !changesPending) { + // nothing to animate + return; + } + // First, remove stuff + for (RecyclerView.ViewHolder holder : mPendingRemovals) { + animateRemoveImpl(holder); + } + mPendingRemovals.clear(); + // Next, move stuff + if (movesPending) { + final ArrayList moves = new ArrayList<>(); + moves.addAll(mPendingMoves); + mMovesList.add(moves); + mPendingMoves.clear(); + Runnable mover = new Runnable() { + @Override + public void run() { + for (MoveInfo moveInfo : moves) { + animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, + moveInfo.toX, moveInfo.toY); + } + moves.clear(); + mMovesList.remove(moves); + } + }; + if (removalsPending) { + View view = moves.get(0).holder.itemView; + ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); + } else { + mover.run(); + } + } + // Next, change stuff, to run in parallel with move animations + if (changesPending) { + final ArrayList changes = new ArrayList<>(); + changes.addAll(mPendingChanges); + mChangesList.add(changes); + mPendingChanges.clear(); + Runnable changer = new Runnable() { + @Override + public void run() { + for (ChangeInfo change : changes) { + animateChangeImpl(change); + } + changes.clear(); + mChangesList.remove(changes); + } + }; + if (removalsPending) { + RecyclerView.ViewHolder holder = changes.get(0).oldHolder; + ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); + } else { + changer.run(); + } + } + // Next, add stuff + if (additionsPending) { + final ArrayList additions = new ArrayList<>(); + additions.addAll(mPendingAdditions); + mAdditionsList.add(additions); + mPendingAdditions.clear(); + Runnable adder = new Runnable() { + @Override + public void run() { + for (RecyclerView.ViewHolder holder : additions) { + animateAddImpl(holder); + } + additions.clear(); + mAdditionsList.remove(additions); + } + }; + if (removalsPending || movesPending || changesPending) { + long removeDuration = removalsPending ? getRemoveDuration() : 0; + long moveDuration = movesPending ? getMoveDuration() : 0; + long changeDuration = changesPending ? getChangeDuration() : 0; + long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); + View view = additions.get(0).itemView; + ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); + } else { + adder.run(); + } + } + } + + @Override + public boolean animateAdd(final RecyclerView.ViewHolder holder) { + resetAnimation(holder); + holder.itemView.setAlpha(0); + mPendingAdditions.add(holder); + return true; + } + + @Override + public boolean animateRemove(final RecyclerView.ViewHolder holder) { + resetAnimation(holder); + mPendingRemovals.add(holder); + return true; + } + + @Override + public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, + int toX, int toY) { + final View view = holder.itemView; + fromX += (int) holder.itemView.getTranslationX(); + fromY += (int) holder.itemView.getTranslationY(); + resetAnimation(holder); + int deltaX = toX - fromX; + int deltaY = toY - fromY; + if (deltaX == 0 && deltaY == 0) { + dispatchMoveFinished(holder); + return false; + } + if (deltaX != 0) { + view.setTranslationX(-deltaX); + } + if (deltaY != 0) { + view.setTranslationY(-deltaY); + } + mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); + return true; + } + + @Override + public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + if (oldHolder == newHolder) { + // Don't know how to run change animations when the same view holder is re-used. + // run a move animation to handle position changes. + return animateMove(oldHolder, fromX, fromY, toX, toY); + } + final float prevTranslationX = oldHolder.itemView.getTranslationX(); + final float prevTranslationY = oldHolder.itemView.getTranslationY(); + final float prevAlpha = oldHolder.itemView.getAlpha(); + resetAnimation(oldHolder); + int deltaX = (int) (toX - fromX - prevTranslationX); + int deltaY = (int) (toY - fromY - prevTranslationY); + // recover prev translation state after ending animation + oldHolder.itemView.setTranslationX(prevTranslationX); + oldHolder.itemView.setTranslationY(prevTranslationY); + oldHolder.itemView.setAlpha(prevAlpha); + if (newHolder != null) { + // carry over translation values + resetAnimation(newHolder); + newHolder.itemView.setTranslationX(-deltaX); + newHolder.itemView.setTranslationY(-deltaY); + newHolder.itemView.setAlpha(0); + } + mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); + return true; + } + + @Override + public boolean isRunning() { + return (!mPendingAdditions.isEmpty() + || !mPendingChanges.isEmpty() + || !mPendingMoves.isEmpty() + || !mPendingRemovals.isEmpty() + || !mMoveAnimations.isEmpty() + || !mRemoveAnimations.isEmpty() + || !mAddAnimations.isEmpty() + || !mChangeAnimations.isEmpty() + || !mMovesList.isEmpty() + || !mAdditionsList.isEmpty() + || !mChangesList.isEmpty()); + } + + @Override + public void endAnimation(RecyclerView.ViewHolder item) { + final View view = item.itemView; + // this will trigger end callback which should set properties to their target values. + view.animate().cancel(); + // TODO if some other animations are chained to end, how do we cancel them as well? + for (int i = mPendingMoves.size() - 1; i >= 0; i--) { + MoveInfo moveInfo = mPendingMoves.get(i); + if (moveInfo.holder == item) { + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item); + mPendingMoves.remove(i); + } + } + endChangeAnimation(mPendingChanges, item); + if (mPendingRemovals.remove(item)) { + view.setAlpha(1); + dispatchRemoveFinished(item); + } + if (mPendingAdditions.remove(item)) { + view.setAlpha(1); + dispatchAddFinished(item); + } + + for (int i = mChangesList.size() - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + endChangeAnimation(changes, item); + if (changes.isEmpty()) { + mChangesList.remove(i); + } + } + for (int i = mMovesList.size() - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + for (int j = moves.size() - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + if (moveInfo.holder == item) { + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(i); + } + break; + } + } + } + for (int i = mAdditionsList.size() - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + if (additions.remove(item)) { + view.setAlpha(1); + dispatchAddFinished(item); + if (additions.isEmpty()) { + mAdditionsList.remove(i); + } + } + } + + // animations should be ended by the cancel above. + //noinspection PointlessBooleanExpression,ConstantConditions + if (mRemoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mRemoveAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mAddAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mAddAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mChangeAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mChangeAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mMoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mMoveAnimations list"); + } + dispatchFinishedWhenDone(); + } + + @Override + public void endAnimations() { + int count = mPendingMoves.size(); + for (int i = count - 1; i >= 0; i--) { + MoveInfo item = mPendingMoves.get(i); + View view = item.holder.itemView; + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item.holder); + mPendingMoves.remove(i); + } + count = mPendingRemovals.size(); + for (int i = count - 1; i >= 0; i--) { + RecyclerView.ViewHolder item = mPendingRemovals.get(i); + dispatchRemoveFinished(item); + mPendingRemovals.remove(i); + } + count = mPendingAdditions.size(); + for (int i = count - 1; i >= 0; i--) { + RecyclerView.ViewHolder item = mPendingAdditions.get(i); + item.itemView.setAlpha(1); + dispatchAddFinished(item); + mPendingAdditions.remove(i); + } + count = mPendingChanges.size(); + for (int i = count - 1; i >= 0; i--) { + endChangeAnimationIfNecessary(mPendingChanges.get(i)); + } + mPendingChanges.clear(); + if (!isRunning()) { + return; + } + + int listCount = mMovesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + count = moves.size(); + for (int j = count - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + RecyclerView.ViewHolder item = moveInfo.holder; + View view = item.itemView; + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(moveInfo.holder); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(moves); + } + } + } + listCount = mAdditionsList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + count = additions.size(); + for (int j = count - 1; j >= 0; j--) { + RecyclerView.ViewHolder item = additions.get(j); + View view = item.itemView; + view.setAlpha(1); + dispatchAddFinished(item); + additions.remove(j); + if (additions.isEmpty()) { + mAdditionsList.remove(additions); + } + } + } + listCount = mChangesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + count = changes.size(); + for (int j = count - 1; j >= 0; j--) { + endChangeAnimationIfNecessary(changes.get(j)); + if (changes.isEmpty()) { + mChangesList.remove(changes); + } + } + } + + cancelAll(mRemoveAnimations); + cancelAll(mMoveAnimations); + cancelAll(mAddAnimations); + cancelAll(mChangeAnimations); + + dispatchAnimationsFinished(); + } + + private void animateRemoveImpl(final RecyclerView.ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimator animation = view.animate(); + mRemoveAnimations.add(holder); + animation.setDuration(getRemoveDuration()).alpha(0).setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchRemoveStarting(holder); + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + view.setAlpha(1); + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + void animateAddImpl(final RecyclerView.ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimator animation = view.animate(); + mAddAnimations.add(holder); + animation.alpha(1).setDuration(getAddDuration()) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchAddStarting(holder); + } + + @Override + public void onAnimationCancel(Animator animator) { + view.setAlpha(1); + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + dispatchAddFinished(holder); + mAddAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { + final View view = holder.itemView; + final int deltaX = toX - fromX; + final int deltaY = toY - fromY; + if (deltaX != 0) { + view.animate().translationX(0); + } + if (deltaY != 0) { + view.animate().translationY(0); + } + // TODO: make EndActions end listeners instead, since end actions aren't called when + // vpas are canceled (and can't end them. why?) + // need listener functionality in VPACompat for this. Ick. + final ViewPropertyAnimator animation = view.animate(); + mMoveAnimations.add(holder); + animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchMoveStarting(holder); + } + + @Override + public void onAnimationCancel(Animator animator) { + if (deltaX != 0) { + view.setTranslationX(0); + } + if (deltaY != 0) { + view.setTranslationY(0); + } + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + dispatchMoveFinished(holder); + mMoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + void animateChangeImpl(final ChangeInfo changeInfo) { + final RecyclerView.ViewHolder holder = changeInfo.oldHolder; + final View view = holder == null ? null : holder.itemView; + final RecyclerView.ViewHolder newHolder = changeInfo.newHolder; + final View newView = newHolder != null ? newHolder.itemView : null; + if (view != null) { + final ViewPropertyAnimator oldViewAnim = view.animate().setDuration( + getChangeDuration()); + mChangeAnimations.add(changeInfo.oldHolder); + oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); + oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); + oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchChangeStarting(changeInfo.oldHolder, true); + } + + @Override + public void onAnimationEnd(Animator animator) { + oldViewAnim.setListener(null); + view.setAlpha(1); + view.setTranslationX(0); + view.setTranslationY(0); + dispatchChangeFinished(changeInfo.oldHolder, true); + mChangeAnimations.remove(changeInfo.oldHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + if (newView != null) { + final ViewPropertyAnimator newViewAnimation = newView.animate(); + mChangeAnimations.add(changeInfo.newHolder); + newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()) + .alpha(1).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchChangeStarting(changeInfo.newHolder, false); + } + + @Override + public void onAnimationEnd(Animator animator) { + newViewAnimation.setListener(null); + newView.setAlpha(1); + newView.setTranslationX(0); + newView.setTranslationY(0); + dispatchChangeFinished(changeInfo.newHolder, false); + mChangeAnimations.remove(changeInfo.newHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + } + + private void endChangeAnimation(List infoList, RecyclerView.ViewHolder item) { + for (int i = infoList.size() - 1; i >= 0; i--) { + ChangeInfo changeInfo = infoList.get(i); + if (endChangeAnimationIfNecessary(changeInfo, item)) { + if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { + infoList.remove(changeInfo); + } + } + } + } + + private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { + if (changeInfo.oldHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); + } + if (changeInfo.newHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); + } + } + + private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) { + boolean oldItem = false; + if (changeInfo.newHolder == item) { + changeInfo.newHolder = null; + } else if (changeInfo.oldHolder == item) { + changeInfo.oldHolder = null; + oldItem = true; + } else { + return false; + } + item.itemView.setAlpha(1); + item.itemView.setTranslationX(0); + item.itemView.setTranslationY(0); + dispatchChangeFinished(item, oldItem); + return true; + } + + private void resetAnimation(RecyclerView.ViewHolder holder) { + if (sDefaultInterpolator == null) { + sDefaultInterpolator = new ValueAnimator().getInterpolator(); + } + holder.itemView.animate().setInterpolator(sDefaultInterpolator); + endAnimation(holder); + } + + /** + * Check the state of currently pending and running animations. If there are none + * pending/running, call {@link #dispatchAnimationsFinished()} to notify any + * listeners. + */ + void dispatchFinishedWhenDone() { + if (!isRunning()) { + dispatchAnimationsFinished(); + } + } + + void cancelAll(List viewHolders) { + for (int i = viewHolders.size() - 1; i >= 0; i--) { + viewHolders.get(i).itemView.animate().cancel(); + } + } + + /** + * {@inheritDoc} + *

+ * If the payload list is not empty, DefaultItemAnimator returns true. + * When this is the case: + *

    + *
  • If you override {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, both + * ViewHolder arguments will be the same instance. + *
  • + *
  • + * If you are not overriding {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, + * then DefaultItemAnimator will call {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and + * run a move animation instead. + *
  • + *
+ */ + @Override + public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, + @NonNull List payloads) { + return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); + } } From 7fedfa5a15c36c5e91acfa9ed83f8340979914c6 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 15:28:59 +0800 Subject: [PATCH 22/26] support default animator --- .../ui/rv/anim/DefaultItemAnimator.java | 75 +++++-------------- library/src/main/res/values/ids.xml | 1 + 2 files changed, 21 insertions(+), 55 deletions(-) diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java index ec441021..523808d0 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java @@ -27,10 +27,10 @@ public class DefaultItemAnimator extends SimpleItemAnimator { private static TimeInterpolator sDefaultInterpolator; - private ArrayList mPendingRemovals = new ArrayList<>(); - private ArrayList mPendingAdditions = new ArrayList<>(); - private ArrayList mPendingMoves = new ArrayList<>(); - private ArrayList mPendingChanges = new ArrayList<>(); + private final ArrayList mPendingRemovals = new ArrayList<>(); + private final ArrayList mPendingAdditions = new ArrayList<>(); + private final ArrayList mPendingMoves = new ArrayList<>(); + private final ArrayList mPendingChanges = new ArrayList<>(); ArrayList> mAdditionsList = new ArrayList<>(); ArrayList> mMovesList = new ArrayList<>(); @@ -106,16 +106,13 @@ public void runPendingAnimations() { moves.addAll(mPendingMoves); mMovesList.add(moves); mPendingMoves.clear(); - Runnable mover = new Runnable() { - @Override - public void run() { - for (MoveInfo moveInfo : moves) { - animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, - moveInfo.toX, moveInfo.toY); - } - moves.clear(); - mMovesList.remove(moves); + Runnable mover = () -> { + for (MoveInfo moveInfo : moves) { + animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, + moveInfo.toX, moveInfo.toY); } + moves.clear(); + mMovesList.remove(moves); }; if (removalsPending) { View view = moves.get(0).holder.itemView; @@ -130,15 +127,12 @@ public void run() { changes.addAll(mPendingChanges); mChangesList.add(changes); mPendingChanges.clear(); - Runnable changer = new Runnable() { - @Override - public void run() { - for (ChangeInfo change : changes) { - animateChangeImpl(change); - } - changes.clear(); - mChangesList.remove(changes); + Runnable changer = () -> { + for (ChangeInfo change : changes) { + animateChangeImpl(change); } + changes.clear(); + mChangesList.remove(changes); }; if (removalsPending) { RecyclerView.ViewHolder holder = changes.get(0).oldHolder; @@ -153,15 +147,12 @@ public void run() { additions.addAll(mPendingAdditions); mAdditionsList.add(additions); mPendingAdditions.clear(); - Runnable adder = new Runnable() { - @Override - public void run() { - for (RecyclerView.ViewHolder holder : additions) { - animateAddImpl(holder); - } - additions.clear(); - mAdditionsList.remove(additions); + Runnable adder = () -> { + for (RecyclerView.ViewHolder holder : additions) { + animateAddImpl(holder); } + additions.clear(); + mAdditionsList.remove(additions); }; if (removalsPending || movesPending || changesPending) { long removeDuration = removalsPending ? getRemoveDuration() : 0; @@ -263,7 +254,6 @@ public void endAnimation(RecyclerView.ViewHolder item) { final View view = item.itemView; // this will trigger end callback which should set properties to their target values. view.animate().cancel(); - // TODO if some other animations are chained to end, how do we cancel them as well? for (int i = mPendingMoves.size() - 1; i >= 0; i--) { MoveInfo moveInfo = mPendingMoves.get(i); if (moveInfo.holder == item) { @@ -316,31 +306,6 @@ public void endAnimation(RecyclerView.ViewHolder item) { } } } - - // animations should be ended by the cancel above. - //noinspection PointlessBooleanExpression,ConstantConditions - if (mRemoveAnimations.remove(item) && DEBUG) { - throw new IllegalStateException("after animation is cancelled, item should not be in " - + "mRemoveAnimations list"); - } - - //noinspection PointlessBooleanExpression,ConstantConditions - if (mAddAnimations.remove(item) && DEBUG) { - throw new IllegalStateException("after animation is cancelled, item should not be in " - + "mAddAnimations list"); - } - - //noinspection PointlessBooleanExpression,ConstantConditions - if (mChangeAnimations.remove(item) && DEBUG) { - throw new IllegalStateException("after animation is cancelled, item should not be in " - + "mChangeAnimations list"); - } - - //noinspection PointlessBooleanExpression,ConstantConditions - if (mMoveAnimations.remove(item) && DEBUG) { - throw new IllegalStateException("after animation is cancelled, item should not be in " - + "mMoveAnimations list"); - } dispatchFinishedWhenDone(); } diff --git a/library/src/main/res/values/ids.xml b/library/src/main/res/values/ids.xml index 9ee8ed70..45384ecd 100644 --- a/library/src/main/res/values/ids.xml +++ b/library/src/main/res/values/ids.xml @@ -1,6 +1,7 @@ + \ No newline at end of file From 16e1fb7235e0a6eb8f71bb162908cbbceb825e06 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 16:02:00 +0800 Subject: [PATCH 23/26] support default animator --- .../me/chan/texas/TexasViewDemoActivity.java | 20 +- .../me/chan/texas/renderer/TexasView.java | 26 ++- .../ui/rv/{anim => }/DefaultItemAnimator.java | 215 +++++++++++------- .../renderer/ui/rv/TexasRecyclerViewImpl.java | 6 +- .../texas/renderer/ui/rv/anim/AnimRecord.java | 19 -- .../renderer/ui/rv/anim/AnimTracker.java | 50 ---- .../ui/rv/anim/DefaultTexasItemAnimator.java | 152 ------------- .../renderer/ui/rv/anim/SegmentAnimType.java | 6 - 8 files changed, 163 insertions(+), 331 deletions(-) rename library/src/main/java/me/chan/texas/renderer/ui/rv/{anim => }/DefaultItemAnimator.java (80%) delete mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java delete mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimTracker.java delete mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java delete mode 100644 library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimType.java diff --git a/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java b/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java index 0dc51380..557681c7 100644 --- a/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java +++ b/app/src/main/java/me/chan/texas/TexasViewDemoActivity.java @@ -32,7 +32,6 @@ import me.chan.texas.renderer.TexasView; import me.chan.texas.renderer.TouchEvent; import me.chan.texas.renderer.selection.Selection; -import me.chan.texas.renderer.ui.rv.anim.SegmentAnimType; import me.chan.texas.text.BreakStrategy; import me.chan.texas.text.Document; import me.chan.texas.text.Paragraph; @@ -374,16 +373,15 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { renderOption.setTypeface(typeface); mTexasView.refresh(renderOption); + final Object ADD = new Object(); mTexasView.setSegmentAnimator(new TexasView.SegmentAnimator() { - @Nullable @Override - protected Animator onCreateAnimator(Segment segment, View itemView, SegmentAnimType type) { - if (segment.getTag() != SegmentAnimType.APPEARANCE || type != SegmentAnimType.APPEARANCE) { + protected Animator onCreateAddAnimator(Segment segment, View itemView) { + if (segment.getTag() != ADD) { return null; } - Log.d("chan_debug", "onCreateAnimator: " + type + " -> " + segment); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.play(ObjectAnimator.ofFloat(itemView, "alpha", 0, 1)) .with(ObjectAnimator.ofFloat(itemView, "translationY", -itemView.getHeight(), 0)); @@ -398,6 +396,16 @@ public void onAnimationCancel(Animator animation) { }); return animatorSet; } + + @Override + protected Animator onCreateRemoveAnimator(Segment segment, View view) { + return null; + } + + @Override + protected Animator onCreateMoveAnimator(Segment segment, View view, int fromX, int fromY, int toX, int toY) { + return null; + } }); findViewById(me.chan.texas.debug.R.id.add_content).setOnClickListener(new View.OnClickListener() { @@ -410,7 +418,7 @@ protected Document onRead(TexasOption option, @Nullable Document previousDocumen .addSegment( 0, Paragraph.Builder.newBuilder(option) - .tag(SegmentAnimType.APPEARANCE) + .tag(ADD) .text("生活就像点菜,饥饿时菜会点得特别多,但吃一阵就会意识到浪费;如果慢条斯理地盘算怎么点菜,别人已经要吃完了。") .build() ) diff --git a/library/src/main/java/me/chan/texas/renderer/TexasView.java b/library/src/main/java/me/chan/texas/renderer/TexasView.java index d67ca83e..4fae416f 100644 --- a/library/src/main/java/me/chan/texas/renderer/TexasView.java +++ b/library/src/main/java/me/chan/texas/renderer/TexasView.java @@ -47,7 +47,6 @@ import me.chan.texas.misc.PaintSet; import me.chan.texas.renderer.core.worker.LoadingWorker; import me.chan.texas.renderer.selection.Selection; -import me.chan.texas.renderer.ui.rv.anim.SegmentAnimType; import me.chan.texas.source.Source; import me.chan.texas.text.BreakStrategy; import me.chan.texas.text.Document; @@ -1059,18 +1058,23 @@ public interface SegmentDecoration { public abstract static class SegmentAnimator { - public final Animator createAnimator(Segment segment, View itemView, SegmentAnimType type) { - return onCreateAnimator(segment, itemView, type); + public Animator createAddAnimator(Segment segment, View view) { + return onCreateAddAnimator(segment, view); } - /** - * @param segment segment - * @param itemView itemView - * @param type type - * @return Animator, 返回空则代表不显示动画 - */ - @Nullable - protected abstract Animator onCreateAnimator(Segment segment, View itemView, SegmentAnimType type); + protected abstract Animator onCreateAddAnimator(Segment segment, View view); + + public Animator createRemoveAnimator(Segment segment, View view) { + return onCreateRemoveAnimator(segment, view); + } + + protected abstract Animator onCreateRemoveAnimator(Segment segment, View view); + + public Animator createMoveAnimator(Segment segment, View view, int fromX, int fromY, int toX, int toY) { + return onCreateMoveAnimator(segment, view, fromX, fromY, toX, toY); + } + + protected abstract Animator onCreateMoveAnimator(Segment segment, View view, int fromX, int fromY, int toX, int toY); } diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java similarity index 80% rename from library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java rename to library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java index 523808d0..f5b2bcb8 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java @@ -1,4 +1,4 @@ -package me.chan.texas.renderer.ui.rv.anim; +package me.chan.texas.renderer.ui.rv; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -8,6 +8,7 @@ import android.view.ViewPropertyAnimator; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SimpleItemAnimator; @@ -15,6 +16,10 @@ import java.util.ArrayList; import java.util.List; +import me.chan.texas.R; +import me.chan.texas.renderer.TexasView; +import me.chan.texas.text.Segment; + /** * This implementation of {@link RecyclerView.ItemAnimator} provides basic * animations on remove, add, and move events that happen to the items in @@ -23,7 +28,8 @@ * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator) */ public class DefaultItemAnimator extends SimpleItemAnimator { - private static final boolean DEBUG = false; + @Nullable + private TexasView.SegmentAnimator mSegmentItemAnimator; private static TimeInterpolator sDefaultInterpolator; @@ -85,6 +91,10 @@ public String toString() { } } + public void setSegmentItemAnimator(@Nullable TexasView.SegmentAnimator segmentItemAnimator) { + mSegmentItemAnimator = segmentItemAnimator; + } + @Override public void runPendingAnimations() { boolean removalsPending = !mPendingRemovals.isEmpty(); @@ -170,7 +180,7 @@ public void runPendingAnimations() { @Override public boolean animateAdd(final RecyclerView.ViewHolder holder) { resetAnimation(holder); - holder.itemView.setAlpha(0); +// holder.itemView.setAlpha(0); mPendingAdditions.add(holder); return true; } @@ -195,12 +205,12 @@ public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int dispatchMoveFinished(holder); return false; } - if (deltaX != 0) { - view.setTranslationX(-deltaX); - } - if (deltaY != 0) { - view.setTranslationY(-deltaY); - } +// if (deltaX != 0) { +// view.setTranslationX(-deltaX); +// } +// if (deltaY != 0) { +// view.setTranslationY(-deltaY); +// } mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); return true; } @@ -220,15 +230,15 @@ public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.Vie int deltaX = (int) (toX - fromX - prevTranslationX); int deltaY = (int) (toY - fromY - prevTranslationY); // recover prev translation state after ending animation - oldHolder.itemView.setTranslationX(prevTranslationX); - oldHolder.itemView.setTranslationY(prevTranslationY); - oldHolder.itemView.setAlpha(prevAlpha); +// oldHolder.itemView.setTranslationX(prevTranslationX); +// oldHolder.itemView.setTranslationY(prevTranslationY); +// oldHolder.itemView.setAlpha(prevAlpha); if (newHolder != null) { // carry over translation values resetAnimation(newHolder); - newHolder.itemView.setTranslationX(-deltaX); - newHolder.itemView.setTranslationY(-deltaY); - newHolder.itemView.setAlpha(0); +// newHolder.itemView.setTranslationX(-deltaX); +// newHolder.itemView.setTranslationY(-deltaY); +// newHolder.itemView.setAlpha(0); } mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); return true; @@ -253,7 +263,11 @@ public boolean isRunning() { public void endAnimation(RecyclerView.ViewHolder item) { final View view = item.itemView; // this will trigger end callback which should set properties to their target values. - view.animate().cancel(); + Animator animator = (Animator) view.getTag(R.id.me_chan_texas_item_anim_tag); + if (animator != null) { + animator.cancel(); + view.setTag(R.id.me_chan_texas_item_anim_tag, null); + } for (int i = mPendingMoves.size() - 1; i >= 0; i--) { MoveInfo moveInfo = mPendingMoves.get(i); if (moveInfo.holder == item) { @@ -396,91 +410,124 @@ public void endAnimations() { private void animateRemoveImpl(final RecyclerView.ViewHolder holder) { final View view = holder.itemView; - final ViewPropertyAnimator animation = view.animate(); mRemoveAnimations.add(holder); - animation.setDuration(getRemoveDuration()).alpha(0).setListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - dispatchRemoveStarting(holder); - } - @Override - public void onAnimationEnd(Animator animator) { - animation.setListener(null); - view.setAlpha(1); - dispatchRemoveFinished(holder); - mRemoveAnimations.remove(holder); - dispatchFinishedWhenDone(); - } - }).start(); + Animator animator = null; + if (mSegmentItemAnimator != null) { + Segment segment = (Segment) view.getTag(R.id.me_chan_texas_item_tag); + animator = mSegmentItemAnimator.createRemoveAnimator(segment, view); + } + + if (animator == null) { + dispatchRemoveStarting(holder); + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } else { + animator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchRemoveStarting(holder); + } + + @Override + public void onAnimationEnd(Animator animator) { + animator.removeListener(this); + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }); + animator.start(); + view.setTag(R.id.me_chan_texas_item_anim_tag, animator); + } } void animateAddImpl(final RecyclerView.ViewHolder holder) { final View view = holder.itemView; - final ViewPropertyAnimator animation = view.animate(); mAddAnimations.add(holder); - animation.alpha(1).setDuration(getAddDuration()) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - dispatchAddStarting(holder); - } - @Override - public void onAnimationCancel(Animator animator) { - view.setAlpha(1); - } + Animator animator = null; + if (mSegmentItemAnimator != null) { + Segment segment = (Segment) view.getTag(R.id.me_chan_texas_item_tag); + animator = mSegmentItemAnimator.createAddAnimator(segment, view); + } - @Override - public void onAnimationEnd(Animator animator) { - animation.setListener(null); - dispatchAddFinished(holder); - mAddAnimations.remove(holder); - dispatchFinishedWhenDone(); - } - }).start(); + if (animator == null) { + dispatchAddStarting(holder); + dispatchAddFinished(holder); + mAddAnimations.remove(holder); + dispatchFinishedWhenDone(); + } else { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchAddStarting(holder); + } + + @Override + public void onAnimationCancel(Animator animator) { + view.setAlpha(1); + } + + @Override + public void onAnimationEnd(Animator animator) { + animator.removeListener(this); + dispatchAddFinished(holder); + mAddAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }); + animator.start(); + } } void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { final View view = holder.itemView; final int deltaX = toX - fromX; final int deltaY = toY - fromY; - if (deltaX != 0) { - view.animate().translationX(0); - } - if (deltaY != 0) { - view.animate().translationY(0); - } - // TODO: make EndActions end listeners instead, since end actions aren't called when - // vpas are canceled (and can't end them. why?) - // need listener functionality in VPACompat for this. Ick. - final ViewPropertyAnimator animation = view.animate(); + mMoveAnimations.add(holder); - animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - dispatchMoveStarting(holder); - } + Animator animator = null; + if (mSegmentItemAnimator != null) { + Segment segment = (Segment) view.getTag(R.id.me_chan_texas_item_tag); + animator = mSegmentItemAnimator.createMoveAnimator(segment, view, fromX, fromY, toX, toY); + } - @Override - public void onAnimationCancel(Animator animator) { - if (deltaX != 0) { - view.setTranslationX(0); + if (animator == null) { + dispatchMoveStarting(holder); + dispatchMoveFinished(holder); + mMoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } else { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchMoveStarting(holder); } - if (deltaY != 0) { - view.setTranslationY(0); + + @Override + public void onAnimationCancel(Animator animator) { + if (deltaX != 0) { + view.setTranslationX(0); + } + if (deltaY != 0) { + view.setTranslationY(0); + } } - } - @Override - public void onAnimationEnd(Animator animator) { - animation.setListener(null); - dispatchMoveFinished(holder); - mMoveAnimations.remove(holder); - dispatchFinishedWhenDone(); - } - }).start(); + @Override + public void onAnimationEnd(Animator animator) { + animator.removeListener(this); + dispatchMoveFinished(holder); + mMoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }); + animator.start(); + view.setTag(R.id.me_chan_texas_item_anim_tag, animator); + } } void animateChangeImpl(final ChangeInfo changeInfo) { @@ -577,7 +624,6 @@ private void resetAnimation(RecyclerView.ViewHolder holder) { if (sDefaultInterpolator == null) { sDefaultInterpolator = new ValueAnimator().getInterpolator(); } - holder.itemView.animate().setInterpolator(sDefaultInterpolator); endAnimation(holder); } @@ -594,7 +640,10 @@ void dispatchFinishedWhenDone() { void cancelAll(List viewHolders) { for (int i = viewHolders.size() - 1; i >= 0; i--) { - viewHolders.get(i).itemView.animate().cancel(); + Animator animator = (Animator) viewHolders.get(i).itemView.getTag(R.id.me_chan_texas_item_anim_tag); + if (animator != null) { + animator.cancel(); + } } } diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java index f70d539a..55d3c678 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/TexasRecyclerViewImpl.java @@ -15,8 +15,6 @@ import me.chan.texas.renderer.TexasView; import me.chan.texas.renderer.TouchEvent; import me.chan.texas.renderer.ui.TexasRendererAdapter; -import me.chan.texas.renderer.ui.rv.anim.DefaultItemAnimator; -import me.chan.texas.renderer.ui.rv.anim.DefaultTexasItemAnimator; import me.chan.texas.renderer.ui.text.ParagraphView; import me.chan.texas.text.Document; import me.chan.texas.text.Paragraph; @@ -31,7 +29,7 @@ public class TexasRecyclerViewImpl extends RecyclerView implements TexasRecycler private OnClickedListener mOnClickedListener; private ScrollAction mScrollAction; private final TexasLinearLayoutManagerImpl mTexasLinearLayoutManager; - private final DefaultTexasItemAnimator mItemAnimator = new DefaultTexasItemAnimator(); + private final DefaultItemAnimator mItemAnimator = new DefaultItemAnimator(); public TexasRecyclerViewImpl(@NonNull Context context, TexasLinearLayoutManagerImpl texasLinearLayoutManager) { super(context); @@ -49,7 +47,7 @@ protected void onClicked(MotionEvent event) { } }; - setItemAnimator(new DefaultItemAnimator()); + setItemAnimator(mItemAnimator); } public void scrollToPosition(int position, boolean smooth, int offset) { diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java deleted file mode 100644 index 4885e20a..00000000 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimRecord.java +++ /dev/null @@ -1,19 +0,0 @@ -package me.chan.texas.renderer.ui.rv.anim; - -import android.animation.Animator; - -class AnimRecord { - public static final int PHASE_PENDING = 0; - public static final int PHASE_POSTPONED = 1; - public static final int PHASE_RUNNING = 2; - - public final SegmentAnimType type; - public int phase; - public final Animator animator; - - AnimRecord(SegmentAnimType type, Animator animator) { - this.type = type; - this.animator = animator; - this.phase = PHASE_PENDING; - } -} \ No newline at end of file diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimTracker.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimTracker.java deleted file mode 100644 index 5e7a540d..00000000 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/AnimTracker.java +++ /dev/null @@ -1,50 +0,0 @@ -package me.chan.texas.renderer.ui.rv.anim; - -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -class AnimTracker { - private final HashMap mRecords = new HashMap<>(); - - public void add(RecyclerView.ViewHolder holder, AnimRecord record) { - mRecords.put(holder, record); - } - - public boolean remove(RecyclerView.ViewHolder holder) { - return mRecords.remove(holder) != null; - } - - @Nullable - public AnimRecord get(RecyclerView.ViewHolder holder) { - return mRecords.get(holder); - } - - public void advanceTo(RecyclerView.ViewHolder holder, int phase) { - AnimRecord record = mRecords.get(holder); - if (record != null) { - record.phase = phase; - } - } - - public List holdersByPhase(int phase) { - List result = new ArrayList<>(); - for (HashMap.Entry entry : mRecords.entrySet()) { - if (entry.getValue().phase == phase) { - result.add(entry.getKey()); - } - } - return result; - } - - public List allHolders() { - return new ArrayList<>(mRecords.keySet()); - } - - public boolean isEmpty() { - return mRecords.isEmpty(); - } -} \ No newline at end of file diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java deleted file mode 100644 index ae4224b3..00000000 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/DefaultTexasItemAnimator.java +++ /dev/null @@ -1,152 +0,0 @@ -package me.chan.texas.renderer.ui.rv.anim; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.core.view.ViewCompat; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.List; - -import me.chan.texas.R; -import me.chan.texas.renderer.TexasView; -import me.chan.texas.text.Segment; - -@RestrictTo(RestrictTo.Scope.LIBRARY) -public class DefaultTexasItemAnimator extends RecyclerView.ItemAnimator { - - @Nullable - private TexasView.SegmentAnimator mSegmentItemAnimator; - private final AnimTracker mTracker = new AnimTracker(); - - public void setSegmentItemAnimator(@Nullable TexasView.SegmentAnimator segmentItemAnimator) { - mSegmentItemAnimator = segmentItemAnimator; - } - - @Override - public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { - endAnimation(viewHolder); - return createAnimator(viewHolder, SegmentAnimType.DISAPPEARANCE); - } - - @Override - public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { - endAnimation(viewHolder); - return createAnimator(viewHolder, SegmentAnimType.APPEARANCE); - } - - private boolean createAnimator(RecyclerView.ViewHolder holder, SegmentAnimType type) { - if (mSegmentItemAnimator == null) { - return false; - } - - Segment segment = (Segment) holder.itemView.getTag(R.id.me_chan_texas_item_tag); - if (segment == null) { - return false; - } - - Animator animator = mSegmentItemAnimator.createAnimator(segment, holder.itemView, type); - if (animator == null) { - return false; - } - - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animation.removeListener(this); - if (mTracker.remove(holder)) { - dispatchAnimationFinished(holder); - } - } - }); - mTracker.add(holder, new AnimRecord(type, animator)); - return true; - } - - @Override - public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { - endAnimation(viewHolder); - return createAnimator(viewHolder, SegmentAnimType.PERSISTENCE); - } - - @Override - public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { - endAnimation(oldHolder); - endAnimation(newHolder); - boolean created = createAnimator(newHolder, SegmentAnimType.CHANGE); - if (!created) { - return false; - } - if (oldHolder != newHolder) { - dispatchAnimationFinished(oldHolder); - } - return true; - } - - @Override - public void runPendingAnimations() { - if (mTracker.isEmpty()) { - return; - } - - List pending = mTracker.holdersByPhase(AnimRecord.PHASE_PENDING); - if (pending.isEmpty()) { - return; - } - - for (RecyclerView.ViewHolder holder : pending) { - mTracker.advanceTo(holder, AnimRecord.PHASE_POSTPONED); - ViewCompat.postOnAnimation(holder.itemView, () -> { - AnimRecord record = mTracker.get(holder); - if (record != null && record.phase == AnimRecord.PHASE_POSTPONED) { - mTracker.advanceTo(holder, AnimRecord.PHASE_RUNNING); - startAnimation(holder); - } - }); - } - } - - private void startAnimation(@NonNull RecyclerView.ViewHolder holder) { - AnimRecord record = mTracker.get(holder); - if (record == null) { - return; - } - - dispatchAnimationStarted(holder); - record.animator.start(); - } - - @Override - public void endAnimation(@NonNull RecyclerView.ViewHolder item) { - AnimRecord record = mTracker.get(item); - if (record == null) { - return; - } - - boolean isRunning = record.phase == AnimRecord.PHASE_RUNNING; - record.animator.cancel(); - // running: cancel() 已同步触发 onAnimationEnd → dispatch,不再重复 - if (!isRunning && mTracker.remove(item)) { - dispatchAnimationFinished(item); - } - } - - @Override - public void endAnimations() { - if (mTracker.isEmpty()) { - return; - } - - for (RecyclerView.ViewHolder holder : mTracker.allHolders()) { - endAnimation(holder); - } - } - - @Override - public boolean isRunning() { - return !mTracker.isEmpty(); - } -} diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimType.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimType.java deleted file mode 100644 index e30762c3..00000000 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/anim/SegmentAnimType.java +++ /dev/null @@ -1,6 +0,0 @@ -package me.chan.texas.renderer.ui.rv.anim; - -public enum SegmentAnimType { - APPEARANCE, DISAPPEARANCE, - PERSISTENCE, CHANGE -} From fed4a21c4a933eb558f778867c14e8d1974e03e8 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 16:03:00 +0800 Subject: [PATCH 24/26] support default animator --- .../me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java index f5b2bcb8..58df8bf9 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java @@ -640,9 +640,11 @@ void dispatchFinishedWhenDone() { void cancelAll(List viewHolders) { for (int i = viewHolders.size() - 1; i >= 0; i--) { - Animator animator = (Animator) viewHolders.get(i).itemView.getTag(R.id.me_chan_texas_item_anim_tag); + View view = viewHolders.get(i).itemView; + Animator animator = (Animator) view.getTag(R.id.me_chan_texas_item_anim_tag); if (animator != null) { animator.cancel(); + view.setTag(R.id.me_chan_texas_item_anim_tag, null); } } } From df016ac070664a14ed7351a66d00746b7170f945 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 16:25:26 +0800 Subject: [PATCH 25/26] support default animator --- .../renderer/ui/rv/DefaultItemAnimator.java | 74 ++++--------------- 1 file changed, 14 insertions(+), 60 deletions(-) diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java index 58df8bf9..71818e8a 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java @@ -180,7 +180,6 @@ public void runPendingAnimations() { @Override public boolean animateAdd(final RecyclerView.ViewHolder holder) { resetAnimation(holder); -// holder.itemView.setAlpha(0); mPendingAdditions.add(holder); return true; } @@ -195,7 +194,6 @@ public boolean animateRemove(final RecyclerView.ViewHolder holder) { @Override public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { - final View view = holder.itemView; fromX += (int) holder.itemView.getTranslationX(); fromY += (int) holder.itemView.getTranslationY(); resetAnimation(holder); @@ -205,12 +203,6 @@ public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int dispatchMoveFinished(holder); return false; } -// if (deltaX != 0) { -// view.setTranslationX(-deltaX); -// } -// if (deltaY != 0) { -// view.setTranslationY(-deltaY); -// } mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); return true; } @@ -223,22 +215,10 @@ public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.Vie // run a move animation to handle position changes. return animateMove(oldHolder, fromX, fromY, toX, toY); } - final float prevTranslationX = oldHolder.itemView.getTranslationX(); - final float prevTranslationY = oldHolder.itemView.getTranslationY(); - final float prevAlpha = oldHolder.itemView.getAlpha(); resetAnimation(oldHolder); - int deltaX = (int) (toX - fromX - prevTranslationX); - int deltaY = (int) (toY - fromY - prevTranslationY); - // recover prev translation state after ending animation -// oldHolder.itemView.setTranslationX(prevTranslationX); -// oldHolder.itemView.setTranslationY(prevTranslationY); -// oldHolder.itemView.setAlpha(prevAlpha); if (newHolder != null) { // carry over translation values resetAnimation(newHolder); -// newHolder.itemView.setTranslationX(-deltaX); -// newHolder.itemView.setTranslationY(-deltaY); -// newHolder.itemView.setAlpha(0); } mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); return true; @@ -536,50 +516,24 @@ void animateChangeImpl(final ChangeInfo changeInfo) { final RecyclerView.ViewHolder newHolder = changeInfo.newHolder; final View newView = newHolder != null ? newHolder.itemView : null; if (view != null) { - final ViewPropertyAnimator oldViewAnim = view.animate().setDuration( - getChangeDuration()); mChangeAnimations.add(changeInfo.oldHolder); - oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); - oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); - oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - dispatchChangeStarting(changeInfo.oldHolder, true); - } - - @Override - public void onAnimationEnd(Animator animator) { - oldViewAnim.setListener(null); - view.setAlpha(1); - view.setTranslationX(0); - view.setTranslationY(0); - dispatchChangeFinished(changeInfo.oldHolder, true); - mChangeAnimations.remove(changeInfo.oldHolder); - dispatchFinishedWhenDone(); - } - }).start(); + dispatchChangeStarting(changeInfo.oldHolder, true); + view.setAlpha(1); + view.setTranslationX(0); + view.setTranslationY(0); + dispatchChangeFinished(changeInfo.oldHolder, true); + mChangeAnimations.remove(changeInfo.oldHolder); + dispatchFinishedWhenDone(); } if (newView != null) { - final ViewPropertyAnimator newViewAnimation = newView.animate(); mChangeAnimations.add(changeInfo.newHolder); - newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()) - .alpha(1).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animator) { - dispatchChangeStarting(changeInfo.newHolder, false); - } - - @Override - public void onAnimationEnd(Animator animator) { - newViewAnimation.setListener(null); - newView.setAlpha(1); - newView.setTranslationX(0); - newView.setTranslationY(0); - dispatchChangeFinished(changeInfo.newHolder, false); - mChangeAnimations.remove(changeInfo.newHolder); - dispatchFinishedWhenDone(); - } - }).start(); + dispatchChangeStarting(changeInfo.newHolder, false); + newView.setAlpha(1); + newView.setTranslationX(0); + newView.setTranslationY(0); + dispatchChangeFinished(changeInfo.newHolder, false); + mChangeAnimations.remove(changeInfo.newHolder); + dispatchFinishedWhenDone(); } } From 56636da12d938e4548cb337287ea0d7a0834c8d0 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 17 Mar 2026 16:57:00 +0800 Subject: [PATCH 26/26] support default animator --- .../java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java b/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java index 71818e8a..f27fdc21 100644 --- a/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java +++ b/library/src/main/java/me/chan/texas/renderer/ui/rv/DefaultItemAnimator.java @@ -5,7 +5,6 @@ import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.view.View; -import android.view.ViewPropertyAnimator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -27,7 +26,7 @@ * * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator) */ -public class DefaultItemAnimator extends SimpleItemAnimator { +class DefaultItemAnimator extends SimpleItemAnimator { @Nullable private TexasView.SegmentAnimator mSegmentItemAnimator;