The problem happens because the Chronometer class calculates seconds by truncating the division of milliseconds, when it should round the result.
This is not a problem when counting up, but it is when counting down.
I opened an issue on the issue tracker to request the modification of the calculation in the Chronometer class:
https://issuetracker.google.com/issues/297733248
Adding an arbitrary amount of milliseconds to the base of the chronometer might work in some devices, but in slow devices the problem might still happen.
My solution (while the issue gets resolved in the issue tracker) was to create a custom 'MyChronometer' class, based on the framework's Chronometer class, but with the correct calculation (see calculation in the 'updateText' method):
@SuppressLint("AppCompatCustomView")
@RemoteView
public class MyChronometer extends TextView {
public interface OnChronometerTickListener {
void onChronometerTick(MyChronometer chronometer);
}
private long mBase;
private boolean mVisible;
private boolean mStarted;
private boolean mRunning;
private final StringBuilder mRecycle = new StringBuilder(8);
private OnChronometerTickListener mOnChronometerTickListener;
private boolean mCountDown;
public MyChronometer(Context context) {
this(context, null, 0);
}
public MyChronometer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyChronometer(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyChronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
mBase = SystemClock.elapsedRealtime();
updateText(mBase);
}
public void setCountDown(boolean countDown) {
mCountDown = countDown;
updateText(SystemClock.elapsedRealtime());
}
public void setBase(long base) {
mBase = base;
dispatchChronometerTick();
updateText(SystemClock.elapsedRealtime());
}
public long getBase() {
return mBase;
}
public void setOnChronometerTickListener(OnChronometerTickListener listener) {
mOnChronometerTickListener = listener;
}
public OnChronometerTickListener getOnChronometerTickListener() {
return mOnChronometerTickListener;
}
public void start() {
mStarted = true;
updateRunning();
}
public void stop() {
mStarted = false;
updateRunning();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mVisible = false;
updateRunning();
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
mVisible = visibility == VISIBLE;
updateRunning();
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
updateRunning();
}
private synchronized void updateText(long now) {
long diffMillis = mCountDown ? mBase - now : now - mBase;
int seconds = Math.round(diffMillis / 1000f);
String text;
if (seconds < 0) {
text = "-" + DateUtils.formatElapsedTime(mRecycle, -seconds);
} else {
text = DateUtils.formatElapsedTime(mRecycle, seconds);
}
setText(text);
}
private void updateRunning() {
boolean running = mVisible && mStarted && isShown();
if (running != mRunning) {
if (running) {
updateText(SystemClock.elapsedRealtime());
dispatchChronometerTick();
postDelayed(mTickRunnable, 1000);
} else {
removeCallbacks(mTickRunnable);
}
mRunning = running;
}
}
private final Runnable mTickRunnable = new Runnable() {
@Override
public void run() {
if (mRunning) {
updateText(SystemClock.elapsedRealtime());
dispatchChronometerTick();
postDelayed(mTickRunnable, 1000);
}
}
};
void dispatchChronometerTick() {
if (mOnChronometerTickListener != null) {
mOnChronometerTickListener.onChronometerTick(this);
}
}
@Override
public CharSequence getAccessibilityClassName() {
return MyChronometer.class.getName();
}
}