-
Notifications
You must be signed in to change notification settings - Fork 75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Team-01][Android] Todo 화면 및 레포지토리 구현 #151
base: team-01
Are you sure you want to change the base?
Changes from 1 commit
c7119d3
fd11ce4
b596a2d
eb126f0
dd55cf2
8ff7958
0708b5a
b3ee3c3
120d3b9
4e18c86
4860095
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package com.example.todo_list.tasks | ||
|
||
import android.graphics.Canvas | ||
import android.util.Log | ||
import android.view.View | ||
import androidx.recyclerview.widget.ItemTouchHelper | ||
import androidx.recyclerview.widget.RecyclerView | ||
import com.example.todo_list.R | ||
import kotlin.math.min | ||
|
||
class ItemTouchHelperCallback(val listener: ItemTouchHelperListener) : ItemTouchHelper.Callback() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 클래스가 왜 필요했는지에 대한 주석이 있어도 좋습니다. 리사이클러뷰를 그냥 사용하지 않고 추가 기능을 넣은 이유라던가, 이게 없으면 무엇이 동작하지 않는다던가 등의 설명정도면 어떨까요? 혹은, 클래스 이름 자체를 어떤 역할을 하는것인지 구체적으로 적어봐도 좋겠네요. |
||
|
||
private var clamp = 0f | ||
private var currentDx = 0f | ||
|
||
override fun getMovementFlags( | ||
recyclerView: RecyclerView, | ||
viewHolder: RecyclerView.ViewHolder | ||
): Int { | ||
Log.d("TAGcode", "${ItemTouchHelper.LEFT}") | ||
return makeMovementFlags(0, ItemTouchHelper.LEFT) | ||
} | ||
|
||
override fun onMove( | ||
recyclerView: RecyclerView, | ||
viewHolder: RecyclerView.ViewHolder, | ||
target: RecyclerView.ViewHolder | ||
): Boolean { | ||
return listener.onItemMove(viewHolder.adapterPosition, target.adapterPosition) | ||
} | ||
|
||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { | ||
Log.d("TAGonSwiped1", "$direction") | ||
Log.d("TAGonSwiped2", "${viewHolder.adapterPosition}") //<< 몇번째 목록을 조작했는지 | ||
// direction = 방향 >> 왼쪽으로 스와이프했을때 작동 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. direction 에 대한 주석 좋아보입니다! 조금 자세하게 적으면 direction 범위가 무엇일 때 방향이 어딘지 적어볼 수도 있겠네요. |
||
listener.onItemSwipe(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 view = getView(viewHolder) | ||
val isClamped = getTag(viewHolder) | ||
val newX = clampViewPositionHorizontal( | ||
dX, | ||
isClamped, | ||
isCurrentlyActive | ||
) // newX 만큼 이동(고정 시 이동 위치/고정 해제 시 이동 위치 결정) | ||
|
||
// 고정시킬 시 애니메이션 추가 | ||
if (newX == -clamp) { | ||
getView(viewHolder).animate().translationX(-clamp).setDuration(100L).start() | ||
return | ||
} | ||
|
||
currentDx = newX | ||
getDefaultUIUtil().onDraw( | ||
c, | ||
recyclerView, | ||
view, | ||
newX, | ||
dY, | ||
actionState, | ||
isCurrentlyActive | ||
) | ||
} | ||
} | ||
|
||
fun setClamp(clamp: Float) { this.clamp = clamp } | ||
private fun getView(viewHolder: RecyclerView.ViewHolder) : View = viewHolder.itemView.findViewById(R.id.todo_item) | ||
private fun getTag(viewHolder: RecyclerView.ViewHolder) : Boolean = viewHolder.itemView.tag as? Boolean ?: false | ||
|
||
private fun clampViewPositionHorizontal( | ||
dX: Float, | ||
isClamped: Boolean, | ||
isCurrentlyActive: Boolean | ||
) : Float { | ||
// RIGHT 방향으로 swipe 막기 | ||
val max = 0f | ||
|
||
// 고정할 수 있으면 | ||
val newX = if (isClamped) { | ||
// 현재 swipe 중이면 swipe되는 영역 제한 | ||
if (isCurrentlyActive) | ||
// 오른쪽 swipe일 때 | ||
if (dX < 0) dX/3 - clamp | ||
// 왼쪽 swipe일 때 | ||
else dX - clamp | ||
// swipe 중이 아니면 고정시키기 | ||
else -clamp | ||
} | ||
// 고정할 수 없으면 newX는 스와이프한 만큼 | ||
else dX / 2 | ||
|
||
// newX가 0보다 작은지 확인 | ||
return min(newX, max) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.example.todo_list.tasks | ||
|
||
interface ItemTouchHelperListener { | ||
fun onItemMove(from_position: Int, to_position: Int): Boolean | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이쪽 변수 이름도 되도록이면 컨벤션에 맞게 부탁드립니다! |
||
fun onItemSwipe(position: Int) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package com.example.todo_list.tasks | ||
|
||
import android.view.LayoutInflater | ||
import android.view.ViewGroup | ||
import androidx.databinding.DataBindingUtil | ||
import androidx.recyclerview.widget.DiffUtil | ||
import androidx.recyclerview.widget.ListAdapter | ||
import androidx.recyclerview.widget.RecyclerView | ||
import com.example.todo_list.R | ||
import com.example.todo_list.databinding.TodoItemBinding | ||
import com.example.todo_list.tasks.data.Task | ||
|
||
class TodoAdapter : ListAdapter<Task, TodoAdapter.TodoViewHolder>(diffUtil), | ||
ItemTouchHelperListener { | ||
inner class TodoViewHolder(private val binding: TodoItemBinding) : RecyclerView.ViewHolder(binding.root) { | ||
fun bind(task: Task) { | ||
binding.task = task | ||
} | ||
} | ||
|
||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = | ||
DataBindingUtil.inflate<TodoItemBinding>( | ||
LayoutInflater.from(parent.context), | ||
R.layout.todo_item, | ||
parent, | ||
false | ||
).let { | ||
TodoViewHolder(it) | ||
} | ||
|
||
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) { | ||
val item = getItem(position) | ||
holder.bind(item) | ||
} | ||
|
||
override fun onItemMove(from_position: Int, to_position: Int): Boolean { | ||
return true | ||
} | ||
|
||
override fun onItemSwipe(position: Int) { | ||
} | ||
} | ||
|
||
private val diffUtil = object : DiffUtil.ItemCallback<Task>() { | ||
override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean { | ||
return oldItem.id == newItem.id | ||
} | ||
|
||
override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean { | ||
return oldItem == newItem | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.example.todo_list.tasks | ||
|
||
import android.widget.TextView | ||
import androidx.databinding.BindingAdapter | ||
|
||
@BindingAdapter("setTitle") | ||
fun setTitle(view: TextView, title: String) { | ||
view.text = title | ||
} | ||
|
||
@BindingAdapter("setContent") | ||
fun setContent(view: TextView, content: String) { | ||
view.text = content | ||
} | ||
|
||
@BindingAdapter("setUser") | ||
fun setUser(view: TextView, user: String) { | ||
view.text = user | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 각 값들이 String이 주입되는 형태인데, BindingAdapter을 새로 사용하신 이유가 있으실까요? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package com.example.todo_list.tasks | ||
|
||
import android.content.Context | ||
import android.util.AttributeSet | ||
import android.view.LayoutInflater | ||
import android.widget.ImageButton | ||
import android.widget.TextView | ||
import androidx.constraintlayout.widget.ConstraintLayout | ||
import androidx.recyclerview.widget.ItemTouchHelper | ||
import androidx.recyclerview.widget.RecyclerView | ||
import com.example.todo_list.R | ||
import com.example.todo_list.tasks.data.Task | ||
|
||
class TasksView(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) { | ||
|
||
private lateinit var tvTitle: TextView | ||
private lateinit var tvBadgeCount: TextView | ||
private lateinit var btnAddTask: ImageButton | ||
private lateinit var recyclerViewTodo: RecyclerView | ||
private val todosAdapter = TodoAdapter() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 기존 코드처럼 뷰 변수를 따로 선언하고 커스텀뷰 구성을 할 수도 있으나, 요즘은 ViewBinding 을 이용하여 이런 코드들을 간단하게 만드는 편입니다. 즉 Activity/Fragment 등이 아닌 커스텀 뷰 에서도 바인딩을 사용 할 수 있습니다. 사용하는게 어렵지 않고 꽤 많은 코드를 줄일 수 있는데, 학습이라 생각하고 ViewBinding 을 사용하도록 구현해보는것을 추천드립니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. binding을 이용해 뷰를 참조하도록 수정했습니다. |
||
init { | ||
initViews() | ||
initAttributes(attrs) | ||
} | ||
|
||
private fun initAttributes(attrs: AttributeSet?) { | ||
context.theme.obtainStyledAttributes( | ||
attrs, | ||
R.styleable.TodoView, | ||
0, | ||
0).apply { | ||
try { | ||
tvTitle.text = getString(R.styleable.TodoView_title) | ||
tvBadgeCount.text = getString(R.styleable.TodoView_badge_count) | ||
} finally { | ||
recycle() | ||
} | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 커스텀 뷰 요소를 만드는 기본적인 방법입니다! 추가로, 위에 말씀드린 뷰바인딩/데이터 바인딩을 이용한 방법으로 구현하면 이런 필드 또한 별도 Styleable 로 선언하지 않고 사용 할 수도 있습니다. binding 레이아웃에서 사용하는 뷰는 해당 커스텀 뷰의 필드를 참조하여 xml 에서 설정 가능한 어트리뷰트를 자동으로 만들어줍니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 부분은 머리로는 이해가 되는데 코드가 안따라줍니다...🥲 좀 더 고민해보고 적용해보도록 하겠습니다! |
||
|
||
private fun initViews() { | ||
val layoutInflater = context.getSystemService( | ||
Context.LAYOUT_INFLATER_SERVICE | ||
) as LayoutInflater | ||
|
||
addView( | ||
layoutInflater.inflate(R.layout.tasks_view, this, false) | ||
) | ||
|
||
tvTitle = findViewById(R.id.todo_title) | ||
tvBadgeCount = findViewById(R.id.todo_badge) | ||
btnAddTask = findViewById(R.id.btn_task_add) | ||
recyclerViewTodo = findViewById(R.id.recyclerview_todo) | ||
|
||
recyclerViewTodo.adapter = todosAdapter | ||
recyclerViewTodo.setHasFixedSize(true) | ||
val touchHelper = ItemTouchHelperCallback(todosAdapter) | ||
val helper = ItemTouchHelper(touchHelper) | ||
helper.attachToRecyclerView(recyclerViewTodo) | ||
} | ||
|
||
fun addTasks(tasks: List<Task>) { | ||
todosAdapter.submitList(tasks) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<vector android:height="24dp" android:tint="#BAB8B8" | ||
android:viewportHeight="24" android:viewportWidth="24" | ||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> | ||
</vector> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<shape xmlns:android="http://schemas.android.com/apk/res/android" | ||
android:shape="oval"> | ||
|
||
<solid android:color="@color/number_badge_bg" /> | ||
|
||
</shape> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요런 기능은 별도의 함수로 묶으면 보기에 조금 더 편해서 나중에 좋은 점이 있습니다. 혹은 task 가 바뀌게 되더라도 별도의 함수로 묶어 놓으면 바꿔야 할 부분이 명확하게 보일거 같습니다. 분리된 메소드의 위치는 activity/viewmodel 두 군데가 있을텐데 어느 부분이 더 일관적일지 보셔서 추가하시면 좋을거같네요~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ViewModel을 앱 전체에서 사용할 수 있도록 파일명 및 위치를 변경했습니다.
ViewModel에서 함수로 묶어 사용하는 것이 더 나을 것 같다고 판단해서 ViewModel에 구현했습니다.
감사합니다!