Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,129 +1,89 @@
package com.runnect.runnect.presentation.mypage.editname

import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.core.view.isVisible
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.runnect.runnect.R
import com.runnect.runnect.binding.BindingActivity
import com.runnect.runnect.databinding.ActivityMyPageEditNameBinding
import com.runnect.runnect.presentation.state.UiState
import com.runnect.runnect.presentation.ui.theme.RunnectTheme
import com.runnect.runnect.util.analytics.Analytics
import com.runnect.runnect.util.analytics.EventName
import com.runnect.runnect.util.analytics.EventName.Param
import com.runnect.runnect.util.extension.hideKeyboard
import com.runnect.runnect.util.extension.showToast
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

@AndroidEntryPoint
class MyPageEditNameActivity :
BindingActivity<ActivityMyPageEditNameBinding>(R.layout.activity_my_page_edit_name) {
class MyPageEditNameActivity : AppCompatActivity() {
private val viewModel: MyPageEditNameViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.vm = viewModel
binding.lifecycleOwner = this
enableEdgeToEdge()
Analytics.logEvent(EventName.VIEW_EDIT_PROFILE)
initLayout()
addListener()
addObserver()
}

private fun initLayout() {
val nickName = intent.getStringExtra(EXTRA_NICK_NAME)
val profileImgResId = intent.getIntExtra(EXTRA_PROFILE, R.drawable.user_profile_basic)
viewModel.setNickName(nickName = nickName!!)
viewModel.setProfileImg(profileImgResId = profileImgResId)
}

private fun addListener() {
binding.ivMyPageEditNameBack.setOnClickListener {
setResult(RESULT_CANCELED)
finish()
if (savedInstanceState == null) {
val nickname = intent.getStringExtra(EXTRA_NICK_NAME) ?: ""
val profileImgResId = intent.getIntExtra(EXTRA_PROFILE, R.drawable.user_profile_basic)
viewModel.intent(EditNameIntent.Init(nickname, profileImgResId))
}

binding.tvMyPageEditNameFinish.setOnClickListener {
viewModel.updateNickName()
}

binding.etMyPageEditName.setOnEditorActionListener(object :
TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == EditorInfo.IME_ACTION_DONE) {
hideKeyboard(binding.etMyPageEditName)
return true
}
return false
setContent {
RunnectTheme {
val state by viewModel.state.collectAsState()
MyPageEditNameScreen(
state = state,
onBackClick = {
setResult(RESULT_CANCELED)
finish()
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right)
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
onNicknameChange = { viewModel.intent(EditNameIntent.UpdateNickname(it)) },
onSubmitClick = { viewModel.intent(EditNameIntent.Submit) },
)
}
})
}

private fun addObserver() {
viewModel.uiState.observe(this) {
when (it) {
UiState.Empty -> binding.indeterminateBar.isVisible = false
UiState.Loading -> binding.indeterminateBar.isVisible = true
UiState.Success -> {
binding.indeterminateBar.isVisible = false
Analytics.logEvent(
EventName.ACTION_EDIT_PROFILE_COMPLETE,
Param.CHANGED_FIELDS to "nickname"
)
setResult(
RESULT_OK,
Intent().putExtra(EXTRA_NICK_NAME, viewModel.nickName.value)
)
finish()
}
}

UiState.Failure -> {
binding.indeterminateBar.isVisible = false
if (viewModel.statusCode.value == 400) {
showToast(getString(R.string.my_page_edit_name_redundant_warning))
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.effect.collect { effect ->
when (effect) {
is EditNameEffect.NavigateSuccess -> {
Analytics.logEvent(
EventName.ACTION_EDIT_PROFILE_COMPLETE,
Param.CHANGED_FIELDS to "nickname"
)
setResult(
RESULT_OK,
Intent().putExtra(EXTRA_NICK_NAME, effect.newNickname)
)
finish()
}
EditNameEffect.ShowDuplicateError -> {
showToast(getString(R.string.my_page_edit_name_redundant_warning))
}
}
}
}
}
viewModel.nickName.observe(this) {
with(binding.tvMyPageEditNameFinish) {
if (it.isNullOrEmpty()) {
isActivated = false
isClickable = false
} else {
isActivated = true
isClickable = true
}
}
}
}

//키보드 밖 터치 시, 키보드 내림
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
val focusView = currentFocus
if (focusView != null) {
val rect = Rect()
focusView.getGlobalVisibleRect(rect)
val x = ev!!.x.toInt()
val y = ev.y.toInt()
if (!rect.contains(x, y)) {
hideKeyboard(focusView)
}
}
return super.dispatchTouchEvent(ev)
}

@Deprecated("Use onBackPressedDispatcher")
override fun onBackPressed() {
finish()
@Suppress("DEPRECATION")
super.onBackPressed()
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right)
}

companion object {
const val EXTRA_NICK_NAME = "nickname"
const val EXTRA_PROFILE = "profile_img"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.runnect.runnect.presentation.mypage.editname

import com.runnect.runnect.R

data class EditNameUiState(
val nickname: String = "",
val profileImgResId: Int = R.drawable.user_profile_basic,
val isLoading: Boolean = false,
)

sealed interface EditNameIntent {
data class Init(val nickname: String, val profileImgResId: Int) : EditNameIntent
data class UpdateNickname(val name: String) : EditNameIntent
data object Submit : EditNameIntent
}

sealed interface EditNameEffect {
data class NavigateSuccess(val newNickname: String) : EditNameEffect
data object ShowDuplicateError : EditNameEffect
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package com.runnect.runnect.presentation.mypage.editname

import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.runnect.runnect.R
import com.runnect.runnect.presentation.ui.theme.G1
import com.runnect.runnect.presentation.ui.theme.G3
import com.runnect.runnect.presentation.ui.theme.M1
import com.runnect.runnect.presentation.ui.theme.M2
import com.runnect.runnect.presentation.ui.theme.RunnectTheme

@Composable
fun MyPageEditNameScreen(
state: EditNameUiState,
onBackClick: () -> Unit,
onNicknameChange: (String) -> Unit,
onSubmitClick: () -> Unit,
) {
val focusManager = LocalFocusManager.current

Box(modifier = Modifier.fillMaxSize().statusBarsPadding()) {
Column(modifier = Modifier.fillMaxSize()) {
EditNameToolbar(
onBackClick = onBackClick,
onSubmitClick = onSubmitClick,
submitEnabled = state.nickname.isNotEmpty() && !state.isLoading,
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 22.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(147.dp))
AsyncImage(
model = state.profileImgResId,
contentDescription = null,
modifier = Modifier.size(96.dp),
)
Spacer(modifier = Modifier.height(48.dp))
NicknameTextField(
value = state.nickname,
onValueChange = onNicknameChange,
onDone = { focusManager.clearFocus() },
enabled = !state.isLoading,
)
}
}

if (state.isLoading) {
CircularProgressIndicator(
color = G3,
modifier = Modifier.align(Alignment.Center),
)
}
}
}

@Composable
private fun EditNameToolbar(
onBackClick: () -> Unit,
onSubmitClick: () -> Unit,
submitEnabled: Boolean,
) {
val textStyle = RunnectTheme.textStyle
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.padding(horizontal = 15.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(R.drawable.all_back_arrow),
contentDescription = stringResource(R.string.my_page_edit_name_back),
modifier = Modifier
.size(24.dp)
.clickable(onClick = onBackClick),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
Spacer(modifier = Modifier.width(24.dp))
Text(
text = stringResource(R.string.my_page_edit_name_title),
style = textStyle.bold17.copy(fontSize = 18.sp),
color = G1,
modifier = Modifier.weight(1f),
)
Text(
text = stringResource(R.string.my_page_edit_name_finish),
style = textStyle.bold15.copy(fontSize = 16.sp),
color = if (submitEnabled) M1 else G3,
modifier = Modifier
.clickable(enabled = submitEnabled, onClick = onSubmitClick)
.padding(8.dp),
)
}
}

@Composable
private fun NicknameTextField(
value: String,
onValueChange: (String) -> Unit,
onDone: () -> Unit,
enabled: Boolean = true,
) {
val textStyle = RunnectTheme.textStyle
// OutlinedTextField enforces a 56dp minimum height (M3 internal contentPadding),
// which clips text inside the 44dp height the XML version used.
// BasicTextField + decorationBox gives identical layout to the original AppCompatEditText.
BasicTextField(
value = value,
onValueChange = { if (it.length <= 7) onValueChange(it) },
modifier = Modifier
.fillMaxWidth()
.height(44.dp),
enabled = enabled,
textStyle = textStyle.semiBold15.copy(textAlign = TextAlign.Center, color = G1),
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { onDone() }),
decorationBox = { innerTextField ->
Box(
modifier = Modifier
.fillMaxSize()
.border(width = 1.dp, color = M2, shape = RoundedCornerShape(10.dp))
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
if (value.isEmpty()) {
Text(
text = stringResource(R.string.my_page_edit_name_guide),
style = textStyle.semiBold15.copy(textAlign = TextAlign.Center),
color = G3,
modifier = Modifier.fillMaxWidth(),
)
}
innerTextField()
}
},
)
}
Loading
Loading