안녕하세요. 9기 SSAFYcial 오희주입니다.
앞으로 다음 기수에 모바일 트랙에 들어오고 싶은 분, 그리고 모바일 반이 궁금하신 분들을 위해서 모바일 트랙을 진행하며 나온 결과물을 공개합니다. 싸피에서 직접적으로 진행한 프로젝트 같은 결과물은 공개할 수 없기 때문에 배운 것을 복습하는 차원에서 진행한 사이드 프로젝트 코드를 가져왔습니다.
https://github.com/noion0511/Log.M
https://play.google.com/store/apps/details?id=com.likewhile.meme&hl=ko&gl=US
1학기를 마치며 버전을 2로 업데이트하게 되었습니다. 싸피를 다니며 남는 시간에 개발을 하다보니까 많은 기능을 추가하지는 못했지만, 많은 오류들을 잡고 기능을 추가하게 되었습니다.
LogM version2(7)의 추가 및 수정된 기능은 다음과 같습니다.
1. 처음 디비를 생성했을 때, 예시 메모를 생성하기
2. 리스트 메모의 기능 수정
- 리스트 아이템은 한줄만 작성이 가능하도록 수정
- 리스트 아이템 순서를 바꾸기 위해 길게 선택했을 시, 색 변경 추가
- 리스트 아이템 삭제 스와이프 시, 선택된 아이템 색 변경 추가
- 리스트 아이템 교환 기능 수정
- 리스트 메모 저장 애니메이션 추가
3. 메모에 사진 첨부 기능 추가
4. 메모 정렬 기능 중 시간에 따른 정렬 부분에서 단위를 날짜에서 시간으로 수정
5. 달력 모드에서 메모를 생성하지 않은 날에도 마커가 찍히는 오류 수정
6. 화면 가로세로 회전 막기
일단 메모 한 개가 기본으로 앱 다운했을 때, 화면에 아무것도 없는 상황보다 환영하는 메모가 하나 있는 것이 더 좋을 것 같다는 생각이 들어서 먼저 생성되어 있도록 수정했습니다.
class MemoDBHelper(val context: Context) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_TABLE_SQL)
val memoItem = TextMemoItem(title = "first Memo", content = "hello, world")
insertMemo(memoItem, db)
}
...
}
배웠던 ItemTouchHelper.Callback에 관한 코드를 복습하며, 작성 리스트의 아이템들의 순서를 바꾸고, 삭제할 수 있도록 했습니다. 이러한 기능이 있는 리스트를 보며 후에 제 앱에도 똑같은 기능을 넣고 싶다고 생각한 적이 있는데 이번에 실현하게 되었습니다.
class ListItemTouchHelperCallback(private val adapter: ListAdapter) : ItemTouchHelper.Callback() {
private var enabled: Boolean = true
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
//리싸이클러뷰의 아이템 타입을 확인해서 버튼이 아니라 edittext면 교환 및 삭제가 가능
return if (enabled && viewHolder.itemViewType == ListAdapter.ITEM_TYPE_NORMAL) {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
val swipeFlags = ItemTouchHelper.END
makeMovementFlags(dragFlags, swipeFlags)
//아이템 타입이 플러스 버튼이라면 마지막 위치에 고정
} else {
makeMovementFlags(0, 0)
}
}
//아이템을 교환하는 함수
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
//교환하는 a와 b 중 플러스 버튼이 있다면 교환 금지
if(target.itemViewType == ListAdapter.ITEM_TYPE_ADD_BUTTON || viewHolder.itemViewType == ListAdapter.ITEM_TYPE_ADD_BUTTON) {
return false
}
//아니라면 리싸이클러뷰 오댑터의 onItemMove 함수를 실행하여 교환
adapter.onItemMove(viewHolder.layoutPosition, target.layoutPosition)
viewHolder.itemView.setBackgroundColor(ContextCompat.getColor(viewHolder.itemView.context, R.color.teal_700))
return true
}
//선택된 리스트 아이템의 색을 변화
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
viewHolder?.itemView?.setBackgroundColor(ContextCompat.getColor(viewHolder.itemView.context, R.color.teal_700))
}
//스와이프된 아이템을 삭제
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.layoutPosition
adapter.removeItem(position)
}
//소속된 아이템의 롱클릭 드래그를 허용
override fun isLongPressDragEnabled(): Boolean {
return true
}
//소속된 아이템의 스와이프를 허용
override fun isItemViewSwipeEnabled(): Boolean {
return true
}
//선택해제 아이템의 색을 원래대로 복구
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.setBackgroundColor(Color.WHITE)
}
//수정모드와 읽기모드를 구분하는 트리거
fun setEnabled(enabled: Boolean) {
this.enabled = enabled
}
}
이 외에도 그전까지는 앱이 세로에서 가로로 전환되면 기존의 화면을 지우고, 새 화면을 생성한다는 생각을 해본 적이 없었습니다. 그냥 화면이 가로에서 세로로 또는 그 반대로 전환되는거라고만 생각을 했고 그래서 앱이 강제종료되는 버그를 생성해냈습니다. 이 버그를 고치기 위해서 메모앱의 가로세로 회전을 막았습니다.
<activity
android:name=".ui.view.MemoEditActivity"
android:exported="false"
android:windowSoftInputMode="adjustResize"
android:screenOrientation="portrait">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.view.MainActivity" />
</activity>
<activity
android:name=".ui.view.MainActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
그리고 바인딩을 위해 어댑터를 만들어서 사용했습니다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="com.likewhile.meme.data.model.TextMemoItem" />
<variable
name="memo"
type="com.likewhile.meme.data.model.TextMemoItem" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:elevation="2dp">
<LinearLayout
android:id="@+id/widget_memo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/textViewTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_weight="1"
android:text="@{memo.title}"
android:textColor="@color/black"
android:textSize="16sp"
android:singleLine="true"/>
<ImageView
android:id="@+id/fixedIconImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:contentDescription="@string/fixed_icon_description"
app:srcCompat="@drawable/baseline_push_pin_24"
app:visibility="@{memo.isFixed ? View.VISIBLE : View.GONE}" />
</LinearLayout>
<TextView
android:id="@+id/textViewContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{memo.content}"
android:textColor="@color/black"
android:textSize="14sp"
android:maxHeight="200dp"/>
<TextView
android:id="@+id/textViewDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:formattedDate="@{memo.date}"
android:textColor="@color/black"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>
@BindingAdapter("formattedDate")
fun TextView.setFormattedDate(date: Date?) {
date?.let {
text = DateFormatUtil.formatDate(it)
} ?: run {
text = ""
}
}
더 많은 코드는 깃허브를 참고 바랍니다.
싸피에서 체계적으로 모바일 개발에 대해서 배우며 독학할 때와는 달리 점처럼 찍혀있던 지식들을 이어가며 학습할 수 있었습니다. 왜 이렇게 사용하는 가에 궁금했던 부분에 대해서도 배울 수 있었고, 혼자 공부하며 알아차리지 못한 오류가 발생하는 부분에 대해서도 알 수 있었습니다.
대학교 4년동안 키운 실력을 두배로 곱할 수 있었던 반년인 것 같습니다. 다른 분들에게도 싸피 모바일 트랙을 추천합니다.
싸피 모바일 트랙과 로그엠에 많은 관심 부탁드립니다 ~
'SSAFYcial' 카테고리의 다른 글
싸피 9기 2학기 공통 프로젝트 후기 - 우수상 (0) | 2023.08.28 |
---|---|
[7월 기획 기사] 구미 캠퍼스 학생들을 위한 앱을 소개합니다! (0) | 2023.07.24 |
[5월 기획 기사] 남은 반년이 기대되는 이유 (0) | 2023.05.30 |
[4월 기획 기사] 모바일반 최초 취업자 인터뷰 (0) | 2023.04.27 |
[3월 기획 기사] 안드로이드, 일주일 배운 결과물을 공개합니다! (0) | 2023.03.30 |