-
Notifications
You must be signed in to change notification settings - Fork 528
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
Fix #161: Exploration player contentcard supports rich-text part -2 #229
Changes from all commits
a1ff29a
48f1848
3ea6068
51e238d
0d9b33c
0c2fd6c
7e5ef04
1cf96e6
ffbb96c
a95d52f
309a10a
1af3774
c64e9c7
f987294
e45d1c1
f7f19c2
78e64dc
736f515
31efdbb
755ed03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package org.oppia.app.player.content | ||
|
||
import android.content.Context | ||
import android.text.Spannable | ||
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.content_card_item.view.* | ||
import kotlinx.android.synthetic.main.interation_feedback_card_item.view.* | ||
import org.oppia.app.R | ||
import org.oppia.app.databinding.ContentCardItemBinding | ||
import org.oppia.app.databinding.InterationFeedbackCardItemBinding | ||
import org.oppia.util.data.HtmlParser | ||
|
||
// TODO(#216): Make use of generic data-binding-enabled RecyclerView adapter | ||
/** Adapter to bind the contents to the [RecyclerView]. It handles rich-text content. */ | ||
class ContentCardAdapter( | ||
private val context: Context, | ||
private val entityType: String, | ||
private val entityId: String, | ||
val contentList: MutableList<ContentViewModel> | ||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { | ||
|
||
val VIEW_TYPE_CONTENT = 1 | ||
val VIEW_TYPE_INTERACTION_FEEDBACK = 2 | ||
|
||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { | ||
return when (viewType) { | ||
VIEW_TYPE_CONTENT -> { | ||
val inflater = LayoutInflater.from(parent.getContext()) | ||
val binding = | ||
DataBindingUtil.inflate<ContentCardItemBinding>( | ||
inflater, | ||
R.layout.content_card_item, | ||
parent, /* attachToParent= */ | ||
false | ||
) | ||
ContentViewHolder(binding, context, entityType, entityId) | ||
} | ||
VIEW_TYPE_INTERACTION_FEEDBACK -> { | ||
val inflater = LayoutInflater.from(parent.getContext()) | ||
val binding = | ||
DataBindingUtil.inflate<InterationFeedbackCardItemBinding>( | ||
inflater, | ||
R.layout.interation_feedback_card_item, | ||
parent, | ||
/* attachToParent= */false | ||
) | ||
InteractionFeedbackViewHolder(binding, context, entityType, entityId) | ||
} | ||
else -> throw IllegalArgumentException("Invalid view type") | ||
} | ||
} | ||
|
||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { | ||
when (holder.itemViewType) { | ||
VIEW_TYPE_CONTENT -> (holder as ContentViewHolder).bind(contentList!!.get(position).htmlContent) | ||
VIEW_TYPE_INTERACTION_FEEDBACK -> (holder as InteractionFeedbackViewHolder).bind(contentList!!.get(position).htmlContent) | ||
} | ||
} | ||
|
||
// Determines the appropriate ViewType according to the content_id. | ||
override fun getItemViewType(position: Int): Int { | ||
val contentId = contentList.get(position).contentId | ||
return if (!contentId.contains("content") && !contentId.contains("Feedback")) { | ||
VIEW_TYPE_INTERACTION_FEEDBACK | ||
} else { | ||
VIEW_TYPE_CONTENT | ||
} | ||
} | ||
|
||
override fun getItemCount(): Int { | ||
return contentList!!.size | ||
} | ||
|
||
private class ContentViewHolder( | ||
val binding: ViewDataBinding, | ||
private val context: Context, | ||
private val entityType: String, | ||
private val entityId: String | ||
) : RecyclerView.ViewHolder(binding.root) { | ||
internal fun bind(rawString: String?) { | ||
binding.setVariable(BR.htmlContent, rawString) | ||
binding.executePendingBindings(); | ||
val html: Spannable = HtmlParser(context, entityType, entityId).parseHtml(rawString, binding.root.tv_contents) | ||
binding.root.tv_contents.text = html | ||
} | ||
} | ||
|
||
private class InteractionFeedbackViewHolder( | ||
val binding: ViewDataBinding, | ||
private val context: Context, | ||
private val entityType: String, | ||
private val entityId: String | ||
) : | ||
RecyclerView.ViewHolder(binding.root) { | ||
internal fun bind(rawString: String?) { | ||
binding.setVariable(BR.htmlContent, rawString) | ||
binding.executePendingBindings(); | ||
val html: Spannable = | ||
HtmlParser(context, entityType, entityId).parseHtml(rawString, binding.root.tv_interaction_feedback) | ||
binding.root.tv_interaction_feedback.text = html | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package org.oppia.app.player.content | ||
|
||
import android.content.Context | ||
import android.os.Bundle | ||
import android.util.Log | ||
import androidx.fragment.app.Fragment | ||
import android.view.LayoutInflater | ||
import android.view.View | ||
import android.view.ViewGroup | ||
import android.widget.Button | ||
|
||
import androidx.recyclerview.widget.LinearLayoutManager | ||
import androidx.recyclerview.widget.RecyclerView | ||
import org.oppia.app.R | ||
import org.oppia.app.fragment.InjectableFragment | ||
import org.oppia.data.backends.gae.NetworkModule | ||
import org.oppia.data.backends.gae.model.GaeExplorationContainer | ||
import org.oppia.data.backends.gae.model.GaeState | ||
import org.oppia.data.backends.gae.model.GaeSubtitledHtml | ||
import retrofit2.Call | ||
import retrofit2.Callback | ||
import retrofit2.Response | ||
import java.lang.Exception | ||
|
||
import java.util.ArrayList | ||
import javax.inject.Inject | ||
|
||
/** Fragment that displays contents that supports rich-text. */ | ||
class ContentListFragment : InjectableFragment() { | ||
|
||
@Inject | ||
lateinit var contentListFragmentPresenter: ContentListFragmentPresenter | ||
|
||
override fun onAttach(context: Context?) { | ||
super.onAttach(context) | ||
fragmentComponent.inject(this) | ||
} | ||
|
||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { | ||
return contentListFragmentPresenter.handleCreateView(inflater, container) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package org.oppia.app.player.content | ||
|
||
import android.os.Bundle | ||
import android.view.LayoutInflater | ||
import android.view.View | ||
import android.view.ViewGroup | ||
import androidx.fragment.app.Fragment | ||
import androidx.lifecycle.LiveData | ||
import androidx.lifecycle.Observer | ||
import androidx.lifecycle.Transformations | ||
import org.oppia.app.R | ||
import org.oppia.app.databinding.ContentListFragmentBinding | ||
import org.oppia.app.model.EphemeralState | ||
import org.oppia.app.player.state.StateViewModel | ||
import org.oppia.app.viewmodel.ViewModelProvider | ||
import org.oppia.domain.exploration.ExplorationProgressController | ||
import org.oppia.util.data.AsyncResult | ||
import org.oppia.util.logging.Logger | ||
import javax.inject.Inject | ||
|
||
/** Presenter for [ContentListFragment]. */ | ||
class ContentListFragmentPresenter @Inject constructor( | ||
private val fragment: Fragment, | ||
private val viewModelProvider: ViewModelProvider<ContentViewModel>, | ||
private val explorationProgressController: ExplorationProgressController, | ||
private val logger: Logger | ||
) { | ||
|
||
private var entityType: String = "" | ||
private var entityId: String = "" | ||
|
||
lateinit var contentCardAdapter: ContentCardAdapter | ||
|
||
var contentList: MutableList<ContentViewModel> = ArrayList() | ||
|
||
fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? { | ||
val binding = ContentListFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false) | ||
|
||
entityType = fragment.arguments!!.getString("entityType") | ||
entityId = fragment.arguments!!.getString("exploration_id") | ||
|
||
binding.recyclerview.apply { | ||
contentCardAdapter = | ||
ContentCardAdapter(context, entityType, entityId, contentList); | ||
binding.recyclerview.adapter = contentCardAdapter | ||
} | ||
subscribeToCurrentState() | ||
|
||
return binding.root | ||
} | ||
|
||
private fun subscribeToCurrentState() { | ||
ephemeralStateLiveData.observe(fragment, Observer<EphemeralState> { result -> | ||
logger.d("StateFragment", "getCurrentState: ${result.state.content.html}") | ||
if (!result.state.content.contentId.equals("")) { | ||
getContentViewModel().contentId = result.state.content.contentId | ||
}else{ | ||
getContentViewModel().contentId = "content" | ||
} | ||
getContentViewModel().htmlContent = result.state.content.html | ||
bindContentList() | ||
}) | ||
} | ||
|
||
private fun bindContentList() { | ||
contentList.add(getContentViewModel()) | ||
contentCardAdapter.notifyDataSetChanged() | ||
} | ||
|
||
private fun getContentViewModel(): ContentViewModel { | ||
return viewModelProvider.getForFragment(fragment, ContentViewModel::class.java) | ||
} | ||
|
||
private val ephemeralStateLiveData: LiveData<EphemeralState> by lazy { | ||
getEphemeralState() | ||
} | ||
|
||
private fun getEphemeralState(): LiveData<EphemeralState> { | ||
return Transformations.map(explorationProgressController.getCurrentState(), ::processCurrentState) | ||
} | ||
|
||
private fun processCurrentState(ephemeralStateResult: AsyncResult<EphemeralState>): EphemeralState { | ||
if (ephemeralStateResult.isFailure()) { | ||
logger.e("StateFragment", "Failed to retrieve ephemeral state", ephemeralStateResult.getErrorOrNull()!!) | ||
} | ||
return ephemeralStateResult.getOrDefault(EphemeralState.getDefaultInstance()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package org.oppia.app.player.content | ||
|
||
import androidx.databinding.ObservableField | ||
import androidx.lifecycle.ViewModel | ||
import org.oppia.app.fragment.FragmentScope | ||
import javax.inject.Inject | ||
|
||
/** [ViewModel] for content-card state. */ | ||
@FragmentScope | ||
class ContentViewModel @Inject constructor() : ViewModel() { | ||
|
||
var contentId = "" | ||
var htmlContent = "" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<layout xmlns:android="http://schemas.android.com/apk/res/android"> | ||
<data> | ||
<variable | ||
name="htmlContent" | ||
type="String"/> | ||
</data> | ||
<FrameLayout | ||
android:layout_width="match_parent" | ||
android:layout_height="wrap_content" | ||
android:layout_margin="4dp" | ||
android:background="@drawable/bg_blue_card" | ||
android:orientation="vertical"> | ||
<TextView | ||
android:id="@+id/tv_contents" | ||
android:layout_width="match_parent" | ||
android:layout_height="wrap_content" | ||
android:layout_margin="4dp" | ||
android:gravity="left" | ||
android:padding="8dp" | ||
android:text="@{htmlContent}"/> | ||
</FrameLayout> | ||
</layout> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<layout xmlns:android="http://schemas.android.com/apk/res/android" | ||
xmlns:app="http://schemas.android.com/apk/res-auto" | ||
xmlns:tools="http://schemas.android.com/tools"> | ||
<androidx.constraintlayout.widget.ConstraintLayout | ||
android:layout_width="match_parent" | ||
android:layout_height="wrap_content" | ||
tools:context=".player.content.ContentListFragment"> | ||
<androidx.recyclerview.widget.RecyclerView | ||
android:id="@+id/recyclerview" | ||
android:layout_width="match_parent" | ||
android:layout_height="wrap_content" | ||
android:layout_marginStart="8dp" | ||
android:layout_marginTop="4dp" | ||
android:layout_marginEnd="8dp" | ||
android:divider="@android:color/transparent" | ||
android:dividerHeight="8dp" | ||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" | ||
veena14cs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
app:layout_constraintEnd_toEndOf="parent" | ||
app:layout_constraintStart_toStartOf="parent" | ||
app:layout_constraintTop_toTopOf="parent"/> | ||
</androidx.constraintlayout.widget.ConstraintLayout> | ||
</layout> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<layout xmlns:android="http://schemas.android.com/apk/res/android"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For file name, should this be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How shall I name it then, it relates to the learners interaction that should be shown on the right side. |
||
<data> | ||
<variable | ||
name="htmlContent" | ||
type="String"/> | ||
</data> | ||
<FrameLayout | ||
android:layout_width="wrap_content" | ||
android:layout_height="wrap_content" | ||
android:layout_marginStart="24dp" | ||
android:layout_marginTop="4dp" | ||
android:layout_marginEnd="4dp" | ||
android:layout_marginBottom="4dp" | ||
android:orientation="vertical"> | ||
<TextView | ||
android:id="@+id/tv_interaction_feedback" | ||
android:layout_width="wrap_content" | ||
android:layout_height="wrap_content" | ||
android:layout_alignParentRight="true" | ||
android:background="@drawable/bg_white_card" | ||
android:gravity="right|center" | ||
android:padding="8dp" | ||
android:text="@{htmlContent}"/> | ||
</FrameLayout> | ||
</layout> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest including the viewType parameter in this exception cause since it'll help provide context if the exception is ever thrown.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't understand what viewType parameter you mean. Can you please elaborate on this.
We have only two viewTypes over here one is content and other is learner's interaction.