2019年7月3日水曜日

ViewPager + FragmentPagerAdapter での setVisibleUserHint の挙動

以下は androidx.fragment:fragment:1.0.0 での挙動です。1.1.0-alpha07 で別の挙動にするためのオプションが追加されています。



ViewPager + FragmentPagerAdapter の構成はよく使うと思います。

FragmentPagerAdapter では、ViewPager のページが切り替わる処理の中で、
以前の PrimaryItem の Fragment に対して setUserVisibleHint(false) を呼び、
新しい PrimaryItem の Fragment に対して setUserVisibleHint(true) を呼んでいます。


挙動を確認するために、FragmentPagerAdapter の getItem() で返す Fragment に次のようにログを入れました。 class SimpleFragment : Fragment() { ... override fun setUserVisibleHint(isVisibleToUser: Boolean) { println("$position : setUserVisibleHint : before super : to = $isVisibleToUser, current = $userVisibleHint") super.setUserVisibleHint(isVisibleToUser) println("$position : setUserVisibleHint : after super : to = $isVisibleToUser, current = $userVisibleHint") } override fun onStart() { super.onStart() println("$position : onStart : $userVisibleHint") } }

1. レイアウトより前のタイミング(例えば onCreate())で adapter をセットしている場合 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewPager.adapter = MyPager(supportFragmentManager) } } この場合、ログは次のようになります。 System.out: 0 : setUserVisibleHint : before super : to = false, current = true System.out: 0 : setUserVisibleHint : after super : to = false, current = false System.out: 1 : setUserVisibleHint : before super : to = false, current = true System.out: 1 : setUserVisibleHint : after super : to = false, current = false System.out: 0 : setUserVisibleHint : before super : to = true, current = false System.out: 0 : setUserVisibleHint : after super : to = true, current = true System.out: 0 : onStart : true System.out: 1 : onStart : false まず position 0 と 1 の Fragment の setUserVisibleHint が false にセットされます。

super.setUserVisibleHint() が呼ばれる前の時点で取得した userVisibleHint が true ですね。 実は Fragment の mUserVisibleHint はデフォルトが true です。 package androidx.fragment.app; ... public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner, ViewModelStoreOwner { ... // Hint provided by the app that this fragment is currently visible to the user. boolean mUserVisibleHint = true; ... }
次に position 0 の Fragment の setUserVisibleHint が true にセットされます。

その後、Fragment の onStart() が呼ばれます。

つまり
  • 1. position 0 の Fragment の setUserVisibleHint() : true → false
  • 2. position 1 の Fragment の setUserVisibleHint() : true → false
  • 3. position 0 の Fragment の setUserVisibleHint() : false → true
  • 4. position 0 の Fragment の onStart() : setUserVisibleHint は true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は false
という流れです。



2. レイアウトより前のタイミング(例えば onCreate())で adapter をセットし、currentItem を変更している場合

onCreate() で ViewPager に adapter をセットした後 currentItem を 1 にしてみます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewPager.adapter = MyPager(supportFragmentManager) viewPager.currentItem = 1 } } この場合のログはこうなります。 System.out: 1 : setUserVisibleHint : before super : to = false, current = true System.out: 1 : setUserVisibleHint : after super : to = false, current = false System.out: 0 : setUserVisibleHint : before super : to = false, current = true System.out: 0 : setUserVisibleHint : after super : to = false, current = false System.out: 2 : setUserVisibleHint : before super : to = false, current = true System.out: 2 : setUserVisibleHint : after super : to = false, current = false System.out: 1 : setUserVisibleHint : before super : to = true, current = false System.out: 1 : setUserVisibleHint : after super : to = true, current = true System.out: 1 : onStart : true System.out: 0 : onStart : false System.out: 2 : onStart : false まず position 1, 0, 2 の Fragment の setUserVisibleHint が false にセットされ、
次に position 1 の Fragment の setUserVisibleHint が true にセットされ、
その後、Fragment の onStart() が呼ばれます。

つまり
  • 1. position 1 の Fragment の setUserVisibleHint() : true → false
  • 2. position 0 の Fragment の setUserVisibleHint() : true → false
  • 3. position 2 の Fragment の setUserVisibleHint() : true → false
  • 4. position 1 の Fragment の setUserVisibleHint() : false → true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は true
  • 6. position 0 の Fragment の onStart() : setUserVisibleHint は false
  • 7. position 2 の Fragment の onStart() : setUserVisibleHint は false
という流れです。



3. レイアウトより後のタイミングで adapter をセットしている場合 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Handler().postDelayed( { viewPager.adapter = MyPager(supportFragmentManager) }, 1000 ) } } onCreate() から1秒後に adapter をセットしてみます。この場合ログは次のようになります。 System.out: 0 : setUserVisibleHint : before super : to = false, current = true System.out: 0 : setUserVisibleHint : after super : to = false, current = false System.out: 1 : setUserVisibleHint : before super : to = false, current = true System.out: 1 : setUserVisibleHint : after super : to = false, current = false System.out: 0 : setUserVisibleHint : before super : to = true, current = false System.out: 0 : setUserVisibleHint : after super : to = true, current = true System.out: 0 : onStart : true System.out: 1 : onStart : false 「1. レイアウトより前のタイミング(例えば onCreate())で adapter をセットしている場合」と同じですね。
  • 1. position 0 の Fragment の setUserVisibleHint() : true → false
  • 2. position 1 の Fragment の setUserVisibleHint() : true → false
  • 3. position 0 の Fragment の setUserVisibleHint() : false → true
  • 4. position 0 の Fragment の onStart() : setUserVisibleHint は true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は false
という流れです。



4. レイアウトより後のタイミングで adapter をセットし、currentItem を変更している場合

onCreate() から1秒後に adapter をセットし、currentItem を 1 にしてみます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Handler().postDelayed( { viewPager.adapter = MyPager(supportFragmentManager) viewPager.currentItem = 1 }, 1000 ) } } この場合ログは次のようになります。 System.out: 0 : setUserVisibleHint : before super : to = false, current = true System.out: 0 : setUserVisibleHint : after super : to = false, current = false System.out: 1 : setUserVisibleHint : before super : to = false, current = true System.out: 1 : setUserVisibleHint : after super : to = false, current = false System.out: 0 : setUserVisibleHint : before super : to = true, current = false System.out: 0 : setUserVisibleHint : after super : to = true, current = true System.out: 0 : onStart : true System.out: 1 : onStart : false System.out: 2 : setUserVisibleHint : before super : to = false, current = true System.out: 2 : setUserVisibleHint : after super : to = false, current = false System.out: 0 : setUserVisibleHint : before super : to = false, current = true System.out: 0 : setUserVisibleHint : after super : to = false, current = false System.out: 1 : setUserVisibleHint : before super : to = true, current = false System.out: 1 : setUserVisibleHint : after super : to = true, current = true System.out: 2 : onStart : false 「2. レイアウトより前のタイミング(例えば onCreate())で adapter をセットし、currentItem を変更している場合」と同じになりません!

まず「3. レイアウトより後のタイミングで adapter をセットしている場合」と同じことが起こります。

つまり
  • 1. position 0 の Fragment の setUserVisibleHint() : true → false
  • 2. position 1 の Fragment の setUserVisibleHint() : true → false
  • 3. position 0 の Fragment の setUserVisibleHint() : false → true
  • 4. position 0 の Fragment の onStart() : setUserVisibleHint は true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は false
という流れが最初にあります。

その後
  • 6. position 2 の Fragment の setUserVisibleHint() : true → false
  • 7. position 0 の Fragment の setUserVisibleHint() : true → false
  • 8. position 1 の Fragment の setUserVisibleHint() : false → true
  • 9. position 2 の Fragment の onStart() : setUserVisibleHint は false
という流れになります。 adapter をセットしたタイミングで 1 〜 5 までの流れが起こり、currentItem を 1 にしたタイミングで 6 〜 9 の流れが起こります。

しかも position 1 が表示されているのに、onStart() 時の setUserVisibleHint 値を見ると position 0 が表示されているかのようです。


このように、レイアウトよりも後のタイミングで adapter をセットすると、その時点で position 0 の Fragment を PrimaryItem として onStart() まで呼ばれてしまいます。
そのため、レイアウトよりも後のタイミングで adapter をセットし、currentItem を 0 以外に変更する場合は setUserVisibleHint() や onStart() が呼ばれるタイミングに注意が必要です。


0 件のコメント:

コメントを投稿