diff --git a/app/build.gradle b/app/build.gradle index c0dffcdea98..cf879eee0f2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -75,6 +75,7 @@ dependencies { ) testImplementation( 'androidx.test:core:1.2.0', + 'androidx.test.espresso:espresso-contrib:3.1.0', 'androidx.test.espresso:espresso-core:3.2.0', 'androidx.test.espresso:espresso-intents:3.1.0', 'androidx.test.ext:junit:1.1.1', @@ -85,6 +86,7 @@ dependencies { ) androidTestImplementation( 'androidx.test:core:1.2.0', + 'androidx.test.espresso:espresso-contrib:3.1.0', 'androidx.test.espresso:espresso-core:3.2.0', 'androidx.test.espresso:espresso-intents:3.1.0', 'androidx.test.ext:junit:1.1.1', diff --git a/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt b/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt index cac6ffe695a..3b1e50b91a3 100644 --- a/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt @@ -12,11 +12,13 @@ import org.oppia.app.viewmodel.ViewModelProvider import org.oppia.domain.UserAppHistoryController import org.oppia.domain.exploration.ExplorationDataController import org.oppia.domain.exploration.TEST_EXPLORATION_ID_5 +import org.oppia.domain.exploration.TEST_EXPLORATION_ID_6 import org.oppia.util.data.AsyncResult import org.oppia.util.logging.Logger import javax.inject.Inject private const val EXPLORATION_ID = TEST_EXPLORATION_ID_5 +private const val EXPLORATION_ID_1 = TEST_EXPLORATION_ID_6 /** The controller for [HomeFragment]. */ @FragmentScope @@ -51,6 +53,7 @@ class HomeFragmentPresenter @Inject constructor( } fun playExplorationButton(v: View) { + explorationDataController.stopPlayingExploration() explorationDataController.startPlayingExploration( EXPLORATION_ID ).observe(fragment, Observer> { result -> @@ -64,4 +67,20 @@ class HomeFragmentPresenter @Inject constructor( } }) } + + fun playExplorationButton_1(v: View) { + explorationDataController.stopPlayingExploration() + explorationDataController.startPlayingExploration( + EXPLORATION_ID_1 + ).observe(fragment, Observer> { result -> + when { + result.isPending() -> logger.d("HomeFragment", "Loading exploration") + result.isFailure() -> logger.e("HomeFragment", "Failed to load exploration", result.getErrorOrNull()!!) + else -> { + logger.d("HomeFragment", "Successfully loaded exploration") + routeToExplorationListener.routeToExploration(EXPLORATION_ID_1) + } + } + }) + } } diff --git a/app/src/main/java/org/oppia/app/player/state/InteractionAdapter.kt b/app/src/main/java/org/oppia/app/player/state/InteractionAdapter.kt new file mode 100755 index 00000000000..1499a1c79f6 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/InteractionAdapter.kt @@ -0,0 +1,166 @@ +package org.oppia.app.player.state + +import android.text.Spannable +import android.util.Log +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.databinding.library.baseAdapters.BR +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.item_selection_interaction_items.view.item_selection_checkbox +import kotlinx.android.synthetic.main.item_selection_interaction_items.view.item_selection_contents_text_view +import kotlinx.android.synthetic.main.multiple_choice_interaction_items.view.multiple_choice_content_text_view +import kotlinx.android.synthetic.main.multiple_choice_interaction_items.view.multiple_choice_radio_button +import org.oppia.app.R +import org.oppia.app.databinding.ItemSelectionInteractionItemsBinding +import org.oppia.app.databinding.MultipleChoiceInteractionItemsBinding +import org.oppia.app.model.InteractionObject +import org.oppia.app.model.StringList +import org.oppia.app.player.state.itemviewmodel.SelectionInteractionContentViewModel +import org.oppia.app.player.state.itemviewmodel.SelectionInteractionCustomizationArgsViewModel +import org.oppia.app.player.state.listener.ItemClickListener +import org.oppia.util.parser.HtmlParser + +private const val VIEW_TYPE_RADIO_BUTTONS = 1 +private const val VIEW_TYPE_CHECKBOXES = 2 +private const val INTERACTION_ADAPTER_TAG = "Interaction Adapter" + +/** + * Adapter to bind the interactions to the [RecyclerView]. It handles MultipleChoiceInput + * and ItemSelectionInput interaction views. + * */ +class InteractionAdapter( + private val htmlParserFactory: HtmlParser.Factory, + private val entityType: String, + private val explorationId: String, + private val itemList: MutableList, + private val selectionInteractionCustomizationArgsViewModel: SelectionInteractionCustomizationArgsViewModel, + private val itemClickListener: ItemClickListener +) : RecyclerView.Adapter() { + + private var itemSelectedPosition = -1 + private var selectedAnswerIndex = -1 + private var selectedHtmlStringList = mutableListOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + VIEW_TYPE_RADIO_BUTTONS -> { + val inflater = LayoutInflater.from(parent.context) + val binding = + DataBindingUtil.inflate( + inflater, + R.layout.multiple_choice_interaction_items, + parent, + /* attachToParent= */ false + ) + MultipleChoiceViewHolder(binding) + } + VIEW_TYPE_CHECKBOXES -> { + val inflater = LayoutInflater.from(parent.context) + val binding = + DataBindingUtil.inflate( + inflater, + R.layout.item_selection_interaction_items, + parent, + /* attachToParent= */ false + ) + ItemSelectionViewHolder(binding) + } + else -> throw IllegalArgumentException("Invalid view type") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder.itemViewType) { + VIEW_TYPE_RADIO_BUTTONS -> (holder as MultipleChoiceViewHolder).bind( + itemList[position].htmlContent, + position, + itemSelectedPosition + ) + VIEW_TYPE_CHECKBOXES -> (holder as ItemSelectionViewHolder).bind( + itemList[position] + ) + } + } + + // Determines the appropriate ViewType according to the interaction type. + override fun getItemViewType(position: Int): Int { + return if (selectionInteractionCustomizationArgsViewModel.interactionId == "ItemSelectionInput") { + if (selectionInteractionCustomizationArgsViewModel.maxAllowableSelectionCount > 1) { + VIEW_TYPE_CHECKBOXES + } else { + VIEW_TYPE_RADIO_BUTTONS + } + } else { + VIEW_TYPE_RADIO_BUTTONS + } + } + + override fun getItemCount(): Int { + return itemList.size + } + + inner class ItemSelectionViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { + internal fun bind(selectionInteractionContentViewModel: SelectionInteractionContentViewModel) { + binding.setVariable(BR.htmlContent, selectionInteractionContentViewModel.htmlContent) + binding.executePendingBindings() + val htmlResult: Spannable = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml( + selectionInteractionContentViewModel.htmlContent, + binding.root.item_selection_contents_text_view + ) + binding.root.item_selection_contents_text_view.text = htmlResult + binding.root.item_selection_checkbox.isChecked = selectionInteractionContentViewModel.isAnswerSelected + binding.root.setOnClickListener { + if (binding.root.item_selection_checkbox.isChecked) { + itemList[adapterPosition].isAnswerSelected = false + selectedHtmlStringList.remove(binding.root.item_selection_contents_text_view.text.toString()) + } else { + if (selectedHtmlStringList.size != selectionInteractionCustomizationArgsViewModel.maxAllowableSelectionCount) { + itemList[adapterPosition].isAnswerSelected = true + selectedHtmlStringList.add(binding.root.item_selection_contents_text_view.text.toString()) + } else { + Log.d( + INTERACTION_ADAPTER_TAG, + "You cannot select more than ${selectionInteractionCustomizationArgsViewModel.maxAllowableSelectionCount} options" + ) + } + } + notifyDataSetChanged() + val interactionObjectBuilder = InteractionObject.newBuilder() + if (selectedHtmlStringList.size >= 0) { + interactionObjectBuilder.setOfHtmlString = StringList.newBuilder().addAllHtml(selectedHtmlStringList).build() + } else { + if (selectedAnswerIndex >= 0) { + interactionObjectBuilder.nonNegativeInt = selectedAnswerIndex + } + + } + itemClickListener.onItemClick(interactionObjectBuilder.build()) + } + } + } + + inner class MultipleChoiceViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { + internal fun bind(rawString: String, position: Int, selectedPosition: Int) { + binding.setVariable(BR.htmlContent, rawString) + binding.executePendingBindings() + val htmlResult: Spannable = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml( + rawString, + binding.root.multiple_choice_content_text_view + ) + binding.root.multiple_choice_content_text_view.text = htmlResult + binding.root.multiple_choice_radio_button.isChecked = selectedPosition == position + binding.root.setOnClickListener { + itemSelectedPosition = adapterPosition + selectedAnswerIndex = adapterPosition + notifyDataSetChanged() + val interactionObjectBuilder = InteractionObject.newBuilder() + if (selectedAnswerIndex >= 0) { + interactionObjectBuilder.nonNegativeInt = selectedAnswerIndex + } + itemClickListener.onItemClick(interactionObjectBuilder.build()) + } + } + } +} diff --git a/app/src/main/java/org/oppia/app/player/state/StateAdapter.kt b/app/src/main/java/org/oppia/app/player/state/StateAdapter.kt old mode 100644 new mode 100755 index c05f0b123df..18f22227a9f --- a/app/src/main/java/org/oppia/app/player/state/StateAdapter.kt +++ b/app/src/main/java/org/oppia/app/player/state/StateAdapter.kt @@ -8,13 +8,19 @@ import androidx.databinding.ViewDataBinding import androidx.recyclerview.widget.RecyclerView import androidx.databinding.library.baseAdapters.BR import kotlinx.android.synthetic.main.content_item.view.content_text_view +import kotlinx.android.synthetic.main.selection_interaction_item.view.selection_interaction_frameLayout import kotlinx.android.synthetic.main.state_button_item.view.* import org.oppia.app.R import org.oppia.app.databinding.ContentItemBinding +import org.oppia.app.databinding.SelectionInteractionItemBinding import org.oppia.app.player.state.itemviewmodel.StateButtonViewModel import org.oppia.app.player.state.listener.ButtonInteractionListener import org.oppia.app.databinding.StateButtonItemBinding +import org.oppia.app.model.InteractionObject import org.oppia.app.player.state.itemviewmodel.ContentViewModel +import org.oppia.app.player.state.itemviewmodel.SelectionInteractionCustomizationArgsViewModel +import org.oppia.app.player.state.itemviewmodel.SelectionInteractionContentViewModel +import org.oppia.app.player.state.listener.ItemClickListener import org.oppia.util.parser.HtmlParser @Suppress("unused") @@ -26,6 +32,7 @@ private const val VIEW_TYPE_NUMERIC_INPUT_INTERACTION = 3 @Suppress("unused") private const val VIEW_TYPE_TEXT_INPUT_INTERACTION = 4 private const val VIEW_TYPE_STATE_BUTTON = 5 +const val VIEW_TYPE_SELECTION_INTERACTION = 6 /** Adapter to inflate different items/views inside [RecyclerView]. The itemList consists of various ViewModels. */ class StateAdapter( @@ -37,6 +44,9 @@ class StateAdapter( ) : RecyclerView.Adapter() { + lateinit var stateButtonViewModel: StateButtonViewModel + private var interactionObjectBuilder: InteractionObject = InteractionObject.newBuilder().build() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { // TODO(#249): Generalize this binding to make adding future interactions easier. @@ -58,11 +68,22 @@ class StateAdapter( inflater, R.layout.content_item, parent, - /* attachToParent= */false + /* attachToParent= */ false ) ContentViewHolder(binding) } - else -> throw IllegalArgumentException("Invalid view type") as Throwable + VIEW_TYPE_SELECTION_INTERACTION -> { + val inflater = LayoutInflater.from(parent.context) + val binding = + DataBindingUtil.inflate( + inflater, + R.layout.selection_interaction_item, + parent, + /* attachToParent= */ false + ) + SelectionInteractionViewHolder(binding) + } + else -> throw IllegalArgumentException("Invalid view type") } } @@ -74,18 +95,21 @@ class StateAdapter( VIEW_TYPE_CONTENT -> { (holder as ContentViewHolder).bind((itemList[position] as ContentViewModel).htmlContent) } + VIEW_TYPE_SELECTION_INTERACTION -> { + (holder as SelectionInteractionViewHolder).bind(itemList[position] as SelectionInteractionCustomizationArgsViewModel) + } } } override fun getItemViewType(position: Int): Int { return when (itemList[position]) { + is ContentViewModel -> VIEW_TYPE_CONTENT + is SelectionInteractionCustomizationArgsViewModel -> VIEW_TYPE_SELECTION_INTERACTION is StateButtonViewModel -> { + stateButtonViewModel = itemList[position] as StateButtonViewModel VIEW_TYPE_STATE_BUTTON } - is ContentViewModel -> { - VIEW_TYPE_CONTENT - } - else -> throw IllegalArgumentException("Invalid type of data at $position: ${itemList[position]}") + else -> throw IllegalArgumentException("Invalid type of data $position") } } @@ -93,6 +117,18 @@ class StateAdapter( return itemList.size } + inner class ContentViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { + internal fun bind(rawString: String) { + binding.setVariable(BR.htmlContent, rawString) + binding.executePendingBindings() + val htmlResult: Spannable = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml( + rawString, + binding.root.content_text_view + ) + binding.root.content_text_view.text = htmlResult + } + } + private class StateButtonViewHolder( val binding: ViewDataBinding, private val buttonInteractionListener: ButtonInteractionListener @@ -112,15 +148,35 @@ class StateAdapter( } } - inner class ContentViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { - internal fun bind(rawString: String) { - binding.setVariable(BR.htmlContent, rawString) - binding.executePendingBindings() - val htmlResult: Spannable = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml( - rawString, - binding.root.content_text_view - ) - binding.root.content_text_view.text = htmlResult + inner class SelectionInteractionViewHolder( + private val binding: ViewDataBinding + ) : RecyclerView.ViewHolder(binding.root), ItemClickListener { + + override fun onItemClick(interactionObject: InteractionObject) { + interactionObjectBuilder = interactionObject + } + + internal fun bind(customizationArgs: SelectionInteractionCustomizationArgsViewModel) { + val items: Array? + val choiceInteractionContentList: MutableList = ArrayList() + val gaeCustomArgsInString = customizationArgs.choiceItems.toString().replace("[", "").replace("]", "") + items = gaeCustomArgsInString.split(",").toTypedArray() + for (values in items) { + val selectionContentViewModel = SelectionInteractionContentViewModel() + selectionContentViewModel.htmlContent = values + selectionContentViewModel.isAnswerSelected = false + choiceInteractionContentList.add(selectionContentViewModel) + } + val interactionAdapter = + InteractionAdapter( + htmlParserFactory, + entityType, + explorationId, + choiceInteractionContentList, + customizationArgs, + this as ItemClickListener + ) + binding.root.selection_interaction_frameLayout.setAdapter(interactionAdapter) } } } diff --git a/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt index a23bf8dec48..37a21a94c8a 100755 --- a/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt @@ -22,6 +22,7 @@ import org.oppia.app.player.audio.AudioFragment import org.oppia.app.player.audio.CellularDataDialogFragment import org.oppia.app.player.exploration.ExplorationActivity import org.oppia.app.player.state.itemviewmodel.ContentViewModel +import org.oppia.app.player.state.itemviewmodel.SelectionInteractionCustomizationArgsViewModel import org.oppia.app.player.state.itemviewmodel.StateButtonViewModel import org.oppia.app.player.state.listener.ButtonInteractionListener import org.oppia.app.viewmodel.ViewModelProvider @@ -37,6 +38,7 @@ import javax.inject.Inject const val STATE_FRAGMENT_EXPLORATION_ID_ARGUMENT_KEY = "STATE_FRAGMENT_EXPLORATION_ID_ARGUMENT_KEY" private const val TAG_CELLULAR_DATA_DIALOG = "CELLULAR_DATA_DIALOG" private const val TAG_AUDIO_FRAGMENT = "AUDIO_FRAGMENT" +private const val TAG_STATE_FRAGMENT = "STATE_FRAGMENT" private const val CONTINUE = "Continue" private const val END_EXPLORATION = "EndExploration" @@ -175,8 +177,8 @@ class StateFragmentPresenter @Inject constructor( ephemeralStateLiveData.observe(fragment, Observer { result -> itemList.clear() currentEphemeralState = result - checkAndAddContentItem() + addInteractionForPendingState() updateDummyStateName() val interactionId = result.state.interaction.id @@ -333,7 +335,6 @@ class StateFragmentPresenter @Inject constructor( oldStateNameList.add(currentEphemeralState.state.name) } } - private fun checkAndAddContentItem() { if (currentEphemeralState.state.hasContent()) { addContentItem() @@ -355,6 +356,40 @@ class StateFragmentPresenter @Inject constructor( stateAdapter.notifyDataSetChanged() } + private fun addInteractionForPendingState() { + if (currentEphemeralState.stateTypeCase.number == EphemeralState.PENDING_STATE_FIELD_NUMBER) { + when (currentEphemeralState.state.interaction.id) { + MULTIPLE_CHOICE_INPUT, ITEM_SELECT_INPUT -> { + addSelectionInteraction() + } + } + } + } + + private fun addSelectionInteraction() { + val customizationArgsMap: Map = + currentEphemeralState.state.interaction.customizationArgsMap + val multipleChoiceInputInteractionViewModel = SelectionInteractionCustomizationArgsViewModel() + val allKeys: Set = customizationArgsMap.keys + + for (key in allKeys) { + logger.d(TAG_STATE_FRAGMENT, key) + } + if (customizationArgsMap.contains("choices")) { + if (customizationArgsMap.contains("maxAllowableSelectionCount")) { + multipleChoiceInputInteractionViewModel.maxAllowableSelectionCount = + currentEphemeralState.state.interaction.customizationArgsMap["maxAllowableSelectionCount"]!!.signedInt + multipleChoiceInputInteractionViewModel.minAllowableSelectionCount = + currentEphemeralState.state.interaction.customizationArgsMap["minAllowableSelectionCount"]!!.signedInt + } + multipleChoiceInputInteractionViewModel.interactionId = currentEphemeralState.state.interaction.id + multipleChoiceInputInteractionViewModel.choiceItems = + currentEphemeralState.state.interaction.customizationArgsMap["choices"]!!.setOfHtmlString.htmlList + } + itemList.add(multipleChoiceInputInteractionViewModel) + stateAdapter.notifyDataSetChanged() + } + private fun updateNavigationButtonVisibility( interactionId: String, hasPreviousState: Boolean, diff --git a/app/src/main/java/org/oppia/app/player/state/customview/SelectionInputInteractionView.kt b/app/src/main/java/org/oppia/app/player/state/customview/SelectionInputInteractionView.kt new file mode 100755 index 00000000000..c5613eb5cc9 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/customview/SelectionInputInteractionView.kt @@ -0,0 +1,43 @@ +package org.oppia.app.player.state.customview + +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout +import androidx.recyclerview.widget.RecyclerView +import org.oppia.app.R +import org.oppia.app.model.InteractionObject +import org.oppia.app.player.state.InteractionAdapter +import org.oppia.app.player.state.listener.InteractionAnswerRetriever +import org.oppia.app.player.state.listener.ItemClickListener + +internal class SelectionInputInteractionView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defaultStyle: Int = 0 +) : FrameLayout(context, attrs, defaultStyle), ItemClickListener, InteractionAnswerRetriever { + private var interactionObjectBuilder: InteractionObject = InteractionObject.newBuilder().build() + + override fun onItemClick(interactionObject: InteractionObject) { + interactionObjectBuilder = interactionObject + } + + private val recyclerView: RecyclerView = RecyclerView(context, attrs, defaultStyle) + + init { + recyclerView.id = R.id.selection_interaction_recyclerview + val params = LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT + ) + this.addView(recyclerView, params) + isClickable = true + isFocusable = true + } + + internal fun setAdapter(adapter: InteractionAdapter) { + recyclerView.adapter = adapter + } + + override fun getPendingAnswer(): InteractionObject { + return interactionObjectBuilder + } +} diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionContentViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionContentViewModel.kt new file mode 100644 index 00000000000..5bfb9e5b059 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionContentViewModel.kt @@ -0,0 +1,9 @@ +package org.oppia.app.player.state.itemviewmodel + +import androidx.lifecycle.ViewModel + +/** [ViewModel] for MultipleChoiceInput values or ItemSelectionInput values. */ +class SelectionInteractionContentViewModel : ViewModel() { + var htmlContent: String = "" + var isAnswerSelected = false +} diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionCustomizationArgsViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionCustomizationArgsViewModel.kt new file mode 100644 index 00000000000..4fe3e090f49 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionCustomizationArgsViewModel.kt @@ -0,0 +1,12 @@ +package org.oppia.app.player.state.itemviewmodel + +import androidx.lifecycle.ViewModel + +/** [ViewModel] for multiple or item-selection input choice list. */ +class SelectionInteractionCustomizationArgsViewModel : ViewModel() { + + var choiceItems: MutableList? = null + var interactionId: String = "" + var maxAllowableSelectionCount: Int = 0 + var minAllowableSelectionCount: Int = 0 +} diff --git a/app/src/main/java/org/oppia/app/player/state/listener/ItemClickListener.kt b/app/src/main/java/org/oppia/app/player/state/listener/ItemClickListener.kt new file mode 100755 index 00000000000..5c9ab7488fd --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/listener/ItemClickListener.kt @@ -0,0 +1,8 @@ +package org.oppia.app.player.state.listener + +import org.oppia.app.model.InteractionObject + +/** This interface helps to get pending answer of MultipleChoice/ItemSelection input interaction. */ +interface ItemClickListener{ + fun onItemClick(interactionObject: InteractionObject) +} diff --git a/app/src/main/res/layout/home_fragment.xml b/app/src/main/res/layout/home_fragment.xml index a7d2e051f24..a7be246c6a4 100644 --- a/app/src/main/res/layout/home_fragment.xml +++ b/app/src/main/res/layout/home_fragment.xml @@ -33,5 +33,15 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/welcome_text_view"/> +