Skip to content

Commit

Permalink
Merge pull request #712 from hyperskill/feature/fill_blanks_input
Browse files Browse the repository at this point in the history
Fill in the blanks with inputs
  • Loading branch information
ivan-magda authored Oct 24, 2023
2 parents 889ed5a + 368d166 commit c94f752
Show file tree
Hide file tree
Showing 60 changed files with 2,519 additions and 23 deletions.
1 change: 1 addition & 0 deletions androidHyperskillApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies {
implementation(libs.android.ui.appcompat)
implementation(libs.android.ui.constraintlayout)
implementation(libs.android.ui.swiperefreshlayout)
implementation(libs.android.ui.flexbox)
implementation(libs.android.ui.core.ktx)
implementation(libs.android.ui.fragment)
implementation(libs.android.ui.fragment.ktx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.hyperskill.app.android.step_quiz.view.factory
import androidx.fragment.app.Fragment
import org.hyperskill.app.android.step_quiz_choice.view.fragment.ChoiceStepQuizFragment
import org.hyperskill.app.android.step_quiz_code.view.fragment.CodeStepQuizFragment
import org.hyperskill.app.android.step_quiz_fill_blanks.fragment.FillBlanksStepQuizFragment
import org.hyperskill.app.android.step_quiz_matching.view.fragment.MatchingStepQuizFragment
import org.hyperskill.app.android.step_quiz_parsons.view.fragment.ParsonsStepQuizFragment
import org.hyperskill.app.android.step_quiz_sorting.view.fragment.SortingStepQuizFragment
Expand Down Expand Up @@ -42,6 +43,9 @@ object StepQuizFragmentFactory {
BlockName.PARSONS ->
ParsonsStepQuizFragment.newInstance(step, stepRoute)

BlockName.FILL_BLANKS ->
FillBlanksStepQuizFragment.newInstance(step, stepRoute)

else ->
UnsupportedStepQuizFragment.newInstance()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package org.hyperskill.app.android.step_quiz_fill_blanks.delegate

import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexboxItemDecoration
import com.google.android.flexbox.FlexboxLayoutManager
import org.hyperskill.app.android.R
import org.hyperskill.app.android.databinding.LayoutStepQuizFillBlanksBindingBinding
import org.hyperskill.app.android.step_quiz.view.delegate.StepQuizFormDelegate
import org.hyperskill.app.android.step_quiz_fill_blanks.dialog.FillBlanksInputDialogFragment
import org.hyperskill.app.step_quiz.domain.model.submissions.Reply
import org.hyperskill.app.step_quiz.presentation.StepQuizFeature
import org.hyperskill.app.step_quiz_fill_blanks.model.FillBlanksItem
import org.hyperskill.app.step_quiz_fill_blanks.model.InvalidFillBlanksConfigException
import org.hyperskill.app.step_quiz_fill_blanks.presentation.FillBlanksItemMapper
import org.hyperskill.app.step_quiz_fill_blanks.presentation.FillBlanksResolver
import ru.nobird.android.ui.adapterdelegates.dsl.adapterDelegate
import ru.nobird.android.ui.adapters.DefaultDelegateAdapter
import ru.nobird.android.view.base.ui.extension.setTextIfChanged
import ru.nobird.android.view.base.ui.extension.showIfNotExists
import ru.nobird.app.core.model.mutate

class FillBlanksStepQuizFormDelegate(
private val binding: LayoutStepQuizFillBlanksBindingBinding,
private val fragmentManager: FragmentManager,
private val onQuizChanged: (Reply) -> Unit
) : StepQuizFormDelegate {

private val fillBlanksAdapter = DefaultDelegateAdapter<FillBlanksItem>().apply {
addDelegate(textAdapterDelegate())
addDelegate(
inputAdapterDelegate(::onInputItemClick)
)
}

private val fillBlanksMapper: FillBlanksItemMapper = FillBlanksItemMapper()

private var resolveState: ResolveState = ResolveState.NOT_RESOLVED

init {
with(binding.stepQuizFillBlanksRecycler) {
itemAnimator = null
adapter = fillBlanksAdapter
isNestedScrollingEnabled = false
layoutManager = FlexboxLayoutManager(context)
addItemDecoration(
FlexboxItemDecoration(context).apply {
setOrientation(FlexboxItemDecoration.HORIZONTAL)
setDrawable(
ContextCompat.getDrawable(context, R.drawable.bg_step_quiz_fill_blanks_item_vertical_divider)
)
}
)
}
}

override fun setState(state: StepQuizFeature.StepQuizState.AttemptLoaded) {
val resolveState = resolve(resolveState, state)
this.resolveState = resolveState
if (resolveState == ResolveState.RESOLVE_SUCCEED) {
val fillBlanksData = fillBlanksMapper.map(
state.attempt,
(state.submissionState as? StepQuizFeature.SubmissionState.Loaded)?.submission
)
fillBlanksAdapter.items = fillBlanksData?.fillBlanks ?: emptyList()
binding.root.post { binding.stepQuizFillBlanksRecycler.requestLayout() }
}
}

private fun resolve(
currentResolveState: ResolveState,
state: StepQuizFeature.StepQuizState.AttemptLoaded
): ResolveState =
if (currentResolveState == ResolveState.NOT_RESOLVED) {
val dataset = state.attempt.dataset
if (dataset != null) {
try {
FillBlanksResolver.resolve(dataset)
ResolveState.RESOLVE_SUCCEED
} catch (e: InvalidFillBlanksConfigException) {
ResolveState.RESOLVE_FAILED
}
} else {
ResolveState.RESOLVE_FAILED
}
} else {
currentResolveState
}

override fun createReply(): Reply =
Reply.fillBlanks(
blanks = fillBlanksAdapter.items.mapNotNull { item ->
when (item) {
is FillBlanksItem.Input -> item.inputText
is FillBlanksItem.Text -> null
}
}
)

fun onInputItemModified(index: Int, text: String) {
fillBlanksAdapter.items = fillBlanksAdapter.items.mutate {
val inputItem = get(index) as FillBlanksItem.Input
set(index, inputItem.copy(inputText = text))
}
fillBlanksAdapter.notifyItemChanged(index)
onQuizChanged(createReply())
}

private fun textAdapterDelegate() =
adapterDelegate<FillBlanksItem, FillBlanksItem.Text>(R.layout.item_step_quiz_fill_blanks_text) {
val textView = itemView as TextView
onBind { textItem ->
textView.updateLayoutParams<FlexboxLayoutManager.LayoutParams> {
isWrapBefore = textItem.startsWithNewLine
}
textView.setTextIfChanged(
HtmlCompat.fromHtml(textItem.text, HtmlCompat.FROM_HTML_MODE_COMPACT)
)
}
}

private fun inputAdapterDelegate(onClick: (index: Int, String) -> Unit) =
adapterDelegate<FillBlanksItem, FillBlanksItem.Input>(R.layout.item_step_quiz_fill_blanks_input) {
val textView = itemView as TextView
textView.setOnClickListener {
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
onClick(position, textView.text.toString())
}
}
onBind { inputItem ->
textView.setTextIfChanged(inputItem.inputText ?: "")
}
}

private fun onInputItemClick(index: Int, text: String) {
FillBlanksInputDialogFragment
.newInstance(index, text)
.showIfNotExists(fragmentManager, FillBlanksInputDialogFragment.TAG)
}

private enum class ResolveState {
NOT_RESOLVED,
RESOLVE_SUCCEED,
RESOLVE_FAILED
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.hyperskill.app.android.step_quiz_fill_blanks.dialog

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.hyperskill.app.android.R
import org.hyperskill.app.android.databinding.FragmentFillBlanksInputBinding
import ru.nobird.android.view.base.ui.extension.argument

class FillBlanksInputDialogFragment : BottomSheetDialogFragment() {
companion object {
const val TAG: String = "FillBlanksInputDialogFragment"

private const val ARG_INDEX = "INDEX"
private const val ARG_TEXT = "TEXT"

fun newInstance(
index: Int,
text: String
): FillBlanksInputDialogFragment =
FillBlanksInputDialogFragment().apply {
this.index = index
this.text = text
}
}

private var index: Int by argument()
private var text: String by argument()

private val fillBlanksInputBinding: FragmentFillBlanksInputBinding by viewBinding(
FragmentFillBlanksInputBinding::bind
)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.TopCornersRoundedBottomSheetDialog)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? =
inflater.inflate(R.layout.fragment_fill_blanks_input, container, false)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

if (savedInstanceState != null) {
index = savedInstanceState.getInt(ARG_INDEX)
text = savedInstanceState.getString(ARG_TEXT) ?: return
}
with(fillBlanksInputBinding) {
fillBlanksInputField.append(text)
fillBlanksInputField.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
super.dismiss()
}
false
}
fillBlanksInputField.post {
fillBlanksInputField.requestFocus()
val inputMethodManager =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.showSoftInput(fillBlanksInputField, InputMethodManager.SHOW_IMPLICIT)
}
}
}

override fun onPause() {
(parentFragment as? Callback)
?.onSyncInputItemWithParent(
index = index,
text = fillBlanksInputBinding.fillBlanksInputField.text.toString()
)
super.onPause()
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(ARG_INDEX, index)
outState.putString(ARG_TEXT, text)
}

interface Callback {
fun onSyncInputItemWithParent(index: Int, text: String)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.hyperskill.app.android.step_quiz_fill_blanks.fragment

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.hyperskill.app.android.databinding.LayoutStepQuizDescriptionBinding
import org.hyperskill.app.android.databinding.LayoutStepQuizFillBlanksBindingBinding
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_fill_blanks.delegate.FillBlanksStepQuizFormDelegate
import org.hyperskill.app.android.step_quiz_fill_blanks.dialog.FillBlanksInputDialogFragment
import org.hyperskill.app.step.domain.model.Step
import org.hyperskill.app.step.domain.model.StepRoute

class FillBlanksStepQuizFragment :
DefaultStepQuizFragment(),
FillBlanksInputDialogFragment.Callback {

companion object {
fun newInstance(
step: Step,
stepRoute: StepRoute
): FillBlanksStepQuizFragment =
FillBlanksStepQuizFragment().apply {
this.step = step
this.stepRoute = stepRoute
}
}

private var _binding: LayoutStepQuizFillBlanksBindingBinding? = null
private val binding: LayoutStepQuizFillBlanksBindingBinding
get() = requireNotNull(_binding)

private var fillBlanksStepQuizFormDelegate: FillBlanksStepQuizFormDelegate? = null

override val quizViews: Array<View>
get() = arrayOf(binding.stepQuizFillBlanksContainer)
override val skeletonView: View
get() = binding.stepQuizFillBlanksSkeleton.root

override val descriptionBinding: LayoutStepQuizDescriptionBinding? = null

override fun createStepView(layoutInflater: LayoutInflater, parent: ViewGroup): View {
val binding = LayoutStepQuizFillBlanksBindingBinding.inflate(layoutInflater, parent, false)
this._binding = binding
return binding.root
}

override fun createStepQuizFormDelegate(): StepQuizFormDelegate =
FillBlanksStepQuizFormDelegate(
binding = binding,
fragmentManager = childFragmentManager,
onQuizChanged = ::syncReplyState
).also {
this.fillBlanksStepQuizFormDelegate = it
}

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

override fun onSyncInputItemWithParent(index: Int, text: String) {
fillBlanksStepQuizFormDelegate?.onInputItemModified(index, text)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:shape="rectangle">
<stroke
android:width="1dp"
android:color="@color/color_on_surface_alpha_60" />
<corners android:radius="@dimen/step_quiz_fill_blanks_input_radius" />
</shape>
</item>

<item android:drawable="@drawable/selectable_item_rounded_background" />
</layer-list>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="1dp"
android:color="@color/color_primary" />
<corners android:radius="@dimen/step_quiz_fill_blanks_input_radius" />
</shape>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size
android:width="16dp"
android:height="16dp" />
</shape>
11 changes: 11 additions & 0 deletions androidHyperskillApp/src/main/res/font/menlo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<font
app:fontStyle="normal"
app:fontWeight="400"
app:font="@font/menlo_regular" />

</font-family>
Loading

0 comments on commit c94f752

Please sign in to comment.