Skip to content

ALTAPPS-1259: Android display matching problem as table #1052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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,109 +1,100 @@
package org.hyperskill.app.android.step_quiz_matching.view.delegate

import android.util.TypedValue
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import org.hyperskill.app.android.R
import org.hyperskill.app.android.databinding.LayoutStepQuizSortingBinding
import org.hyperskill.app.android.databinding.LayoutStepQuizMatchingBinding
import org.hyperskill.app.android.step_quiz.view.delegate.StepQuizFormDelegate
import org.hyperskill.app.android.step_quiz_matching.view.adapter.MatchingItemOptionAdapterDelegate
import org.hyperskill.app.android.step_quiz_matching.view.adapter.MatchingItemTitleAdapterDelegate
import org.hyperskill.app.android.step_quiz_matching.view.mapper.MatchingItemMapper
import org.hyperskill.app.android.step_quiz_matching.view.model.MatchingItem
import org.hyperskill.app.android.step_quiz_table.view.adapter.TableSelectionItemAdapterDelegate
import org.hyperskill.app.android.step_quiz_table.view.fragment.TableColumnSelectionBottomSheetDialogFragment
import org.hyperskill.app.android.step_quiz_table.view.model.TableChoiceItem
import org.hyperskill.app.android.step_quiz_table.view.model.TableSelectionItem
import org.hyperskill.app.step_quiz.presentation.StepQuizFeature
import org.hyperskill.app.step_quiz.presentation.StepQuizResolver
import org.hyperskill.app.step_quiz.presentation.submission
import org.hyperskill.app.submissions.domain.model.Reply
import ru.nobird.android.ui.adapters.DefaultDelegateAdapter
import ru.nobird.app.core.model.swap
import ru.nobird.android.view.base.ui.extension.showIfNotExists
import ru.nobird.app.core.model.mutate

class MatchingStepQuizFormDelegate(
binding: LayoutStepQuizSortingBinding,
binding: LayoutStepQuizMatchingBinding,
private val fragmentManager: FragmentManager,
private val onQuizChanged: (Reply) -> Unit
) : StepQuizFormDelegate {

private val optionsAdapter = DefaultDelegateAdapter<MatchingItem>()
private val matchingItemMapper = MatchingItemMapper()

companion object {
const val SKELETON_TITLE_HEIGHT = 50f
private val matchingAdapter = DefaultDelegateAdapter<TableSelectionItem>().apply {
addDelegate(TableSelectionItemAdapterDelegate(::onItemClick))
}

init {
optionsAdapter += MatchingItemTitleAdapterDelegate()
optionsAdapter += MatchingItemOptionAdapterDelegate(optionsAdapter, ::moveOption)

with(binding.sortingSkeleton.firstSkeleton) {
layoutParams =
(layoutParams as ViewGroup.MarginLayoutParams).apply {
rightMargin = context.resources.getDimensionPixelOffset(R.dimen.step_quiz_matching_item_margin)
}
layoutParams.height =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SKELETON_TITLE_HEIGHT, resources.displayMetrics)
.toInt()
}

with(binding.sortingSkeleton.secondSkeleton) {
layoutParams =
(layoutParams as ViewGroup.MarginLayoutParams).apply {
leftMargin = context.resources.getDimensionPixelOffset(R.dimen.step_quiz_matching_item_margin)
}
}

with(binding.sortingRecycler) {
adapter = optionsAdapter
with(binding.matchingRecycler) {
itemAnimator = null
adapter = matchingAdapter
isNestedScrollingEnabled = false
layoutManager = LinearLayoutManager(context)

(itemAnimator as? SimpleItemAnimator)
?.supportsChangeAnimations = false
}
}

private fun moveOption(position: Int, direction: MatchingItemOptionAdapterDelegate.SortingDirection) {
val targetPosition =
when (direction) {
MatchingItemOptionAdapterDelegate.SortingDirection.UP ->
position - 2

MatchingItemOptionAdapterDelegate.SortingDirection.DOWN ->
position + 2
}

optionsAdapter.items = optionsAdapter.items.swap(position, targetPosition)
optionsAdapter.notifyItemChanged(position)
optionsAdapter.notifyItemChanged(targetPosition)
onQuizChanged(createReply())
private fun onItemClick(index: Int, rowTitle: String, choiceItems: List<TableChoiceItem>) {
TableColumnSelectionBottomSheetDialogFragment
.newInstance(
index = index,
rowTitle = rowTitle,
chosenColumns = choiceItems,
isCheckBox = false
)
.showIfNotExists(fragmentManager, TableColumnSelectionBottomSheetDialogFragment.TAG)
}

override fun setState(state: StepQuizFeature.StepQuizState.AttemptLoaded) {
val matchingItems = matchingItemMapper
.mapToMatchingItems(state.attempt, StepQuizResolver.isQuizEnabled(state))
matchingAdapter.items = MatchingItemMapper.mapToTableSelectionItems(
attempt = state.attempt,
submission = state.submission,
isEnabled = StepQuizResolver.isQuizEnabled(state)
)
}

optionsAdapter.items =
if (state.submissionState is StepQuizFeature.SubmissionState.Loaded) {
val ordering =
(state.submissionState as StepQuizFeature.SubmissionState.Loaded).submission.reply?.ordering
?: emptyList()
matchingItems.sortedBy {
when (it) {
is MatchingItem.Title ->
it.id * 2
override fun createReply(): Reply =
createReplyInternal(matchingAdapter.items)

is MatchingItem.Option ->
ordering.indexOf(it.id) * 2 + 1
}
}
} else {
matchingItems
private fun createReplyInternal(items: List<TableSelectionItem>): Reply =
Reply.matching(
ordering = items.map { item ->
item.tableChoices.firstOrNull { it.answer }?.id
}
)

fun updateTableSelectionItem(index: Int, answers: List<TableChoiceItem>) {
val items = matchingAdapter.items
val newItems = swapAnswers(currentRowIndex = index, answers = answers, rows = items)
matchingAdapter.items = newItems
onQuizChanged(createReplyInternal(newItems))
}

override fun createReply(): Reply =
Reply(
ordering = optionsAdapter
.items
.filterIsInstance<MatchingItem.Option>()
.map(MatchingItem.Option::id)
)
private fun swapAnswers(
currentRowIndex: Int,
answers: List<TableChoiceItem>,
rows: List<TableSelectionItem>
): List<TableSelectionItem> {
val selectedAnswerIndex =
answers
.indexOfFirst { it.answer }
.takeIf { it != -1 }
?: return rows
val rowIndexToSwap =
rows
.indexOfFirst { it.tableChoices[selectedAnswerIndex].answer }
.takeIf { it != -1 }
return rows.mutate {
val currentRow = get(currentRowIndex)
if (rowIndexToSwap != null) {
val rowToSwap = get(rowIndexToSwap)
set(currentRowIndex, currentRow.copy(tableChoices = rowToSwap.tableChoices))
set(rowIndexToSwap, rowToSwap.copy(tableChoices = currentRow.tableChoices))
} else {
set(currentRowIndex, currentRow.copy(tableChoices = answers))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import org.hyperskill.app.android.databinding.LayoutStepQuizDescriptionBinding
import org.hyperskill.app.android.databinding.LayoutStepQuizSortingBinding
import org.hyperskill.app.android.databinding.LayoutStepQuizMatchingBinding
import org.hyperskill.app.android.step_quiz.view.delegate.StepQuizFormDelegate
import org.hyperskill.app.android.step_quiz.view.fragment.DefaultStepQuizFragment
import org.hyperskill.app.android.step_quiz_matching.view.delegate.MatchingStepQuizFormDelegate
import org.hyperskill.app.android.step_quiz_table.view.fragment.TableColumnSelectionBottomSheetDialogFragment
import org.hyperskill.app.android.step_quiz_table.view.model.TableChoiceItem
import org.hyperskill.app.step.domain.model.Step
import org.hyperskill.app.step.domain.model.StepRoute
import org.hyperskill.app.step_quiz.presentation.StepQuizFeature
import ru.nobird.app.presentation.redux.container.ReduxView

class MatchingStepQuizFragment :
DefaultStepQuizFragment(),
ReduxView<StepQuizFeature.State, StepQuizFeature.Action.ViewAction> {
ReduxView<StepQuizFeature.State, StepQuizFeature.Action.ViewAction>,
TableColumnSelectionBottomSheetDialogFragment.Callback {
companion object {
fun newInstance(step: Step, stepRoute: StepRoute): Fragment =
MatchingStepQuizFragment().apply {
Expand All @@ -25,30 +28,42 @@ class MatchingStepQuizFragment :
}
}

private var _binding: LayoutStepQuizSortingBinding? = null
private var _binding: LayoutStepQuizMatchingBinding? = null
private val binding get() = _binding!!

private var matchingStepQuizFormDelegate: MatchingStepQuizFormDelegate? = null

override val quizViews: Array<View>
get() = arrayOf(binding.sortingRecycler)
get() = arrayOf(binding.matchingRecycler)

override val skeletonView: View
get() = binding.sortingSkeleton.root
get() = binding.matchingSkeleton.root

override val descriptionBinding: LayoutStepQuizDescriptionBinding
get() = binding.sortingStepDescription
get() = binding.matchingStepDescription

override fun createStepView(layoutInflater: LayoutInflater, parent: ViewGroup): View {
val binding = LayoutStepQuizSortingBinding.inflate(layoutInflater, parent, false).also {
val binding = LayoutStepQuizMatchingBinding.inflate(layoutInflater, parent, false).also {
_binding = it
}
return binding.root
}

override fun onDestroyView() {
_binding = null
matchingStepQuizFormDelegate = null
super.onDestroyView()
}

override fun createStepQuizFormDelegate(): StepQuizFormDelegate =
MatchingStepQuizFormDelegate(binding, onQuizChanged = ::syncReplyState)
override fun createStepQuizFormDelegate(): StepQuizFormDelegate {
val delegate = MatchingStepQuizFormDelegate(
binding, childFragmentManager, onQuizChanged = ::syncReplyState
)
this.matchingStepQuizFormDelegate = delegate
return delegate
}

override fun onSyncChosenColumnsWithParent(index: Int, chosenRows: List<TableChoiceItem>) {
matchingStepQuizFormDelegate?.updateTableSelectionItem(index, chosenRows)
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
package org.hyperskill.app.android.step_quiz_matching.view.mapper

import org.hyperskill.app.android.step_quiz_matching.view.model.MatchingItem
import org.hyperskill.app.android.step_quiz_table.view.model.TableChoiceItem
import org.hyperskill.app.android.step_quiz_table.view.model.TableSelectionItem
import org.hyperskill.app.step_quiz.domain.model.attempts.Attempt
import org.hyperskill.app.step_quiz.domain.model.attempts.Pair
import org.hyperskill.app.submissions.domain.model.Submission

class MatchingItemMapper {
fun mapToMatchingItems(attempt: Attempt, isEnabled: Boolean): List<MatchingItem> =
attempt
.dataset
?.pairs
?.mapIndexed { index, pair ->
listOf(
MatchingItem.Title(index, pair.first ?: "", isEnabled),
MatchingItem.Option(index, pair.second ?: "", isEnabled)
)
}
?.flatten()
?: emptyList()
object MatchingItemMapper {

fun mapToTableSelectionItems(
attempt: Attempt,
submission: Submission?,
isEnabled: Boolean
): List<TableSelectionItem> {
val pairs = attempt.dataset?.pairs ?: return emptyList()
val ordering = submission?.reply?.ordering
return pairs.mapIndexed { index, pair ->
TableSelectionItem(
id = index,
titleText = pair.first ?: "",
tableChoices = mapPairsToTableChoices(pairs, index, ordering),
isEnabled = isEnabled
)
}
}

private fun mapPairsToTableChoices(
pairs: List<Pair>,
rowIndex: Int,
ordering: List<Int?>?
): List<TableChoiceItem> =
pairs.mapIndexed { choiceIndex, choicePair ->
TableChoiceItem(
id = choiceIndex,
text = choicePair.second ?: "",
answer = ordering?.getOrNull(rowIndex) == choiceIndex
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ import by.kirich1409.viewbindingdelegate.viewBinding
import org.hyperskill.app.android.R
import org.hyperskill.app.android.core.view.ui.widget.ProgressableWebViewClient
import org.hyperskill.app.android.databinding.ItemCompoundSelectionCheckboxBinding
import org.hyperskill.app.submissions.domain.model.Cell
import org.hyperskill.app.android.step_quiz_table.view.model.TableChoiceItem
import ru.nobird.android.ui.adapterdelegates.AdapterDelegate
import ru.nobird.android.ui.adapterdelegates.DelegateViewHolder
import ru.nobird.android.ui.adapters.selection.SelectionHelper

class TableColumnMultipleSelectionItemAdapterDelegate(
private val selectionHelper: SelectionHelper,
private val onClick: (Cell) -> Unit
) : AdapterDelegate<Cell, DelegateViewHolder<Cell>>() {
override fun isForViewType(position: Int, data: Cell): Boolean =
private val onClick: (TableChoiceItem) -> Unit
) : AdapterDelegate<TableChoiceItem, DelegateViewHolder<TableChoiceItem>>() {
override fun isForViewType(position: Int, data: TableChoiceItem): Boolean =
true

override fun onCreateViewHolder(parent: ViewGroup): DelegateViewHolder<Cell> =
override fun onCreateViewHolder(parent: ViewGroup): DelegateViewHolder<TableChoiceItem> =
ViewHolder(createView(parent, R.layout.item_compound_selection_checkbox))

private inner class ViewHolder(root: View) : DelegateViewHolder<Cell>(root) {
private inner class ViewHolder(root: View) : DelegateViewHolder<TableChoiceItem>(root) {
private val viewBinding: ItemCompoundSelectionCheckboxBinding by viewBinding(
ItemCompoundSelectionCheckboxBinding::bind
)
Expand All @@ -31,15 +31,15 @@ class TableColumnMultipleSelectionItemAdapterDelegate(

init {
root.setOnClickListener {
onClick(itemData as Cell)
onClick(itemData as TableChoiceItem)
}
tableColumnText.webViewClient = ProgressableWebViewClient(tableColumnTextProgress)
}

override fun onBind(data: Cell) {
itemView.isSelected = selectionHelper.isSelected(adapterPosition)
tableColumnCheckBox.isChecked = selectionHelper.isSelected(adapterPosition)
tableColumnText.setText(data.name)
override fun onBind(data: TableChoiceItem) {
itemView.isSelected = selectionHelper.isSelected(bindingAdapterPosition)
tableColumnCheckBox.isChecked = selectionHelper.isSelected(bindingAdapterPosition)
tableColumnText.setText(data.text)
}
}
}
Loading
Loading