I'm new to Android (this is my first application) and I made a GridView in an Activity which I'd like it to be responsive according to screen position (portrait / landscape). Of course I made some custom layout values of GridView rows width for portrait as well as for landscape, and it works perfectly.
Except that, on every screen orientation change, the GridView is reloading again. And that's not good for user experience. I only like the GridView (and definitely its rows) to adjust to new screen width without doing all the onCreate() instructions (retrieving GridView's data from internet, assigning retrieved data in a HashMap object, setting the GridView's Adapter, then display the GridView's rows).
I went through many forum threads and some Google documentation that talk about restoring simple values, but never found an answer for restoring a GridView without having it to reload again.
I read about onSaveInstanceState() and onRestoreInstanceState(), but I didn't know how to use them in my case (GridView case).
Could any one provide a simple code sample for this? Or at least some headlines to follow?
Here's some of what I tried in my code, in the Activity class :
private Parcelable mListInstanceState;
private Parcelable mAdapterInstanceState;
private GridView gridView = null;
private MyAdapter myAdapter = null;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
gridView = (GridView) findViewById(R.id.gridview);
// Check whether we're recreating a previously destroyed instance
if(savedInstanceState != null) {
// Restore value of members from saved state
mListInstanceState = savedInstanceState.getParcelable("gridview");
mAdapterInstanceState = savedInstanceState.getParcelable("adapter");
}
else {
// Retrieve data from internet, fill HashMap object,
// set Adapter and display GridView only for the first time the activity is created
attemptToDisplayGirdViewItems();
}
}
#Override
protected void onSaveInstanceState(Bundle state) {
// Save activity state
super.onSaveInstanceState(state);
state.putParcelable("gridview", gridView.onSaveInstanceState());
// state.putParcelable("adapter", myAdapter);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
// Retrieve and restore activity state
// Restore state members from saved instance
mListInstanceState = savedInstanceState.getParcelable("gridview");
mAdapterInstanceState = savedInstanceState.getParcelable("adapter");
super.onRestoreInstanceState(savedInstanceState);
}
You don't need to save the views. If they have ID, they'll be saved automatically (such as scroll position of a GridView). What you need to save is the adapter's data set.
Your adapter presumably holds a list of items. Unless these items are primitives or String or implement Serializable, they need to implement Parcelable for this to work.
See here how to do that: How can I make my custom objects Parcelable?
Your adapter needs a getItems method which returns the data set - an ArrayList of your parcelable items. It also needs to have a setItems(...) method or a constructor taking a list of items as parameter.
Then your activity will look like this:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
gridView = (GridView) findViewById(R.id.gridview);
myAdapter = new MyAdapter(); // Create empty adapter.
if(savedInstanceState != null) {
ArrayList<MyItem> items = savedInstanceState.getParcelableArrayList("myAdapter");
myAdapter.setItems(items); // Load saved data if any.
}
gridView.setAdapter(myAdapter);
}
#Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
state.putParcelableArrayList("myAdapter", myAdapter.getItems());
}
Related
I have two Spinners. In onCreate i setup listeners for them. At some other places i populate them.
Now I am not sure what the best practice is for handling these spinners when screen orientation changes. Should i store all values and selected item in sharedPreferences somehow or in savedInstanceState?
If you can, please advise me in a prefered way and also include some sample code for handling spinners. The goal here is to keep the values and selected item thru the lifecycle.
I will include code at request or if needed.
Thanks
Try this, onSaveInstanceState for Screen Orientation for saving your selected value of spinner,According to my choice shared preference is not good choice for saving selected values of spinner.
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("mySpinner", mySpinner.getSelectedItemPosition());
// do this for each or your Spinner
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// initialize all your visual fields
if (savedInstanceState != null) {
mySpinner.setSelection(savedInstanceState.getInt("mySpinner", 0));
// do this for each of your text views
}
}
Add android:configChanges="orientation|screenSize" in your AndroidManifest file, it will keep spinner value selected on orientation change.
User Spinner Like this:
mSpinner = findViewById(R.id.spinner);
ArrayList<String> stringArrayList = new ArrayList<>();
for (int i = 0; i < 6; i++) {
stringArrayList.add("List Item " + i);
}
ArrayAdapter arrayAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, stringArrayList);
spinner.setAdapter(arrayAdapter);
I'm new to Android and I'm trying to do the following task on my school project:
I have a grid view of movies which the user can scroll endlessly.
When the app starts I fetch the first 20 movies and each time the user scrolls to the bottom of the grid I execute an AsyncTask to fetch 20 more movies and add them to the Adapter.
When the user clicks on a movie he goes to a new child activity to see the movie details.
I'm having troubles maintaining the GridView's scroll position in the following cases:
When the user goes to the details activity and returns to the main activity of the movies.
When the user changes the device orientation.
And when dealing with theses 2 cases I also need to take in consideration that maybe the user scrolled a lot and had 100 movies in the adapter and when he goes back the activity start from the start with only the first 20 movies, so I would be able to scroll to his last position.
Can someone please tell me how can I give the best user experience in my project by not losing the user's scroll position at any case?
I don't know if this is the best practice, but in my case it is.
I decided to set my adapter as a global static variable, in this way I maintain the amount of data loaded via the API, and I don't need to perform a request for every time the user moves between activities.
For maintaining the scroll position I used the onItemClickListener when moving to the details activity and the savedInstanceState when changing orientation.
Here is my code for that:
//Static variables
private static MoviesAdapter mMoviesAdapter;
private static int mGridViewPosition = 0;
//Call this method when user clicks the back button
public static void ClearStaticData(){
mMoviesAdapter.clear();
mMoviesAdapter = null;
}
#Override
public void onSaveInstanceState(Bundle outState) {
int index = mGridView.getFirstVisiblePosition();
outState.putInt(GRID_VIEW_POSITION, index);
super.onSaveInstanceState(outState);
}
#Override
public View onCreateView(...) {
if (mMoviesAdapter == null) {
mMoviesAdapter = new MoviesAdapter(...);
} else {
RestoreGridPosition();
}
}
private void RestoreGridPosition(){
if(mGridViewPosition > 0 && mMoviesAdapter.getCount() >= mGridViewPosition)
mGridView.setSelection(mGridViewPosition);
}
Since I fill my adapter via API call, I think this is probably the best solution to save the data and not to perform requests every time.
Try not finishing mainActivity once a gridItem is clicked so when user navigates back to mainActivity (from detailsActivity) he will have all the data that was there before.
You can handle this situation with activity's lifecycle callbacks:
You can get currently visible GridView item's position like this:
int mCurrentPosition = gridview.getFirstVisiblePosition();
When an orientation change is occurring the activity is recreated and going through the following stages:
onSaveInstanceState
onRestoreInstanceState
You can then save the position before orientation change is happening and get it back when its being restored.
Save Your Activity State
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
// Save the user's current scroll state
savedInstanceState.putInt(STATE_POSITION, mCurrentPosition);
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(savedInstanceState);
}
Restore Your Activity State
public void onRestoreInstanceState(Bundle savedInstanceState) {
// Always call the superclass so it can restore the view hierarchy
super.onRestoreInstanceState(savedInstanceState);
// Restore state members from saved instance
mCurrentPosition = savedInstanceState.getInt(STATE_POSITION);
}
Here once you have the previous position you can move to the desired position in the gridView:
gridview.smoothScrollToPosition(int mCurrentPosition)
This is taken from android docs: Recreating an Activity
Scrolling gridView to position GridView scrolling stackoverflow
In my application I populate a GridView's adapter with random data from a file. The data is shown to the user as a TextView per item. If the user touch an item, the item changes the background colour.
The problem is that if the user touches an item and then rotates the device, the item returns to its original aspect (with the normal background colour)
I've tried different approaches:
Implementing my own adapter
Extending BaseAdapter
Using ArrayAdapter
Using selectors for the TextView
Extending the TextView item with custom styles (from here and here)
Disabling the View within the GridView's onItemClick(AdapterView<?> parent, View view, int position, long id)
What I want to do is keep the Views' colour/style/aspect when I rotate the device
Note:
Why I load data radomly from a file?
The file contains different words. Every time the player start the activity (it is a game) different words in random order are shown inside the GridView. The user hast to point to the right word. If the user make a mistake, the word changes the colour (indeed, I prefer to disable the View). The process is repeated till the user makes the right choice.
You can save the selected states of your list using onSaveInstanceState.
As you click on an item in your list you can assign a state to a boolean array.
Implement the onSaveInstanceState method in your Fragment/Activity.
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBooleanArray(BundleArgs.STATES, mAdapter.getStates());
}
And then in onCreateView you pass those values to your adapter.
if (savedInstanceState != null) {
states = savedInstanceState.getBooleanArray(BundleArgs.STATES);
//Declare adapter and pass states to it
myAdapter = new Adapter(context, values, states);
}
That's a mistake I've seen several times repeated on SO.
The data and the view that represents the data are totally different entities and they should be treated separately.
You need to keep the state of your data in another data element and preserve that data element during rotation. For example (it's just an example, there' several ways of doing it):
// possible states
private static final int NORMAL = 0;
private static final int RIGHT = 1;
private static final int WRONG = 2;
Map<String, Integer> states; // here you keep the states
then on every click, on the code that checks the answer and change color:
// process the click/state change
states.put(word, newState);
then on rotation:
public void onSaveInstanceState(Bundle outState) {
outState.putSerializable("states", states);
}
and on create
// onCreate
if (savedInstanceState != null) {
states = (Map<String, Integer>) savedInstanceState.getSerializable("states");
} else {
states = new HashMap<String, Integer>();
}
and then back on your custom adapter you have to check the state and modify the view accordingly.
// inside getView
int state = 0
if(states.containsKey(word)){
state = states.get(word).intValue();
}
switch(state){
// deal with the cases and set the color
}
I have application with listview(one line contains textview, imageview, ratingbar) and my own adapter. When I rotate the screen after sorting listview (I choose this option in menu), it backs to form before sorting.
I tried override onSaveInstanceState:
#Override
public void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putParcelable("listView", listView.onSaveInstanceState());
}
and fragment onCreate:
listView.setAdapter(adapter);
if(savedInstanceState!=null)
{
mListInstanceState=savedInstanceState.getParcelable("listView");
listView.onRestoreInstanceState(mListInstanceState);
}
But it doesn't work. Should I override onRestoreInstanceState too or use something else?
The problem is when orientation change your Activity is recreated and when onCreate() is called you are setting the adapter with the default list items.
If you don’t maintain different layout for landscape/portrait, you can simply avoid the Activity recreate by just adding configChanges in manifest for your Activity
android:configChanges="orientation|screenSize" // add this line
This will avoid recreating the Activity and same layout will be used to fit on screen width. Make sure your listView layout width is set to match_parent.
If you still want your Activity to recreate, then you need to remember the last selected filter when onSaveInstanceState is called on orientation change
#Override
public void onSaveInstanceState(Bundle outState)= {
outState.putString(“selectedFilter","some name");
super.onSaveInstanceState(outState);
}
And then when onCreate in called after rotate, you can get the selectedFilter name
String filterName = null;
if(savedInstanceState != null){
filterName = savedInstanceState.getString("lastFilter");
}
Finally , set the listView with items based on the filter name.
if(filterName != null && filterName.equalsIgnoreCase("some name")){
// filtered list items
} else {
// default list items
}
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.