diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/layoutManager/CenterSmoothScroller.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/layoutManager/CenterSmoothScroller.kt new file mode 100644 index 000000000..7aa83f849 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/layoutManager/CenterSmoothScroller.kt @@ -0,0 +1,19 @@ +package com.emmsale.presentation.common.layoutManager + +import android.content.Context +import androidx.recyclerview.widget.LinearSmoothScroller +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class CenterSmoothScroller @Inject constructor( + @ApplicationContext context: Context, +) : LinearSmoothScroller(context) { + + override fun calculateDtToFit( + viewStart: Int, + viewEnd: Int, + boxStart: Int, + boxEnd: Int, + snapPreference: Int, + ): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/layoutManager/EndSmoothScroller.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/layoutManager/EndSmoothScroller.kt new file mode 100644 index 000000000..85118711b --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/layoutManager/EndSmoothScroller.kt @@ -0,0 +1,13 @@ +package com.emmsale.presentation.common.layoutManager + +import android.content.Context +import androidx.recyclerview.widget.LinearSmoothScroller +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class EndSmoothScroller @Inject constructor( + @ApplicationContext context: Context, +) : LinearSmoothScroller(context) { + + override fun getVerticalSnapPreference(): Int = SNAP_TO_END +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt index 8308b5c0c..103e016e3 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt @@ -5,7 +5,6 @@ import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearSmoothScroller import com.emmsale.R import com.emmsale.data.model.Comment import com.emmsale.databinding.ActivityFeedDetailBinding @@ -13,6 +12,8 @@ import com.emmsale.presentation.base.NetworkActivity import com.emmsale.presentation.common.extension.hideKeyboard import com.emmsale.presentation.common.extension.showKeyboard import com.emmsale.presentation.common.extension.showSnackBar +import com.emmsale.presentation.common.layoutManager.CenterSmoothScroller +import com.emmsale.presentation.common.layoutManager.EndSmoothScroller import com.emmsale.presentation.common.recyclerView.DividerItemDecoration import com.emmsale.presentation.common.views.InfoDialog import com.emmsale.presentation.common.views.WarningDialog @@ -26,6 +27,7 @@ import com.emmsale.presentation.ui.profile.ProfileActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint class FeedDetailActivity : @@ -37,6 +39,12 @@ class FeedDetailActivity : override val viewModel: FeedDetailViewModel by viewModels() + @Inject + lateinit var centerSmoothScroller: CenterSmoothScroller + + @Inject + lateinit var endSmoothScroller: EndSmoothScroller + private val bottomMenuDialog: BottomMenuDialog by lazy { BottomMenuDialog(this) } private val feedAndCommentsAdapter = FeedAndCommentsAdapter( @@ -71,9 +79,21 @@ class FeedDetailActivity : private fun BottomMenuDialog.addCommentUpdateButton(commentId: Long) { addMenuItemBelow(context.getString(R.string.all_update_button_label)) { - viewModel.startEditComment(commentId) binding.stiwCommentUpdate.requestFocusOnEditText() showKeyboard() + viewModel.startEditComment(commentId) + startToEditComment(commentId) + } + } + + private fun startToEditComment(commentId: Long) { + val position = getCommentPosition(commentId) + 1 // 게시글을 포함한 멀티 뷰 타입 리사이클러 뷰라서 1을 더함 + + lifecycleScope.launch { + delay(KEYBOARD_SHOW_WAITING_TIME) + binding.rvFeedAndComments + .layoutManager + ?.startSmoothScroll(endSmoothScroller.apply { targetPosition = position }) } } @@ -212,17 +232,39 @@ class FeedDetailActivity : private fun observeFeedDetail() { viewModel.feedDetailUiState.observe(this) { val feedAndComments = listOf(it.feedUiState) + it.commentsUiState.commentUiStates - feedAndCommentsAdapter.submitList(feedAndComments) { - if (highlightCommentId == INVALID_COMMENT_ID || isNotRealFirstFetch()) return@submitList - viewModel.highlightComment(highlightCommentId) - viewModel.isAlreadyFirstFetched = true - } + feedAndCommentsAdapter.submitList(feedAndComments) { handleHighlightComment() } } } - private fun isNotRealFirstFetch(): Boolean = + private fun handleHighlightComment() { + if (highlightCommentId == INVALID_COMMENT_ID || isNotRealFirstEnter()) return + + viewModel.isAlreadyFirstFetched = true + viewModel.highlightCommentOnFirstEnter(highlightCommentId) + highlightCommentOnFirstEnter() + } + + private fun isNotRealFirstEnter(): Boolean = viewModel.isAlreadyFirstFetched || viewModel.commentUiStates.isEmpty() + private fun highlightCommentOnFirstEnter() { + val position = + getCommentPosition(highlightCommentId) + 1 // 게시글을 포함한 멀티 뷰 타입 리사이클러 뷰라서 1을 더함 + + binding.rvFeedAndComments.scrollToPosition(position) + lifecycleScope.launch { + delay(100L) // 버그 때문에 + binding.rvFeedAndComments + .layoutManager + ?.startSmoothScroll(centerSmoothScroller.apply { targetPosition = position }) + } + } + + private fun getCommentPosition(commentId: Long): Int = viewModel.commentUiStates + .indexOfFirst { commentUiState -> + commentUiState.comment.id == commentId + } + private fun observeUiEvent() { viewModel.uiEvent.observe(this, ::handleUiEvent) } @@ -274,8 +316,6 @@ class FeedDetailActivity : binding.btiwCommentPost.clearText() scrollToLastPosition() } - - is FeedDetailUiEvent.CommentHighlight -> highlightComment(uiEvent.commentId) } } @@ -284,26 +324,10 @@ class FeedDetailActivity : binding.rvFeedAndComments.smoothScrollToPosition(commentsCount) } - private fun highlightComment(commentId: Long) { - val position = viewModel.commentUiStates - .indexOfFirst { - it.comment.id == commentId - } - - binding.rvFeedAndComments.scrollToPosition(position + 1) - lifecycleScope.launch { - delay(200L) - binding.rvFeedAndComments.layoutManager?.startSmoothScroll( - object : LinearSmoothScroller(this@FeedDetailActivity) { - override fun getVerticalSnapPreference(): Int = SNAP_TO_START - }.apply { targetPosition = position + 1 }, - ) - } - } - companion object { private const val KEY_HIGHLIGHT_COMMENT_ID = "KEY_HIGHLIGHT_COMMENT_ID" private const val INVALID_COMMENT_ID: Long = -1 + private const val KEYBOARD_SHOW_WAITING_TIME = 300L fun startActivity( context: Context, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt index 255506ea8..ba48d0a5f 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt @@ -28,6 +28,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates @@ -69,6 +70,8 @@ class FeedDetailViewModel @Inject constructor( private val _canSubmitComment = NotNullMutableLiveData(true) val canSubmitComment: NotNullLiveData = _canSubmitComment + private var unhighlightJob: Job? = null + private val _uiEvent = SingleLiveEvent() val uiEvent: LiveData = _uiEvent @@ -198,12 +201,21 @@ class FeedDetailViewModel @Inject constructor( fun startEditComment(commentId: Long) { _editingCommentId.value = commentId - highlightComment(commentId) + unhighlightJob?.cancel() + _feedDetailUiState.value = _feedDetailUiState.value.highlightComment(commentId) } fun cancelEditComment() { _editingCommentId.value = null - unhighlightComment() + _feedDetailUiState.value = _feedDetailUiState.value.unhighlightComment() + } + + fun highlightCommentOnFirstEnter(commentId: Long) { + _feedDetailUiState.value = _feedDetailUiState.value.highlightComment(commentId) + unhighlightJob = viewModelScope.launch { + delay(COMMENT_HIGHLIGHTING_DURATION_ON_FIRST_ENTER) + _feedDetailUiState.value = _feedDetailUiState.value.unhighlightComment() + } } fun reportComment(commentId: Long): Job = command( @@ -223,20 +235,13 @@ class FeedDetailViewModel @Inject constructor( }, ) - fun highlightComment(commentId: Long) { - _feedDetailUiState.value = _feedDetailUiState.value.highlightComment(commentId) - _uiEvent.value = FeedDetailUiEvent.CommentHighlight(commentId) - } - - private fun unhighlightComment() { - _feedDetailUiState.value = _feedDetailUiState.value.unhighlightComment() - } - companion object { const val KEY_FEED_ID: String = "KEY_FEED_ID" private const val DEFAULT_FEED_ID: Long = -1 private const val DELETED_FEED_FETCH_ERROR_CODE = 403 private const val REPORT_DUPLICATE_ERROR_CODE = 400 + + private const val COMMENT_HIGHLIGHTING_DURATION_ON_FIRST_ENTER = 2000L } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiEvent.kt index a25ff414b..5194643c2 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiEvent.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiEvent.kt @@ -11,5 +11,4 @@ sealed interface FeedDetailUiEvent { object CommentReportFail : FeedDetailUiEvent object CommentReportComplete : FeedDetailUiEvent object CommentPostComplete : FeedDetailUiEvent - data class CommentHighlight(val commentId: Long) : FeedDetailUiEvent }