вторник, 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!

неделя, 30 ноември 2014 г.

Decorating Android views

In this post I will tell you of my struggle to decorate Android views. A battle that lasted for more than two weeks and which, I dare to say, I was finally able to win! I am really proud of it - now I can add custom behavior to my views, while retaining the reusability, code separation, readability and other beautiful properties :)

When did it started? - some while ago when I realised I was faced with a messy code that was adding few features over ordinary ViewPager. I had inheritance, some methods were made static so that they could be called from inadequate places and most code was centralised in one ubiquitous master-of-all-gods class. However, for me it was obvious that we just wanted to add few decorations to the standard ViewPager exactly as promoted by the Decorator design pattern defined in the book of the Gang of four. I, thought, this should be easy. And, furthermore, I wanted to apply this to ViewPager, but from the very beginning it was obvious to me that this would be only my POC - the same strategy should be applicable to all other Android views I feel like decorating.

I am not going to give you the correct solution straight away, rather I will guide you through all my failures until we get there. I will do that as I believe there are some wisdoms to be learned on the long way. Still, if you do not feel like reading too much, feel free to browse down to "The working solution".

Where do we start at?

I have created a special project in Google Code to more easily demonstrate what I am speaking about. It is accessible here - a git repository in which  I will be marking with tags the different stages of interest. The first such is the "base_version" tag - in it we have very simple app showing few pages of text and extended ViewPager that notifies its users when they get out of the pager boundries (try to navigate before the first item or after the last). Note the NotifyOutOfBoundsViewPager, taken from a StackOverflow post.:

public class NotifyOutOfBoundsViewPager extends ViewPager {
    public interface OnSwipeOutListener {
        public void onSwipeOutAtStart();

        public void onSwipeOutAtEnd();
    }

    private float mStartDragX;
    private OnSwipeOutListener onSwipeOutListener;

    public NotifyOutOfBoundsViewPager(Context context) {
        super(context);
    }

    public NotifyOutOfBoundsViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mStartDragX = x;
            break;
        case MotionEvent.ACTION_MOVE:
            if (mStartDragX < x && getCurrentItem() == 0) {
                onSwipeOutListener.onSwipeOutAtStart();
            } else if (mStartDragX > x && getCurrentItem() == getAdapter().getCount() - 1) {
                onSwipeOutListener.onSwipeOutAtEnd();
            }
            break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    public void setOnSwipeOutListener(OnSwipeOutListener listener) {
        this.onSwipeOutListener = listener;
    }
}
This is cool, but now I wanted to add also a timer that will not allow me to read too long this short text. Basically I want a count down that will take some action when the time expires. I really could have added this logic with yet one more extension of the `NotifyOutOfBoundsViewPager` but now it gets even more weird - it turns out i can not get timer in another situation without getting the other extenstion - the notification for out of bounds. I really am felling that we should try to add these two: the notification and the timer as decorations to the view pager so that we can reuse them in other situations too.

Defining the decorator

The first step was obvious to me. I had to define an extension of ViewPager that would support one more parameter - the embedded decoration. Here is how it goes:
public class DecoratedViewPager extends ViewPager {
    private ViewPager viewPager;

    public DecoratedViewPager(Context context, ViewPager viewPager) {
        super(context);
        this.viewPager = viewPager;
    }

    public DecoratedViewPager(Context context, AttributeSet attrs, ViewPager viewPager) {
        super(context, attrs);
        this.viewPager = viewPager;
    }

    @Override
    public void addFocusables(ArrayList arg0, int arg1, int arg2) {
        viewPager.addFocusables(arg0, arg1, arg2);
    }

    @Override
    public void addTouchables(ArrayList arg0) {
        viewPager.addTouchables(arg0);
    }
...2600+ lines more of the same like
How I have reached to this code:

  1.  I am using Eclipse ADT.
  2. After I defined the constructors and the setter, I went on the bottom of the class ALT + SHIFT + V (override methods), selected them all and then created the needed overrides.
  3. After that I did simple string replacement of `super.` with `viewPager.` and I got the result above. 
Now, however I was faced with a lot of errors of the kind:


This was logical - I am not able to decorate protected methods. So I went over and deleted all the protected methods (I did not know how to do that automatically so it was quite frustrating a work). Then I did one more change visible in the following version of the decorated pager:

public class DecoratedViewPager extends ViewPager {
    private ViewPager viewPager;

    public DecoratedViewPager(Context context, ViewPager viewPager) {
        super(context);
        this.viewPager = viewPager;
    }

    public DecoratedViewPager(Context context, AttributeSet attrs, ViewPager viewPager) {
        super(context, attrs);
        this.viewPager = viewPager;
    }

    @Override
    public void addFocusables(ArrayList arg0, int arg1, int arg2) {
        if (viewPager != null) {
            viewPager.addFocusables(arg0, arg1, arg2);
        } else {
            super.addFocusables(arg0, arg1, arg2);
        }
    }

    @Override
    public boolean arrowScroll(int arg0) {
        return (viewPager != null) ? viewPager.arrowScroll(arg0) : super.arrowScroll(arg0);
    }
    
Here, I reminded myself that the decorated `viewPager` will be null while the super constructor runs (actually I did not know by the time, I found out about it later on, but I add the change now for completeness of the code), so I needed to add these `!= null` checks. Thankfully I was able to automate this last change using Ruby, so I got my class consisting of 3100 lines+ automatically. Now I change the definition of the notify out of bounds pager to be using this decoration and we get to the second tag in the repository "decorator_defined".

Hooking up the decorator

Note that as a result of the previous section we still have not completed the refactoring of the application we develop - we have defined the decorator, but we have not actually started using it in our application. What I needed was a way to construct the decorated view pager at runtime (because some configurations, e.g. the listeneres were done at runtime) and then place it in the layout. Actually I was not entirely clear how to do that, so I posted a question in StackOverflow and soon a suggestion came that looked promising: I had to use `LayoutInflater.setFactory` which would allow me to place runtime constructed instances instead of layout-defined. Nice :)
So I went there and added the following in my fragment:
    private class ViewPagerFactory implements Factory {
        private static final String VIEW_PAGER = "android.support.v4.view.ViewPager";

        private Factory mBaseFactory;

        public ViewPagerFactory(Factory factory) {
            this.mBaseFactory = factory;
        }

        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            if (name.equals(VIEW_PAGER)) {
                ViewPager viewPager = new ViewPager(context, attrs);
                return new NotifyOutOfBoundsViewPager(context, attrs, viewPager);
            } else {
                if (mBaseFactory != null) {
                    return mBaseFactory.onCreateView(name, context, attrs);
                } else {
                    return null;
                }
            }
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        inflater.setFactory(new ViewPagerFactory(this.getActivity()));
        return inflater.inflate(R.layout.sample_pager_fragment, container, false);
    }
Here I define a custom factory, that replaces only the ViewPagers with my custom, runtime-constructed instance. It looks fine, but when I ran the application with these changes I got the following error:
"java.lang.IllegalStateException: A factory has already been set on this LayoutInflater" and my application crashed. Short search and I found a post explaining both the cause of the problem and the "solution". Well, after reading the explanation, I was clear that I might be causing some issues with the compatibility replacing the support layout inflator factory with my own, but I just wanted to see what happens. So my method became:
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        try {
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
            field.setAccessible(true);
            field.setBoolean(inflater, false);
            inflater.setFactory(new ViewPagerFactory(this.getActivity()));
        } catch (IllegalAccessException e) {
            Log.e(LOG_TAG, "Error while tweaking the inflator factory");
            throw new RuntimeException(e);
        } catch (IllegalArgumentException e) {
            Log.e(LOG_TAG, "Error while tweaking the inflator factory");
            throw new RuntimeException(e);
        } catch (NoSuchFieldException e) {
            Log.e(LOG_TAG, "Error while tweaking the inflator factory");
            throw new RuntimeException(e);
        }
        return inflater.inflate(R.layout.sample_pager_fragment, container, false);
    }
The code including this change can be seen in tag "decorator_integrated_v1". Finally! Yes! I was able to run the application without any further immediate crashes. Yes, but... nothing was loaded in the pager itself. Why? Well, I came to the conclusion quite fast - I am setting the adapter to the decorator, but it delegates to the widgets it wraps. Thus the adapter is actually set to a ViewPager, that is not the one placed in the layout. Oh gosh, how confusing! I thought that things might get fixed if I make all the decorated setters set in both instances (wrap and current instance) or only in the current instance. None of those worked and I got even more confused. I was already feeling that I was building house of cards, placing the most narrow level at the bottom:

  • the data was not placed in the correct view pager
  • there was the problem with nulls of the decorated viewPager while constructing the decorator
  • I had to give up on decorating the protected methods earlier already
  • I was not sure that my solution would combine with the support library at all.
  • I had a class with thousands of lines!
Thus, it was the time to confess to myself that this might not be the best approach to it all.

The working solution

So I reflected again and finally I came up with the following conclusion: It is wrong to try to construct full-blown Android views for the purpose of decoration. If I did I came to be with just too many methods purposed for the same thing (say setAdapter) and I was also faced with the need to replace runtime constructed widget in the layout. Rather I decided that I need to only add the decorations at runtime to a widget defined in the layout. How did I do that?
public class DecoratedViewPager extends ViewPager {
    private ViewPagerDecoration decoration;

    public DecoratedViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public DecoratedViewPager(Context context) {
        super(context);
    }

    public void setDecoration(ViewPagerDecoration decoration) {
        this.decoration = decoration;
        decoration.setViewPager(this);
        decoration.decorateConstructor(this);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (decoration != null) {
            decoration.decorateOnInterceptTouchEvent(event);
        }
        return super.onInterceptTouchEvent(event);
    }
You see how beautiful it gets - I define the custom view `DecoratedViewPager` that possibly aggregates `ViewPagerDecoration`. It will define decorations of the pager api (like we do with `decoration.decorateOnInterceptTouchEvent`). Here is how the definition of this method looks like in the decoration:
    private ViewPagerDecoration viewPagerDecoration;
    /**
     * Adds logic to the on intercept touch event of the view pager.
     * 
     * @param event the event that was intercepted.
     */
    public void decorateOnInterceptTouchEvent(MotionEvent event) {
        if (viewPagerDecoration != null) {
            viewPagerDecoration.decorateOnInterceptTouchEvent(event);
        }
    }
The cool thing about this approach is that in such way we can easily decorate even protected methods of the view pager and we need to define methods in the decoration and delegations in the decorator only when the need comes (thus no more thousand lines classes). Last, but not least, I also provide a cool way to decorate even the listeners we set for the decorated pager:
    /** Decoration for {@link OnPageChangeListener} that can be extended in the further extending classes. */
    protected class DecoratedOnPageChangeListener implements OnPageChangeListener {
        private OnPageChangeListener pageChangeListener;

        public DecoratedOnPageChangeListener(OnPageChangeListener pageChangeListener) {
            this.pageChangeListener = pageChangeListener;
        }

        @Override
        public void onPageSelected(int position) {
            if (pageChangeListener != null) {
                pageChangeListener.onPageSelected(position);
            }
        }

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            if (pageChangeListener != null) {
                pageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
            }
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            if (pageChangeListener != null) {
                pageChangeListener.onPageScrollStateChanged(state);
            }
        }
    }

    /**
     * Decorates the given listener
     * 
     * @param listener The on page change listener to decorate.
     * @return The decorate on page listener
     */
    public OnPageChangeListener decorateOnPageChangeListener(OnPageChangeListener listener) {
        if (viewPagerDecoration != null) {
            return viewPagerDecoration.decorateOnPageChangeListener(listener);
        } else {
            return listener;
        }
    }
With the method in `ViewPagerDecorationTimer` being defined as follows:
    private class TimerViewOnPageChangeListener extends DecoratedOnPageChangeListener {

        public TimerViewOnPageChangeListener(OnPageChangeListener pageChangeListener) {
            super(pageChangeListener);
        }

        @Override
        public void onPageSelected(int position) {
            updateTimeRemainingUI(timer.getRemainingMillis());
            super.onPageSelected(position);
        }
    }

    @Override
    public OnPageChangeListener decorateOnPageChangeListener(OnPageChangeListener listener) {
        return super.decorateOnPageChangeListener(new TimerViewOnPageChangeListener(listener));
    }
With such approach we can seamlessly apply multiple decorations even to the listeners we set for the view pager. There is a tag "working_solution" in the repository with this solution shown working. Check it out and you will see how I imagine decoartion of Android views should work. Oh, and did I mentioned that I also provide additional method in my decorated view pager for the so-longed-for method `getCurrentView`?

Hopefully this post would help you write your android application with better architecture. I, personally, do not see a reason why all extensions to views would not happen as decorations, as, good practise advises us, not to put any business logic in them.