2021年2月27日土曜日

ShapeDrawable + BitmapShader で elevation の影を出す

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



2021年2月25日木曜日

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

大事なポイントは FragmentStateAdapter を継承した Adapter で getItemId() と containsItem() を実装すること、notifyDataSetChanged() だとうまく動かないので DiffUtil.calculateDiff() を使うことです。
  1. class ViewPager2Activity : AppCompatActivity() {  
  2.   
  3.     private val binding by lazy { ActivityViewPager2Binding.inflate(layoutInflater) }  
  4.   
  5.     override fun onCreate(savedInstanceState: Bundle?) {  
  6.         super.onCreate(savedInstanceState)  
  7.         setContentView(binding.root)  
  8.   
  9.         val list = listOf(  
  10.             PageType.MAIN,  
  11.             PageType.FAVORITE,  
  12.             PageType.SETTING,  
  13.         )  
  14.   
  15.         val list2 = listOf(  
  16.             PageType.MAIN,  
  17.             PageType.SETTING,  
  18.         )  
  19.   
  20.         val adapter = PagerAdapter(list, this)  
  21.         binding.pager.adapter = adapter  
  22.   
  23.         TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position ->  
  24.             tab.text = list[position].toString()  
  25.         }.attach()  
  26.   
  27.         binding.checkbox.setOnCheckedChangeListener { _, isChecked ->  
  28.             adapter.updateList(if (isChecked) list else list2)  
  29.         }  
  30.     }  
  31. }  
  32.   
  33. enum class PageType {  
  34.     MAIN,  
  35.     FAVORITE,  
  36.     SETTING  
  37. }  
  38.   
  39. private class PagerAdapter(  
  40.     initial: List<PageType>,  
  41.     fragmentActivity: FragmentActivity  
  42. ) : FragmentStateAdapter(fragmentActivity) {  
  43.   
  44.     private val list = mutableListOf<PageType>()  
  45.   
  46.     init {  
  47.         list.addAll(initial)  
  48.     }  
  49.   
  50.     override fun getItemCount(): Int {  
  51.         return list.size  
  52.     }  
  53.   
  54.     override fun createFragment(position: Int): Fragment {  
  55.         return PageFragment.newInstance(list[position])  
  56.     }  
  57.   
  58.     override fun getItemId(position: Int): Long {  
  59.         return list[position].ordinal.toLong()  
  60.     }  
  61.   
  62.     override fun containsItem(itemId: Long): Boolean {  
  63.         return list.any { it.ordinal.toLong() == itemId }  
  64.     }  
  65.   
  66.     fun updateList(newList: List<PageType>) {  
  67.         // notifyDataSetChanged() だとうまく動かない  
  68.   
  69.         val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {  
  70.             override fun getOldListSize(): Int {  
  71.                 return list.size  
  72.             }  
  73.   
  74.             override fun getNewListSize(): Int {  
  75.                 return newList.size  
  76.             }  
  77.   
  78.             override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {  
  79.                 return list[oldItemPosition] == newList[newItemPosition]  
  80.             }  
  81.   
  82.             override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {  
  83.                 return list[oldItemPosition] == newList[newItemPosition]  
  84.             }  
  85.         })  
  86.   
  87.         list.clear()  
  88.         list.addAll(newList)  
  89.   
  90.         diff.dispatchUpdatesTo(this)  
  91.     }  
  92. }  
  93.   
  94. class PageFragment : Fragment() {  
  95.   
  96.     private val color = Color.rgb(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256))  
  97.   
  98.     override fun onCreateView(  
  99.         inflater: LayoutInflater,  
  100.         container: ViewGroup?,  
  101.         savedInstanceState: Bundle?  
  102.     ): View {  
  103.         return TextView(inflater.context).apply {  
  104.             setBackgroundColor(color)  
  105.             text = (requireArguments().getSerializable("type") as PageType).toString()  
  106.         }  
  107.     }  
  108.   
  109.     companion object {  
  110.         fun newInstance(type: PageType): PageFragment {  
  111.             return PageFragment().apply {  
  112.                 arguments = bundleOf("type" to type)  
  113.             }  
  114.         }  
  115.     }  
  116. }  

2021年2月24日水曜日

ViewPager2

  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }  
  4.   
  5.     override fun onCreate(savedInstanceState: Bundle?) {  
  6.         super.onCreate(savedInstanceState)  
  7.         setContentView(binding.root)  
  8.   
  9.         val adapter = PagerAdapter(this)  
  10.         binding.pager.adapter = adapter  
  11.   
  12.         // MDC の TabLayout と組み合わせるときは TabLayoutMediator を使う  
  13.         // TabLayoutMediator の attach は ViewPager2 に adapter をセットした後に行う  
  14.         TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position ->  
  15.             tab.text = adapter.getTitle(position)  
  16.         }.attach()  
  17.     }  
  18. }  
  19.   
  20. private class PagerAdapter(  
  21.     fragmentActivity: FragmentActivity  
  22. ) : FragmentStateAdapter(fragmentActivity) {  
  23.   
  24.     override fun getItemCount(): Int {  
  25.         return ...  
  26.     }  
  27.   
  28.     override fun createFragment(position: Int): Fragment {  
  29.         return PageFragment.newInstance(...)  
  30.     }  
  31.   
  32.     fun getTitle(position: Int): String {  
  33.         return ...  
  34.     }  
  35. }  

2021年2月18日木曜日

縁取り TextView

縁の設定をして super.onDraw() を呼び、中の設定をして super.onDraw() を呼ぶ。
TextPaint.setColor() ではなく TextView.setTextColor() を使わないとうまく色が変わらない。
  1. class OutlineTextView : AppCompatTextView {  
  2.   
  3.     constructor(context: Context) : super(context)  
  4.     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)  
  5.     constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(  
  6.         context,  
  7.         attrs,  
  8.         defStyleAttr  
  9.     )  
  10.       
  11.     private val outlineWidth = 10 * context.resources.displayMetrics.density  
  12.   
  13.     override fun onDraw(canvas: Canvas?) {  
  14.         setTextColor(Color.RED)  
  15.         paint.apply {  
  16.             style = Paint.Style.FILL_AND_STROKE  
  17.             strokeWidth = outlineWidth  
  18.         }  
  19.         super.onDraw(canvas)  
  20.   
  21.         setTextColor(Color.BLACK)  
  22.         paint.apply {  
  23.             style = Paint.Style.FILL  
  24.             strokeWidth = 0f  
  25.         }  
  26.         super.onDraw(canvas)  
  27.     }  
  28. }  




2021年2月12日金曜日

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

ViewBinding のドキュメントでは Fragment で使う時の実装はこのようになっています。
  1. class LoginFragment : Fragment() {  
  2.   
  3.     private var _binding: FragmentLoginBinding? = null  
  4.     private val binding: FragmentLoginBinding  
  5.         get() = _binding!!  
  6.   
  7.     override fun onCreateView(  
  8.         inflater: LayoutInflater,  
  9.         container: ViewGroup?,  
  10.         savedInstanceState: Bundle?  
  11.     ): View {  
  12.         _binding = FragmentLoginBinding.inflate(inflater, container, false)  
  13.         return binding.root  
  14.     }  
  15.   
  16.     override fun onDestroyView() {  
  17.         super.onDestroyView()  
  18.         _binding = null  
  19.     }  
  20. }  
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 を参考に変更を加えています。
  1. class FragmentViewBindingDelegate<T : ViewBinding>(  
  2.     val fragment: Fragment,  
  3.     val viewBindingFactory: (View) -> T  
  4. ) : ReadOnlyProperty<Fragment, T> {  
  5.   
  6.     private var binding: T? = null  
  7.   
  8.     private val viewLifecycleOwnerObserver = Observer<LifecycleOwner?> {  
  9.         if (it == null) {  
  10.             binding = null  
  11.         }  
  12.     }  
  13.   
  14.     private val observer = object : DefaultLifecycleObserver {  
  15.   
  16.         override fun onCreate(owner: LifecycleOwner) {  
  17.             fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerObserver)  
  18.         }  
  19.   
  20.         override fun onDestroy(owner: LifecycleOwner) {  
  21.             fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerObserver)  
  22.             fragment.lifecycle.removeObserver(this)  
  23.         }  
  24.     }  
  25.   
  26.     init {  
  27.         if (fragment.lifecycle.currentState != Lifecycle.State.DESTROYED) {  
  28.             fragment.lifecycle.addObserver(observer)  
  29.         }  
  30.     }  
  31.   
  32.     override fun getValue(thisRef: Fragment, property: KProperty<*>): T {  
  33.         val binding = binding  
  34.         if (binding != null) {  
  35.             return binding  
  36.         }  
  37.   
  38.         val view = thisRef.view  
  39.         checkNotNull(view) {  
  40.             "Should get bindings when the view is not null."  
  41.         }  
  42.   
  43.         return viewBindingFactory(view).also { this.binding = it }  
  44.     }  
  45. }  
  46.   
  47. fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =  
  48.     FragmentViewBindingDelegate(this, viewBindingFactory)  
これを使うと最初のコードはこうなります。
  1. class LoginFragment : Fragment() {  
  2.   
  3.     private val binding by viewBinding(FragmentLoginBinding::bind)  
  4.   
  5.     override fun onCreateView(  
  6.         inflater: LayoutInflater,  
  7.         container: ViewGroup?,  
  8.         savedInstanceState: Bundle?  
  9.     ): View {  
  10.         return inflater.inflate(R.layout.fragment_login, container, false)  
  11.     }  
  12. }  

2021年2月10日水曜日

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

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

実際に試してみましょう。
  1. class MainFragment : Fragment() {  
  2.   
  3.     private val observer = Observer<LifecycleOwner?> {  
  4.         println("--- lifecycleOwner : $it")  
  5.     }  
  6.   
  7.     override fun onCreate(savedInstanceState: Bundle?) {  
  8.         super.onCreate(savedInstanceState)  
  9.         println("onCreate")  
  10.   
  11.         viewLifecycleOwnerLiveData.observeForever(observer)  
  12.     }  
  13.   
  14.     override fun onAttach(context: Context) {  
  15.         super.onAttach(context)  
  16.         println("onAttach")  
  17.     }  
  18.   
  19.     override fun onCreateView(  
  20.         inflater: LayoutInflater,  
  21.         container: ViewGroup?,  
  22.         savedInstanceState: Bundle?  
  23.     ): View {  
  24.         println("onCreateView")  
  25.         return TextView(inflater.context)  
  26.     }  
  27.   
  28.     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {  
  29.         super.onViewCreated(view, savedInstanceState)  
  30.         println("onViewCreated")  
  31.     }  
  32.   
  33.     override fun onStart() {  
  34.         super.onStart()  
  35.         println("onStart")  
  36.     }  
  37.   
  38.     override fun onResume() {  
  39.         super.onResume()  
  40.         println("onResume")  
  41.     }  
  42.   
  43.     override fun onPause() {  
  44.         super.onPause()  
  45.         println("onPause")  
  46.     }  
  47.   
  48.     override fun onStop() {  
  49.         super.onStop()  
  50.         println("onStop")  
  51.     }  
  52.   
  53.     override fun onActivityCreated(savedInstanceState: Bundle?) {  
  54.         super.onActivityCreated(savedInstanceState)  
  55.         println("onActivityCreated")  
  56.     }  
  57.   
  58.     override fun onDestroyView() {  
  59.         super.onDestroyView()  
  60.         println("onDestroyView")  
  61.     }  
  62.   
  63.     override fun onDetach() {  
  64.         super.onDetach()  
  65.         println("onDetach")  
  66.     }  
  67.   
  68.     override fun onDestroy() {  
  69.         super.onDestroy()  
  70.         println("onDestroy")  
  71.         viewLifecycleOwnerLiveData.removeObserver(observer)  
  72.     }  
  73. }  
  1. : onAttach  
  2. : onCreate  
  3. : onCreateView  
  4. : --- lifecycleOwner : androidx.fragment.app.FragmentViewLifecycleOwner@6493c3f  
  5. : onViewCreated  
  6. : onActivityCreated  
  7. : onStart  
  8. : onResume  
  9. : ----- detach ----- [ここで Activity から detach ]  
  10. : onPause  
  11. : onStop  
  12. : onDestroyView  
  13. : --- lifecycleOwner : null  
  14. : ----- attach ----- [ここで Activity に attach ]  
  15. : onCreateView  
  16. : --- lifecycleOwner : androidx.fragment.app.FragmentViewLifecycleOwner@50fd6c  
  17. : onViewCreated  
  18. : onActivityCreated  
  19. : onStart  
  20. : onResume  
  21.  [ここで 画面回転 ]  
  22. : onPause  
  23. : onStop  
  24. : onDestroyView  
  25. : --- lifecycleOwner : null  
  26. : onDestroy  
  27. : onDetach  
  28. : onAttach  
  29. : onCreate  
  30. : onCreateView  
  31. : --- lifecycleOwner : androidx.fragment.app.FragmentViewLifecycleOwner@9f4c99a  
  32. : onViewCreated  
  33. : onActivityCreated  
  34. : onStart  
  35. : onResume  
  36.  [ここでバックキーを押して Activity を終了 ]  
  37. : onPause  
  38. : onStop  
  39. : onDestroyView  
  40. : --- lifecycleOwner : null  
  41. : onDestroy  
  42. : onDetach  
onCreateView() で TextView を返しているので onCreateView() の後に FragmentViewLifecycleOwner のインスタンスが流れてきて、onDestroyView() が呼ばれた後に null が流れてきています。


onCreateView() で null を返すと
  1. class MainFragment : Fragment() {  
  2.   
  3.     ...  
  4.   
  5.     override fun onCreateView(  
  6.         inflater: LayoutInflater,  
  7.         container: ViewGroup?,  
  8.         savedInstanceState: Bundle?  
  9.     ): View? {  
  10.         println("onCreateView")  
  11.         return null  
  12.     }  
  13.   
  14.     ...  
  15. }    
  1. : onAttach  
  2. : onCreate  
  3. : onCreateView  
  4. : onActivityCreated  
  5. : onStart  
  6. : onResume  
  7. : ----- detach -----  
  8. : onPause  
  9. : onStop  
  10. : onDestroyView  
  11. : --- lifecycleOwner : null  
  12. : ----- attach -----  
  13. : onCreateView  
  14. : onActivityCreated  
  15. : onStart  
  16. : onResume  
  17. : onPause  
  18. : onStop  
  19. : onDestroyView  
  20. : --- lifecycleOwner : null  
  21. : onDestroy  
  22. : onDetach  
  23. : onAttach  
  24. : onCreate  
  25. : onCreateView  
  26. : onActivityCreated  
  27. : onStart  
  28. : onResume  
  29. : onPause  
  30. : onStop  
  31. : onDestroyView  
  32. : --- lifecycleOwner : null  
  33. : onDestroy  
  34. : onDetach  
onCreateView() の後にはなにも流れてきませんが、onDestroyView() の後には null が流れてきます。



2021年2月4日木曜日

Animator メモ

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