How to Build a Multi‑Language Adaptive Countdown Timer with Smooth Animations in Android
This article explains how to implement a flexible Android countdown timer that adapts to multi‑language layouts, adds digit‑rolling animations, and optimizes performance by pausing when off‑screen, providing complete code samples and layout strategies for production use.
Introduction
Countdown timers are widely used in e‑commerce apps to boost click‑through and order rates. While Chinese apps often display the timer in a single line, international apps must handle longer texts and multiple languages, requiring a responsive layout and smooth animation without hurting performance.
1. Basic Countdown Functionality
1.1 Requirements and Principle
The widget shows two states – "X days HH:MM:SS until start" and "X days HH:MM:SS until end" – so an activity‑status flag is needed. Hours, minutes and seconds are independent and are displayed in separate TextView instances.
1.2 Implementation
The core is Android’s CountDownTimer. A custom callback interface is defined to expose three events:
public interface OnCountDownTimerListener {
/** Called while the timer is running */
void onRemain(long millisUntilFinished);
/** Called when the timer finishes */
void onFinish();
/** Called every minute for reporting */
void onArrivalOneMinute();
}The interface is used by the view to notify the host about remaining time, completion and per‑minute reporting.
1.2.1 View Construction and Binding
In init() the widget finds its child TextView objects:
private void init() {
mDayTextView = findViewById(R.id.days_tv);
mHourTextView = findViewById(R.id.hours_tv);
mMinTextView = findViewById(R.id.min_tv);
mSecondTextView = findViewById(R.id.sec_tv);
mHeaderText = findViewById(R.id.header_tv);
mDayText = findViewById(R.id.new_arrival_day);
}1.2.2 Private Helper Methods
setSecond(long millis)converts the remaining milliseconds into days, hours, minutes and seconds, pads single‑digit values with a leading zero, updates visibility of the day view, and either initializes or flips the numbers:
private void setSecond(long millis) {
long day = millis / ONE_DAY;
long hour = millis / ONE_HOUR - day * 24;
long min = millis / ONE_MIN - day * 24 * 60 - hour * 60;
long sec = millis / ONE_SEC - day * 24 * 60 * 60 - hour * 60 * 60 - min * 60;
// pad with leading zero
if (hour.length() == 1) hour = "0" + hour;
if (min.length() == 1) min = "0" + min;
if (sec.length() == 1) sec = "0" + sec;
// update day visibility
if (day == 0) {
mDayTextView.setVisibility(GONE);
mDayText.setVisibility(GONE);
} else {
setDayText(day);
mDayTextView.setVisibility(VISIBLE);
mDayText.setVisibility(VISIBLE);
}
// set or flip numbers
if (mFirstSetTimer) {
mHourTextView.setInitialNumber(hour);
mMinTextView.setInitialNumber(min);
mSecondTextView.setInitialNumber(sec);
mFirstSetTimer = false;
} else {
mHourTextView.flipNumber(hour);
mMinTextView.flipNumber(min);
mSecondTextView.flipNumber(sec);
}
}1.2.3 Public API
Clients can set the listener, the initial time and the header text, then start or cancel the timer:
public void setDownTimerListener(OnCountDownTimerListener listener) { mOnCountDownTimerListener = listener; }
public void setDownTime(long millis) { mMillis = millis; }
public void setHeaderText(int eventStatus) {
if (eventStatus == HomeItemViewNewArrival.EVENT_NOT_START) {
mHeaderText.setText("Start in");
} else {
mHeaderText.setText("Ends in");
}
}
public void startDownTimer(int eventStatus) {
mArrivalOneMinuteFlag = Constant.SIXTY;
mFirstSetTimer = true;
setSecond(mMillis);
createCountDownTimer(eventStatus);
mCountDownTimer.start();
}
public void cancelDownTimer() { mCountDownTimer.cancel(); }2. Layout Adaptation for Multi‑Language
Because the title and timer length vary across languages, two layouts are prepared: one with the timer at the end of the line and another with the timer on the next line. During measurement the code decides which view to show:
private boolean isShortCountDownTimerViewShow() {
String languageCode = LocaleManager.getInstance().getCurrentLanguage();
if (Constant.EN_US.equals(languageCode) || Constant.EN_GB.equals(languageCode) || Constant.EN_AU.equals(languageCode)) {
return true; // English variants stay on the same line
} else {
// measure title and timer widths
View header = inflate(mContext, R.layout.main_view_header_new_arrival, null);
TextView title = header.findViewById(R.id.new_arrival_txt);
LinearLayout timer = header.findViewById(R.id.count_down_timer_short);
int wSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
title.measure(wSpec, wSpec);
timer.measure(wSpec, wSpec);
return (timer.getMeasuredWidth() + title.getMeasuredWidth() <= mContext.getResources().getDimensionPixelSize(R.dimen.qb_px_302));
}
}Depending on the result, either the short‑line view or the long‑line view is made VISIBLE while the other is set to GONE.
if (isShortCountDownTimerViewShow()) {
initCountDownTimerView(mBaseCountDownTimerViewShort, bean);
mBaseCountDownTimerViewShort.setVisibility(VISIBLE);
mBaseCountDownTimerViewLong.setVisibility(GONE);
} else {
initCountDownTimerView(mBaseCountDownTimerViewLong, bean);
mBaseCountDownTimerViewShort.setVisibility(GONE);
mBaseCountDownTimerViewLong.setVisibility(VISIBLE);
}3. Digit‑Rolling Animation
3.1 Principle
Each digit is a two‑digit component that scrolls vertically. The old digit moves upward out of view while the new digit scrolls in from below. The animation is driven by a ValueAnimator that interpolates the vertical offset from the maximum move height to zero.
3.2 Implementation
The custom NumberFlipView extends TextView. In the constructor the maximum move height and paint attributes are set according to the UI design:
public NumberFlipView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mResources = context.getResources();
mMaxMoveHeight = mResources.getDimensionPixelSize(R.dimen.qb_px_18);
setPaint();
}
private void setPaint() {
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setFakeBoldText(true);
mPaint.setTextSize(mResources.getDimensionPixelSize(R.dimen.qb_px_14));
}During onDraw() the view splits the old and new numbers into arrays, compares each digit, and draws either a static digit or a moving pair:
if (mNewNumberArray.get(i).equals(mOldNumberArray.get(i))) {
canvas.drawText(mNewNumberArray.get(i), ...);
} else {
canvas.drawText(mOldNumberArray.get(i), ..., mOldNumberMoveHeight + ...);
canvas.drawText(mNewNumberArray.get(i), ..., mNewNumberMoveHeight + ...);
}The animator updates the offsets and calls invalidate() on each frame:
mNumberAnimator = ValueAnimator.ofFloat(mMaxMoveHeight, 0);
mNumberAnimator.addUpdateListener(animation -> {
mNewNumberMoveHeight = (float) animation.getAnimatedValue();
mOldNumberMoveHeight = mNewNumberMoveHeight - mMaxMoveHeight;
invalidate();
});
mNumberAnimator.setDuration(FLIP_NUMBER_DURATION);
mNumberAnimator.start();4. Performance Optimisation
Because the timer is often placed inside a ListView or a fragment, it should pause when the view leaves the visible area or when the app goes to the background. Overriding lifecycle callbacks achieves this:
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopCountDownTimerAndAnimation(); // pause when view is detached
}
@Override
public void onStop() {
super.onStop();
stopNewArrivalCountDownTimerAndAnimation(); // pause when fragment stops
}
@Override
public void onResume() {
super.onResume();
refreshNewArrival(); // restart when fragment resumes
}Helper methods iterate over visible ListView children, locate the countdown view instances and invoke their pause or restart methods.
5. Full Usage Example
In an activity or fragment the timer is instantiated, configured and started as follows:
if (view != null) {
view.setDownTime(mDuration);
view.setHeaderText(mEventStatus);
view.startDownTimer(mEventStatus);
view.setDownTimerListener(new BaseCountDownTimerView.OnCountDownTimerListener() {
@Override public void onRemain(long millis) { /* optional */ }
@Override public void onFinish() {
view.cancelDownTimer();
// handle end‑of‑event logic
}
@Override public void onArrivalOneMinute() { /* reporting */ }
});
}The accompanying XML layouts define the title TextView and three NumberFlipView instances for hours, minutes and seconds.
With these components the countdown timer adapts to different language lengths, provides a smooth digit‑rolling effect, and conserves resources by pausing when not visible.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
vivo Internet Technology
Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
