Хронометр не обновляется в виджетах приложения ListView

У меня есть виджет приложения, отображающий таймеры. Таймеры могут быть запущены, остановлены или возобновлены.

На Android 8 таймеры иногда не отмечаются (50/50). Некоторые пользователи жаловались на проблему на Android 7, но я не совсем уверен, что это та же проблема. Все работает хорошо на Nexus 5 с установленным Android 6. Если просмотреть список вниз (пока хронометр невидим) и прокрутка вверх - таймер начнет тикать. Если я поставлю еще один Chronometer над ListView и запустим - хронометр будет хорошо гадать

ActivitiesRemoteViewsFactory

public RemoteViews getViewAt(int position) {
...

    RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), getItemLayoutId());

    if (timeLog.getState() == TimeLog.TimeLogState.RUNNING) {          

        remoteViews.setChronometer(R.id.widget_timer,
                SystemClock.elapsedRealtime() - (duration * 1000 + System.currentTimeMillis() - timeLog.getStartDate().getTime()), null, true);
    } else {
        long timerValue = SystemClock.elapsedRealtime() - duration * 1000;
        remoteViews.setChronometer(R.id.widget_timer, timerValue, null, false);

    }

    return remoteViews;

Обновление отправляется из метода AppWidgetProvider onReceive

public void onReceive(Context context, Intent intent) {   
   AppWidgetManager widgetManager = AppWidgetManager.getInstance(ctx);
    ...
   widgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_activities_list);
}

TargetSDK - 25

ОБНОВЛЕНИЕ Проблема также воспроизводится в основном приложении. Что-то не так с ListView, как хорошо работает в RecyclerView. Добавлен простой пример, воспроизводящий проблему на странице https://github.com/zaplitny/WidgetIssue.

Ответы

Ответ 1

Виджет Chronometer был обновлен между API 25 и API 26, чтобы предотвратить обновление невидимых таймеров. (Это мое лучшее предположение, глядя на базовый код.) В частности, метод updateRunning() был изменен, чтобы учесть, отображается ли виджет или нет. От Chronometer.java:

Для API 25: boolean running = mVisible && mStarted;

Для API 26: boolean running = mVisible && mStarted && isShown();

Это isShown() является источником вашей проблемы, и это останавливает повторное использование ваших Chronometers. Для API 26+ повторно используемые Chronometers не обновляются, кажется, когда они находятся в ListView.

Существует несколько способов решить эту проблему. Во-первых, это не повторное использование Chronometers. В getView() ваших адаптеров вы можете использовать следующий код:

if (view == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    view = LayoutInflater.from(getContext()).inflate(R.layout.widget_timer, parent, false);
}

С помощью этого кода вы всегда будете создавать новые Chronometers и никогда не будете их повторно использовать.

Альтернативой этому является вызов chronometer.start() после того, как виджет выложен, а isShown() будет истинным. Добавьте следующий код в getView():

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
    chronometer.start();
} else {
    view.post(new Runnable() {
        @Override
        public void run() {
            chronometer.start();
        }
    });
} 

В любом случае работает, и есть другие способы.

Ответ 2

Добавьте это в свой манифест приложения:

 <!-- Widget Receiver -->
        <receiver android:name=".widgets.WidgetProvider">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/widget_provider" />
        </receiver>

Теперь, когда вы хотите обновить виджет из приложения, вызовите это:

                  try {
                        Intent intent = new Intent(ctxt, WidgetProvider.class);
                        intent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
                        int ids[] = AppWidgetManager.getInstance(ctxt).getAppWidgetIds(new ComponentName(ctxt, WidgetProvider.class));
                        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS,ids);
                        ctxt.sendBroadcast(intent);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

Я использовал это для обновления моего виджета. Спасибо.