SOPT 26๊ธฐ Appjam '๋ถ์คํฐ'
Faster / Easier / Together
ํ๋ก์ ํธ ๊ธฐ๊ฐ 2020.06 ~ ์งํ์ค
-
๋น ๋ฅด๊ฒ ์ถ๋ ฅํ๋ ํธ๋ฆฌํจ
-
๋ํ์์ ์ํ ๋น ๋ฅด๊ณ ๊ฐํธํ ์ธ์ ์๋น์ค
-
์ฌ์ ์ธ์ ์ฃผ๋ฌธ ์๋น์ค
-
Android Studio
-
Zeplin
-
Postman
-
๋ณ์๋ช ์ ๊ธฐ๋ณธ์ ์ผ๋ก camelCase๋ก ์์ฑ
-
ID NAMING : ๋ทฐ์ด๋ฆ_์์ ฏ์ค์ธ๋ง_๊ธฐ๋ฅ์ด๋ฆ
-
์ปค๋ฐํ๊ธฐ ์ ์ reformat code๋ฅผ ์คํ์์ผ์ ์ฝ๋๋ฅผ ์ ๋ฆฌํด์ค๋ค.
-
๊ฐ์ธ Branch๋ฅผ ์ด๋ฆ์ผ๋ก ๋ง๋ ๋ค ๊ฐ๋ฐํ๋ค.
-
๊ฐ์ธ Branch์์ develop branch๋ก PR์ ๋ณด๋ธ๋ค.
-
๋ชจ๋ ๊ธฐ๋ฅ์ด ์๋ฒฝํ๋ฉด์, ๋ชจ๋ ํ์์ด ๋์ํ ๋ Master ๋ธ๋์น๋ก PR์ ๋ณด๋ธ๋ค.
-
Minimum SDK version 24
-
Language : Kotlin
-
Retrofit : REST API Library
-
Gson : Json Data process Library
-
Glide : Image Process Library
- application,bindingadapter,data,listener,ui,util๋ก ๋๋ถ๋ฅ
- ํจํค์ง ๋ด๋ถ์ ์ธ๋ถ ํจํค์ง๋ก ๋๋ ์ ๋ฆฌ
//์๋ช
์ฃผ๊ธฐ๋ฅผ ๊ณต์ ํ๊ธฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
implementation "androidx.appcompat:appcompat:1.1.0"
//LiveData๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
// CardView Library
implementation 'androidx.cardview:cardview:1.0.0'
//Lottie Library
implementation 'com.airbnb.android:lottie:3.4.1'
// Koin for Kotlin
implementation "org.koin:koin-core:$koin_version"
// Koin extended & experimental features
implementation "org.koin:koin-core-ext:$koin_version"
// Koin for Unit tests
testImplementation "org.koin:koin-test:$koin_version"
// Koin for Java developers
implementation "org.koin:koin-java:$koin_version"
// Koin for Android
implementation "org.koin:koin-android:$koin_version"
// Koin Android Scope features
implementation "org.koin:koin-android-scope:$koin_version"
// Koin Android ViewModel features
implementation "org.koin:koin-android-viewmodel:$koin_version"
// Koin Android Experimental features
implementation "org.koin:koin-android-ext:$koin_version"
// Koin AndroidX Scope features
implementation "org.koin:koin-androidx-scope:$koin_version"
// Koin AndroidX ViewModel features
implementation "org.koin:koin-androidx-viewmodel:$koin_version"
// Koin AndroidX Experimental features
implementation "org.koin:koin-androidx-ext:$koin_version"
//ํ์ผํฝ์ปค ๋ผ์ด๋ธ๋ฌ๋ฆฌ
implementation 'com.droidninja:filepicker:2.2.4'
//Material Components
implementation 'com.google.android.material:material:1.3.0-alpha01'
//TedPermission ๋ผ์ด๋ธ๋ฌ๋ฆฌ
implementation 'gun0912.ted:tedpermission:2.2.3'
//coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4"
//Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01"
//Naver map
implementation "com.naver.maps:map-sdk:3.8.0"
//coordinator layout
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
//pdfium
implementation 'com.github.barteksc:pdfium-android:1.9.0'
- ๋๋ถ๋ถ์ ๋ ์ด์์์ ConstraintLayout์ผ๋ก ๊ตฌ์ฑ
- chain ๊ณผ match_parent ๋ฅผ ์ ๊ทน ํ์ฉํ์ฌ ๋ทฐ ๊ตฌ์ฑ
item_order_condition.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/item_order_prodress_cl_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="26dp"
android:layout_marginEnd="26dp"
android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="@id/item_order_progress_tv_list"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<View
android:layout_width="match_parent"
android:layout_height="3dp"
android:background="@drawable/bg_progress_receipt"
setGradation="@{conditionRes.status}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/item_order_condition_iv_cicle_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
changeCircleF="@{conditionRes.status}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
...
</androidx.constraintlayout.widget.ConstraintLayout>
- Constraint Chain์ ์ด์ฉํด ๊ฐ์ด๋ฐ ์ ๋ ฌ๋ก ๋ฐฐ์น
activity_store_file_option.kt
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/option4-1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/option4">
<LinearLayout
android:id="@+id/linearcut1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/linearcut2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/order_option_btn_cut_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/sel_order_option_btn_cut_1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4dp"
android:fontFamily="@font/noto_sans_kr_regular"
android:text="1๊ฐ"
android:textColor="#7d7d7d"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/linearcut2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/linearcut3"
app:layout_constraintStart_toEndOf="@id/linearcut1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<ImageView
android:id="@+id/order_option_btn_cut_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/sel_order_option_btn_cut_2" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4dp"
android:fontFamily="@font/noto_sans_kr_regular"
android:text="2๊ฐ"
android:textColor="#7d7d7d"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/linearcut3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/linearcut4"
app:layout_constraintStart_toEndOf="@id/linearcut2"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/order_option_btn_cut_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
app:srcCompat="@drawable/sel_order_option_btn_cut_3" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4dp"
android:fontFamily="@font/noto_sans_kr_regular"
android:text="3๊ฐ"
android:textColor="#7d7d7d"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/linearcut4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/linearcut3"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/order_option_btn_cut_4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
app:srcCompat="@drawable/sel_order_option_btn_cut_4" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4dp"
android:fontFamily="@font/noto_sans_kr_regular"
android:text="4๊ฐ"
android:textColor="#7d7d7d"
android:textSize="12sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
- kotlin collection์์ ์ ๊ณตํ๋ ํ์ฅํจ์ ์ฌ์ฉ
- split() ํจ์๋ฅผ ์ด์ฉํด uri์์ ํ์ผ๋ช ์ ๋ถ๋ฆฌํ๋ค
BoosterUtil.kt
fun getFileName(uri: Uri?): String? {
if (uri == null) {
return ""
}
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
cursor?.moveToNext()
val path = cursor?.getString(cursor.getColumnIndex("_data"))
cursor?.close()
val filePath = path?.split("/")
return filePath?.get(filePath.size - 1)
}
- ๊ธฐ์กด ํด๋์ค์ custom ํจ์๋ฅผ ํ์ฅํ์ฌ ์ฌ์ฉ
- BindingAdapter์์TextView์ ImageView ๋ฑ์ view ์์์ ํ์ฅํจ์๋ฅผ ๊ตฌํํ์ฌ ์ฌ์ฉ
BindingAdapter.kt
@BindingAdapter("setCancelVisible")
fun TextView.setCancelVisible(status : Int) {
if (status!=1){
visibility = GONE
}
}
@BindingAdapter("setFavStar")
fun ImageView.setFavStar(status : Int) {
if (status==0){
setImageResource(R.drawable.store_detail_ic_star_inactive)
}else{
setImageResource(R.drawable.store_detail_ic_star_active)
}
}
- ์กํฐ๋นํฐ๋ฅผ ์ด๋ํ๋ ๋ฒํผ ํด๋ฆญ์ ์ฌ๋ฌ ๋ฒ ์ฐ์์ผ๋ก ๋น ๋ฅด๊ฒ ํ ๋ ๋๊ฐ์ ์กํฐ๋นํฐ ์ฌ๋ฌ ๊ฐ๊ฐ ๊ณ์ํด์ ์์ธ๋ค.
- ktx(kotlin-extension)์ ํ์ฉํ์ฌ ์ค๋ณต ํด๋ฆญ ๋ฐฉ์ง ๊ตฌํ
class OnlyOneClickListener(
private val clickListener: View.OnClickListener,
private val interval: Long = 300
) :
View.OnClickListener {
private var clickable = true
override fun onClick(view: View?) {
if (clickable) {
clickable = false
view?.run {
postDelayed({
clickable = true
}, interval)
clickListener.onClick(view)
}
} else {
Log.e(TAG, "waiting for a while")
}
}
}
fun View.onlyOneClickListener(action: (v: View) -> Unit) {
val listener = View.OnClickListener { action(it) }
setOnClickListener(OnlyOneClickListener(listener))
}
act_main_btn_store.setOnClickListener {
val intent = Intent(this@MainActivity, StoreListActivity::class.java)
startActivity(intent)
}
act_main_btn_store.onlyOneClickListener {
val intent = Intent(this@MainActivity, StoreListActivity::class.java)
startActivity(intent)
}
- ์ฐ๋ฌ์ ํฐ์น์ ๋ถํ์ํ clickEvent๊ฐ ์ผ์ด๋์ง ์๋๋ก ๋ง์ ์ ์๋ค.
- ๋ทฐ ์คํฌ๋กค์ ํ์ดํ ๋ ์ด์์์ด ์๋จ์ ๊ณ ์ ๋์ฑ๋ก RecyclerView๊ฐ ์คํฌ๋กค ๋์ผํ๋ค.
- CollapsingToolbarLayout๋ฅผ ์ฌ์ฉํ์ฌ ํ์ดํ ์๋จ ๊ณ ์
- addOnOffsetChangedListener ์์์ ๋ทฐ์ alpha ๊ฐ์ ์กฐ์ ํ์ฌ toolbar fade out ํจ๊ณผ ๊ตฌํ
frag_store_list.xml
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ui.storeList.StoreListFragment">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/frag_store_list_appBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/frag_store_list_toolBar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:toolbarId="@+id/frag_store_list_toolBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/frag_store_list_toolBar"
android:layout_width="match_parent"
android:layout_height="97dp"
android:background="@color/white"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay">
<ImageView
android:id="@+id/frag_store_list_iv_map"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|top"
android:layout_marginTop="4dp"
android:layout_marginEnd="2dp"
android:layout_marginBottom="4dp"
android:src="@drawable/store_detail_ic_map_blue"
app:layout_collapseMode="parallax" />
</androidx.appcompat.widget.Toolbar>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/frag_store_list_cl_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:background="@android:color/transparent"
app:layout_collapseMode="pin">
<TextView
android:id="@+id/frag_store_list_tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="9dp"
android:fontFamily="@font/noto_sans_kr_bold"
android:text="๋งค์ฅ"
android:textColor="@color/black"
android:textSize="26sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
...
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/frag_store_list_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</androidx.recyclerview.widget.RecyclerView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
StoreListFragment.kt
frag_store_list_appBar.addOnOffsetChangedListener(OnOffsetChangedListener { frag_store_list_appBar, verticalOffset ->
if (frag_store_list_appBar.totalScrollRange == 0 || verticalOffset == 0) {
frag_store_list_iv_map.alpha = 1f
return@OnOffsetChangedListener
}
val ratio = verticalOffset.toFloat() / frag_store_list_appBar.totalScrollRange.toFloat()
frag_store_list_iv_map.alpha = 1f- abs(ratio)
})
- ์ ๋๋ฉ์ด์ ์ ๋ํ๋ ์ข ๋ ์๊ธฐ์๋ ๋ทฐ๋ฅผ ๋ง๋ค ์ ์์๋ค.
- ํ์ง๋ง ์ ๋๋ฉ์ด์ ์ ์ ์ฉํ๋ ๋์์ด๋๊ฐ ์๊ตฌํ๋ ์ ํํ ๋ทฐ(๊ทธ๋ฆผ์ ๋ฑ)์ ๋ง๋๋ ๋ฐ์๋ ์ฝ๊ฐ์ ์ด๋ ค์์ด ์์๋ค.
- ์ฃผ๋ฌธ ํํฉ ์ ๋ฐ์ดํธ๋ฅผ ์ํด List๋ฅผ ๋น๊ฒจ์ ์๋ก๊ณ ์นจํ ์ ์๋๋ก ํ๋ค
- ์ํ๋ List Layout์ SwipeRefreshLayout์ผ๋ก ๊ฐ์ผ๋ค
- SwipeRefreshLayout์ setOnRefreshListener๋ฅผ ์ถ๊ฐํด ํต์ ํจ์๋ฅผ ์ํํ๋ค
fragment_store_list.kt
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/frag_store_list_srl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/frag_store_list_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:clipToPadding="false"
android:paddingTop="8dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
StoreListFragment.kt
private fun refresh(){
frag_store_list_srl.apply{
setOnRefreshListener {
viewModel.getStoreList(univIdx)
this@apply.isRefreshing = false
}
}
}
- ์ฌ์ฉ์๊ฐ ์ํ ๋ ๋ ์ด์์์ ๋น๊ฒจ List๋ฅผ ์ ๋ฐ์ดํธํ ์ ์๋ค
- ํ์ผ์ ๊ฐ์ ธ์ฌ ์ ์๋ ์ปค์คํ ์ ์ฅ์๋ฅผ ๊ตฌํํ๋ค.
- DroidNinja ์ Android-FilePicker(https://github.com/DroidNinja/Android-FilePicker) ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ์ฌ ์ปค์คํ ํ์ผ ์ ์ฅ์ ๊ตฌํ
private fun fileAdd() {
val builder: AlertDialog.Builder =
AlertDialog.Builder(this, R.style.MyAlertDialogStyle2)
builder.setTitle("์ถ๊ฐํ ํ์ผ์ ์ข
๋ฅ๋ฅผ ์ ํํด์ฃผ์ธ์")
builder.setPositiveButton("์ด๋ฏธ์ง") { dialogInterface: DialogInterface, i: Int ->
FilePickerBuilder.instance
.setMaxCount(1)
.setActivityTheme(R.style.LibAppTheme) //optional
.setActivityTitle("์ด๋ฏธ์ง ์ ํ")
.pickPhoto(this, REQUEST_CODE_PHOTO);
}
builder.setNegativeButton("๋ฌธ์") { dialogInterface: DialogInterface, i: Int ->
FilePickerBuilder.instance
.setMaxCount(1)
.setActivityTheme(R.style.LibAppTheme) //optional
.setActivityTitle("๋ฌธ์ ์ ํ")
.pickFile(this, REQUEST_CODE_DOC);
}
builder.show()
}
- File Picker Open Source๋ฅผ ๋ถ์ํด์ ๋ถ์คํฐ ํ๋ก์ ํธ์ ์ ์ฉํ๋ ๊ฒ์ด ๊น๋ค๋ก์ ๋ค. ํ์ง๋ง ํ๋ก์ ํธ์ ์๊ตฌ์ฌํญ์ ๋ง๊ฒ ํ ๋ง ๋ฐ ๊ธฐ๋ฅ์ ์์ ํ์ฌ ์ฑ๊ณต์ ์ผ๋ก ํ์ผ์ ์ ๋ก๋ ํ ์ ์์๋ค.
- form-data ๋ก pdf, image ํ์ผ์ ์๋ฒ์ ์ ์กํด์ผ ํ๋ค.
- ๊ฒฝ๋ก๋ฅผ ํตํด File ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ์ค ๋ค์ RequestBody -> Multipart.Part ์์ผ๋ก ๋ณํํ ๋ค์ ํต์ ์ ์งํํ๋ค.
BoosterService.kt
@Multipart
@POST("/orders/{order_idx}/file")
suspend fun postUploadFile(
@Header("token") token: String,
@Path("order_idx") orderIdx: Int,
@Part file: MultipartBody.Part?,
@Part thumbnail: MultipartBody.Part?
): ApiWrapper<com.example.booster.data.datasource.model.File>
FileStorageViewModel.kt
var requestBody: RequestBody? = null
var requestBody2: RequestBody? = null
when (file?.file_extension) {
".png" -> {
requestBody = RequestBody.create(
MediaType.parse("image/png"), imageFile
)
requestBody2 = RequestBody.create(
MediaType.parse("image/png"), imageFile
)
}
".pdf" -> {
requestBody = RequestBody.create(
MediaType.parse("application/pdf"), docFile
)
requestBody2 = RequestBody.create(
MediaType.parse("image/png"), thumbnailFile
)
}
".docx" -> requestBody = RequestBody.create(
MediaType.parse("multipart/form-data"), docFile
)
".jpeg", ".jpg" -> {
requestBody = RequestBody.create(
MediaType.parse("image/jpeg"), imageFile
)
requestBody2 = RequestBody.create(
MediaType.parse("image/jpeg"), imageFile
)
}
}
Log.e(
"pdfcheck",
"check: " + requestBody + " " + file?.file_extension + " " + file?.file_name
)
val multipartBody =
MultipartBody.Part.createFormData("file", file?.file_name, requestBody)
val multipartBody2 =
MultipartBody.Part.createFormData("thumbnail", "png", requestBody2)
- MediaType ๋ณํ ๋ฌธ๊ตฌ๊ฐ ํ๋ฆฌ๊ณ , ๋ถ ํ์ํ ํค๋๋ฅผ ๋ฃ์ด์ ์ฒ์์ ์ํ์ฐฉ์ค๋ฅผ ๋ง์ด ๊ฒช์์ง๋ง, ๊ฒฐ๊ตญ ํด๋ด์ ๋ ํ ๋ฒ์ ์ฑ์ฅ์ ์ด๋ฃฉํ๋ค.
- pdf๋ฅผ ์ ์ฅ์๋ก๋ถํฐ ๋ฐ์์์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋ฅ์ ์ ๊ณตํ๊ณ ์ฒซ ํ์ด์ง(์ธ๋ค์ผ)๋ฅผ ์ด๋ฏธ์ง๋ก ์ถ์ถํ๋ค.
- PdfRenderer ๋ฅผ ์ด์ฉํด์ pdf ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋ฅ ์ ๊ณต
val fileDescriptor: ParcelFileDescriptor?
fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
val pdfRenderer: PdfRenderer?
pdfRenderer = PdfRenderer(fileDescriptor)
val pageCount: Int = pdfRenderer.pageCount
pdfviewer_act_main_total_page.text = pageCount.toString()
Toast.makeText(this, "pageCount = $pageCount", Toast.LENGTH_LONG).show()
val parentlayout = LinearLayout(this)
parentlayout.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT)
parentlayout.orientation = LinearLayout.HORIZONTAL
if (pageCount != 1) {
pdfviewer_act_main_hs.removeView(pdfviewer_act_main_ll)
pdfviewer_act_main_hs.addView(parentlayout)
}
for (i in 0 until pageCount) {
pdfviewer_act_main_cur_page.text = (i + 1).toString()
val imageView = ImageView(this)
imageView.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
val rendererPage = pdfRenderer.openPage(i)
val rendererPageWidth: Int = rendererPage.width
val rendererPageHeight: Int = rendererPage.height
val bitmap =
Bitmap.createBitmap(rendererPageWidth, rendererPageHeight, Bitmap.Config.ARGB_8888)
rendererPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
imageView.setImageBitmap(bitmap)
if (pageCount == 1) {
pdfviewer_act_main_ll.addView(imageView)
}else {
parentlayout.addView(imageView)
}
rendererPage!!.close()
}
pdfRenderer.close()
fileDescriptor.close()
- PdfRenderer๋ฅผ ์ด์ฉํด์ ์ธ๋ค์ผ ์ด๋ฏธ์ง(bitmap) ์ถ์ถ
object PDFThumbnailUtils {
fun convertPDFtoBitmap(context: Context, uri: Uri, pageNumber: Int): Bitmap? {
val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, "r")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val pdfRenderer = parcelFileDescriptor?.let { PdfRenderer(it) }
val currentPage = pdfRenderer?.openPage(pageNumber)
val bitmap = Bitmap.createBitmap(currentPage?.width!!, currentPage?.height!!, Bitmap.Config.ARGB_8888)
currentPage?.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
return bitmap
} else {
TODO("VERSION.SDK_INT < LOLLIPOP")
}
}
}
- ์ค์ ๋ฆฌ์ฌ์ดํด๋ฌ๋ทฐ ๋ฐ์ดํฐ์ ์ ์ฉ
inner class ViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
fun bind(file: File) {
if (file.file_extension == ".png" || file.file_extension == ".jpeg" || file.file_extension == ".jpg") {
Glide.with(itemView.context).load(file.file_path).into(itemView.iv_file)
} else {
val uri = file.file_uri
if (uri != null) {
val bitmap =
PDFThumbnailUtils.convertPDFtoBitmap(
itemView.context,
uri,
PAGE_NUMBER
)
if (bitmap != null) {
itemView.iv_file.setImageBitmap(bitmap)
}
- pdf๋ ์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์ ๊ณต ํ ์ ์์์ง๋ง, hwp,ppt ๋ฑ ์คํผ์ค ๊ธฐ๋ฐ ๋ฌธ์๋ค์ ์ ๊ณตํ๊ธฐ์ ๊น๋ค๋ก์ ๋ค. ๋ฐฉ๋ฒ์ ์ฐพ์๋ณด๋๋ก ํ๊ฒ ๋ค.
- bitmap ํํ์ ์ธ๋ค์ผ์ Multipart๋ฅผ ์ด์ฉํ์ฌ ์๋ฒ์ ์ ๋ก๋ํ๊ธฐ ์ํด ์ด๋ฏธ์ง๋ฅผ ํ์ผํํ๋ก ๋ณํํ์ฌ ์ ์กํ๋ค.
- bitmap์ pngํํ ํ์ผ๋ก ๋ณํ
private fun bitmapToFile(bitmap:Bitmap): java.io.File? {
// Get the context wrapper
val wrapper = ContextWrapper(applicationContext)
var file = wrapper.getDir("Images",Context.MODE_PRIVATE)
file = java.io.File(file,"${UUID.randomUUID()}.png")
try{
// Compress the bitmap and save in jpg format
val stream:OutputStream = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG,100,stream)
stream.flush()
stream.close()
}catch (e: IOException){
e.printStackTrace()
}
// Return the saved bitmap uri
return file
}
- Local Storage์์ ๊ฐ์ ธ์จ bitmap ํ์ผ์ด ์๋๋ผ filepath๋ฅผ ๋ง๋ค์ด์ฃผ๊ธฐ ์ํ์ฌ ํจ์๋ฅผ ์ ์ํ์ฌ ์ฌ์ฉํ์๊ณ ์ฑ๊ณต์ ์ผ๋ก ์ ๋ก๋๊ฐ ๊ฐ๋ฅํ์๋ค.
- ๋งค์ฅ ๋ฆฌ์คํธ๋ฅผ ์ง๋ ์์ ๋ง์ปค๋ก ๋ํ๋ผ ์ ์๊ณ , ๋ง์ปค๋ฅผ ํตํด ์์ธ์ ๋ณด ํ์ด์ง๋ก ์ด๋ํ์ฌ ์ ๋ณด๋ฅผ ์ ๊ณตํ ์ ์๋ค.
- ๋ค์ด๋ฒ์ง๋ API๋ฅผ ์ด์ฉํด์ ๋งค์ฅ์ ์๋, ๊ฒฝ๋ ์ ๋ณด๋ฅผ ๋ฆฌ์คํธ๋ก ๋ฐ์์์ ๋ง์ปค๋ก ํ์ํ๋ค.
StoreListFragment.kt
markers.clear()
for(i in 0 .. it.size-1){
markers.add(
MarkerData(
latitude = it[i].store_x_location,
longitude = it[i].store_y_location,
name = it[i].store_name,
idx = it[i].store_idx
)
)
}
MapActvity.kt
@UiThread
override fun onMapReady(nMap: NaverMap) {
val uiSettings = nMap.uiSettings
uiSettings.isZoomControlEnabled = true
uiSettings.isLocationButtonEnabled = true
nMap.locationSource
nMap.locationTrackingMode
uiSettings.isScaleBarEnabled = false
if (university == "์ญ์ค๋ํ๊ต"){
cameraUpdate = CameraUpdate.scrollTo(LatLng(37.496575, 126.957427))
}... //์ ํํ ํ๊ต ๋ณ๋ก focus ๋ง์ถ๊ธฐ
act_map_txt_univ.text = university
nMap.moveCamera(cameraUpdate)
draw(nMap)
}
//์ค์ ์ง๋๋ฅผ ๊ทธ๋ฆฌ๊ธฐ ์์ํ๋ค.
fun draw(nMap: NaverMap){
for(i in 0 until markers.size){
repeat(1000) {
array.plusAssign(Marker().apply {
position = LatLng(markers[i].latitude!!.toDouble(), markers[i].longitude!!.toDouble())
icon = OverlayImage.fromResource(R.drawable.store_map_ic_marker)
tag = markers[i].name
width = Marker.SIZE_AUTO
height = Marker.SIZE_AUTO
})
}
}
//๋ง์ปค ํด๋ฆญ์ tag ๋์ฐ๊ธฐ
val infoWindow = InfoWindow()
infoWindow.adapter = object : InfoWindow.DefaultTextAdapter(this) {
override fun getText(infoWindow: InfoWindow): CharSequence {
return infoWindow.marker?.tag as CharSequence? ?:""
}
}
//๋ฐ์์จ ๋งค์ฅ ๋ฆฌ์คํธ๋ณ๋ก ๋ง์ปค๋ฅผ ๋์์ค๋ค. ๋ง์ปค ํด๋ฆญ์ ๋ง์ปค ์ด๋ฏธ์ง ๋ฐ๊ฟ + tag๋์ฐ๊ธฐ + tagํด๋ฆญ์ ์์ธํ์ด์ง๋ก ์ด๋
array.forEach { marker ->
marker.map = nMap
marker.setOnClickListener {
for ( i in 0 until array.size){
array[i].icon = OverlayImage.fromResource(R.drawable.store_map_ic_marker)
}
marker.icon = OverlayImage.fromResource(R.drawable.store_map_ic_marker_click)
val cameraUpdate = CameraUpdate.scrollTo(marker.position)
nMap.moveCamera(cameraUpdate)
infoWindow.open(marker)
infoWindow.setOnClickListener {
val intent = Intent(this, StoreDetailActivity::class.java)
for(i in 0 .. array.size){
if(markers[i].name == marker.tag){
val idx = markers[i].idx
intent.putExtra("storeIdx", idx)
break
}
}
startActivity(intent)
false
}
false
}
}
}
- ์ฌ๋ฌ๊ฐ์ ๋งค์ฅ์ ์ง๋์ ๋ง์ปค๋ก ๋์์ค์ผ๋ก์จ ์ฌ์ฉ์๊ฐ ์ง๊ด์ ์ผ๋ก ๋งค์ฅ์ ์์น๋ฅผ ํ์ธํ ์ ์๊ฒ ํ๋ค.
- ์ฑ์ ์ฌ๊ตฌ๋ํ์ฌ๋ ์ฌ์ฉ์์ ํ๋๊ธฐ๋ก์ ๋ค์ ๋ณผ ์ ์๊ฒ ์ฌ์ฉ์๋ณ ํ ํฐ์ ์ ์ฅํ๋ค. ํด๋น ํ ํฐ์ผ๋ก ํต์ ์ ํ์ฌ ์ฌ์ฉ์๋ณ ํ๋์ ์๋ณํ๋ค.
- SharedPreferences๋ฅผ ์ด์ฉํ์ฌ ๋ก๊ทธ์ธ์ ์๋ฒ๋ก๋ถํฐ ๋ฐ์ ํ ํฐ์ ์ ์ฅํ์ฌ, ์ ์ญ์์ ํด๋น ํ ํฐ์ ์ ๊ทผํ ์ ์๋ค.
- ๋ํ Interceptor๋ก ๋ง๋ค์ด๋ฌ ํต์ ํ ๋๋ง๋ค ํ์ํ token๊ฐ์ ๋ฐ๋ก ๋ฃ์ด์ฃผ์ง ์๊ณ ๋ฏธ๋ฆฌ sharedpreferences์ ์ ์ฅํด๋ token๊ฐ์ ์ฌ์ฉํ ์ ์๋ค.
UserManager.kt
object UserManager {
private lateinit var pref: SharedPreferences
fun init(context: Context) {
pref = context.getSharedPreferences("Booster", Context.MODE_PRIVATE)
}
var token: String?
get() = pref.getString("token", null)
set(value) = pref.edit {
it.putString("token", value)
}
}
CookiesInterceptor
class CookiesInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request =
chain.request().newBuilder().header("Content-Type", "application/json")
.header("token", UserManager.token?:"")
.build()
return chain.proceed(request)
}
}
- ๋ก๊ทธ์ธ ํต์ ์ ์๋ฒ๋ก ํ ํฐ ๊ฐ์ ๋ฐ์์จ๋ค.
LoginActivity.kt
intent.putExtra("token", response.body()!!.data.accessToken)
BottomTabActivity.kt
if(intent.hasExtra("token")){
bottom_vp.currentItem = 0
token = intent.getStringExtra("token")
UserManager.token = token
}
- ์ด๊ธฐ์ ์ฌ๋ฌ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ณ test๋ฅผ ํ ๋ ๋์ผํ ํ ํฐ์ ์ฌ์ฉํด์ ์ฃผ๋ฌธํํฉ ๋ฐ ์์ธ๋ด์ญ ์ ๋ณด๊ฐ ๊ต์ฅํ ๋ง์์ ๋ณด๊ธฐ ํ๋ค์๋๋ฐ, ๊ฐ ์ฌ์ฉ์๋ณ ํ ํฐ์ ์ฌ์ฉํ๋ ๊ธฐ๋ฅ testํ๊ธฐ ํธํด์ก๊ณ , ์ฌ์ฉ์๋ณ ๊ด๋ฆฌ๋ฅผ ํ ์ ์์ด ์ข์๊ณ , ์ดํ ์ด ๊ธฐ๋ฅ์ ๋ ๋ฐฐ์๋ณด๊ณ ๊ณต๋ถํ๊ณ ์ถ๋ค.
- ์ํฐ๋ก EditText๋ฅผ ๋์ฌ ๋ focus๋ฅผ ํด์ ํ ์ ์๋๋ก ๊ตฌํํ๋ค.
- setOnFocuseChangeListener์ setOnKeyListener ํจ๊ป ์ฌ์ฉํ์ฌ focus๋ฅผ ์ค์ ๋ฐ ํด์ ํ๋ค.
JoinActivity.kt
join_edt_pw_chk.setOnFocusChangeListener { v, hasFocus ->
join_edt_pw_chk.isSelected = hasFocus
}
join_edt_pw_chk.setOnKeyListener(View.OnKeyListener { v, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_ENTER) {
v.clearFocus()
val keyboard: InputMethodManager =
getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
keyboard.hideSoftInputFromWindow(join_edt_pw_chk.windowToken, 0)
return@OnKeyListener true
}
false
})
- KeyEvent๋ฅผ ์ด์ฉํ์ฌ ์ํ๋ ์ ๋ ฅ์ ๋ฐ๋ผ focus๋ฅผ ํด์ ํ ์ ์๋ค
- ๋น๋ฐ๋ฒํธ ์ ๋ ฅ๊ณผ ๋น๋ฐ๋ฒํธ ํ์ธ ์ ๋ ฅ์ ์ค์๊ฐ์ผ๋ก ์ฒดํฌํ์ฌ TextView์ visibility๋ฅผ ๋ฐ๊พผ๋ค.
- addTextChangedListener๋ฅผ ์ด์ฉํ์ฌ ์ ๋ ฅ์ ๋ณํ๋ฅผ ์ค์๊ฐ์ผ๋ก ์ฒดํฌํ ์ ์๊ฒ๋ ํ์๋ค.
JoinActivity.kt
join_edt_pw_chk.addTextChangedListener {
if (join_edt_pw.text.toString() == join_edt_pw_chk.text.toString()) {
join_tv_pw_check_fail.visibility = View.INVISIBLE
pwChk = true
} else {
join_tv_pw_check_fail.visibility = View.VISIBLE
}
}
- EditText์ text๋ฅผ ์ค์๊ฐ์ผ๋ก ๋น๊ตํ์ฌ ์ฌ์ฉ์์๊ฒ ์ ์ ํ ๊ฒฝ๊ณ ๊ฐ ๊ฐ๋ฅํ๋๋ก ํ๋ค.
- ๋จ์ํ ๋ฐ์ดํฐ ํ์๋ฅผ ์ํด ๋ถํ์ํ๊ฒ api ์์ฒญ์ ํ๊ฒ ๋์ด ์ฝ๋๊ฐ ๋ถํ์ํ๊ฒ ๊ธธ์ด์ง๋ ๋ฌธ์ ๊ฐ ์์๋ค.
- putExtra์ getStringExtra๋ฅผ ์ด์ฉํ์ฌ Fragment์ Activity๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ ์ ์๊ฒ๋ ํ์๋ค.
MypageFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mypage_tv_goto_edit.setOnClickListener {
val intent = Intent(context, EditProfileActivity::class.java)
intent.putExtra("id", mypage_tv_id.text.toString())
intent.putExtra("univ", univIdx.toString())
intent.putExtra("name", mypage_tv_name.text.toString())
startActivity(intent)
}
mypage_tv_goto_myengine.setOnClickListener {
val intent = Intent(context, MyengineActivity::class.java)
startActivity(intent)
}
}
EditProfileActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_edit_profile)
var extraId = intent.getStringExtra("id")
var extraUnivIdx = intent.getStringExtra("univ")
var extraName = intent.getStringExtra("name")
}
- ๊ฐ๋จํ data๋ฅผ View๊ฐ์ ์ ๋ฌํ๋ฉฐ ๋ถํ์ํ ํต์ ์ ์ค์๋ค.
- ๊น์์ง - jineee
- ๊น์ฐฌ์ - ghkdua1829
- ๊น์งํ - jiHyeonMon
- ์์ ๋ก - chop-sui
- ์ด์ ๋ฏผ - danmin20