-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #712 from hyperskill/feature/fill_blanks_input
Fill in the blanks with inputs
- Loading branch information
Showing
60 changed files
with
2,519 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 151 additions & 0 deletions
151
...g/hyperskill/app/android/step_quiz_fill_blanks/delegate/FillBlanksStepQuizFormDelegate.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
94 changes: 94 additions & 0 deletions
94
.../org/hyperskill/app/android/step_quiz_fill_blanks/dialog/FillBlanksInputDialogFragment.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
...a/org/hyperskill/app/android/step_quiz_fill_blanks/fragment/FillBlanksStepQuizFragment.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
androidHyperskillApp/src/main/res/drawable/bg_step_quiz_fill_blanks_input_item.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
9 changes: 9 additions & 0 deletions
9
androidHyperskillApp/src/main/res/drawable/bg_step_quiz_fill_blanks_input_text_field.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
6 changes: 6 additions & 0 deletions
6
...oidHyperskillApp/src/main/res/drawable/bg_step_quiz_fill_blanks_item_vertical_divider.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.