Aleksandar Ilic
Aleksandar Ilic

Reputation: 1556

Vertical list in landscape and horizontal list in portrait within the same Activity?

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.

PhotoEditorActivity Portrait mode PhotoEditorActivity Landscape mode

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

Answers (5)

Mikey
Mikey

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

mindriot
mindriot

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

Aleksandar Ilic
Aleksandar Ilic

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

Sam
Sam

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

Sparky
Sparky

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

Related Questions