2021年3月5日金曜日

ViewPager2 で 遠くのページに smoothScroll するときは 3ページ前から作られる

最初 index : 0

タブクリックで index : 15 のページに移動

タブクリックで index : 4 のページに移動


したときの Fragment のライフサイクルの結果は次のようになります。 : onCreate : 0 : onCreate : 12 : onCreate : 13 : onCreate : 14 : onCreate : 15 : onDestroy : 0 : onDestroy : 12 : onCreate : 7 : onDestroy : 13 : onCreate : 6 : onCreate : 5 : onDestroy : 14 : onCreate : 4 : onDestroy : 15 : onDestroy : 7 index : 0 から index : 15 のページに移動するとき、index : 12 のページから作られていることがわかります。
同じように index : 15 から index : 4 のページに移動するときは index : 7 のページから作られていることがわかります。


この 3 ページ前からというロジックは ViewPager2 の setCurrentItemInternal() に実装されています。 void setCurrentItemInternal(int item, boolean smoothScroll) { ... // For smooth scroll, pre-jump to nearby item for long jumps. if (Math.abs(item - previousItem) > 3) { mRecyclerView.scrollToPosition(item > previousItem ? item - 3 : item + 3); // TODO(b/114361680): call smoothScrollToPosition synchronously (blocked by b/114019007) mRecyclerView.post(new SmoothScrollToPosition(item, mRecyclerView)); } else { mRecyclerView.smoothScrollToPosition(item); } }



2021年3月4日木曜日

ViewPager2 の offscreenPageLimit のデフォルト値は 1 ではない。

ViewPager2 にも ViewPager と同様 offscreenPageLimit を指定することができます。

ViewPager2 の offscreenPageLimit のデフォルト値は 1 ではなく、ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT (= -1) です。

そのため FragmentStateAdapter を使っている場合、デフォルトでは隣の画面の Fragment はスワイプなどで表示が必要になるタイミングまで作成されません。
ViewPager と同じ挙動にするには、明示的に offscreenPageLimit に 1 を指定する必要があります。 val pager: ViewPager2 = ... pager.offscreenPageLimit = 1

2021年2月27日土曜日

ShapeDrawable + BitmapShader で elevation の影を出す

背景(background)の Drawable が ShapeDrawable の場合、elevation を指定すると影がでます。(1番目)
一方、BitmapDrawable では elevation を指定しても影がでません。(2番目)
しかし背景(background)の Drawable が BitmapDrawable でも ViewOutlineProvider を使うと影が出るようになります。(4番目) val provider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { val radius = 32 * resources.displayMetrics.density outline.setRoundRect(0, 0, view.width, view.height, radius) } } findViewById<View>(R.id.frameLayout4).apply { outlineProvider = provider clipToOutline = true } ViewOutlineProvider を使わずに、ShapeDrawable と BitmapShader でも影がでるようにすることができます。(2番目) val r = 16 * resources.displayMetrics.density val shapeDrawable = ShapeDrawable( RoundRectShape(floatArrayOf(r, r, r, r, r, r, r, r), null, null) ) val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image_round) shapeDrawable.shaderFactory = object: ShapeDrawable.ShaderFactory() { override fun resize(width: Int, height: Int): Shader { val bmp = bitmap.scale(width, height, false) return BitmapShader(bmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) } } findViewById<View>(R.id.frameLayout2).background = shapeDrawable



2021年2月25日木曜日

ViewPager2 でページを動的に追加・削除する

大事なポイントは FragmentStateAdapter を継承した Adapter で getItemId() と containsItem() を実装すること、notifyDataSetChanged() だとうまく動かないので DiffUtil.calculateDiff() を使うことです。
class ViewPager2Activity : AppCompatActivity() { private val binding by lazy { ActivityViewPager2Binding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) val list = listOf( PageType.MAIN, PageType.FAVORITE, PageType.SETTING, ) val list2 = listOf( PageType.MAIN, PageType.SETTING, ) val adapter = PagerAdapter(list, this) binding.pager.adapter = adapter TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position -> tab.text = list[position].toString() }.attach() binding.checkbox.setOnCheckedChangeListener { _, isChecked -> adapter.updateList(if (isChecked) list else list2) } } } enum class PageType { MAIN, FAVORITE, SETTING } private class PagerAdapter( initial: List<PageType>, fragmentActivity: FragmentActivity ) : FragmentStateAdapter(fragmentActivity) { private val list = mutableListOf<PageType>() init { list.addAll(initial) } override fun getItemCount(): Int { return list.size } override fun createFragment(position: Int): Fragment { return PageFragment.newInstance(list[position]) } override fun getItemId(position: Int): Long { return list[position].ordinal.toLong() } override fun containsItem(itemId: Long): Boolean { return list.any { it.ordinal.toLong() == itemId } } fun updateList(newList: List<PageType>) { // notifyDataSetChanged() だとうまく動かない val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() { override fun getOldListSize(): Int { return list.size } override fun getNewListSize(): Int { return newList.size } override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return list[oldItemPosition] == newList[newItemPosition] } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return list[oldItemPosition] == newList[newItemPosition] } }) list.clear() list.addAll(newList) diff.dispatchUpdatesTo(this) } } class PageFragment : Fragment() { private val color = Color.rgb(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256)) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return TextView(inflater.context).apply { setBackgroundColor(color) text = (requireArguments().getSerializable("type") as PageType).toString() } } companion object { fun newInstance(type: PageType): PageFragment { return PageFragment().apply { arguments = bundleOf("type" to type) } } } }

2021年2月24日水曜日

ViewPager2

class MainActivity : AppCompatActivity() { private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) val adapter = PagerAdapter(this) binding.pager.adapter = adapter // MDC の TabLayout と組み合わせるときは TabLayoutMediator を使う // TabLayoutMediator の attach は ViewPager2 に adapter をセットした後に行う TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position -> tab.text = adapter.getTitle(position) }.attach() } } private class PagerAdapter( fragmentActivity: FragmentActivity ) : FragmentStateAdapter(fragmentActivity) { override fun getItemCount(): Int { return ... } override fun createFragment(position: Int): Fragment { return PageFragment.newInstance(...) } fun getTitle(position: Int): String { return ... } }

2021年2月18日木曜日

縁取り TextView

縁の設定をして super.onDraw() を呼び、中の設定をして super.onDraw() を呼ぶ。
TextPaint.setColor() ではなく TextView.setTextColor() を使わないとうまく色が変わらない。 class OutlineTextView : AppCompatTextView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) private val outlineWidth = 10 * context.resources.displayMetrics.density override fun onDraw(canvas: Canvas?) { setTextColor(Color.RED) paint.apply { style = Paint.Style.FILL_AND_STROKE strokeWidth = outlineWidth } super.onDraw(canvas) setTextColor(Color.BLACK) paint.apply { style = Paint.Style.FILL strokeWidth = 0f } super.onDraw(canvas) } }




2021年2月12日金曜日

viewLifecycleOwnerLiveData を使って Fragment の onDestroyView() で自動で null がセットされる ViewBinding 用の property delegates を作る

ViewBinding のドキュメントでは Fragment で使う時の実装はこのようになっています。 class LoginFragment : Fragment() { private var _binding: FragmentLoginBinding? = null private val binding: FragmentLoginBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentLoginBinding.inflate(inflater, container, false) return binding.root } override fun onDestroyView() { super.onDestroyView() _binding = null } } Fragment はそのライフサイクル中にViewが破棄されることがあるので onDestroyView() で View への参照を外しておく必要があります。

Fragment.viewLifecycleOwnerLiveData で LiveData<LifecycleOwner?> が取れます」で紹介した viewLifecycleOwnerLiveData を使うと、onDestroyView() で null を代入する処理を自動でやってくれる ViewBinding 用の property delegates を作ることができます。

https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c を参考に変更を加えています。 class FragmentViewBindingDelegate<T : ViewBinding>( val fragment: Fragment, val viewBindingFactory: (View) -> T ) : ReadOnlyProperty<Fragment, T> { private var binding: T? = null private val viewLifecycleOwnerObserver = Observer<LifecycleOwner?> { if (it == null) { binding = null } } private val observer = object : DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerObserver) } override fun onDestroy(owner: LifecycleOwner) { fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerObserver) fragment.lifecycle.removeObserver(this) } } init { if (fragment.lifecycle.currentState != Lifecycle.State.DESTROYED) { fragment.lifecycle.addObserver(observer) } } override fun getValue(thisRef: Fragment, property: KProperty<*>): T { val binding = binding if (binding != null) { return binding } val view = thisRef.view checkNotNull(view) { "Should get bindings when the view is not null." } return viewBindingFactory(view).also { this.binding = it } } } fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) = FragmentViewBindingDelegate(this, viewBindingFactory) これを使うと最初のコードはこうなります。 class LoginFragment : Fragment() { private val binding by viewBinding(FragmentLoginBinding::bind) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return inflater.inflate(R.layout.fragment_login, container, false) } }

2021年2月10日水曜日

Fragment.viewLifecycleOwnerLiveData で LiveData<LifecycleOwner?> が取れます

AndroidX の Fragment に getViewLifecycleOwnerLiveData() というメソッドがあります。 @NonNull public LiveData<LifecycleOwner> getViewLifecycleOwnerLiveData() { return mViewLifecycleOwnerLiveData; } これは LiveData<LifecycleOwner?> を返してくれます。
この LiveData には、onCreateView() が non-null な View を返した後に getViewLifecycleOwner() で取得できるのと同じ LifecycleOwner がセットされ、onDestroyView() が呼ばれた後に null がセットされます。

実際に試してみましょう。 class MainFragment : Fragment() { private val observer = Observer<LifecycleOwner?> { println("--- lifecycleOwner : $it") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) println("onCreate") viewLifecycleOwnerLiveData.observeForever(observer) } override fun onAttach(context: Context) { super.onAttach(context) println("onAttach") } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { println("onCreateView") return TextView(inflater.context) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) println("onViewCreated") } override fun onStart() { super.onStart() println("onStart") } override fun onResume() { super.onResume() println("onResume") } override fun onPause() { super.onPause() println("onPause") } override fun onStop() { super.onStop() println("onStop") } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) println("onActivityCreated") } override fun onDestroyView() { super.onDestroyView() println("onDestroyView") } override fun onDetach() { super.onDetach() println("onDetach") } override fun onDestroy() { super.onDestroy() println("onDestroy") viewLifecycleOwnerLiveData.removeObserver(observer) } } : onAttach : onCreate : onCreateView : --- lifecycleOwner : androidx.fragment.app.FragmentViewLifecycleOwner@6493c3f : onViewCreated : onActivityCreated : onStart : onResume : ----- detach ----- [ここで Activity から detach ] : onPause : onStop : onDestroyView : --- lifecycleOwner : null : ----- attach ----- [ここで Activity に attach ] : onCreateView : --- lifecycleOwner : androidx.fragment.app.FragmentViewLifecycleOwner@50fd6c : onViewCreated : onActivityCreated : onStart : onResume [ここで 画面回転 ] : onPause : onStop : onDestroyView : --- lifecycleOwner : null : onDestroy : onDetach : onAttach : onCreate : onCreateView : --- lifecycleOwner : androidx.fragment.app.FragmentViewLifecycleOwner@9f4c99a : onViewCreated : onActivityCreated : onStart : onResume [ここでバックキーを押して Activity を終了 ] : onPause : onStop : onDestroyView : --- lifecycleOwner : null : onDestroy : onDetach onCreateView() で TextView を返しているので onCreateView() の後に FragmentViewLifecycleOwner のインスタンスが流れてきて、onDestroyView() が呼ばれた後に null が流れてきています。


onCreateView() で null を返すと class MainFragment : Fragment() { ... override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { println("onCreateView") return null } ... } : onAttach : onCreate : onCreateView : onActivityCreated : onStart : onResume : ----- detach ----- : onPause : onStop : onDestroyView : --- lifecycleOwner : null : ----- attach ----- : onCreateView : onActivityCreated : onStart : onResume : onPause : onStop : onDestroyView : --- lifecycleOwner : null : onDestroy : onDetach : onAttach : onCreate : onCreateView : onActivityCreated : onStart : onResume : onPause : onStop : onDestroyView : --- lifecycleOwner : null : onDestroy : onDetach onCreateView() の後にはなにも流れてきませんが、onDestroyView() の後には null が流れてきます。



2021年2月4日木曜日

Animator メモ

single object, single property → ObjectAnimator val animator = ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f) single object, multiple property, parallel → PropertyValuesHolder + ObjectAnimator val scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 4f) val scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 4f) val animator = ObjectAnimator.ofPropertyValuesHolder(view, scaleX, scaleY) single object, multiple property, sequential → ObjectAnimator + AnimatorSet val scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X, 4f) val scaleY = ObjectAnimator.ofFloat(view, View.SCALE_Y, 4f) val set = AnimatorSet() set.playSequentially(scaleX, scaleY) multiple object, multiple property, parallel → ObjectAnimator +AnimatorSet val move = ObjectAnimator.ofFloat(view1, View.TRANSLATION_Y, 100f) val rotate = ObjectAnimator.ofFloat(view2, View.ROTATION, 360f) val set = AnimatorSet() set.playTogether(move, rotate) multiple object, multiple property, sequential → ObjectAnimator +AnimatorSet val move = ObjectAnimator.ofFloat(view1, View.TRANSLATION_Y, 100f) val rotate = ObjectAnimator.ofFloat(view2, View.ROTATION, 360f) val set = AnimatorSet() set.playSequentially(move, rotate)

2021年1月8日金曜日

<fragment> と FragmentContainerView では Fragment のライフサイクルメソッドの呼ばれるタイミングが違う

動作確認は androidx.fragment:fragment-ktx:1.2.5 でしています。今後のバージョンで動作が変わる可能性があります。

fragment-ktx:1.2.5 で <fragment> を使うと FragmentContainerView を使うようにメッセージがでます。


このメッセージに従って FragmentContainerView に変えたとして、大体は問題ないと思いますが、問題が起こる場合もあります。

<fragment> と FragmentContainerView で Fragment のライフサイクルメソッドの呼ばれる順番を確認してみます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val f = supportFragmentManager .findFragmentById(R.id.mainFragment) as MainFragment Log.d("MainActivity", "onCreate : ${f.view}") } } <?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/mainFragment" android:name="yanzm.sample.myapplication.MainFragment" android:layout_width="match_parent" android:layout_height="match_parent" /> class MainFragment : Fragment() { override fun onAttach(context: Context) { super.onAttach(context) Log.d("MainFragment", "onAttach") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d("MainFragment", "onCreate") } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { Log.d("MainFragment", "onCreateView") return View(inflater.context) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) Log.d("MainFragment", "onActivityCreated") } override fun onStart() { super.onStart() Log.d("MainFragment", "onStart") } override fun onResume() { super.onResume() Log.d("MainFragment", "onResume") } } ↑ の <fragment> の場合のコードを実行すると次のようになります。 D/MainFragment: onAttach D/MainFragment: onCreate D/MainFragment: onCreateView D/MainActivity: onCreate : android.view.View{7009652 V.ED..... ......I. 0,0-0,0 #7f0800ca app:id/mainFragment} D/MainFragment: onActivityCreated D/MainFragment: onStart D/MainFragment: onResume Fragment の onAttach(), onCreate(), onCreateView() が呼ばれた後に Activity の onCreate() が呼ばれています。そのため、Activity の onCreate() で Fragment の View にアクセスすると null ではありません。


では FragmentContainerView に変えて見てみましょう。タグ以外は変更なしです。 <?xml version="1.0" encoding="utf-8"?> <androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/mainFragment" android:name="yanzm.sample.myapplication.MainFragment" android:layout_width="match_parent" android:layout_height="match_parent" /> D/MainFragment: onAttach D/MainFragment: onCreate D/MainActivity: onCreate : null D/MainFragment: onCreateView D/MainFragment: onActivityCreated D/MainFragment: onStart D/MainFragment: onResume なんと Fragment の onCreateView() の呼ばれるタイミングが変わっています。 Activity の onCreate() の後に呼ばれるように変わってしまいました。そのため Activity の onCreate() で Fragment の View にアクセスすると null になっています。

Activity の onCreate() の時点で Fragment の onCreateView() がすでに呼ばれている前提の処理だと、FragmentContainerView に変更したときに意図しない動きになってしまうので注意が必要です。