새로운 앱 만들기가 시작되었습니다~~!!
이번에 뭘할까 정말 고민이 많았고 그 사이에 시간도 없어서 미뤄지다가 드디어 오늘 정해졌습니다.
제가 요즘 많이 사용하고 있는 메모 앱을 선택해서 최대한 비슷하게 따라해보고 가능하다면 다른 기능들도 추가적으로 넣어보려고 합니다.
이번 앱 만들기를 통해서 UI 구성이라던가 화면 간 데이터 주고 받는 것에 대해 좀 깊이 알 수 있지 않을까 기대 반 피로 반입니다.
전체 코드는 파일이랑 코드들이 정리가 좀 되면 깃허브 링크로 올릴 예정입니다.
1. 커스텀 Toolbar 만들기
아... 이거 오래 걸렸다.
내가 원하는 모양으로 하려니까 커스텀을 약간 해야하더라
내용이 어려운 게 아니라 관련 정보를 찾는 게 힘들었다.
내가 한 방식은 가장 기본적은 틀은 레이아웃에서 만들고 이외의 세부적인 것(버튼, 타이틀)들은 코틀린 코드 상에서 처리했다.
먼저는 toolbar_layout.xml을 만들고 Toolbar 위젯을 넣었다.
<!-- toolbar_layout.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/white"
app:titleTextColor="@color/black">
</androidx.appcompat.widget.Toolbar>
?attr은 현재 테마에 지정된 값을 사용하라는 의미이고 현재 테마는
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
이다.
이 테마에서 설정한 actionBarSize 값을 사용하게 된다.
그런데 NoActionBar로 테마를 한 이유는 Toolbar를 사용하려면 기존 액티비티의 ActionBar를 사용하지 않아야하기 때문이다.
그리고나서 메인 액티비티의 레이아웃으로 include 한다.
<!-- activity_main.xml -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar_layout" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#979797" />
</LinearLayout>
이제는 MainActivity로 넘어간다.
여기서는 Toolbar 객체를 연결하고 세팅하고 추가적인 버튼이 나오도록 한다.
가장 먼저 Toolbar 객체를 생성&초기화
val toolbar: Toolbar = findViewById(R.id.toolbar)
액션바로 Toolbar를 사용하도록 세팅
2번째 줄의 메소드는 기존의 액션바의 타이들이 나오지 않도록 설정하여 Toolbar에서 설정하는 타이틀이 나오도록 한다.
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.title = "전체"
액션바에 있는 뒤로가기 버튼을 사용가능하게 하고 이를 뒤로가기로 사용하는 것이 아닌 네비게이션 메뉴를 불러오는 것으로 사용한다.
이미지도 변경한다.
val actionBar: ActionBar? = supportActionBar
actionBar?.setDisplayHomeAsUpEnabled(true)
actionBar?.setHomeAsUpIndicator(R.drawable.ic_menu)
액션바에 메뉴를 추가해줬다.
메뉴는 menu.xml을 만들어서 그 안에 항목들을 넣어줬다.
여기서 알게 된 것은 showAsAction 속성을 통해 메뉴에 할당한 아이콘이 드러나게도 할 수 있고 드러나지 않게도 할 수 있다는 것이다.
여기서 쓴 ifRoom은 액션바에 공간이 있을 때만 아이콘으로 메뉴가 보인다.
<!-- menu.xml -->
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:title="search"
android:id="@+id/btn_search"
android:icon="@drawable/ic_search"
app:showAsAction="ifRoom"/>
<item
android:title="moreFunc"
android:id="@+id/btn_more"
android:icon="@drawable/ic_more_func"
app:showAsAction="ifRoom"/>
</menu>
☆ android:showAsAction 속성에는 네가지 종류가 있다.
1. never : 항상 아이콘으로 표시하지 않음(기본값)
2. ifRoom : 아이콘을 표시할 수 있는 공간이 있다면 아이콘으로 표시
3. withText : 타이틀을 아이콘과 함께 표시
4. always : 항상 아이콘으로 표시
진짜 메뉴를 추가해주는 코드는 아래 코드이다.
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu, menu)
return true
}
액션바 메뉴를 눌렀을 때, 메뉴가 선택되었을 때 호출되는 함수를 만들어서 분기가 일어나도록 했다.
아직 나오지는 않았지만 네비게이션 드로어 내용이 약간 들어가있는데 이건 이따가 또 얘기한다.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
drawerLayout.openDrawer(GravityCompat.START)
}
R.id.btn_more -> {
val bottomSheet = BottomSheet()
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
}
}
return super.onOptionsItemSelected(item)
}
}
관련 전체 코드다.
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val toolbar: Toolbar = findViewById(R.id.toolbar)
...
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.title = "전체"
...
val actionBar: ActionBar? = supportActionBar
actionBar?.setDisplayHomeAsUpEnabled(true)
actionBar?.setHomeAsUpIndicator(R.drawable.ic_menu)
...
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
drawerLayout.openDrawer(GravityCompat.START)
}
R.id.btn_more -> {
val bottomSheet = BottomSheet()
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
}
}
return super.onOptionsItemSelected(item)
}
}
2. 상태바 설정
툴바를 커스텀하고 상태바를 보는데 뭔가 안 어울리더라
툴바는 커스텀이고 상태바는 기본 테마를 그대로 가지고 있어서 그런거였다.
처음에는 그냥 둘까 하다가 방법을 찾기 시작했다.
그렇게 찾은 방법은 다음과 같은 코드를 onCreate() 안에서 작성했다.
window.apply {
statusBarColor = Color.WHITE
WindowInsetsControllerCompat(this, this.decorView).isAppearanceLightStatusBars = true
}
이렇게 작성함으로써 상태바의 색깔은 하얀색, 아이콘들은 검은색이 되었다.
☆ 상태바의 색깔은 API 21, 아이콘과 텍스트 색깔은 API 23부터 변경이 가능
☆ 위의 방식과 달리 themes.xml을 수정하여 변경하는 방법
<item name="android:statusBarColor">@color/white</item>
<item name="android:windowLightStatusBar">true</item> ← true : 검은색 / false : 하얀색
3. 커스텀 네비게이션 드로어(미완)
이건 아직 다 완성되지는 않았지만 기본 동작은 완성이 된 상태이기 때문에 적어놓는다.
처음에는 그냥 안드로이드 스튜디오에서 제공하는 네비게이션뷰를 사용해서 했는데 내가 원하는 형태로는 구현이 안 되더라
그래서 다시 커스텀을 시작했다.
뭔가 엄청나게 만지지는 않았다.
먼저 헤더 부분 레이아웃을 만들었다.
<!-- drawer_header_layout.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="100dp"
>
<!-- 헤더에 들어갈 위젯들 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="30dp"
android:paddingVertical="15dp">
<ImageView
android:id="@+id/img_settings"
android:layout_width="30dp"
android:layout_height="30dp"
android:background="@color/white"
android:src="@drawable/ic_settings"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 수평 경계선 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#979797"
android:layout_marginHorizontal="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
그 다음 메인 액티비티의 레이아웃으로 가서 LinearLayout 안에 include 했다.
이 LinearLayout은 layout_gravity 속성을 start로 주었다.
아! 그 전에 메인 액티비티의 레이아웃은 DrawerLayout으로 설정되어야한다.
☆ DrawerLayout은 네비게이션 아이콘을 클릭하거나 사이드를 드래그했을 때 자식 레이아웃 중 선택한 하나를 펼치고 접는 기능을 하게 해주는 레이아웃이다.
☆ 펼치고 접을 자식 레이아웃에서 layout_gravity 속성을 start 또는 end로 해야한다.
☆ start로 하면 레이아웃이 왼쪽에서 오른쪽으로 펼쳐지고 end로 하면 오른쪽에서 왼쪽으로 펼쳐진다.
☆ 스와이프로 Drawer가 열리는 걸 막으려면
(DrawerLayout 객체).setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)를 넣으면 된다.
코드는 다음과 같다.
<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar_layout" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#979797" />
</LinearLayout>
<LinearLayout
android:id="@+id/navigation_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@color/white"
android:fitsSystemWindows="true">
<include layout="@layout/drawer_header_layout"/>
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>
DrawerLayout 내에서 먼저 메인 액티비티에 표시되는 뷰들을 위에 쓰고 마지막으로 Drawer로 사용될 뷰를 배치하면 된다.
여기까지 하고 애뮬레이터를 실행시켜보면 잘 동작한다.
그렇지만 추가적으로 몇 가지를 추가해야하지만 오늘은 한 가지만 추가했다.
바로 뒤로가기 버튼에 관련된 사항이다.
현재 상태에서 네비게이션 드로어를 펼치고 뒤로가기 버튼을 누르면 네비게이션 드로어가 닫히는 게 아니라 앱이 꺼진다.
이런 상황은 막아야하기 때문에 콜백 함수를 사용했다.
private val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
} else {
finish()
}
}
}
이렇게 콜백 함수를 만들고
this.onBackPressedDispatcher.addCallback(this, callback)
onCreate() 안에서 onBackPressedDispatcher에 이 콜백 함수를 추가시켰다.
이렇게 함으로써 네비게이션 드로어가 열려있다면 닫고 열려있지 않다면 기존에 하던 동작을 한다.
예전에는 onBackPressed()를 썼다고 한다.
API 33부터 사용하지 말라고 했단다.
대신 위에서 사용한 onBackPressedDispatcher와 OnBackPressedCallback을 사용하라고 권장한다.
이를 통해서 사용자가 뒤로가기 버튼을 누르거나 제스쳐를 하는 경우 콜백 함수가 호출되고 코드가 실행된다.
onBackPressedDispatcher뒤에 addCallback 메소드를 붙이는데 addCallback 메소드를 통해 여러 개의 콜백 함수를 호출할 수 있다. 물론 콜백함수는 추가된 순서의 역순으로 호출되어 호출된 순서대로 이벤트를 처리할 기회를 얻는다.
OnBackPressedCallback의 매개변수로 true가 들어가는데 이건 아래의 handleOnBackPressed 함수를 실행하겠다는 의미가 된다. false라면 handleOnBackPressed 함수가 호출되지 않고 기존의 뒤로가기 버튼을 눌렀을 때처럼 작동해 액티비티가 finish된다.
OnBackPressedCallback 함수를 비활성화 하고 싶다면 isEnabled를 false로 주면된다.
4. Bottom Sheet Dialog
내가 따라 만들어보는 앱에서 필요한 기능이었다.
그래서 찾아서 만들어봤다.
Bottom Sheet Dialog는 Modal과 Persistent가 있는데 나는 Modal Bottom Sheet로 만들었다.
가장 먼저는 BottomSheetDialogFragment를 상속받는 클래스를 만들었다.
그리고 onCreateView에서 BottomSheet의 레이아웃을 inflate했다.
onViewCreated()에서는 창을 닫을 때 이미지를 눌러 닫을 수 있도록 코드를 추가했다.
Bottom Sheet Dialog는 창을 닫을 때 dismiss()를 사용하더라
// BottomSheet.kt
class BottomSheet : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val clear: ImageView = view.findViewById(R.id.img_bottomSheet_clear)
clear.setOnClickListener {
dismiss()
}
}
}
이 창을 띄우는 버튼은 아까 위에 있던 onOptionsItemSelected에 있다.
그 버튼이 눌리면 BottomeSheet 객체가 만들어지고 화면에 띄우게 된다.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
drawerLayout.openDrawer(GravityCompat.START)
}
// BottomSheet 버튼
R.id.btn_more -> {
val bottomSheet = BottomSheet()
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
}
}
return super.onOptionsItemSelected(item)
}
BottomSheet를 구현하는 중에 UI 관련해서 문제가 있었는데 헤더 레이아웃을 감싸는 상위 레이아웃에서 넓이를 match_parent가 아니라 wrap_contents로 해서 그런거였다.(알려주신 튜터님 감사합니다!!)
상위에서 wrap_contents로 설정해서 아래 레이아웃에 영향이 가서 원하는 결과가 나오지 않았다.
이유를 몰라서 상당히 끙끙거렸는데 튜터님은 금방 찾아내시더라...
<결과화면>
'Android > Kotlin' 카테고리의 다른 글
<정리> 메모 앱 만들기 3일차 (0) | 2023.12.19 |
---|---|
<정리> 메모 앱 만들기 2일차 (0) | 2023.12.15 |
<정리> 이상형월드컵 앱 만들기 5일차 (1) | 2023.12.08 |
<정리> 이상형월드컵 앱 만들기 4일차 (1) | 2023.12.04 |
<정리> 이상형월드컵 앱 만들기 3일차 (0) | 2023.12.01 |