home.


Tagged: android-viewpager


Android: Saving ViewPager layout state without fragments

If you want to save the state of your Views in a ViewPager you have to do the follow:

  1. Save the layout state of all your pager’s views 1) on onSaveInstanceState() and 2) when an item is destroyed in destroyItem()
  2. Restore those in onRestoreInstanceState() and save them to a field
  3. In instatiateItem() reference the above to restore the View’s state

Here’s an example implementation

package com.example.user.myapplication;

import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * Saves the ViewPager's layout states.
 * 
 * In the adapter instantiateItem() method, you must call restoreViewState(). 
 * And in the adapter destroyItem() method, you must call saveViewState()
 */
public class SavingViewStateViewPager extends ViewPager {
    public static final int TAG_RESTORE_VIEWPAGER_STATE_POS = "TAG_RESTORE_VIEWPAGER_STATE_POS".hashCode();
    public static final String BUNDLE_VIEW_PAGER_POS = "pos";
    public static final String BUNDLE_VIEW_STATE = "viewstate";
    public static final String BUNDLE_PAGER_VIEW_STATE = "pagerviewstate";
    public static final String BUNDLE_CHILDREN_VIEW_STATES = "childrenviewstates";
    private HashMap<Integer, SparseArray<Parcelable>> mSavedViewStates = new HashMap();
    public SavingViewStateViewPager(Context context) {
        super(context);
    }
    public SavingViewStateViewPager(Context context, AttributeSet attrs) { super(context, attrs); }

    private void setViewPositionTag(View v, int position) {
        v.setTag(TAG_RESTORE_VIEWPAGER_STATE_POS, position);
    }

    private int getViewPositionFromTag(View v) {
        Object tag = v.getTag(TAG_RESTORE_VIEWPAGER_STATE_POS);
        return Integer.valueOf(tag.toString());
    }

    /**
     * Called when you init your View for your ViewPager.
     *
     * Either restores state if we have any, or gives the view a positional number TAG so
     * when we restore all the adapters view, we know which ones to put where.
     * @param position
     * @param view
     */
    public void restoreViewState(int position, View view) {
        SparseArray<Parcelable> state = mSavedViewStates.get(position);
        if(state==null) {
            Log.d(SavingViewStateViewPager.class.getSimpleName(), "State for view restore was null");
        } else {
            try {
                view.restoreHierarchyState(state);
            } catch(Exception e) {
                Log.d(SavingViewStateViewPager.class.getSimpleName(), "Unable to restore view: " + e.getMessage());
            }                
        }
        setViewPositionTag(view, position);
    }

    /**
     * Called when the ViewPager's adapter destroys the view -- we'll want to restore its state.
     * @param position
     * @param view
     */
    public void saveViewState(int position, View view) {
        SparseArray<Parcelable> hs = new SparseArray<>();
        view.saveHierarchyState(hs);
        mSavedViewStates.put(position, hs);
    }

    /**
     * Iterates through all the views in the ViewPager,
     * grabs their positional tag if exists
     * and then stores their state in a bundle to be retrieved in onRestoreInstanceState().
     * @return
     */
    @Override
    public Parcelable onSaveInstanceState() {
        Bundle b = new Bundle();
        b.putParcelable(BUNDLE_PAGER_VIEW_STATE, super.onSaveInstanceState());

        final ArrayList<Parcelable> viewStates = new ArrayList<>();
        for (int i = 0; i < getChildCount(); i++) {
            final Bundle bundle = new Bundle();
            View v = getChildAt(i);
            Object tag = v.getTag(TAG_RESTORE_VIEWPAGER_STATE_POS);
            if(tag!=null) {
                try {
                    bundle.putInt(BUNDLE_VIEW_PAGER_POS, getViewPositionFromTag(v));
                    SparseArray<Parcelable> hierarchyState = new SparseArray<>();
                    v.saveHierarchyState(hierarchyState);
                    bundle.putSparseParcelableArray(BUNDLE_VIEW_STATE, hierarchyState);
                    viewStates.add(bundle);
                } catch (Exception e) {
                    Log.d(SavingViewStateViewPager.class.getSimpleName(), "View probably didn't have a proper positional tag", e);
                }
            }
        }
        b.putParcelableArrayList(BUNDLE_CHILDREN_VIEW_STATES, viewStates);

        return b;
    }

    /**
     * From the Parcelable, grabs the view states, by position, and
     * sets the mSavedViewStates -- so restoreViewState() has something to use.
     * @param state
     */
    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if(!(state instanceof Bundle)) {
            super.onRestoreInstanceState(state);
        } else {
            Bundle bundle = (Bundle) state;
            super.onRestoreInstanceState(bundle.getParcelable(BUNDLE_PAGER_VIEW_STATE));
            ArrayList<Parcelable> viewStates = bundle.getParcelableArrayList(BUNDLE_CHILDREN_VIEW_STATES);
            if(viewStates==null) {
                Log.d(SavingViewStateViewPager.class.getSimpleName(), "No view states to restore");
                return;
            }
            for (int i = 0; i < viewStates.size(); i++) {
                Bundle stateForChild = (Bundle) viewStates.get(i);
                final SparseArray<Parcelable> childState = stateForChild.getSparseParcelableArray(BUNDLE_VIEW_STATE);
                mSavedViewStates.put(stateForChild.getInt(BUNDLE_VIEW_PAGER_POS), childState);
            }

        }
    }
}
android android-viewpager

Android: Databinding and ViewPagers

Once you have a data binding, you will want to use it with a ViewPager eventually.

The ViewPager inflates layouts. But if you want to inflate layouts with the <data></data> section under a <layout> tag, you must change your layout inflation code.

In instantiateItem():

YourLayout layout = DataBindingUtil.inflate(LayoutInflater.from(MainActivity.this), "R.layout.your_layout", container, false);
// Set your databinding up here
container.addView(layout.getRoot());
return layout.getRoot();

The getRoot() method is there because data binding won’t return the actual layout, but the binding. But getRoot() will give you the layout.

If you have dynamic layouts, i.e. it’s not always “R.layout.your_layout”, then you won’t always return YourLayout. You can instead return a ViewDataBinding.

But the problem with the above is now you can’t bind variables to that Binding, instead you’ll have to cast it to the appropriate generated class:

if(layouts[position]==R.layout.some_layout) {
  SomeLayoutBinding slb = DataBindingUtil.bind(db.getRoot());
  slb.setThing("Hoooo!");
}
android android-databinding android-viewpager

Android Design Library: TabLayout, ViewPager and title icons

The TabLayout’s XML is simple enough:

<android.support.design.widget.TabLayout
    android:id="@+id/tabLayout"
    app:tabMode="fixed"
    app:tabGravity="fill"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
</android.support.design.widget.TabLayout>

In this case, we use fixed and fill to indicate the tabs should take up all the width. If you’ve got a load you can use scrollable with tabMode.

Next we can programmatically set the tabs and their icons:

TabLayout mTabLayout = (TabLayout) findViewById(R.id.tabLayout);
mTabLayout.addTab(mTabLayout.newTab().setText("Hiya").setIcon(android.R.drawable.ic_secure));
mTabLayout.addTab(mTabLayout.newTab().setText("Again Sup").setIcon(android.R.drawable.ic_secure));
mTabLayout.addTab(mTabLayout.newTab().setText("Yeah").setIcon(android.R.drawable.ic_secure));
mTabLayout.addTab(mTabLayout.newTab().setText("Sup man").setIcon(android.R.drawable.ic_secure));
mTabLayout.addTab(mTabLayout.newTab().setText("I'm just saying stuff innit").setIcon(android.R.drawable.ic_btn_speak_now));

But if we have a ViewPager, as in the last tutorial, we can use that, and the titles you specified in such, to control the TabLayout instead:

mTabLayout.setupWithViewPager(mViewPager);

The problem now is that the ViewPager’s titles include no icons. To resolve this, in the ViewPager adapter, we can now return, not a string from getPageTitle, but a Spannable:

@Override
public CharSequence getPageTitle(int position) {
    Drawable image = ContextCompat.getDrawable(MainActivity.this, images[position]);
    image.setBounds(0, 0, image.getIntrinsicWidth(), image.getIntrinsicHeight());
    SpannableString sb = new SpannableString(" " + titles[position]);
    ImageSpan imageSpan = new ImageSpan(image);
    sb.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    return sb;
}

For the above to work you need to disable textAllCaps on the tabTextAppearance property. This means you need a theme property on the TabLayout XML that points to a style.

The above, including many other styles you can use, is below:

<style name="MyCustomTabLayout" parent="Widget.Design.TabLayout">
  <item name="tabMaxWidth">@dimen/tab_max_width</item>
  <item name="tabIndicatorColor">?attr/colorAccent</item>
  <item name="tabIndicatorHeight">2dp</item>
  <item name="tabPaddingStart">12dp</item>
  <item name="tabPaddingEnd">12dp</item>
  <item name="tabBackground">?attr/selectableItemBackground</item>
  <item name="tabTextAppearance">@style/MyCustomTabTextAppearance</item>
  <item name="tabSelectedTextColor">?android:textColorPrimary</item>
</style>
<style name="MyCustomTabTextAppearance" parent="TextAppearance.Design.Tab">
  <item name="android:textSize">14sp</item>
  <item name="android:textColor">?android:textColorSecondary</item>
  <item name="textAllCaps">false</item>
</style>
android android-tablayout android-viewpager

Android: Simple ViewPager with Views

You can quickly and easily use a ViewPager with views–instead of fragments–by first setting its XML:

<android.support.v4.view.ViewPager
    android:id="@+id/viewpager"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</android.support.v4.view.ViewPager>

And then setting its adapter.

ViewPager mViewPager = (ViewPager) findViewById(R.id.viewpager);
mViewPager.setAdapter(new PagerAdapter() {
    String[] titles = {"Eins", "Zwei", "Drei"};
    int[] layouts = {R.layout.layout1, R.layout.layout2, R.layout.layout3};

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        LayoutInflater inflater = LayoutInflater.from(MainActivity.this);
        ViewGroup layout = (ViewGroup) inflater.inflate(layouts[position], container, false);
        container.addView(layout);
        return layout;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View)object);
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return titles[position];
    }

    @Override
    public int getCount() {
        return layouts.length;
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }
});

Note, we’re setting the layouts as IDs, and initialising them in the initantiateItem method.

We include titles, but they won’t show on their own. We’ll use them with a TabLayout next.

android android-viewpager

Page 1 of 1