Posted at

ViewPagerに乗っているViewのスクロール位置を同期する

More than 3 years have passed since last update.


はじめに

何がしたいかというと、以下のようにViewPagerに乗っているViewのスクロール位置を合わせたい感じです。

ScrollSync


考えたこと、試したこと

この動作を実現するために考えたこと、試したことあれこれ。


  • 共通のスクロールリスナーとか共通の何かを持たせて、良い感じに同期させる


    • 図を書いていろいろ考えたりしたけど、だんだんよくわからなくなってきたので考えるのをやめた



  • SharedPreferencesにスクロール位置を保存して、ViewPagerのスクロール時に反映させる


    • 候補として出してはみたけど、これは無いなと思って実装を考える前にやめた



  • ViewPagerのスクロール毎にFragmentManagerからFragmentを取得して、同じResourceIdを元にViewを取得し、各Viewに現在表示しているViewのスクロール位置をセットする


    • とりあえずやりたいことを実現するためにやった

    • これは正直できると思ってなかったけど、頑張ってぬるぽと格闘した結果できた

    • 最近の端末であれば動作は問題無いが、毎回呼ばれるのは気持ち悪いので却下



  • 上記の動作をViewPager.OnPageChangeListenerのonPageScrollStateChangedに実装する


    • ページの変更時のみにしか呼び出されなくなったので、さっきよりは良くなった

    • これ以外に方法が思いつかなかったので、とりあえずこれで作ってみる




結論、どうやって実現するか

ViewPagerのスクロール状態が変わった時にFragmentMangerからFragmentを取得し、現在表示しているViewのスクロールをViewPagerに乗っている他のFragmentの同じResourceIdのViewにスクロール位置を反映させる。

まぁおそらく、言葉で言われてもあんまりよくわからないですよね。

なので、実際に実装してみます。


実装方法

スクロールを同期する処理は5番ですが、多分それだけ見てもあんまりわからないので、一応順番に書いていきます。


前提


  • ViewPagerに乗せるFragmentは全て同じFragmentが使われている

  • もしくは Fragmentに乗っているScrollView等のスクロールを同期させたいViewのResourceIdが同じ名前で統一されている


手順



  1. 必要なライブラリをbuild.gradleに書く


    build.gradle

    dependencies {
    
    compile "com.android.support:appcompat-v7:23.0.1"
    compile "com.android.support:support-v4:23.0.1"
    compile "com.android.support:recyclerview-v7:23.0.1"
    }




  2. 今回のサンプルではDataBindingを使っているので、これも書く


    build.gradle

    android {
    
    // 省略
    dataBinding {
    enabled = true
    }
    // 省略
    }




  3. ViewPagerに乗せるFragmentのレイアウトを作る


    fragment_test.xml

    <?xml version="1.0" encoding="utf-8"?>
    
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
    android:id="@+id/list_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    </ListView>
    </RelativeLayout>
    </layout>





  4. ViewPagerに乗せるFragmentを作る


    TestFragment.java

    public class TestFragment extends Fragment {
    
    private static final String KEY_LIST = "key_list";

    public static TestFragment newInstance(ArrayList<String> list) {
    TestFragment fragment = new TestFragment();
    Bundle args = new Bundle();
    args.putStringArrayList(KEY_LIST, list);
    fragment.setArguments(args);
    return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    FragmentTestBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_test, container, false);
    ArrayList<String> list = getArguments().getStringArrayList(KEY_LIST);
    if(list != null) {
    ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, list);
    binding.listView.setAdapter(arrayAdapter);
    }
    return binding.getRoot();
    }
    }





  5. FragmentPagerAdapterを継承した独自クラスを作成する


    ScrollFragmentPagerAdapter.java

    public class ScrollFragmentPagerAdapter extends FragmentPagerAdapter {
    
    ViewPager mViewPager;
    FragmentManager mFragmentManager;
    int mViewResId;
    /**
    * @param viewResId Fragmentに乗ってるスクロールを持ったViewのResourceId
    * */

    public ScrollSyncFragmentPagerAdapter(FragmentManager fm, ViewPager pager, int viewResId) {
    super(fm);
    mViewPager = pager;
    mFragmentManager = fm;
    mViewResId = viewResId;
    pager.addOnPageChangeListener(this);
    }
    /** 省略 */
    }




  6. ViewPager.OnPageChangeListenerを実装する


    ScrollFragmentPagerAdapter.java

    public class ScrollFragmentPagerAdapter extends FragmentPagerAdapter
    
    implements ViewPager.OnPageChangeListener {
    /** 省略 */
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    }

    @Override
    public void onPageSelected(int position) {
    }

    @Override
    public void onPageScrollStateChanged(int state) {
    }
    /** 省略 */
    }





  7. onPageScrollStateChangedにスクロールを同期するコードを実装する


    ScrollFragmentPagerAdapter.java

    @Override
    
    public void onPageScrollStateChanged(int state) {
    // ドラッグ以外は何もしない
    if (state != ViewPager.SCROLL_STATE_DRAGGING) {
    return;
    }
    try {
    int size = mFragmentManager.getFragments().size();
    // FragmentManagerに乗っているFragmentを取得
    List<Fragment> fragmentList = mFragmentManager.getFragments();
    // 現在表示しているFragmentのViewを取得
    View currentView = fragmentList.get(mViewPager.getCurrentItem()).getView();
    // ViewPager上のFragmentのViewのスクロール位置を1つずつ合わせる
    for (int i = size - 1; i >= mViewPager.getCurrentItem() - 1; i--) {
    if (i < 0) break;
    View rootView = fragmentList.get(i).getView();
    if (i != mViewPager.getCurrentItem() && fragmentList.get(i) != null &&
    rootView != null && currentView != null) {
    // ScrollViewの場合
    if (rootView.findViewById(mViewResId) instanceof ScrollView) {
    rootView.findViewById(mViewResId).setScrollY(currentView.findViewById(mViewResId).getScrollY());
    }
    // ListViewの場合
    else if (rootView.findViewById(mViewResId) instanceof ListView) {
    ListView listView = ((ListView) currentView.findViewById(mViewResId));
    int position = listView.getFirstVisiblePosition();
    int y = listView.getChildAt(0).getTop();
    ((ListView) rootView.findViewById(mViewResId)).setSelectionFromTop(position, y);
    }
    // RecyclerViewの場合(これスクロール位置がちょっとずれる)
    else if (rootView.findViewById(mViewResId) instanceof RecyclerView) {
    RecyclerView recyclerView = ((RecyclerView) currentView.findViewById(mViewResId));
    LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager();
    int position = manager.findLastCompletelyVisibleItemPosition();
    ((RecyclerView) rootView.findViewById(mViewResId)).scrollToPosition(position);
    }
    }
    }
    } catch (NullPointerException e) {
    Log.e("FragmentPagerAdapter", "Message: ", e);
    }
    }




  8. Activityのレイアウトを作成する


    activity_main.xml

    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    
    <RelativeLayout xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

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

    <android.support.v4.view.PagerTabStrip
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

    </android.support.v4.view.ViewPager>

    </RelativeLayout>
    </layout>





  9. Activityを作る


    MainActivity.java

    public class MainActivity extends AppCompatActivity {
    
    ScrollFragmentPagerAdapter adapter;
    ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    adapter = new ScrollFragmentPagerAdapter(getSupportFragmentManager(), binding.pager, R.id.list_view);
    binding.pager.setAdapter(adapter);
    }
    }




  10. いざ実行!

    すればできるはず。



問題点


  • サポートライブラリのバージョンが23.2.0だと上手くスクロールが同期されない


    • 理由はまだよく分かってない。

    • この記事書いている時にサンプルソース見直してて気づいた。つらい。



  • RecyclerViewだとスクロール位置が完全に合わない


    • スクロール系のメソッドを色々探してみたけど、それっぽいのが見つけられなかった

    • 自分でクラス継承してごにょごにょしないとダメかも



  • FragmentManagerからFragment取得して、そのあとView取得して、1つ1つスクロール位置合わせて…っていう実装がなんか微妙


    • なんかもっと良い方法があるような気がする




まとめ

やりたいことは実現できましたが、まだまだ改善の余地があるなぁと思いました。

こんな良い方法があるよ!とか良いライブラリがあるよ!とか知っている方がいらっしゃいましたら、教えていただけると嬉しいです。

今回のソースは以下のGitHubに公開していますので、興味があれば見てみてください。


syarihu/ScrollSyncSample

https://github.com/syarihu/ScrollSyncSample


以上、おつかれさまでした。