Kotlin Drap & Drop 지원하는 RecyclerView (이미지 부분 눌러서 드래그)

|

item_todo.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="wrap_content">

  <ImageView
    android:id="@+id/drag_handle"
    android:layout_width="40dp"
    android:layout_height="40dp"
    android:layout_marginEnd="20dp"
    android:layout_alignParentEnd="true"
    android:layout_centerVertical="true"
    android:src="@drawable/drag_handle"/>

  <TextView
    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="15dp"
    android:layout_marginBottom="15dp"
    android:layout_marginStart="10dp"
    android:layout_marginEnd="20dp"
    android:layout_centerVertical="true"
    android:layout_toStartOf="@id/drag_handle"
    android:textAppearance="@style/TextAppearance.AppCompat.Body2"
    android:textSize="18sp"/>

</RelativeLayout>


ItemTouchHelperViewHolder.kt

interface ItemTouchHelperViewHolder {
    fun onItemSelected()
    fun onItemClear()
}


ItemTouItemTouchHelperAdapter.kt

interface ItemTouchHelperAdapter {
    fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
    fun onItemRemove(position: Int)
}


##

OnRecyclerAdapterEventListener.kt

interface OnRecyclerAdapterEventListener {
    fun onItemClicked(position: Int)
    fun onItemLongClicked(position: Int)
    fun onDragStarted(viewHolder: RecyclerView.ViewHolder)
}


SimpleItemTouchHelperCallback.kt

import android.graphics.Canvas
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.helper.ItemTouchHelper

class SimpleItemTouchHelperCallback(val adapter: ItemTouchHelperAdapter) :
        ItemTouchHelper.Callback() {
    private val MAX_ALPHA = 1.0f

    override fun isItemViewSwipeEnabled(): Boolean {
        return false
    }

    override fun isLongPressDragEnabled(): Boolean {
        return false
    }


    override fun getMovementFlags(recyclerView: RecyclerView,
                                  viewHolder: RecyclerView.ViewHolder): Int {
        var dragFlags: Int
        var swipeFlags: Int

        if (recyclerView.layoutManager is GridLayoutManager) {
            dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or
                    ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
            swipeFlags = 0
        } else {
            dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
            swipeFlags = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
        }

        return makeMovementFlags(dragFlags, swipeFlags)
    }

    override fun onMove(recyclerView: RecyclerView,
                        source: RecyclerView.ViewHolder,
                        target: RecyclerView.ViewHolder): Boolean {
        if (source.itemViewType != target.itemViewType) {
            return false
        }

        adapter.onItemMove(source.adapterPosition, target.adapterPosition)
        return true
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, position: Int) {
        adapter.onItemRemove(viewHolder.adapterPosition)
    }

    override fun onChildDraw(c: Canvas, recyclerView: RecyclerView,
                             viewHolder: RecyclerView.ViewHolder,
                             dX: Float, dY: Float, actionState: Int,
                             isCurrentlyActive: Boolean) {

        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
            val alpha = MAX_ALPHA - Math.abs(dX) / viewHolder.itemView.width
            viewHolder.itemView.alpha = alpha
            viewHolder.itemView.translationX = dX
        } else {
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
        }
    }

    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?,
                                   actionState: Int) {
        if (actionState == ItemTouchHelper.ACTION_STATE_IDLE) {
            if (viewHolder is ItemTouchHelperViewHolder) {
                viewHolder.onItemSelected()
            }
        }

        super.onSelectedChanged(viewHolder, actionState)
    }

    override fun clearView(recyclerView: RecyclerView, viewHolder:
    RecyclerView.ViewHolder) {
        super.clearView(recyclerView, viewHolder)

        viewHolder.itemView.alpha = MAX_ALPHA

        if (viewHolder is ItemTouchHelperViewHolder) {
            viewHolder.onItemClear()
        }
    }
}


TodoListAdapter.kt

import android.content.Context
import android.graphics.Paint
import android.support.v7.widget.RecyclerView
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import com.ran.todolist.R
import com.ran.todolist.common.TodoInfo
import com.ran.todolist.model.ModelManager
import com.ran.todolist.model.OnTodoInfoEventListener
import com.ran.todolist.utils.Log
import com.ran.todolist.utils.recyclerview.ItemTouchHelperAdapter
import com.ran.todolist.utils.recyclerview.OnRecyclerAdapterEventListener
import kotlinx.android.synthetic.main.item_todo.view.*
import java.util.*
import kotlin.collections.ArrayList

class TodoListAdapter(private val ctx: Context, private val categoryId: Long) :
        RecyclerView.Adapter<ViewHolder>(), OnTodoInfoEventListener,
        ItemTouchHelperAdapter {

    private var list: ArrayList<TodoInfo> = ArrayList()

    private var onEventListener: OnRecyclerAdapterEventListener? = null

    fun setOnRecyclerAdapterEventListener(l: OnRecyclerAdapterEventListener) {
        onEventListener = l
    }

    init {
        ModelManager.instance.addOnTodoInfoEventListener(this)
        refresh()
    }

    private fun refresh() {
        list = ModelManager.instance.getList(categoryId)

        for (i in 0 until list.size) {
            Log.i("snowdeer] ${list[i]}")
        }

        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, position: Int): ViewHolder {
        val view: View = LayoutInflater.from(ctx).inflate(R.layout.item_todo, parent, false)
        return ViewHolder(view)
    }

    fun getItem(position: Int): TodoInfo {
        return list[position]
    }

    override fun getItemCount(): Int {
        return list.size
    }

    override fun onModelUpdated(info: TodoInfo) {
        refresh()
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
        viewHolder.text.text = list[position].text
        viewHolder.text.paintFlags = when (list[position].done) {
            true -> Paint.STRIKE_THRU_TEXT_FLAG
            else -> 0
        }

        val info = list[position]
        viewHolder.text.setOnLongClickListener {
            Log.i("onItemLongClicked: $position")
            onEventListener?.onItemLongClicked(position)
            true
        }

        viewHolder.handle.setOnTouchListener(View.OnTouchListener { _, event ->
            if (event.action == MotionEvent.ACTION_DOWN) {
                onEventListener?.onDragStarted(viewHolder);
            }
            false;
        })
    }

    override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
        swap(fromPosition, toPosition)
        return true
    }

    override fun onItemRemove(position: Int) {
        ModelManager.instance.deleteTodoInfo(list[position])
    }

    private fun swap(from: Int, to: Int) {
        ModelManager.instance.swap(list[from], list[to])
        Collections.swap(list, from, to)
        notifyItemMoved(from, to)
    }

}

class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val text: TextView = view.text
    val handle: ImageView = view.drag_handle
}


TodoListFragment.kt

import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.helper.ItemTouchHelper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.ran.todolist.R
import com.ran.todolist.utils.recyclerview.OnRecyclerAdapterEventListener
import com.ran.todolist.utils.recyclerview.SimpleItemTouchHelperCallback
import kotlinx.android.synthetic.main.fragment_todolist.view.*

private const val ARG_TAB_KEY = "ARG_TAB_KEY"

class TodoListFragment : Fragment(), OnRecyclerAdapterEventListener {

    private var categoryId = 0L
    private lateinit var itemTouchHelper: ItemTouchHelper
    private val adapter by lazy {
        TodoListAdapter(activity!!.applicationContext, categoryId)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            categoryId = it.getLong(ARG_TAB_KEY)
        }
    }

    companion object {
        fun newInstance(categoryId: Long): TodoListFragment {
            return TodoListFragment().apply {
                arguments = Bundle().apply {
                    putLong(ARG_TAB_KEY, categoryId)
                }
            }
        }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_todolist, container, false)

        view.recycler_view.layoutManager = LinearLayoutManager(activity)
        view.recycler_view.adapter = adapter

        adapter.setOnRecyclerAdapterEventListener(this)
        val callback = SimpleItemTouchHelperCallback(adapter)
        itemTouchHelper = ItemTouchHelper(callback)
        itemTouchHelper.attachToRecyclerView(view.recycler_view)

        return view
    }

    override fun onItemClicked(position: Int) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun onItemLongClicked(position: Int) {
        val fragment = TodoDeleteFragment()

        fragment.currentItemId = adapter.getItem(position)?.id
        fragment.show(activity?.supportFragmentManager, "TodoInsertFragment")
    }

    override fun onDragStarted(viewHolder: RecyclerView.ViewHolder) {
        itemTouchHelper.startDrag(viewHolder)
    }
}

Kotlin Notification 예제

|

NotificationHandler.kt

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import com.ran.todolist.R

class NotificationHandler(private val ctx: Context) {

    private val notificationManager: NotificationManager by lazy {
        ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    }

    companion object {
        private const val CHANNEL_ID = "com.ran.todolist"
        private const val NOTIFICATION_ID = 1001
    }

    fun createNotificationChannel(id: String, name: String, description: String) {
        val importance = NotificationManager.IMPORTANCE_LOW
        val channel = NotificationChannel(id, name, importance)

        channel.description = description
        channel.enableLights(true)
        channel.lightColor = Color.RED
        channel.enableVibration(true)
        channel.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
        notificationManager.createNotificationChannel(channel)
    }

    fun showNotification(contentText: String, resultIntent: Intent) {
        val pendingIntent = PendingIntent.getActivity(ctx, 0, resultIntent, 0)

        val notification = Notification.Builder(ctx, CHANNEL_ID)
                .setContentText(contentText)
                .setSmallIcon(R.drawable.ic_launcher)
                .setChannelId(CHANNEL_ID)
                .setContentIntent(pendingIntent)
                .build()

        notification.flags = Notification.FLAG_NO_CLEAR

        notificationManager.notify(NOTIFICATION_ID, notification)
    }

    fun dismissNotification() {
        notificationManager.cancel(NOTIFICATION_ID)
    }
}


MainActivity.kt

class MainActivity : AppCompatActivity() {
    private val notificationHandler: NotificationHandler by lazy {
        NotificationHandler(applicationContext)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        notificationHandler.createNotificationChannel("ran", "ran", "ran")
        val resultIntent = Intent(this, this@MainActivity::class.java)
        notificationHandler.showNotification("이건 연습이에요", resultIntent)
    }
}

Kotlin TabLayout에 CustomView 적용하는 예제

|

item_selected_tab.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:orientation="horizontal">

  <TextView
    android:id="@+id/title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="10dp"
    android:layout_gravity="center"
    android:maxWidth="@dimen/tab_max_width"
    android:minWidth="20dp"
    android:singleLine="true"
    android:textColor="@color/textColorPrimary"
    android:textSize="16sp"/>

  <Button
    android:id="@+id/remove_button"
    android:layout_width="24dp"
    android:layout_height="24dp"
    android:layout_gravity="center"
    android:background="@drawable/btn_remove"/>
  
</LinearLayout>


MainActivity.kt

class MainActivity : AppCompatActivity(), TabLayout.OnTabSelectedListener {

    private val tabLayout: TabLayout by lazy {
        findViewById<TabLayout>(R.id.tabs)
    }

    private val adapter: CategoryPagerAdapter by lazy {
        CategoryPagerAdapter(supportFragmentManager)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val toolbar = findViewById<Toolbar>(R.id.toolbar)

        setSupportActionBar(toolbar)

        viewpager.adapter = adapter
        tabLayout.setupWithViewPager(viewpager)
        tabLayout.addOnTabSelectedListener(this)

        add_tab.setOnClickListener {
            showEditCategoryNameDialog(null)
        }
    }

    override fun onTabReselected(tab: TabLayout.Tab?) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun onTabUnselected(tab: TabLayout.Tab?) {
        tab?.customView = null
    }

    override fun onTabSelected(tab: TabLayout.Tab?) {
        val view = LayoutInflater.from(applicationContext).inflate(R.layout.item_selected_tab, null)
        todoInsertFragment.currentTabPosition = tab?.position?.toLong() ?: 0

        val tabInfo = adapter.getTabInfo(tab?.position ?: 0)
        view.title.text = tabInfo?.name

        view.title.setOnLongClickListener(object : View.OnLongClickListener {
            override fun onLongClick(v: View?): Boolean {
                showEditCategoryNameDialog(tabInfo)
                return true
            }

        })

        view.remove_button.setOnClickListener {
            if (tabInfo != null) showDeleteCategoryDialog(tabInfo)
        }

        tab?.customView = view
    }

    private fun showEditCategoryNameDialog(tabInfo: TabInfo?) {
        val dialogLayout = LayoutInflater.from(applicationContext).inflate(R.layout.dialog_edit_name, null)
        val editText = dialogLayout.findViewById<EditText>(R.id.editText)
        editText.setText(tabInfo?.name ?: "")

        AlertDialog.Builder(this)
                .setTitle("이름을 입력하세요.")
                .setView(dialogLayout)
                .setPositiveButton("OK") { _, _ ->
                    if (tabInfo != null) {
                        ModelManager.instance.updateTabInfo(tabInfo.id, editText.text.toString())
                    } else {
                        ModelManager.instance.addTabInfo(editText.text.toString())
                        selectLastTab()
                    }
                }
                .show()
    }

    private fun showDeleteCategoryDialog(tabInfo: TabInfo) {
        AlertDialog.Builder(this)
                .setTitle("Category 삭제")
                .setMessage("${tabInfo.name} 항목을 정말 삭제하시겠습니까?")
                .setPositiveButton("확인") { _, _ ->
                    ModelManager.instance.deleteTabInfo(tabInfo.id)
                    selectNextTab(tabLayout.selectedTabPosition)
                }
                .show()
    }
}

lateinit 및 lazy()

|

lateinit

lateinit는 다음과 같은 특징이 있습니다.

  • var 변수에만 사용 가능
  • Non-null 데이터 타입에만 가능
  • primitive type에는 사용할 수 없음
  • 클래스 생성자에서 사용 못함
  • 로컬 변수로 사용 못함

그럼, 어떤 경우에 사용해야 할까요? 코틀린은 NULL 체크에 대한 검사가 엄격하기 때문에

class Sample {
    private var name: String
}

와 같은 코드는 Property must be initialized or be abstract 오류가 발생합니다.

실제로 초기화 때 값을 지정해주고 코드를 작성하는 것이 더욱 좋지만, 그렇지 못한 경우가 많이 발생합니다. 그럴 때는 아래와 같이 lateinit 키워드를 변수 앞에 적어주면 변수 초기화를 나중에 할 수 있게 됩니다.

class Sample {
    private lateinit var name: String
}


lazy

lazy() 함수는 lateinint와 비슷한 역할을 하는데 다음과 같은 특징이 있습니다.

  • val 변수에만 사용 가능
  • primitive type에도 사용 가능
  • Non-null, Nullable 둘 다 사용 가능
  • 클래스 생성자에서 사용 못함
  • 로컬 변수로 사용 가능
private val lazyExample: String by lazy {
    println("lazyExample - init()")

    "[lazy] lazyExample is initialized."
}

fun test() {
    println("Start...")

    println("1st : $lazyExample")
    println("2nd : $lazyExample")
    println("3rd : $lazyExample")
}

실행 결과는 다음과 같습니다.

Start...
lazyExample - init()
1st : [lazy] lazyExample is initialized.
2nd : [lazy] lazyExample is initialized.
3rd : [lazy] lazyExample is initialized.

위 결과와 같이, 처음 호출될 시점에 초기화를 한 번 하며 그 이후로는 그 결과값만 사용하는 것을 확인할 수 있습니다.

람다(Lambda)

|

람다 함수는 다음과 같은 형태로 사용할 수 있습니다.

(매개 변수) -> { TODO("...) }

실제 코드로 예를 들면 다음과 같습니다.

button.setOnClickListener((v) -> {
    TODO("onClicked !!")
})