背景(background)の Drawable が ShapeDrawable の場合、elevation を指定すると影がでます。(1番目)
一方、BitmapDrawable では elevation を指定しても影がでません。(2番目)
しかし背景(background)の Drawable が BitmapDrawable でも ViewOutlineProvider を使うと影が出るようになります。(4番目)
2021年2月27日土曜日
ShapeDrawable + BitmapShader で elevation の影を出す
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() を使わないとうまく色が変わらない。
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 で使う時の実装はこのようになっています。
「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 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() というメソッドがあります。
この LiveData には、onCreateView() が non-null な View を返した後に getViewLifecycleOwner() で取得できるのと同じ LifecycleOwner がセットされ、onDestroyView() が呼ばれた後に null がセットされます。
実際に試してみましょう。
onCreateView() で null を返すと
@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)