1

I am using an ObjectAnimator to make a long text on a TextView to be scrolled inside a HorizontalScrollView. The scroll animation is working nicely, the text is being scrolled correctly, but I am having trouble to make the animation stop for a few moments before it restarts.

I am using the following classes to make such animations:

 class ObjectScroller {
        ObjectAnimator animator;
        TextView scrollText;
        long duration = 7500;
        long delay = 2500;

        public ObjectScroller(TextView scroll, int itemWidth, int viewWidth) {
            this.scrollText = scroll;
            animator = ObjectAnimator.ofFloat(scrollText, "translationX", viewWidth-(itemWidth + 50));
            animator.setStartDelay(delay);
            animator.setInterpolator(new LinearInterpolator());
            animator.setDuration(duration);
            animator.setRepeatMode(ValueAnimator.RESTART);
            animator.setRepeatCount(ValueAnimator.INFINITE);
        }

        public void startObjectScroll() {
            animator.addListener(new DelayAnimation(delay));
            animator.start();
        }
    }

    class DelayAnimation implements Animator.AnimatorListener {
        private long delayMillis;

        public DelayAnimation(long delayMillis) {
            this.delayMillis = delayMillis;
        }

        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationEnd(Animator animation) {

        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {
            animation.pause();
            new Handler().postDelayed(animation::resume, delayMillis);
        }
    }

Those classes are utilized in a RecyclerView.Adapter (cause those scrolling texts are located inside the items of a RecyclerView) and are utilized as follows:

@Override
    public void onBindViewHolder(@NonNull ViewHolderInterface.MainListViewHolder holder, int position) {

\\------------
      \\omitting code that does not relate to animations
\\------------

        this.beforeDraw(holder.textInfoOne, () -> {
            Paint textPaint = holder.textInfoOne.getPaint();
            String text = holder.textInfoOne.getText().toString();
            int itemWidth = Math.round(textPaint.measureText(text));

            int viewWidth = holder.scrollText.getWidth();

            if (itemWidth > viewWidth) {
                holder.obScroller = new ObjectScroller(holder.textInfoOne, itemWidth,viewWidth);
                holder.obScroller.startObjectScroll();
            }
        });

    }

public void beforeDraw(View view, Runnable runnable) {
        ViewTreeObserver.OnPreDrawListener preDraw = new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                view.getViewTreeObserver().removeOnPreDrawListener(this);
                runnable.run();
                return true;
            }
        };
        view.getViewTreeObserver().addOnPreDrawListener(preDraw);
    }

where beforeDraw is just a method created to allow me to find the views' proper size on the screen to decide if the text should be scrolled or not.

Anyway, the problem I am facing is the fact that, when utilizing the onAnimationRepeat(Animator animation), the animation restarts, and just then it pauses for the time determined as delay to restart scrolling. This delay before resuming the animation is part of what I envisioned, but it isn't everything that I want to achieve, since what I wanted is that the animation also paused after its completion but before its restart.

To be more clear, my desired outcome would be as follows:

Delay -> Start Animation -> Complete Scroll Animation -> Pause at the End -> Restart Animation with Delay -> ...

However, what I am getting so far would be like this:

Delay -> Start Animation -> Complete Scroll Animation -> **No Pause** -> Restart Animation with Delay -> ...

Is it possible to achieve such thing with ObjectAnimator? I changed from Animation to ObjectAnimator precisely because I wanted to pause the animation before restarting it.

Also, I would prefer to have a solution that uses the setRepeatCount(ValueAnimator.INFINITE) option since I want to make sure it will never stop scrolling even if the user does forget his phone unlocked.

Thanks in advance for all the help!

1
  • 1
    It's possible you could achieve the updateListener check with some math on the interpolation values; but I did accomplish what it seems you are after by simply removing the repeat functionality (it won't support what you want for reasons of implementation in the ValueAnimator) and add new Handler().postDelayed(animation::start, delayMillis); to the onAnimationEnd in DelayAnimation. This pauses the animation at its end before restarting it. If the start delay and the "repeat" delay need to be managed separately then you can clear the start delay on the first "end". Commented Jun 17, 2023 at 3:09

2 Answers 2

1

You could use AnimationUpdateListener and control when to pause the animation at the end before repeating using

animator.addUpdateListener(animation -> {
    //target value could the end value which is either viewWidth-(itemWidth + 50) or when you want to pause
    (animation.getAnimatedValue() == targetValue) {
        animation.pause();
        new Handler().postDelayed(animation::resume, delayMillis);
    }
});
Sign up to request clarification or add additional context in comments.

2 Comments

I tried you code, however since i never used ValueAnimator before, it seemed like I need to cast the getAnimatedValue() to a float value and then use an if clause in the line (animation.getAnimatedValue() == targetValue), because it felt incomplete without it. However, the result was the same. Did I, perhaps, do something incorrectly? I didn't get an Exception so at least my casting was not incorrect in that regard
The interpolation never will reach the target value - always 1 interpolation increment less. (e.g. vw=400, iw=25, (400-(25+50))=325 then the last translation-x is 324.6966 (using your duration value). This is [0, 325, <interpolation increment>) -which is read from 0 up to but not including 325 in steps of interpolation increment. So you'd need to know the interpolation increment for that check. The interpolation is not necessarily a perfect integral fit.
0

I must thank both Rajan Kali and 5f3bde39-70a2-4df1-afa2-47f61b answers to being able to find a solution. It might not be the most elegant one and I am willing to look for even better ones, but this one worked for me:

class ObjectScroller {
        ObjectAnimator animator;
        TextView scrollText;
        int viewWidth;
        int itemWidth;
        long duration = 7500;
        long delay = 2500;

        public ObjectScroller(TextView scroll, int itemWidth, int viewWidth) {
            this.scrollText = scroll;
            this.itemWidth = itemWidth;
            this.viewWidth = viewWidth;
            animator = ObjectAnimator.ofFloat(scrollText, 
                                 "translationX", viewWidth-(itemWidth + 50));
            animator.setStartDelay(delay);
            animator.setInterpolator(new LinearInterpolator());
            animator.setDuration(duration);
            animator.setRepeatMode(ValueAnimator.RESTART);
            animator.setRepeatCount(ValueAnimator.INFINITE);
        }

        public void startObjectScroll() {
            animator.addListener(new DelayAnimation(delay));
//This allows the animation to be paused BEFORE it restarts.
// Since my textview is translated viewWidth-(itemWidth + 50) units, 
// I choose to pause when its translation is lower than viewWidth-(itemWidth + 49)
// (since my text is being translated from right to left, which means
// the movement will create a negative value)
// because, as pointed out, it will NEVER reach viewWidth-(itemWidth + 50) AND
// the LinearInterpolator does not necessarily increments in integer rates.
            animator.addUpdateListener(animation -> {
                float value = (float) animation.getAnimatedValue();
                if (value < viewWidth-(itemWidth + 49)) {
                    animation.pause();
                    new Handler().postDelayed(animation::resume, delay);
                }
            });
            animator.start();
        }
    }

    class DelayAnimation implements Animator.AnimatorListener {
        private long delayMillis;

        public DelayAnimation(long delayMillis) {
            this.delayMillis = delayMillis;
        }

        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationEnd(Animator animation) {

        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }
// This code makes the animation to be paused after it has restarted,
// allowing the user to read the beginning of the text more easily
        @Override
        public void onAnimationRepeat(Animator animation) {
            animation.pause();
            new Handler().postDelayed(animation::resume, delayMillis);
        }
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.