Saving and restoring instance state in dynamically created UI? - android

I'm creating an Android app with an activity comprised of a mixture of static views and dynamically created views. The activity contains UI elements held in an XML file and UI elements which are created dynamically based on the content of a model class. (as in, there is an array in my model class and an EditText will be created for every element in the array.. and a Spinner will be created containing every element in a different array)
What's the usual method for saving and restoring the state of the application when dynamically created UI elements are involved? Because I won't always know which UI elements will exist at any one time! Currently, my binding code simply reloads the data from the database when the device orientation changes, losing any changes that the user made.
I've had a good look around on Google/SO for this and I've not found anything related to this problem.
Thanks all
Edit:
For anyone that comes across this in the future, I did a slightly modified version of Yakiv's approach and wound up with this:
for (int i = 0; i < views.size(); i++) {
View currentView = views.get(i);
if (currentView instanceof CheckBox) {
outState.putBoolean("VIEW"+i, (((CheckBox) currentView).isChecked()));
} else if (currentView instanceof EditText) {
outState.putString("VIEW"+i, ((EditText) currentView).getText().toString());
} else if (currentView instanceof Spinner) {
//.....etc. etc.
}
}
Thanks again Yakiv for the awesome idea.

To restore dynamic views state you need to make them as class fields and initialize them in onCreate. Then just use this article to save and restore their state.
private List<View> mViews= new ArrayList<View>();
#Override
public void onCreate(..)
{
LinearLayout parent = ....//initialize it here
initializeViews();
}
public void initializeViews()
{
// create and add 10 (or what ever you need) views to the list here
mViews.add(new View(this));
}
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
int mViewsCount = 0;
for(View view : mViews)
{
savedInstanceState.putInt("mViewId_" + mViewsCount, view.getId());
mViewsCount++;
}
savedInstanceState.putInt("mViewsCount", mViewsCount);
}
#Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
int mViewsCount = savedInstanceState.getInt("mViewsCount");;
for(i = 0; i <= mViewsCount)
{
View view = mViews.get(i);
int viewId = savedInstanceState.getInt("mViewId_" + i);
view.setId(viewId);
mViewsCount++;
}
}
Best wishes.

Related

Control RecyclerView inflated views from outside the adapter: keep selection state

Generally
I want to control the ViewHolder inflated Views of my RecyclerView from outside of the ViewHolder and the RecyclerView classes. In other words, I want to have control of these views from other methods/classes.
My case (en example)
In my specific case, I made a photo gallery activity which allows the user to perform selection and deselection of each inflated view, notifying which items are selected by highlighting them.
For now, the user is able to do that by clicking each generated object / View; then, actions on specific child of RecyclerView / adapter are possible thanks to "setOnClickListener" and "setOnLongClickListener" methods, which perform the corresponding actions in methods inside the ViewHolder class.
But when activity is restarted (i.e. for device rotation) the selection goes lost and the user should perform the selection again (i.e. for deleting photos).
Assuming that positions of the selected photos are kept (for example via bundle, or via an array) is possible to restore selection (i.e. highlighting the corresponding item / views) on the adapter views after that the activity is re-started? If yes, how?
Some code
The code below contains the Recyclerview class and the AdapterView class, which both are child of an activity Class.
private class ImageGalleryAdapter extends RecyclerView.Adapter<ImageGalleryAdapter.MyViewHolder> {
private ArrayList<PhotoObject.PhotoElement> photoAL;
private Context mContext;
public ImageGalleryAdapter(Context context, ArrayList<PhotoObject.PhotoElement> photosToPreviewInGallery) {
mContext = context;
photoAL = photosToPreviewInGallery;
}
#Override
public ImageGalleryAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
// Inflate the layout
View itemView = inflater.inflate(R.layout.item_photo, parent, false);
ImageGalleryAdapter.MyViewHolder viewHolder = new ImageGalleryAdapter.MyViewHolder(itemView);
// Retrieving the itemView
return viewHolder;
}
#Override
public void onBindViewHolder(ImageGalleryAdapter.MyViewHolder holder, int position) {
PhotoObject.PhotoElement previewPhotoInGallery = photoAL.get(position);
ImageView imageView = holder.mPhotoImageView;
GlideApp.with(mContext)
.load(previewPhotoInGallery.getUrl())
.placeholder(R.drawable.ic_cloud_off_red)
.into(imageView);
}
//The method which gives back the number of items to load as photo.
#Override
public int getItemCount() {
return (photoAL.size());
}
// The class that assigns a view holder for each Image and checkbox in the RecyclerView.
public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
public ImageView mPhotoImageView;
public CheckBox mPhotoCheckBox;
public MyViewHolder(View item_view) {
super(item_view);
mPhotoImageView = (ImageView) item_view.findViewById(R.id.item_photo_iv);
mPhotoCheckBox = (CheckBox) item_view.findViewById(R.id.item_photo_checkbox);
item_view.setOnClickListener(this);
item_view.setOnLongClickListener(this);
// Retrieving the item_view
}
// The method for managing the click on an image.
#Override
public void onClick(View view) {
itemSelection(view);
}
// Manages the selection of the items.
private void itemSelection(View item) {
// Retrieving the item
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
if (!item.isSelected()) {
// Add clicked item to the selected ones
MultiPhotoShootingActivity.manageSelection(true, position);
// Visually highlighting the ImageView
item.setSelected(true);
mPhotoCheckBox.setChecked(true);
mPhotoCheckBox.setVisibility(View.VISIBLE);
} else {
// Remove clicked item from the selected ones
MultiPhotoShootingActivity.manageSelection(false, position);
// Removing the visual highlights on the ImageView
item.setSelected(false);
mPhotoCheckBox.setChecked(false);
mPhotoCheckBox.setVisibility(View.INVISIBLE);
}
}
}
// The method for managing the long click on an image.
#Override
public boolean onLongClick(View view) {
int position = getAdapterPosition();
if(position != RecyclerView.NO_POSITION) {
Intent intent = new Intent(mContext, PhotoDetail.class);
intent.putExtra("KEY4URL", activityPhotoObject.getPath(position));
startActivity(intent);
}
// return true to indicate that the click was handled (if you return false onClick will be triggered too)
return true;
}
}
}
Thank you for your time.
You shouldn't "control" views from outside the adapter. Instead, Override onSaveState and onRestoreState in your activity. Make same methods in your adapter with passing the bundle to the adapter in order to save state. save an integer array of positions that were selected into the bundle(that you passed into an adapter). In corresponding way, you can get the array of selected positions from the bundle of On restore state.
activity:
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState){
adapter.onRestoreInstanceState(savedInstanceState);
}
in your adapter:
public void onRestoreInstanceState(Bundle state){
selectedItemsArray = state.getIntArray("my_array_key")
}
#Alessandro
You can handle the Runtime changes by yourself.
In your manifest, you can define the changes that your activity will handle by itself and it will not be restarted.
android:configChanges="orientation|keyboardHidden"
After that, you'll have to handle the Configuration changes that you declared in your manifest using this method in your activity:
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Checks the orientation of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
// Do your thing
}
}
SOLVED
Find out that for solving the problem I had to accomplish two little tasks:
saving and restoring the selected item selection state (for example via an array, as helpfully suggested by #Inkognito);
retrieving the views for applying the selection, based on the position inside the RecyclerView.
So, I had to modify some code.
Before proceeding, I would like to point out that the Activity class has a sub-class, which is the Adapter class (named ImageGalleryAdapter); the Adapter subclass, in turn, has its own subclass, which is the ViewHolder class (named MyViewHolder).
So: Activity class -> Adapter class -> ViewHolder class
Code modified in the parent class (the activity class, in which the RecyclerView is)
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
adapter.onSaveInstanceState(outState);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
adapter.onRestoreInstanceState(savedInstanceState);
}
In the onSaveInstanceState and onRestoreInstanceState methods, I added the references for saving and restoring instance states of the "adapter" sub-class.
Code added in the adapter class (which is inside the RecyclerView class)
private boolean [] selectedItemsArray;
private void onSaveInstanceState(Bundle outState) {
outState.putBooleanArray("my_array_key" , selectedItemsArray = mpsaPO.getItemsSelected());
}
private void onRestoreInstanceState(Bundle state) {
if (state != null) {
selectedItemsArray = state.getBooleanArray("my_array_key");
}
}
The selectedItemsArray is a boolean array in which the information of which elements of the RecyclerView are selected (true = selected; false = not selected) is contained.
Then, adding this element in the saved instance and retrieved via the activity class, makes the app able to know which are the views selected after that the activity is re-created.
Code added inside the onBindViewHolder method, which is inside the adapter class
if (selectedItemsArray != null) {
if (selectedItemsArray[position]) {
holder.itemView.setSelected(true);
holder.mPhotoCheckBox.setChecked(true);
holder.mPhotoCheckBox.setVisibility(View.VISIBLE);
}
}
With this last part of code, we are applying the selection to the corresponding views based on which items/views were selected before that the activity was saved.
The holer object contains the itemView and mPhotoCheckBox objectsm on which we can perform the selection.

Scroll to given position in Android Leanback ListRow

I'm using the Google Leanback widgets in an Android TV application. It utilizes a RowsFragment with ListRows in it.
What I'm trying to determine is if there is any way to programmatically scroll to a particular object within one of the rows. I've dug into the docs for the Leanback widgets but cannot find what I'm looking for.
I had a similar necessity: I needed to set the initial selected item in a ListRow.
I ended up subclassing the ListRowPresenter like this:
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.RowPresenter;
public class CustomPresenter extends ListRowPresenter {
private int mInitialSelectedPosition;
public CustomPresenter(int position) {
this.mInitialSelectedPosition = position;
}
#Override
protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
super.onBindRowViewHolder(holder, item);
ViewHolder vh = (ListRowPresenter.ViewHolder) holder;
vh.getGridView().setSelectedPosition(mInitialSelectedPosition);
}
}
Hopefully this will help you.
In the latest version of Leanback (think v23.3.0+), you can now specify not only the row position but also perform optional tasks on the row. In your case, the task would be programmatic selection like so:
BrowseFragment.setSelectedPosition(0, true, new ListRowPresenter.SelectItemViewHolderTask(2));
No need to implement custom list row presenters or anything
I've done it when I needed to implement "Return to the first item in a Row by pressing Back".
I was calling this method from Activity's onBackPressed().
If this method returns false we call Activity.super.onBackPressed().
If true - we don't.
public boolean onBackPressed(){
boolean consumeBack;
int selectedRowPosition = getRowsFragment().getSelectedPosition();
ListRowPresenter.ViewHolder selectedRow = (ListRowPresenter.ViewHolder) getRowsFragment().getRowViewHolder(selectedRowPosition);
int selectedItemPosition = selectedRow.getSelectedPosition();
if(selectedItemPosition == 0){
consumeBack = false;
} else {
consumeBack = true;
getRowsFragment().setSelectedPosition(selectedRowPosition, true, new ListRowPresenter.SelectItemViewHolderTask(0));
}
return consumeBack;
}
Instead of "0" you can set any position you need.
This answer is suggesting the usage of the latest androidx.leanback library.
In your BrowseSupportFragment create a class variable of type HeadersSupportFragment. After creating your ArrayObjectAdapter and using it by setAdapter(), call the getHeadersSupportFragment(). Then call getSelectedPosition() to get the current selected position and store it in Preferences. Later, use setSelectedPosition() to set the previous position.
Here is an example:
private HeadersSupportFragment hsp;
private ArrayObjectAdapter mRowsAdapter;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
setAdapter(mRowsAdapter);
hsp = getHeadersSupportFragment();
int lastPosition = getSharedPreferences(CONTEXT).getInt(LAST_NUMBER, 0);
hsp.setSelectedPosition(lastPosition);
}
#Override
public void onPause() {
super.onPause();
if(hsp != null){
getSharedPreferences(CONTEXT).edit().putInt(LAST_NUMBER, hsp.getSelectedPosition()).commit();
}
}
If your headersState is enabled, use
SelectItemViewHolderTask task = new SelectItemViewHolderTask(positionX)
boolean isSmoothScroll = false // If you need to ignore the animation
task.setSmoothScroll(isSmothScroll)
setSelectedPosition(positionY, isSmothScroll, task)
However, if your headersState is disabled, then using this code will cause an exception: IllegalStateException: Cannot start headers transition.
In this case you need to use this instead:
getRowsSupportFragment().setSelectedPosition(positionY, isSmothScroll, task)
The difference between them is whether the HeadersSupportFragment is updated or not.

ViewPager's PageAdapter get reset on orientation change

I'm using a ViewPager for a multiple-choice exam app, which chooses out randomly thirty questions out of a bigger set. I do this in the PageAdapter that is supplying the pages to the ViewPager.
The problem is that when an orientation change occurs, not only the pager but also the adapter gets reloaded - I know how to save the current pager position but when the adapter gets reset, it also chooses new questions from the set. What would be the proper way to handle this?
Also, side question - what would be the best way to register the choices on the RadioGroups? Directly by click or in a different way?
I'm fairly new to the Android app developement.
Activity:
public class MyActivity extends SherlockActivity {
ActionBar actionBar;
ViewPager pager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pager = new ViewPager(this);
setContentView(pager);
QuestionsAdapter adapter = new QuestionsAdapter(this);
pager.setAdapter(adapter);
int position = 0;
if (savedInstanceState != null) {
position = savedInstanceState.getInt("Q_NUMBER");
}
pager.setCurrentItem(position);
}
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
int position = pager.getCurrentItem();
savedInstanceState.putInt("Q_NUMBER", position);
}
}
Adapter:
class QuestionsAdapter extends PagerAdapter {
Context context;
QuestionsHelper dbQuestions;
boolean exam;
List<HashMap<String,Object>> examQuestions;
public QuestionsAdapter(Context context, boolean exam) {
this.context = context;
this.examQuestions = GetQuestionsFromDB(30);
}
public Object instantiateItem(View collection, int position) {
LayoutInflater inflater = (LayoutInflater) collection.getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view;
HashMap<String,Object> q;
view = inflater.inflate(R.layout.exam_question_layout, null);
q = getQuestion(position+1);
((TextView)view.findViewById(R.id.q_number)).setText(Integer.toString(position+1)+".");
((TextView)view.findViewById(R.id.q_question)).setText(q.get("question").toString());
((RadioButton)view.findViewById(R.id.q_answer_a)).setText(q.get("answer_a").toString());
((RadioButton)view.findViewById(R.id.q_answer_b)).setText(q.get("answer_b").toString());
((RadioButton)view.findViewById(R.id.q_answer_c)).setText(q.get("answer_c").toString());
((ViewPager)collection).addView(view, 0);
return view;
}
}
Screen Rotation will redraw the entire screen in the new orientation, we can prevent it with overriding configuration changes.
add android:configChanges="orientation|screenSize" under your screen declaration in Android Manifest
android:configChanges="orientation|screenSize"
And Override onConfigurationChanged(Configuration) in your activity like
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
I know how to save the current pager position but when the adapter
gets reset, it also chooses new questions from the set. What would be
the proper way to handle this?
Your questions should really have an id to uniquely identify them. I'm not sure how you get them from the database but when that would happen you would need to store their ids. Also:
Your adapter should have a long array(or integer) holding 30 values representing the ids of the current selected batch of questions
You'll need to implement the following logic in the adapter: if the long array from the previous point is null then assume it's a clean start and get a new set of 30 questions.
If the long array is non null then we are facing a restore from a configuration change and you'll need to use those ids to get the proper questions from the database instead of a random batch
In the Activity you'll save the long array of the adapter in the onSaveInstanceState() method(savedInstanceState.putLongArray)
In the onCreate method of the Activity, when you create the adapter, you'll check the savedInstanceState Bundle to see if it is non-null and it has the long array and set that on the adapter(so it will know which questions to get)
what would be the best way to register the choices on the RadioGroups?
Directly by click or in a different way?
You could use the above method, or create a custom class with Parcelable like it has already been recommended to you in the comments.

Unable to setContentView with my tabhost with nested activities

My code below is based on this: http://web.archive.org/web/20100816175634/http://blog.henriklarsentoft.com/2010/07/android-tabactivity-nested-activities/
My issue is with the history ArrayList that is supposed to store the activities so I can properly utilize the back button, however my app crashes when I hit back. I think it's because history isn't exactly storing Views.
I added:
View view = getLocalActivityManager().startActivity("ViewPagerActivity", new
Intent(this,ViewPagerActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))
.getDecorView();
to my back method to see if it would load it and it works.
public class FeaturedTabGroup extends ActivityGroup {
// Keep this in a static variable to make it accessible for all the nesten activities, lets them manipulate the view
public static FeaturedTabGroup group;
// Need to keep track of the history if you want the back-button to work properly, don't use this if your activities requires a lot of memory.
private ArrayList history;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.history = new ArrayList();
group = this;
// Start the root activity withing the group and get its view
View view = getLocalActivityManager().startActivity("ViewPagerActivity", new
Intent(this,ViewPagerActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))
.getDecorView();
// Replace the view of this ActivityGroup
replaceView(view);
}
public void replaceView(View v) {
// Adds the old one to history
history.add(v);
// Changes this Groups View to the new View.
setContentView(v);
System.out.println("view set successful");
}
public void back() {
if(history.size() > 0) {
history.remove(history.size()-1);
setContentView((Integer) history.get(history.size()-1));
}else {
finish();
}
}
#Override
public void onBackPressed() {
FeaturedTabGroup.group.back();
return;
}
}
EDIT:
For brevity, I'll approach this problem with another question: why does setContentView(v) work, but not when the Views are store in an ArrayList? What happens to the view when it is stored in an arraylist?
Can't you work around this issue with ViewSwitcher (http://developer.android.com/reference/android/widget/ViewSwitcher.html)? It provides a cleaner solution and you could always call showPrevious() and showNext() when needed.
setContentView(int) expects a layout resource ID. In your example, can you try to save the view object itself (i.e. without type cast to integer)? As jcXavier says - viewswitcher is a better way to handle this.
Maybe it's late , but here: (history.size() > 0), change 0 to 1, when size is 1, it should just finish.
Adam,
I know it must be late... but perhaps I can help somebody else.
Here is how I solved the problem:
public void back() {
View v;
if(history.size() > 1) {
history.remove(history.size()-1);
v = (View) history.get(history.size()-1);
FirstGroup.group.setContentView(v);
}else {
finish();
}
}

Listener to be notified after view refreshes

My project consists of a single Activity so far that loads a GridView that is populated by an extended BaseAdapter.
Typically the view is refreshed by calling BaseAdapter.notifyDataSetChanged() from one of my OnClickListener objects.
My problem is that I need to start a timer each time the view is refreshed. I only want to do this when the view has been completely reloaded.
I can't seem to find a listener or method that I can override in either the View or Adapter APIs to perform this, although I presume there is one.
The closest I've found is BaseAdapter.registerDataSetObserver although I'm not sure this is what I'm looking for either.
Can anyone advise please?
Thanks
DataSetObserver won't provide the feature you're looking for. In your adapter try looking at getView() or ViewBinder.setViewBinder() (for the Simple...Adapter classes) once the last view is filled with data you'll be able to know, roughly, when its done.
I'd say your best bet would be to create an anon inner class from this class and add your timer logic in an extension of GridView:
class AdapterDataSetObserver extends DataSetObserver {
#Override
public void onChanged() {
}
#Override
public void onInvalidated() {
}
}
(You can access this member variable as it is not priveate):
/**
* Should be used by subclasses to listen to changes in the dataset
*/
AdapterDataSetObserver mDataSetObserver;
This is the method that you should override (inside gridview) (You may need to make some modifications as some of these member variables may be private - mDataSetObserver is not, however:
#Override
public void setAdapter(ListAdapter adapter) {
if (null != mAdapter) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
resetList();
mRecycler.clear();
mAdapter = adapter;
mOldSelectedPosition = INVALID_POSITION;
mOldSelectedRowId = INVALID_ROW_ID;
if (mAdapter != null) {
mOldItemCount = mItemCount;
mItemCount = mAdapter.getCount();
mDataChanged = true;
checkFocus();
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
int position;
if (mStackFromBottom) {
position = lookForSelectablePosition(mItemCount - 1, false);
} else {
position = lookForSelectablePosition(0, true);
}
setSelectedPositionInt(position);
setNextSelectedPositionInt(position);
checkSelectionChanged();
} else {
checkFocus();
// Nothing selected
checkSelectionChanged();
}
requestLayout();
}
Look for these two lines in the method above and extend your class here:
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);

Categories

Resources