The vertical list in the landscape and horizontal list in the portrait within the same activity?

Desired Result

I want to have a vertical list with custom elements on the left or right of the screen in landscape mode and a horizontal list at the top / bottom of the screen in portrait mode. The Horizontal / Vertical list should be Fragment , so I can reuse it later for the smartphone version. The minimum version of the SDK is 13 (Android 3.2).

My attempt

My custom Activity has one custom LayersFragment and another View . In portrait mode, the fragment is aligned with the parent left. In landscape mode, aligned to the bottom of the parent.

LayersFragment also has a different layout for portrait and landscape mode. In portrait mode Gallery and in landscape mode ListView .

Since Gallery and ListView are subclasses of AdapterView<Adapter> , I use this parent class and BaseAdapter to populate items and listen to OnItemClicks .

PhotoEditorActivity Portrait modePhotoEditorActivity Landscape mode

Resource Information

frag_layers.xml - XML ​​layout for LayersFragment in the 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 - An XML layout for my custom Activity in portrait mode. The 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 user 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 get this ClassCastException when turning 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 turning from "Portrait in a Landscape" (Gallery → List)

 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 look for another solution?

My opinion

The problem occurs when the screen orientation changes. I believe the problem is the "default implementation" of ListView and Gallery . They will try to restore their SavedState to onRestoreInstanceState after changing orientation, but View has changed and a ClassCastException is thrown.

Thanks,

Aleksandar Ilyich

+4
source share
5 answers

Note

The solution described below is an “easy solution” and only avoids ClassCastException , but it is not final yet. Fine tuning is definitely necessary. Since Java Reflection is used and field names are hard-coded, this may fail on platforms where the names are changed or implemented differently.

I will update this answer in more detail as soon as I finish writing my application.

Decision

You must override onRestoreInstanceState in ListView and Gallery . In both of them you have to do the right conversion. In ListView you convert Parceable data to AbsListView$SavedState and to Gallery in AbsSpinner$SavedSate .

Vertical List - Changed 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)); } } 

Horizontal List - Changed 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!"); } } 
0
source

Assuming the fragment is not placed in the backstack and the fragment instance is not saved (mainly because I don’t know what effect it will have), onCreateView will be launched every time the orientation changes. Therefore, you can specify which layout to use based on your current orientation. It is also important to have different identifiers for ListView and Gallery.

Use getFirstVisiblePosition and setSelection to remember the current adapter position. This will only work reliably if the data positions in the adapter do not change, if the fragment is not in the resume state. If the data changes, you will have to recalculate the appropriate position to install the AdapterView.

frag_layers.xml . XML layout for layers. A fragment in the 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 have not tested this code, but it is a similar implementation that I use. For my implementation, delete the code in onResume and onPause, with the exception of super calls. During onPause, instead of saving the first visible position in the settings, I save it directly with my data, which the fragment can then use later when it loads the data into the adapter. It also makes it easier to calculate a new position if data is added before or after the current position. Any data updates are relatively simple. The fragment learns about the update through the listener, changes the Adapter based on the changes, and sets the correct adjusted position of the adapter.

Depending on how many changes were made to the adapter at a time, you can also use Adapter.setNotifyOnChange (false) when creating a new adapter and use Adapter.notifyDataSetChanged () after the adapter updates. This prevents the AdapterView from notifying that the data has been changed until all changes have been made.

+1
source

Instead of trying to bind view variables with one base adapter, why not use two suitable adapters and just pass the relevant information between different views.

Something like that:

 mListView.setPosition(mGallery.getFirstVisiblePosition()); 

and vica versa. You probably need to store this information in onPause (), since I don’t know if you can refer to the first visible position of the View, which is no longer visible.

0
source

This is a very interesting approach. Preliminary fragments, I would say that you had your work cut out for you, because the type of code naturally expects the variants of the XML layout files to have the same types of controls with the same identifiers. But, as you know, with a fragment, all you need to do is say where it goes and associate it with the class. I see no reason why you could not have different layouts (for example, portrait and landscape) to create instances of different classes of fragments.

0
source

if you use two lists that are different from each other (one with expandability), make sure that they have different identifiers in the xml layout.

0
source

All Articles