12

Is it possible to bind a handler for setOnPageChangeListener to a ViewPager in XML file with the Android Binding functionality?

The demos show onClick events but I am curious as to how much event functionality I can implement with it. Any links on the capabilities of Data Binding would be great as well. Thanks.

Hypothetical example:

example_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>
   <variable name="handlers" type="com.example.Handlers"/>
</data>

<android.support.v4.view.ViewPager
    android:id="@+id/pager"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"

    android:onPageChangeListener="@{handlers.pageChanged}" />
</layout>

Handler.java

package com.example.viewmodels;

import android.view.View;

public class Handlers {
    public void pageChanged(View view){}
}

The compilation error is:

Error:(62) No resource identifier found for attribute 'onPageChangeListener' in package 'android'

running-codebase
  • 998
  • 2
  • 12
  • 17

3 Answers3

11

It is possible to do this. You need to implement a custom binding adapter because there is no BindingAdapter classes predefined for View classes from Android support libraries.

For how to implement the custom adapter you may read this.

The code should be something like the below, I haven't tested them thoroughly:

<android.support.v4.view.ViewPager
    android:id="@+id/pager"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:onPageChangeListener="@{handlers}" />

Your BindingAapter code:

@BindingAdapter("onPageChangeListener")
public static void setOnPageChangeListener(ViewPager viewPager, ViewPager.OnPageChangeListener listener) {
    Log.i("setOnPageChangeListener");

    // clear listeners first avoid adding duplicate listener upon calling notify update related code
    viewPager.clearOnPageChangeListeners();
    viewPager.addOnPageChangeListener(listener);
}

P.S. Your handler class being passed should implement ViewPager.OnPageChangeListener.

chubao
  • 5,871
  • 6
  • 39
  • 64
  • 2
    Not good solution. Because ViewPager adds OnPageChangeListener to internal ArrayList, calling notifyChange() for this property will make it to add several listeners. – b1n0m Jun 08 '17 at 08:39
  • @b1n0m updated to remove listeners first before adding listener – chubao Jun 08 '17 at 08:42
  • @David Cheung my listener is returning null when I debug the app. Any idea why it might be happening? – Siddhivinayak Apr 17 '18 at 08:35
  • @DavidCheung if I have a `PageIndicator.setPager(viewpager)`. How can I bind `viewpager` to the `indicator` – mr.icetea Jun 01 '18 at 09:41
  • @David Cheung, I also have a problem because when i add this code circle page indicator doesn't show current page. How to connect it with viewpager? – Zoran Dec 08 '19 at 23:42
11

Here is ultimate solution. Just put this class in your project.

@BindingMethods({
    @BindingMethod(type = ViewPager.class, attribute = "android:offscreenPageLimit", method = "setOffscreenPageLimit"),
    @BindingMethod(type = ViewPager.class, attribute = "android:adapter", method = "setAdapter"),
    @BindingMethod(type = ViewPager.class, attribute = "android:currentPage", method = "setCurrentItem"),
})
public final class ViewPagerBindingAdapter {

    @InverseBindingAdapter(attribute = "android:currentPage", event = "android:currentPageAttrChanged")
    public static int getCurrentPage(@NonNull final ViewPager pager) {
        return pager.getCurrentItem();
    }

    @BindingAdapter(value = {"android:onPageScrolled", "android:onPageSelected", "android:onPageScrollStateChanged",
        "android:currentPageAttrChanged"}, requireAll = false)
    public static void onSetAdapter(@NonNull final ViewPager pager, final OnPageScrolled scrolled, final OnPageSelected selected,
        final OnPageScrollStateChanged scrollStateChanged, final InverseBindingListener currentPageAttrChanged) {

        final ViewPager.OnPageChangeListener newValue;
        if (scrolled == null && selected == null && scrollStateChanged == null && currentPageAttrChanged == null) {
            newValue = null;
        } else {
            newValue = new ViewPager.OnPageChangeListener() {
                @Override
                public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
                    if (scrolled != null) {
                        scrolled.onPageScrolled(position, positionOffset, positionOffsetPixels);
                    }
                }

                @Override
                public void onPageSelected(final int position) {
                    if (selected != null) {
                        selected.onPageSelected(position);
                    }
                    if (currentPageAttrChanged != null) {
                        currentPageAttrChanged.onChange();
                    }
                }

                @Override
                public void onPageScrollStateChanged(final int state) {
                    if (scrollStateChanged != null) {
                        scrollStateChanged.onPageScrollStateChanged(state);
                    }
                }
            };
        }
        final ViewPager.OnPageChangeListener oldValue = ListenerUtil.trackListener(pager, newValue, R.id.page_change_listener);
        if (oldValue != null) {
            pager.removeOnPageChangeListener(oldValue);
        }
        if (newValue != null) {
            pager.addOnPageChangeListener(newValue);
        }
    }

    public interface OnPageScrolled {
        void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
    }

    public interface OnPageSelected {
        void onPageSelected(int position);
    }

    public interface OnPageScrollStateChanged {
        void onPageScrollStateChanged(int state);
    }

    private ViewPagerBindingAdapter() {
        throw new UnsupportedOperationException();
    }
}

Also add id resource in your resources.

<resources>
    <item name="page_change_listener" type="id"/>
</resources>

Then you will be able to use it in xml like:

<android.support.v4.view.ViewPager
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:currentPage="@={viewModel.currentPage}"
                android:offscreenPageLimit="@{viewModel.offscreenPageLimit}"
                android:onPageSelected="@{currentPage -> viewModel.pageSelected(currentPage)}"
                android:adapter="@{adapter}"/>

As you can see, currentPage has inverse binding, so your viewModel will be able to set current page and also get current page if user swipes.

b1n0m
  • 283
  • 3
  • 10
  • are these setting able to update fragment in viewpager dynamically? in realtime? – Azizi Musa Oct 12 '17 at 07:34
  • @azizi-musa, you may set `viewModel.currentPage` value to select another page. If you want to change the fragments in `ViewPager`, you should do this in `PagerAdapter`. – b1n0m Nov 08 '17 at 11:29
  • What's the purpose of the `ListenerUtil.trackListener` stuff, along with adding and removing the listener? – Big McLargeHuge Mar 05 '20 at 21:58
  • @BigMcLargeHuge If someone calls `invalidateAll()` or `notifyChange()`, it would try to add another listener. So it is used to exclude situations when it can accidentally add several listeners. – b1n0m Mar 27 '20 at 12:18
2

Instead of OnPageChangeListener, you can use SimpleOnPageChangeListener and only override the method you're interested in:

@BindingAdapter("onPageChanged")
@JvmStatic
fun addPageChangedListener(viewPager: ViewPager, listener: OnPageChanged) {
    viewPager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
        override fun onPageSelected(position: Int) {
            listener.onPageChanged(position)
        }
    })
}

interface OnPageChanged {
    fun onPageChanged(position: Int)
}

Layout:

<androidx.viewpager.widget.ViewPager
    app:onPageChanged="@{viewModel::handlePageChanged}">

Handler:

fun handlePageChanged(position: Int) {
    println("Page changed: $position")
}

As others have pointed out, you may want to be careful not to attach duplicate listeners.

Big McLargeHuge
  • 14,841
  • 10
  • 80
  • 108