Reputation: 1556
Desired Outcome
I want to have Vertical List with custom items on the left/right side of the screen in landscape mode and Horizontal List on the top/bottom side of the screen in portrait mode. Horizontal/Vertical List should be Fragment
so I can reuse it later for smartphone version. Minimal SDK version is 13 (Android 3.2).
My Attempt
My custom Activity
has single custom LayersFragment
and another View
. In portrait mode fragment is aligned to parent's left. In landscape mode is aligned to parent's bottom.
LayersFragment
has also different layout for portrait and landscape mode. In portrait mode is Gallery
and in landscape mode is ListView
.
Since Gallery
and ListView
are subclasses of AdapterView<Adapter>
I use this parent class and BaseAdapter
to populate items and listen OnItemClicks
.
Resource Details
frag_layers.xml - XML Layout for LayersFragment
in landscape.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
frag_layers.xml - XML Layout for LayersFragment
in portrait mode.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Gallery
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
activity_photo_editor.xml - XML Layout for my custom Activity
in portrait mode. Layout for landscape mode instead of android:layout_alignParentBottom
has android:layout_alignParentLeft
.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<fragment
android:id="@+id/photo_editor_layouts"
class="rs.ailic.android.heritage.ui.LayersFragment"
android:layout_width="match_parent"
android:layout_height="@dimen/photo_editor_layouts_size"
android:layout_alignParentBottom="true" />
<!-- Not relevant. -->
</RelativeLayout>
Code Details
Class LayersFragment.
public class LayersFragment extends Fragment implements OnItemClickListener {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.frag_layers, container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mLayersAdapter = new LayersAdapter();
mLayersView = (AdapterView<Adapter>) getView().findViewById(android.R.id.list);
mLayersView.setOnItemClickListener(this);
mLayersView.setAdapter(mLayersAdapter);
}
@Override
public void onItemClick(AdapterView<?> adapter, View view, int position, long id) {
//Not implemented
}
private class LayersAdapter extends BaseAdapter {
//Not implemented. Returning 0 in getCount().
}
}
My custom Activity
public class PhotoEditorActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_photo_editor);
}
//Not relevant
}
Problem
I'm getting this ClassCastException when rotating from Landscape to Portrait (ListView -> Gallery)
Caused by: java.lang.ClassCastException: android.widget.AbsListView$SavedState cannot be cast to android.widget.AbsSpinner$SavedState
at android.widget.AbsSpinner.onRestoreInstanceState(AbsSpinner.java:421)
at android.view.View.dispatchRestoreInstanceState(View.java:8341)
at android.view.ViewGroup.dispatchThawSelfOnly(ViewGroup.java:2038)
at android.widget.AdapterView.dispatchRestoreInstanceState(AdapterView.java:766)
at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:2024)
at android.view.View.restoreHierarchyState(View.java:8320)
at android.app.Fragment.restoreViewState(Fragment.java:583)
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:801)
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:977)
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:960)
at android.app.FragmentManagerImpl.dispatchStart(FragmentManager.java:1679)
at android.app.Activity.performStart(Activity.java:4413)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1791)
... 12 more
and this one when rotating from Portrait to Landscape (Gallery -> ListView)
Caused by: java.lang.ClassCastException: android.widget.AbsSpinner$SavedState cannot be cast to android.widget.AbsListView$SavedState
at android.widget.AbsListView.onRestoreInstanceState(AbsListView.java:1650)
at android.view.View.dispatchRestoreInstanceState(View.java:8341)
at android.view.ViewGroup.dispatchThawSelfOnly(ViewGroup.java:2038)
at android.widget.AdapterView.dispatchRestoreInstanceState(AdapterView.java:766)
How can I solve this problem or should I look for another solution?
My Opinion
The problem appears when screen orientation change. I believe that the problem is in 'default implementation' of ListView
and Gallery
. They try to restore their SavedState
in onRestoreInstanceState
after orientation change, but the View
has changed and ClassCastException is thrown.
Thank you,
Aleksandar Ilić
Upvotes: 4
Views: 4670
Reputation: 4248
if you're using two listviews that are different (one with expandable),make sure they have different id in the xml layout.
Upvotes: 0
Reputation: 14289
Assuming that the fragment is not placed on the backstack and the fragment instance is not retained (mainly because I don't know the effect either will have), onCreateView will be run every time the orientation changes. Hence, you can designate which layout to use based on the current orientation. It is also critical to have different IDs for the ListView and Gallery.
Use getFirstVisiblePosition and setSelection to remember the current adapter position. This will only reliably work if the data positions in the adapter don't change when the fragment is not in the resume state . If the data does change, you'll have to recalculate the appropriate position to set to the AdapterView.
frag_layers.xml - XML Layout for LayersFragment in landscape.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ListView
android:id="@android:id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
frag_layers.xml - XML Layout for LayersFragment in portrait mode.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Gallery
android:id="@android:id/gallery"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Class LayersFragment.
public class LayersFragment extends Fragment implements OnItemClickListener {
private AdapterView<Adapter> mLayersView;
private LayersAdapter mLayersAdapter;
private int mVisiblePosition = 0;
@Override
public void onPause() {
super.onPause();
SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
if (mLayersView != null) {
editor.putInt("visiblePosition", mLayersView.getFirstVisiblePosition());
} else {
editor.putInt("visiblePosition", 0);
}
editor.commit();
}
@Override
public void onResume() {
super.onResume();
SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE);
mVisiblePosition = prefs.getInt("visiblePosition", 0);
// -- Set the position that was stored in onPause.
mLayersView.setSelection(mVisiblePosition);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
ViewGroup rootView;
rootView = (ViewGroup)inflater.inflate(R.layout.frag_layers, container, false);
switch (getActivity().getResources().getConfiguration().orientation ) {
case Configuration.ORIENTATION_LANDSCAPE:
mLayersView = (AdapterView<Adapter>)rootView.findViewById(R.id.gallery);
break;
default:
mLayersView = (AdapterView<Adapter>)rootView.findViewById(R.id.listView);
break;
}
mLayersAdapter = new LayersAdapter();
mLayersView.setOnItemClickListener(this);
mLayersView.setAdapter(mLayersAdapter);
return rootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// -- Populate mLayersAdapter here or through a data ready listener registered with the activity?
}
@Override
public void onItemClick(AdapterView<?> adapter, View view, int position, long id) {
//Not implemented
}
private class LayersAdapter extends BaseAdapter {
//Not implemented. Returning 0 in getCount().
}
I haven't tested this code but it's a similar implementation that I use. For my implementation, remove the code in onResume and onPause except for the super calls. During onPause, instead of storing the first visible position in the preferences, I store it directly with my data which the fragment can then use later when it loads the data into the adapter. This also makes it relatively simple to calculate the new position if data is added before or after the current position. Any data updates are then relatively straightforward. The fragment learns of the update through a listener, modifies the Adapter based on the changes, and sets the the correct adjusted position of the AdapterView.
Depending on how many changes are made to the adapter at one time, you might also want to use Adapter.setNotifyOnChange(false) when creating a new adapter and use Adapter.notifyDataSetChanged() after updates to the adapter. This prevents notifying the AdapterView the data has changed until after all changes are made.
Upvotes: 1
Reputation: 1556
Note
Solution written below is 'light solution' and only avoids ClassCastException
, it's not final yet. Fine tuning is certainly necessary. Since Java Reflection is used and field names are hardcoded it's possible to fail on platforms where names are changed or differently implemented.
I'll update this answer with more details as soon as I finish writting my application.
Solution
You have to override onRestoreInstanceState
in ListView
and Gallery
. In both of them you have to do proper conversion. In ListView
you convert given Parceable
to AbsListView$SavedState
and in Gallery
to AbsSpinner$SavedSate
.
VerticalList - Modified ListView
public class VerticalList extends ListView {
public VerticalList(Context context) {
super(context);
}
public VerticalList(Context context, AttributeSet attrs) {
super(context, attrs);
}
public VerticalList(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void onRestoreInstanceState(Parcelable state) {
super.onRestoreInstanceState(SavedStateConversion.getAbsListViewSavedState(state));
}
}
HorizontalList - Modified Gallery
public class HorizontalList extends Gallery {
public HorizontalList(Context context) {
super(context);
}
public HorizontalList(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HorizontalList(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void onRestoreInstanceState(Parcelable state) {
super.onRestoreInstanceState(SavedStateConversion.getAbsSpinnerSavedState(state));
}
}
SavedStateConversion - AbsListView$SavedState <-> AbsSpinner$SavedState. Conversion is done using Java Reflection.
public class SavedStateConversion {
private SavedStateConversion() {}
/**
* Converts <code>android.widget.AbsSpinner$SavedState</code> to <code>android.widget.AbsListView$SavedState</code>.
* @param state parcelable representing <code>android.widget.AbsSpinnerSavedState</code>
* @return parcelable representing <code>android.widget.AbsListView$SavedState</code>
*/
public static Parcelable getAbsListViewSavedState(Parcelable state) {
try {
Class<?> gss = Class.forName("android.widget.AbsSpinner$SavedState");
/*
* List of all fields in AbsSpinner$SavedState:
*
* int position;
* long selectedId;
*/
Field selectedIdField = gss.getDeclaredField("selectedId");
selectedIdField.setAccessible(true);
Field positionField = gss.getDeclaredField("position");
positionField.setAccessible(true);
Parcel parcel = Parcel.obtain();
parcel.writeLong(selectedIdField.getLong(state));
parcel.writeLong(0);
parcel.writeInt(0);
parcel.writeInt(positionField.getInt(state));
Class<?> lvss = Class.forName("android.widget.AbsListView$SavedState");
Constructor<?> constructors[] = lvss.getDeclaredConstructors();
Constructor<?> lvssConstructor = constructors[0];
lvssConstructor.setAccessible(true);
return (Parcelable) lvssConstructor.newInstance(parcel);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
throw new RuntimeException("Conversion from AbsSpinner$SavedState to AbsListView$SavedState failed!");
}
/**
* Converts <code>android.widget.AbsListView$SavedState</code> to <code>android.widget.AbsSpinner$SavedState</code>.
* @param state parcelable representing <code>android.widget.AbsListView$SavedState</code>
* @return parcelable representing <code>android.widget.AbsSpinner$SavedState</code>
*/
public static Parcelable getAbsSpinnerSavedState(Parcelable state) {
try {
Class<?> lvss = Class.forName("android.widget.AbsListView$SavedState");
/*
* List of all fields in AbsListView$SavedState:
*
* String filter;
* long firstId;
* int height;
* int position;
* long selectedId;
* int viewTop;
*/
Field selectedIdField = lvss.getDeclaredField("selectedId");
selectedIdField.setAccessible(true);
Field positionField = lvss.getDeclaredField("position");
positionField.setAccessible(true);
Parcel parcel = Parcel.obtain();
parcel.writeLong(selectedIdField.getLong(state));
parcel.writeInt(positionField.getInt(state));
Class<?> gss = Class.forName("android.widget.AbsSpinner$SavedState");
Constructor<?> constructors[] = gss.getDeclaredConstructors();
Constructor<?> gssConstructor = constructors[0];
gssConstructor.setAccessible(true);
return (Parcelable) gssConstructor.newInstance(parcel);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
throw new RuntimeException("Conversion from AbsListView$SavedState to AbsSpinner$SavedState failed!");
}
}
Upvotes: 0
Reputation: 86948
Rather than attempting to bind alternating views to one BaseAdapter, why not use two appropriate Adapters and simply pass the relevant information between the different views.
Something like this:
mListView.setPosition(mGallery.getFirstVisiblePosition());
and vica versa. You'll probably need to save this information in onPause() since I don't know if you can reference the first visible position of a View that is no longer visible.
Upvotes: 0
Reputation: 8477
That is a very interesting approach. Pre-fragments, I'd say you had your work cut out for you, because the code sort of naturally expects variant XML layout files to have the same types of controls with the same IDs. But, as you know, with a Fragment, all you have to do is say where it goes and link it to a class. I see no reason why you couldn't have different layouts (e.g., portrait and landscape) instantiate different Fragment classes.
Upvotes: 0