60
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

リクルートライフスタイルAdvent Calendar 2016

Day 4

Fragmentで自身のPager内での表示状態を監視する

Last updated at Posted at 2016-12-03

この記事は リクルートライフスタイル Advent Calendar 2016 の4日目の記事です。

ビューティー開発Tの@sakuna63です。
Androidエンジニアなら愛してやまない(?)Fragmentについて書きます。

問題

ViewPager内でFragmentを利用する際、Fragmentと対応するページが選択されたタイミングで処理を実行したくなることがあります。例えば、動画やアニGIFの再生を自動で開始するといった場合です。

onViewCreatedやonResumeなどのライフサイクルに合わせて実行すればいいように思えますが、ViewPagerを利用する場合そうはいきません。ViewPagerはsetOffscreenPageLimitで渡される値に応じて、選択されたページから左右数ページ(default:1)のインスタンスを事前に生成しようとするためです。これによって、選択状態でなくともFragmentのライフサイクルが始まってしまいます。

そのため、対応するページが選択されたことをFragmentに通知する必要があります。単純に考えると、OnPageChangeListener#onPageSelectedのタイミングで、Fragmentのインスタンスを取得1し、通知するという方法が考えられます。ただし、この方法はどうもスマートではありません。。。 :thinking:

Fragment#setUserVisibleHintを使う

そこで登場するのがFragment#setUserVisibleHintです。以下のように、Fragment内でオーバーライドすることで、ユーザに対し可視状態になっているかどうかを監視できます。簡単ですね2

class MyFragment extends Fragment {
    //...
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        if (isVisibleToUser) {
            // 表示状態になったときの処理
        } else {
            // 非表示状態になったときの処理
        }
    }
    // ...
}

1つ注意点として、このメソッドはonCreateViewなどのライフサイクルメソッドよりも先に呼び出されることがあります34。ドキュメントにも以下のような記載があります。

Note: This method may be called outside of the fragment lifecycle. and thus has no ordering guarantees with regard to fragment lifecycle method calls.

これで問題解決!!:tada::tada::tada:やったね!!:clap::clap::clap:...では面白くありません。
もう少し掘り下げて、このメソッドがフレームワーク内でどのように呼び出されているのかを見ていきましょう。

SupportLibraryのミラーリポジトリを検索してみたところ、このメソッドはFragmentCompat, FragmentPagerAdapter, FragmentStatePagerManagerの3つから呼び出されていることがわかります。つまり実質的に、Fragment#setUserVisibleHintはViewPagerを使う際にしか呼び出されないということになります。
https://github.com/android/platform_frameworks_support/search?utf8=%E2%9C%93&q=setuservisiblehint

以下はFragment#setUserVisibleHintの呼び出し部分の抜粋です。

FragmentPagerAdapter.java
public abstract class FragmentPagerAdapter extends PagerAdapter {
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        //...
        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }

        return fragment;
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment)object;
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
                mCurrentPrimaryItem.setUserVisibleHint(false);
            }
            if (fragment != null) {
                fragment.setMenuVisibility(true);
                fragment.setUserVisibleHint(true);
            }
            mCurrentPrimaryItem = fragment;
        }
    }
}
FragmentStatePagerAdapter.java
public abstract class FragmentStatePagerAdapter extends PagerAdapter {
    //...
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        //...
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        return fragment;
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment)object;
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
                mCurrentPrimaryItem.setUserVisibleHint(false);
            }
            if (fragment != null) {
                fragment.setMenuVisibility(true);
                fragment.setUserVisibleHint(true);
            }
            mCurrentPrimaryItem = fragment;
        }
    }
}

上記のコードから

  • Fragment生成時(instantiateItem)
  • Fragmentがプライマリになる(= 選択状態になる)時(setPrimaryItem)

の2つのタイミングでのみ呼び出されることがわかります。

Fragment#onHiddenChangedではダメなのか?

名前がそれらしいので勘違いしてしまいますが、このメソッドは FragmentTransaction#show, FragmentTransaction#hideと対応して呼び出されるメソッドになります。なので、今回のユースケースには合致しません。

おまけ - PagerAdapter#getPageWidthとの併用

PagerAdapter#getPageWidthはViewPagerの幅に対するページ幅の比率をfloatで返すメソッドです。例えば、0.5fを返却することで、1画面中に2ページを同時に表示させることができます。

class MyPagerAdapter extends FragmentPagerAdapter {
    //...
    @Override
    public float getPageWidth(int position) {
        return 0.5f;
    }
}

問題

上記のアダプタを使用する場合、同時に表示される2つのページに対してsetUserVisibleHint(true)が呼び出されることが理想です。しかし、現実はそう簡単ではありません。。。:cry:
上記で引用したコードからもわかるように、FragmentPagerAdapter, FragmentStatePagerAdapterはどちらもプライマリのページ(≒ 選択されているページ)は1つであるという前提で実装されてしまってます。そのため、以下2つの問題が生じてしまいます。

  • 表示されているページのうち、片方、具体的にはpositionの小さいページに対してのみsetUserVisibleHint(true)が呼びさ出される
  • 表示されているページの内、positionの大きいページが非表示になった際にsetUserVisibleHint(false)が呼び出されない
    • 1つ前のプライマリページに対してのみsetUserVisibleHint(false)を呼び出そうとするため(引用コード参照)

対応案

上記の問題を解決したコードが以下になります。対策はシンプルで、以下2つの処理を行っています。

  • 画面内に表示されている非プライマリなページに対してもsetUserVisibleHint(true)を呼び出す
  • 画面外に行ってしまった、非元プライマリなページに対して setUserVisibleHint(false)
class MyPagerAdapter extends FragmentPagerAdapter {
    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        super.setPrimaryItem(container, position, object);
        float pageWidth = getPageWidth(position);
        // 画面内に表示されているFragment全てに対してsetUserVisibleHint(true)を呼び出す
        while (pageWidth < 1.0f) {
            position++;
            setFragmentVisibility(position);
            pageWidth += getPageWidth(position);
        }
        // 画面外に行ってしまってFragmentに対してsetUserVisibleHint(false)を呼び出す
        setFragmentVisibility(position + 1);
    }

    private void setFragmentVisibility(int position) {
        Fragment fragment = fm.findFragmentByTag("android:switcher:" + R.id.pager + ":" + position);
        if (fragment != null) {
            fragment.setUserVisibleHint(true);
        }
    }
}

結局、最初に述べたスマートじゃない方法1を利用してしまっていますが、ひとまずこの対応案に落ち着きました。。
何かいい対応案を知っている方がいましたら、是非教えていただきたいです!

次回

次回(5日目)は同期の @tacumai が書いてくれます。

  1. PagerAdapterからFragmentを取得する方法はこちらが参考になります。 2

  2. http://stackoverflow.com/questions/10024739/how-to-determine-when-fragment-becomes-visible-in-viewpager

  3. https://developer.android.com/reference/android/app/Fragment.html#setUserVisibleHint(boolean)

  4. http://stackoverflow.com/questions/24161160/setuservisiblehint-called-before-oncreateview-in-fragment

60
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?