Отображение ProgressBar при длительной операции и смене конф

Activity Lifecycle, Saving Activity State, Managing Tasks, Intent, Intent Filter
Ответить
Hapson
Сообщения: 2
Зарегистрирован: 30 июн 2014, 10:57

Отображение ProgressBar при длительной операции и смене конф

Сообщение Hapson » 30 июн 2014, 11:20

Всем привет!
Попробую обрисовать ситуацию:

Есть Activity, в котором есть ListView.
В onCreate к ListView прикрепляется адаптер, основанный на SimpleCursorAdapter. Адаптер получает данные через CursorLoader и ContentProvider.
В onCreate, перед установкой адаптера я запускаю ProgressBar, который имеется в шаблоне с ListView

Код: Выделить всё

progressBar.setVisibility(View.VISIBLE)
В методе onLoadFinished я прячу ProgressBar.
Это несколько упрощенно, так как в действительности ProgressBar запускается с задержкой в 500 мс через handler.postDelaed. Если задержек в отображении ListView нет, то ProgressBar не мелькает. Но это не главное.

Вот скрины ListView для наглядности
Изображение Изображение Изображение

На третьем скрине видно ProgressBar (в ActionBar). Вот с ним-то и проблемы.
Как видно, ListView имеет в каждом пункте RadioButton.
В БД есть список машин, и у каждой записи есть поле - is_primary, которое может быть 1 или 0.
Если значение поля 1, то RadioButton отмечен, если 0 - нет. Отмечен может быть только один авто.

Так вот, клик на пункте ListView - это два запроса в БД - два update, точнее один applyBatch.
У одной машины устанавливается is_primary в 0, у другой в 1.
ListView связан с курсором, а в ContentProvider курсор обновляется, поэтому все изменения немедленно отображаются на экране.

Обновление записей я изначально сделал в отдельном потоке. Но дальше захотел отображать ProgressBar, в случае если переключение RadioButton займет некоторое время. ProgressBar решил отображать в ActionBar - его-то и видно на 3 скрине.

Бился я с этим долго, вот тут есть тема http://4pda.ru/forum/index.php?showtopic=584019&st=20
В итоге я сейчас имею отдельный AsyncTaskLoader и класс его колбэков - все ради клика и двух Update в БД.
ProgressBar отображается корректно при поворотах девайса. Но некорректно в следующих случаях:

1. Если в момент транзакции нажать Home и тут же снова открыть приложение - ProgressBar отображается слишком долго, так как AsyncTaskLoader перезапускает loadInBackground
2. Если во время транзакции нажать Back и снова вернуться в Activity - ProgressBar вообще не появляется.

Сама транзакция (applyBatch) проходит и изменения видны, так как ListView связан с курсором. Но вот полностью синхронизировать ProgressBar с этой транзакцией не получается. Есть ли способ решить проблему?

Вот код на всякий случай

Код: Выделить всё

public class CarList extends ActionBarActivity implements LoaderCallbacks<Cursor>, OnSelectedDialog {
	
	private Context context;
	private ListView listView;
	private ProgressBar progressBar;
	private CarListSimpleCursorAdapter adapter;
	
	private LoaderManager loaderManager;
	private Loader<Integer> clickLoader;
	private LoaderCallbacks<Integer> clickListener;
	
	private static final int LOADER_CAR_LIST = 1;
	private static final int LOADER_CLICK_LIST = 2;
	
	protected static final int CAR_DELETE = 100;
	protected static final int CAR_EDIT = 110;
	protected static final int CAR_ADD = 120;
	
	private static final String KEY_CAR_ID = "carId";
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
		setContentView(R.layout.car_list_view);
		
		android.support.v7.app.ActionBar actionBar = getSupportActionBar();
		actionBar.setDisplayHomeAsUpEnabled(true);
		
		context = this;
		listView = (ListView)findViewById(R.id.cars_list);
		progressBar = (ProgressBar)findViewById(R.id.wait_dialog_progress_bar);
		loaderManager = getSupportLoaderManager();
		clickListener = new ClickCallbacks(this);
		
		Button addNewCar = (Button)findViewById(R.id.button_add_car_list_cars);
		addNewCar.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				startActivityForResult(new Intent(context, AddCar.class), CAR_ADD);
			}
		});
		
		String[] from = {CarTable.KEY_BRAND};
		int[] to = {R.id.car_list_item_brand_and_model};
		adapter = new CarListSimpleCursorAdapter(context, R.layout.car_list_view_item, null, from, to, 0);
		listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
		listView.setAdapter(adapter);
		showProgress();
		loaderManager.initLoader(LOADER_CAR_LIST, null, this);
		
		
		
		clickLoader = loaderManager.getLoader(LOADER_CLICK_LIST);
		if(clickLoader != null && clickLoader.isStarted()){
			Log.d(Consts.LOG, "onCreate: loader.isStarted");
			loaderManager.initLoader(LOADER_CLICK_LIST, null, clickListener);
			setSupportProgressBarIndeterminateVisibility(true);
		}
		
		listView.setOnItemClickListener(new OnItemClickListener() {
			
			@Override
			public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
				
				RadioButton rb = (RadioButton)view.findViewById(R.id.car_list_radio_button_default_car);
				if(rb.isChecked()){return;}
				
				TextView tv = (TextView)view.findViewById(R.id.car_list_hidden_id_car);
				int newPrimaryCarId = Integer.parseInt(tv.getText().toString());
				
				Bundle args = new Bundle();
				args.putInt(KEY_CAR_ID, newPrimaryCarId);
				setSupportProgressBarIndeterminateVisibility(true);
				
				if(clickLoader != null){
					Log.d(Consts.LOG, "onClick: restartLoader");
					loaderManager.restartLoader(LOADER_CLICK_LIST, args, clickListener);
				}else{
					Log.d(Consts.LOG, "onClick: initLoader");
					clickLoader = loaderManager.initLoader(LOADER_CLICK_LIST, args, clickListener);
				}
			}
		});
		
		listView.setOnItemLongClickListener(new OnItemLongClickListener() {

			@Override
			public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
				TextView hiddenIdCar = (TextView)view.findViewById(R.id.car_list_hidden_id_car);
				TextView brandModelView = (TextView)view.findViewById(R.id.car_list_item_brand_and_model);
				
				int idCar = Integer.parseInt(hiddenIdCar.getText().toString());
				String brandModel = brandModelView.getText().toString();
				
				Car car = new Car(idCar, "", brandModel, 0, 0, 0, 0, 0, 0);
				
				DialogFragment dialog = CarListLongPressedDialog.newInstance(
						CarListLongPressedDialog.LONG_PRESS_DIALOG, car);
				dialog.show(getSupportFragmentManager(), "tag_dialog");
				return true;
			}
			
		});
	}
	
	@Override
	protected void onSaveInstanceState(Bundle outState) {
		super.onSaveInstanceState(outState);
	}
	
	@Override
	public void onDestroy(){
		if(progressHandler != null){
			progressHandler.removeCallbacksAndMessages(null);
		}
		super.onDestroy();
	}
	
	@Override
	public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
		String[] projection = CarTable.getProjection();
		String sortOrder = CarTable.KEY_ID;
		CursorLoader cursorLoader = new CursorLoader(context, Uri.parse(DataProvider.CAR_URL), projection, null, null, sortOrder);
		return cursorLoader;
	}
	
	@Override
	public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
		hideProgress();
		adapter.swapCursor(cursor);
	}
	
	@Override
	public void onLoaderReset(Loader<Cursor> loader) {
		adapter.swapCursor(null);
	}
	
	/*** <ProgressRing> ***/
	
	private Handler progressHandler;
	private Runnable progressRunnable;
	
	private void showProgress(){
		progressHandler = new Handler();
		progressRunnable = new Runnable() {
			@Override
			public void run() {
				AlphaAnimation anim = new AlphaAnimation(0F, 1F);
				anim.setDuration(500);
				progressBar.startAnimation(anim);
				progressBar.setVisibility(View.VISIBLE);
			}
		};
		progressHandler.postDelayed(progressRunnable, 500);
	}
	
	private void hideProgress(){
		if(progressHandler != null){
			progressHandler.removeCallbacks(progressRunnable);
		}
		progressBar.setVisibility(View.GONE);
	}
	/*** </ProgressRing> ***/
	
	@Override
	public void onSelectLongPressDialog(int typeAction, Car car){
		switch(typeAction){
			case CAR_EDIT:
				editCar(car);
				break;
			case CAR_DELETE:
				DialogFragment dialog = CarListLongPressedDialog.newInstance(
						CarListLongPressedDialog.DELETE_DIALOG, car);
				dialog.show(getSupportFragmentManager(), "deleteCar");
				break;
		}
	}
	
	private void editCar(Car car){
		Intent intent = new Intent(this, EditCar.class);
		intent.putExtra(EditCar.CAR_OBJECT, car);
		startActivityForResult(intent, CAR_EDIT);
	}
	
	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);
		switch(requestCode){
			case CAR_EDIT:
				if(resultCode == RESULT_OK && loaderManager != null){
					loaderManager.getLoader(LOADER_CAR_LIST).onContentChanged();
				}else if(requestCode == Consts.RESULT_ERROR){
					Toast.makeText(context, R.string.sss_system_error, Toast.LENGTH_LONG).show();
				}
				break;
			case CAR_ADD:
				if(resultCode == RESULT_OK && loaderManager != null){
					loaderManager.getLoader(LOADER_CAR_LIST).onContentChanged();
				}
				break;
		}
		
	}

	@Override
	public void deleteCar(Car car) {
		Uri uri = Uri.parse(DataProvider.CAR_URL +"/delete");
		String selection = CarTable.KEY_ID +"=?";
		String[] selectionArgs = {String.valueOf(car.id)};
		
		DataAsyncQueryHandler asyncQueryHandler = new DataAsyncQueryHandler(
				getContentResolver(), new DataAsyncQueryHandler.AsyncQueryListener() {
			
			@Override
			public void onUpdateComplete(int token, Object cookie, int result) {}
			
			@Override
			public void onQueryComplete(int token, Object cookie, Cursor cursor) {}
			
			@Override
			public void onInsertComplete(int token, Object cookie, Uri uri) {}
			
			@Override
			public void onDeleteComplete(int token, Object cookie, int result) {
				if(loaderManager != null){
					loaderManager.getLoader(LOADER_CAR_LIST).onContentChanged();
				}
			}
			
			@Override
			public void onApplyBranchComplete(ContentProviderResult[] result) {}
		});
		asyncQueryHandler.startDelete(1, null, uri, selection, selectionArgs);
	}
	
	
	/*** <Click Loader> ***/
	
	public static class ClickLoader extends AsyncTaskLoader<Integer>{
		
		private WeakReference<CarList> activity;
		private int carId;
		private ContentProviderResult[] result;
		private boolean loading = false;
		
		public ClickLoader(Context context, Bundle args) {
			super(context);
			this.carId = args.getInt(KEY_CAR_ID, -1);
			this.activity = new WeakReference<CarList>((CarList)context);
		}

		@Override
		public Integer loadInBackground() {
			Log.d(Consts.LOG, "Loader: loadInBackground");
			if(carId == -1){return -1;}
			CarList activity = this.activity.get();
			if(activity == null){return -1;}
			
			final ArrayList<ContentProviderOperation> list = new ArrayList<ContentProviderOperation>();
			list.add(
				ContentProviderOperation.newUpdate(Uri.parse(DataProvider.CAR_URL +"/update")).
				withValue(CarTable.KEY_PRIMARY_CAR, 0).
				withSelection(CarTable.KEY_PRIMARY_CAR +"=?", new String[]{"1"}).build()
			);
			list.add(
				ContentProviderOperation.newUpdate(Uri.parse(DataProvider.CAR_URL +"/update")).
				withValue(CarTable.KEY_PRIMARY_CAR, 1).
				withSelection(CarTable.KEY_ID +"=?", new String[]{String.valueOf(carId)}).build()
			);
			try {
				result = activity.getContentResolver().applyBatch(DataProvider.AUTHORITY, list);
			} catch (RemoteException | OperationApplicationException e) {
				// TODO ERROR
				loading = false;
				return -1;
			}
			loading = false;
			if(result.length == 2){
				return 1;
			}else{
				return -1;
			}
		}
		
		@Override
		protected void onForceLoad() {
			super.onForceLoad();
			Log.d(Consts.LOG, "Loader: onForceLoad");
		}
		
		@Override
		protected void onStartLoading() {
			super.onStartLoading();
			Log.d(Consts.LOG, "Loader: onStartLoading");
			
			if(result == null && !loading){
				loading = true;
				forceLoad();
			}else if(result != null){
				deliverResult(1);
			}
			
		}
		
		@Override
		protected void onStopLoading() {
			super.onStopLoading();
			Log.d(Consts.LOG, "Loader: onStopLoading");
		}
		
		@Override
		protected void onReset() {
			super.onReset();
			Log.d(Consts.LOG, "Loader: onReset");
		}
	}
	
	public static class ClickCallbacks implements LoaderCallbacks<Integer>{
		
		private WeakReference<CarList> activity;
		
		public ClickCallbacks(CarList activity){
			this.activity = new WeakReference<CarList>(activity);
		}
		
		@Override
		public Loader<Integer> onCreateLoader(int arg0, Bundle args) {
			Log.d(Consts.LOG, "Callbacks: onCreateLoader");
			CarList activity = this.activity.get();
			return new ClickLoader(activity, args);
		}

		@Override
		public void onLoadFinished(Loader<Integer> loader, Integer result) {
			Log.d(Consts.LOG, "Callbacks: onLoadFinished");
			CarList activity = this.activity.get();
			if(activity != null){
				activity.setSupportProgressBarIndeterminateVisibility(false);
				if(result == -1){
					Toast.makeText(activity, R.string.sss_system_error, Toast.LENGTH_LONG).show();
				}
			}
		}

		@Override
		public void onLoaderReset(Loader<Integer> arg0) {
			Log.d(Consts.LOG, "Callbacks: onLoaderReset");
			
		}
	}
}

Viewer
Сообщения: 180
Зарегистрирован: 30 апр 2014, 11:42

Re: Отображение ProgressBar при длительной операции и смене

Сообщение Viewer » 30 июн 2014, 15:00

1. Если в момент транзакции нажать Home и тут же снова открыть приложение - ProgressBar отображается слишком долго, так как AsyncTaskLoader перезапускает loadInBackground
2. Если во время транзакции нажать Back и снова вернуться в Activity - ProgressBar вообще не появляется.
Поведение ProgressBar в AtionBar может отличаться в зависимости от версии Android, в последних версиях состояние ProgressBar при выходе из Activity сохраняется в более старых версиях состояние может не сохраняться и при вызове supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); он не показывается. (это легко проверить поворотом, если в момент до поворота экрана прогресс отображался, то после поворота в одних версиях он продолжает отображаться, а в других версиях он гаснет)

Выход здесь один, нужно в onCreate определять в каком состоянии должен находится ProgressBar и принудительно устанавливать это состояние вызовом setProgressBarIndeterminateVisibility.

Hapson
Сообщения: 2
Зарегистрирован: 30 июн 2014, 10:57

Re: Отображение ProgressBar при длительной операции и смене

Сообщение Hapson » 30 июн 2014, 15:28

Viewer писал(а):
Выход здесь один, нужно в onCreate определять в каком состоянии должен находится ProgressBar и принудительно устанавливать это состояние вызовом setProgressBarIndeterminateVisibility.
В том и вопрос.
Чтобы узнать, нужно ли отображать ProgressBar, нужно узнать, не выполняется ли в данный момент транзакция инициированная кликом.
Пока только один выход вижу - вынести транзакцию в сервис.

Ответить