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)