вторник, 2 декември 2014 г.

The missing ViewPager getCurrentView

While working on my decoration of Android views I came to the point where I was in  need to ge the currently visualized view (page) in ordinary ViewPager. It turned out this was not as simple as I would hope - there were many posts in the internet asking for such simple API function: calls without any sensible answer (I am citing the top questions only from Stackoverflow, though there also other places that ask for the same functionality).

 - Post number 1
 - Post number 2
 - Post number 3
 - Post number 4

It turned out that I needed the same method in order to finish my decorated ViewPager. I also wanted to avoid the coupling between adapter and the pager itself when I implemented this method and all currently proposed solutions involved such to one extent or the other. It seems I was able to achieve what I was aiming at.

The working solution

How did I solve it? I basically build over my decorator solution, so be sure to read about it before you can fully understand my approach. You can also see the source of this solution here (the code was built to demonstrate the decorator, but the getCurrentView is also featured in it).

What I did: I wrapped the adapters being set to my decorated view pager with a little extension letting me keep track of the current view. This extension is placed transparantely to the user of the decorated view pager:
    @Override
    public void setAdapter(PagerAdapter adapter) {
        super.setAdapter(new DecoratedPagerAdapter(this, adapter));
    }

    @Override
    public PagerAdapter getAdapter() {
        DecoratedPagerAdapter adapter = getDecoratedPagerAdapter();
        return adapter.getPagerAdapter();
    }
And now the simple implementation of the `DecoratedPagerAdapter`:
class DecoratedPagerAdapter extends PagerAdapter {
    private PagerAdapter pagerAdapter;
    /** Stores the views in the actual position of the adapter. */
    private SparseArray instantiatedViewsSparsedArray;
    private ViewPager viewPager;

    public DecoratedPagerAdapter(ViewPager viewPager, PagerAdapter decoratedPagerAdapter) {
        this.pagerAdapter = decoratedPagerAdapter;
        decoratedPagerAdapter.registerDataSetObserver(new DataSetObserver() {
            @Override
            public void onChanged() {
                notifyDataSetChanged();
            }
        });
        this.viewPager = viewPager;
        this.instantiatedViewsSparsedArray = new SparseArray();
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Object object = pagerAdapter.instantiateItem(container, position);
        ViewPager viewPager = (ViewPager) container;
        int childCount = viewPager.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = viewPager.getChildAt(i);
            if (isViewFromObject(child, object)) {
                instantiatedViewsSparsedArray.put(position, child);
                break;
            }
        }
        return object;
    }

    @Override
    public int getItemPosition(Object object) {
        int childCount = viewPager.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = viewPager.getChildAt(i);
            if (isViewFromObject(child, object)) {
                return i;
            }
        }
        return POSITION_NONE;
    }

    /**
     * @param position the position in the adapter of the view we are interested in.
     * @return The view at position {@code position} in the adapter or null if it has not been initialized.
     */
    View getViewAtPosition(int position) {
        return instantiatedViewsSparsedArray.get(position);
    }

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

    @Override
    public boolean isViewFromObject(View arg0, Object arg1) {
        return pagerAdapter.isViewFromObject(arg0, arg1);
    }

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

    /**
     * The method is deliberately package-protected.
     * 
     * @return The pager adapter that is wrapped with current view tracking pager adapter.
     */
    PagerAdapter getPagerAdapter() {
        return pagerAdapter;
    }
}
I basically cache the views loaded in the view pager in a sparsed array. Item is inserted when it is intitialized by the lazy-loading view pager and is removed from the cache immediately after it is removed from the lazy loading-view pager. I believe this implementation should be optimal even for view pagers with windows of the size of dozens. Finally hte base decoration class (in my case ViewPagerDecoration) defines the following method:
    /** Returns the index'th child of the decorated {@link ViewPager} */
    protected View getCurrentView() {
        DecoratedPagerAdapter pagerAdapter = viewPager.getDecoratedPagerAdapter();
        return pagerAdapter.getViewAtPosition(getCurrentItem());
    }
In the source I link to the method is defined as protected and with decorator solution it seems logical to me that this current view would be needed only in extensions. Still, if it is not your case I believe you need small code change to make the method available in the DecoratedViewPager instead. I hope you enjoy this solution and we finally get a fully documented solution of this common question in the Android world. Enjoy!

Няма коментари:

Публикуване на коментар