▶Do it! 안드로이드 앱 프로그래밍(정재곤 지음) 을 참고하여 제작하였습니다.
※ 개발 방법을 일일히 기록한 강의 형식의 글이 아닙니다. 개발하면서 대략적으로 중요하고, 새로 얻게된 지식 부분만 기록한 글입니다. 자세한 내용은 깃허브 파일 참조.
개발 목표 (구현 기능)
메인 화면 만들기
activity_main.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="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</FrameLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:itemBackground="@color/colorPrimary"
app:itemIconTint="@drawable/item_color"
app:itemTextColor="@drawable/item_color"
app:menu="@menu/menu_bottom"/>
</androidx.constraintlayout.widget.ConstraintLayout>
메인으로 사용할 화면.
프래그먼트를 표시할 프레임레이아웃 배치,
밑 부분에 바텀네비게이션을 만들고
메뉴로 사용할 레이아웃 만들기
menu_bottom.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:id="@+id/tab1"
app:showAsAction="ifRoom"
android:enabled="true"
android:icon="@drawable/list_48"
android:title="목록"/>
<item
android:id="@+id/tab2"
app:showAsAction="ifRoom"
android:enabled="true"
android:icon="@drawable/write_48"
android:title="작성"/>
<item
android:id="@+id/tab3"
app:showAsAction="ifRoom"
android:enabled="true"
android:icon="@drawable/graph_48"
android:title="통계"/>
</menu>
리스트 화면 만들기
repositories {
maven { url 'https://jitpack.io' }
} // 라이브러리 주소
dependencies{
implementation 'lib.kingja.switchbutton:switchbutton:1.1.8'
// 라디오 버튼
implementation 'com.github.channguyen:rsv:1.0.1'
// 시크바 연속x, 구간 선택
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0-alpha'
// 그래프
}
리스트 화면 구성을 위해 build.gradle(Module:app)에 추가.
fragment_1.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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/fragment1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="60dp"
android:orientation="vertical"
tools:context=".Fragment1">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<lib.kingja.switchbutton.SwitchMultiButton
android:id="@+id/switchButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:padding="8dp"
app:strokeRadius="5dp"
app:strokeWidth="1dp"
app:selectedTab="0"
app:selectedColor="#eb7b00"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:switchTabs="@array/switch_tabs"
app:textSize="14sp" />
<Button
android:id="@+id/todayWriteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="오늘 작성"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/select_button"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="8dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"/>
</LinearLayout>
앞에서 추가한 라디오 버튼으로 버튼 만들고,
리싸이클러뷰를 활용하여 일기를 표시할 것이기 때문에 리싸이클러뷰 사용
note_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_marginBottom="4dp"
app:cardBackgroundColor="#FFFFFFFF"
app:cardCornerRadius="10dp"
app:cardElevation="5dp">
<LinearLayout
android:id="@+id/layout1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="visible">
<ImageView
android:id="@+id/moodImageView"
android:layout_width="54dp"
android:layout_height="54dp"
android:layout_gravity="center_vertical"
android:padding="5dp"
app:srcCompat="@drawable/smile5_48" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/contentsTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="내용"
android:textSize="24sp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<ImageView
android:id="@+id/weatherImageView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentLeft="true"
android:src="@drawable/weather_icon_1" />
<ImageView
android:id="@+id/pictureExistsImageView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginLeft="10dp"
android:layout_toRightOf="@+id/weatherImageView"
android:src="@drawable/picture_128" />
<TextView
android:id="@+id/locationTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="전주시 덕진구"
android:textSize="16sp" />
<TextView
android:id="@+id/dateTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:text="01월 01일"
android:textColor="#FF0000FF"
android:textSize="16sp" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:id="@+id/pictureImageView"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_gravity="center_vertical"
android:layout_margin="10dp"
android:padding="5dp"
android:src="@drawable/noimagefound" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/contentsTextView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="내용"
android:textSize="24sp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<ImageView
android:id="@+id/moodImageView2"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentLeft="true"
android:padding="5dp"
app:srcCompat="@drawable/smile5_48" />
<ImageView
android:id="@+id/weatherImageView2"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginLeft="10dp"
android:layout_toRightOf="@+id/moodImageView2"
android:src="@drawable/weather_icon_1" />
<TextView
android:id="@+id/locationTextView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="전주시 덕진구"
android:textSize="16sp" />
<TextView
android:id="@+id/dateTextView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:text="01월 01일"
android:textColor="#FF0000FF"
android:textSize="16sp" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
리싸이클러뷰에 넣을 아이템을 추가.
내용 중심인 경우와 사진 중심인 경우로 나눔
Note.class
package org.techtown.diary
class Note(var _id : Int?, var weather : String?, var address : String?, var locationX : String?, var locationY : String?,
var contents : String?, var mood : String?, var picture : String?, var createDataStr : String?) {
}
아이템의 데이터를 담아둘 클래스 정의
NoteAdapter.class
package org.techtown.diary
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.note_item.view.*
class NoteAdapter(private val items: ArrayList<Note>) :
RecyclerView.Adapter<NoteAdapter.ViewHolder>(), OnNoteItemClickListener{
lateinit var listener : OnNoteItemClickListener
private var layoutType = 0
// 내용 중심인지 사진 중심인지 판단하기 위한 변수
override fun getItemCount() = items.size
fun getItem(position: Int): Note? {
return items[position]
} // 아이템 반환
fun addItem(item: Note?) {
if (item != null) {
items.add(item)
}
} // 아이템 추가
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflatedView = LayoutInflater.from(parent.context)
.inflate(R.layout.note_item,parent,false)
return ViewHolder(inflatedView,this,layoutType)
} // 새로 만들어준 뷰홀더 생성
override fun onBindViewHolder(holder: NoteAdapter.ViewHolder, position: Int) {
val item = items[position]
holder.bind(item)
holder.setLayoutType(layoutType)
} // 데이터 뷰홀더에 바인딩
fun setOnItemClickListener(listener: OnNoteItemClickListener){
this.listener = listener
}
override fun onItemClick(holder: ViewHolder, view: View, position: Int) {
listener?.onItemClick(holder,view,position)
}
fun switchLayout(position: Int){
layoutType = position
} // 내용,사진 레이아웃 변경 함수
class ViewHolder(itemView : View, listener: OnNoteItemClickListener, layoutType : Int) : RecyclerView.ViewHolder(itemView){
init {
itemView.setOnClickListener(View.OnClickListener {
var position = adapterPosition
listener?.onItemClick(this,itemView,position)
setLayoutType(layoutType)
})
}
fun bind(item : Note){
var mood = item.mood
var moodIndex = Integer.parseInt(mood)
setMoodImage(moodIndex) // 기분 설정
var picturePath = item.picture
if(picturePath != null && !picturePath.equals("")){
itemView.pictureExistsImageView.visibility = View.VISIBLE
itemView.pictureImageView.visibility = View.VISIBLE
itemView.pictureImageView.setImageURI(Uri.parse("file://$picturePath"))
}
else{
itemView.pictureExistsImageView.visibility = View.GONE
itemView.pictureImageView.visibility = View.GONE
itemView.pictureImageView.setImageResource(R.drawable.noimagefound)
}
// 사진 설정
var weather = item.weather
var weatherIndex = Integer.parseInt(weather)
setWeatherImage(weatherIndex) // 날씨 설정
itemView.contentsTextView.text = item.contents
itemView.contentsTextView2.text = item.contents
// 텍스트 설정
itemView.locationTextView.text = item.address
itemView.locationTextView2.text = item.address
// 주소 설정
itemView.dateTextView.text = item.createDataStr
itemView.dateTextView2.text = item.createDataStr
// 날짜 설정
}
fun setMoodImage( moodIndex : Int){
when(moodIndex){
0 -> {
itemView.moodImageView.setImageResource(R.drawable.smile1_48)
itemView.moodImageView2.setImageResource(R.drawable.smile1_48)
}
1 -> {
itemView.moodImageView.setImageResource(R.drawable.smile2_48)
itemView.moodImageView2.setImageResource(R.drawable.smile2_48)
}
2 -> {
itemView.moodImageView.setImageResource(R.drawable.smile3_48)
itemView.moodImageView2.setImageResource(R.drawable.smile3_48)
}
3 -> {
itemView.moodImageView.setImageResource(R.drawable.smile4_48)
itemView.moodImageView2.setImageResource(R.drawable.smile4_48)
}
4 -> {
itemView.moodImageView.setImageResource(R.drawable.smile5_48)
itemView.moodImageView2.setImageResource(R.drawable.smile5_48)
}
else -> {
itemView.moodImageView.setImageResource(R.drawable.smile3_48)
itemView.moodImageView2.setImageResource(R.drawable.smile3_48)
}
}
} // moodIndex에 따른 기분 이미지 출력
fun setWeatherImage(weatherIndex : Int){
when(weatherIndex){
0 -> {
itemView.weatherImageView.setImageResource(R.drawable.weather_icon_1)
itemView.weatherImageView2.setImageResource(R.drawable.weather_icon_1)
}
1 -> {
itemView.weatherImageView.setImageResource(R.drawable.weather_icon_2)
itemView.weatherImageView2.setImageResource(R.drawable.weather_icon_2)
}
2 -> {
itemView.weatherImageView.setImageResource(R.drawable.weather_icon_3)
itemView.weatherImageView2.setImageResource(R.drawable.weather_icon_3)
}
3 -> {
itemView.weatherImageView.setImageResource(R.drawable.weather_icon_4)
itemView.weatherImageView2.setImageResource(R.drawable.weather_icon_4)
}
4 -> {
itemView.weatherImageView.setImageResource(R.drawable.weather_icon_5)
itemView.weatherImageView2.setImageResource(R.drawable.weather_icon_5)
}
5 -> {
itemView.weatherImageView.setImageResource(R.drawable.weather_icon_6)
itemView.weatherImageView2.setImageResource(R.drawable.weather_icon_6)
}
6 -> {
itemView.weatherImageView.setImageResource(R.drawable.weather_icon_7)
itemView.weatherImageView2.setImageResource(R.drawable.weather_icon_7)
}
else -> {
itemView.weatherImageView.setImageResource(R.drawable.weather_icon_1)
itemView.weatherImageView2.setImageResource(R.drawable.weather_icon_1)
}
}
} // weatherIndex에 따른 날씨 이미지 출력
fun setLayoutType(layoutType: Int){
if( layoutType == 0){
itemView.layout1.visibility = View.VISIBLE
itemView.layout2.visibility = View.GONE
}
else if( layoutType == 1){
itemView.layout1.visibility = View.GONE
itemView.layout2.visibility = View.VISIBLE
}
} // 레이아웃 타입에 따라 내용,사진 레이아웃 하나는 비활성화, 하나는 활성화
}
}
리싸이클러뷰를 위한 NoteAdapter 클래스 정의
import kotlinx.android.synthetic.main.note_item.view.* 선언해줌으로써 findviewbyid 사용하지 않고 직접 지정
OnNoteItemClickListener
package org.techtown.diary
import android.view.View
interface OnNoteItemClickListener {
fun onItemClick(holder: NoteAdapter.ViewHolder,view : View, position : Int)
}
아이템클릭에 다른 함수 오버로딩을 위해 인터페이스 정의
Fragment1
package org.techtown.diary
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.fragment_1.view.*
class Fragment1 : Fragment() {
private var _context: Context? = null
lateinit var recyclerView : RecyclerView
lateinit var adapter: NoteAdapter
private var listener: OnTabItemSelectedListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
this._context = context
if(context is OnTabItemSelectedListener){
listener = context
} // is == 자바의 instanceof (자료형의 일치)
} // 프래그먼트가 액티비티에 올라갈 때 호출
override fun onDetach() {
super.onDetach()
if (_context != null){
_context = null
listener = null
}
} // 액티비티에서 내려갈 때
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
var rootView = inflater.inflate(R.layout.fragment_1, container, false) as ViewGroup
// as 형변환
initUI(rootView)
return rootView
}
private fun initUI (rootView : ViewGroup){
rootView.todayWriteButton.setOnClickListener{
listener?.onTabSelected(1)
} // 클릭시 작성 레이아웃으로
rootView.switchButton.setOnSwitchListener { position, tabText ->
adapter.switchLayout(position)
adapter.notifyDataSetChanged()
} // 내용,사진 위주의 레이아웃으로 바꿔주고 갱신
adapter = NoteAdapter(arrayListOf<Note>())
recyclerView = rootView.recyclerView
val layoutManager = LinearLayoutManager(context)
recyclerView.layoutManager = layoutManager
adapter.addItem(
Note(
0,
"0",
"전주시 덕진구",
"123",
"",
"1. 테스트중!",
"0",
"capture1.jpg",
"3월 14일"
)
)
adapter.addItem(
Note(
1,
"1",
"군산시 나운동",
"",
"",
"2. 안녕하세요",
"1",
"capture1.jpg",
"1월 1일"
)
)
adapter.addItem(
Note(
2,
"2",
"전북대학교",
"",
"",
"3. ABC123",
"2",
null,
"8월 5일"
)
) // 테스트용 임의 아이템 3개
recyclerView.adapter = adapter
// 리싸이클러뷰에 어댑터 연결
adapter.setOnItemClickListener(object : OnNoteItemClickListener {
override fun onItemClick(holder: NoteAdapter.ViewHolder, view: View, position: Int) {
val item = adapter.getItem(position)
Toast.makeText(context, "아이템 선택됨 : " + item?.contents, Toast.LENGTH_SHORT).show()
}
}) // 아이템 선택 됐을때 호출할 함수
}
}
목록 프래그먼트의 리싸이클러뷰에 NoteAdapter 연결
일기 리스트 화면 완성