Skip to content

Commit

Permalink
Add landscape carousel
Browse files Browse the repository at this point in the history
  • Loading branch information
adhiamboperes committed Aug 1, 2024
1 parent a18c45c commit 2c7b33f
Showing 24 changed files with 703 additions and 204 deletions.
2 changes: 2 additions & 0 deletions app/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -146,6 +146,7 @@ LISTENERS = [
"src/main/java/org/oppia/android/app/player/state/listener/StateKeyboardButtonListener.kt",
"src/main/java/org/oppia/android/app/player/state/listener/SubmitNavigationButtonListener.kt",
"src/main/java/org/oppia/android/app/policies/RouteToPoliciesListener.kt",
"src/main/java/org/oppia/android/app/profile/ProfileClickListener.kt",
"src/main/java/org/oppia/android/app/profile/RouteToAdminPinListener.kt",
"src/main/java/org/oppia/android/app/profileprogress/ProfilePictureClickListener.kt",
"src/main/java/org/oppia/android/app/profileprogress/RouteToCompletedStoryListListener.kt",
@@ -413,6 +414,7 @@ VIEWS_WITH_RESOURCE_IMPORTS = [
"src/main/java/org/oppia/android/app/customview/PromotedStoryCardView.kt",
"src/main/java/org/oppia/android/app/customview/SegmentedCircularProgressView.kt",
"src/main/java/org/oppia/android/app/customview/VerticalDashedLineView.kt",
"src/main/java/org/oppia/android/app/profile/ProfileListView.kt",
"src/main/java/org/oppia/android/app/survey/SurveyMultipleChoiceOptionView.kt",
"src/main/java/org/oppia/android/app/survey/SurveyNpsItemOptionView.kt",
"src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt",
Original file line number Diff line number Diff line change
@@ -58,7 +58,7 @@ import org.oppia.android.app.player.stopplaying.StopExplorationDialogFragment
import org.oppia.android.app.player.stopplaying.UnsavedExplorationDialogFragment
import org.oppia.android.app.policies.PoliciesFragment
import org.oppia.android.app.profile.AdminSettingsDialogFragment
import org.oppia.android.app.profile.ProfileChooserFragment
import org.oppia.android.app.profile.ProfileActionChooserFragment
import org.oppia.android.app.profile.ResetPinDialogFragment
import org.oppia.android.app.profileprogress.ProfilePictureEditDialogFragment
import org.oppia.android.app.profileprogress.ProfileProgressFragment
@@ -162,7 +162,7 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto
fun inject(osDeprecationNoticeDialogFragment: OsDeprecationNoticeDialogFragment)
fun inject(policiesFragment: PoliciesFragment)
fun inject(profileAndDeviceIdFragment: ProfileAndDeviceIdFragment)
fun inject(profileChooserFragment: ProfileChooserFragment)
fun inject(profileChooserFragment: ProfileActionChooserFragment)
fun inject(profileEditDeletionDialogFragment: ProfileEditDeletionDialogFragment)
fun inject(profileEditFragment: ProfileEditFragment)
fun inject(profileListFragment: ProfileListFragment)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -7,12 +7,13 @@ import android.view.View
import android.view.ViewGroup
import org.oppia.android.app.fragment.FragmentComponentImpl
import org.oppia.android.app.fragment.InjectableFragment
import org.oppia.android.app.model.Profile
import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
import org.oppia.android.util.platformparameter.PlatformParameterValue
import javax.inject.Inject

/** Fragment that allows user to select a profile or create new ones. */
class ProfileChooserFragment : InjectableFragment(), RouteToAdminPinListener, AddProfileListener {
class ProfileChooserFragment : InjectableFragment(), RouteToAdminPinListener, ProfileClickListener {
@Inject
lateinit var profileChooserFragmentPresenterV1: ProfileChooserFragmentPresenterV1

@@ -48,7 +49,7 @@ class ProfileChooserFragment : InjectableFragment(), RouteToAdminPinListener, Ad
}
}

override fun onAddProfileClicked() {
profileChooserFragmentPresenter.addProfileClickListener()
override fun onProfileClicked(profile: Profile) {
profileChooserFragmentPresenter.onProfileClick(profile)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.oppia.android.app.profile

import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -11,6 +13,8 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.oppia.android.R
import org.oppia.android.app.administratorcontrols.AdministratorControlsActivity
import org.oppia.android.app.classroom.ClassroomListActivity
@@ -21,6 +25,7 @@ import org.oppia.android.app.model.Profile
import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.onboarding.IntroActivity
import org.oppia.android.app.recyclerview.BindableAdapter
import org.oppia.android.app.recyclerview.StartSnapHelper
import org.oppia.android.databinding.ProfileItemBinding
import org.oppia.android.databinding.ProfileSelectionFragmentBinding
import org.oppia.android.domain.oppialogger.OppiaLogger
@@ -83,24 +88,86 @@ class ProfileChooserFragmentPresenter @Inject constructor(
StatusBarColor.statusBarColorUpdate(
R.color.component_color_shared_profile_status_bar_color, activity, false
)
binding = ProfileSelectionFragmentBinding.inflate(
inflater,
container,
/* attachToRoot= */ false
)
binding.apply {

binding = ProfileSelectionFragmentBinding.inflate(inflater, container, false).apply {
viewModel = chooserViewModel
lifecycleOwner = fragment
}

logProfileChooserEvent()
binding.profilesList.isNestedScrollingEnabled = false
subscribeToWasProfileEverBeenAdded()
binding.profilesList.apply {
adapter = createRecyclerViewAdapter()

binding.apply {
when (Resources.getSystem().configuration.orientation) {
Configuration.ORIENTATION_PORTRAIT -> setupPortraitMode()
Configuration.ORIENTATION_LANDSCAPE -> setupLandscapeMode()
}
}

binding.addProfileButton.setOnClickListener { addProfileButtonClickListener() }
binding.addProfilePrompt.setOnClickListener { addProfileButtonClickListener() }

return binding.root
}

private fun ProfileSelectionFragmentBinding.setupPortraitMode() {
profilesList?.apply {
isNestedScrollingEnabled = false
adapter = createRecyclerViewAdapter()
}
}

private fun ProfileSelectionFragmentBinding.setupLandscapeMode() {
val snapHelper = StartSnapHelper()
val layoutManager = profilesListLandscape?.layoutManager as LinearLayoutManager?

profilesListLandscape?.onFlingListener = null

profilesListLandscape?.viewTreeObserver?.addOnGlobalLayoutListener {
if (profilesListLandscape.shouldShowScrollArrows()) {
profileScrollLeft?.visibility = View.VISIBLE
profileScrollRight?.visibility = View.VISIBLE
} else {
profileScrollLeft?.visibility = View.GONE
profileScrollRight?.visibility = View.GONE
}
}

profileScrollLeft?.setOnClickListener {
snapRecyclerView(layoutManager, snapHelper, true)
}

profileScrollRight?.setOnClickListener {
snapRecyclerView(layoutManager, snapHelper, false)
}
}

private fun RecyclerView.shouldShowScrollArrows(): Boolean {
val layoutManager = this.layoutManager as? LinearLayoutManager ?: return false

val visibleItemCount = layoutManager.childCount
val totalItemCount = this.adapter?.itemCount
return totalItemCount != null && totalItemCount > visibleItemCount
}

private fun snapRecyclerView(
layoutManager: LinearLayoutManager?,
snapHelper: StartSnapHelper,
isLeft: Boolean
) {
val newLayoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)

val targetView = snapHelper.findSnapView(layoutManager ?: newLayoutManager)
targetView?.let {
val distance = snapHelper.calculateDistanceToFinalSnap(layoutManager ?: newLayoutManager, it)
val scrollDistance = distance?.get(0) ?: 0
val width = binding.profilesListLandscape?.width ?: 0

val offset = if (isLeft) scrollDistance - width else width - scrollDistance
binding.profilesListLandscape?.smoothScrollBy(offset, 0)
}
}

private fun subscribeToWasProfileEverBeenAdded() {
wasProfileEverBeenAdded.observe(
activity,
@@ -112,7 +179,7 @@ class ProfileChooserFragmentPresenter @Inject constructor(
activity.resources.getInteger(R.integer.profile_chooser_first_time_span_count)
}
val layoutManager = GridLayoutManager(activity, spanCount)
binding.profilesList.layoutManager = layoutManager
binding.profilesList?.layoutManager = layoutManager
}
)
}
@@ -163,13 +230,16 @@ class ProfileChooserFragmentPresenter @Inject constructor(
) {
binding.viewModel = viewModel
binding.profileItemContainer.setOnClickListener {
updateLearnerIdIfAbsent(viewModel.profile)
ensureProfileOnboarded(viewModel.profile)
}
}

/** Click listener for the button to add a new profile. */
fun addProfileClickListener() {
/** Click listener for handling clicks to login to a profile. */
fun onProfileClick(profile: Profile) {
updateLearnerIdIfAbsent(profile)
ensureProfileOnboarded(profile)
}

private fun addProfileButtonClickListener() {
if (chooserViewModel.adminPin.isEmpty()) {
activity.startActivity(
AdminPinActivity.createAdminPinActivityIntent(
Original file line number Diff line number Diff line change
@@ -61,7 +61,7 @@ private val COLORS_LIST = listOf(
R.color.component_color_avatar_background_24_color
)

/** The presenter for [ProfileChooserFragment]. */
/** The presenter for [ProfileActionChooserFragment]. */
@FragmentScope
class ProfileChooserFragmentPresenterV1 @Inject constructor(
private val fragment: Fragment,
@@ -130,7 +130,7 @@ class ProfileChooserFragmentPresenterV1 @Inject constructor(
return when (wasProfileEverBeenAddedResult) {
is AsyncResult.Failure -> {
oppiaLogger.e(
"ProfileChooserFragment",
"ProfileActionChooserFragment",
"Failed to retrieve the information on wasProfileEverBeenAdded",
wasProfileEverBeenAddedResult.error
)
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
import org.oppia.android.util.platformparameter.PlatformParameterValue
import javax.inject.Inject

/** The ViewModel for [ProfileChooserFragment]. */
/** The ViewModel for [ProfileActionChooserFragment]. */
@FragmentScope
class ProfileChooserViewModel @Inject constructor(
fragment: Fragment,
@@ -30,7 +30,7 @@ class ProfileChooserViewModel @Inject constructor(
) : ObservableViewModel() {

private val routeToAdminPinListener = fragment as RouteToAdminPinListener
private val addProfileListener = fragment as AddProfileListener
private val profileClickListener = fragment as ProfileClickListener

/** Observable field to track if the add profile button should be shown. */
val canAddProfile = ObservableField(true)
@@ -62,7 +62,7 @@ class ProfileChooserViewModel @Inject constructor(
is AsyncResult.Pending -> emptyList()
is AsyncResult.Success -> profilesResult.value
}.map {
ProfileItemViewModel(it)
ProfileItemViewModel(it, profileClickListener::onProfileClicked)
}

profileList.forEach { profileItemViewModel ->
@@ -144,9 +144,4 @@ class ProfileChooserViewModel @Inject constructor(
fun onAdministratorControlsButtonClicked() {
routeToAdminPinListener.routeToAdminPin()
}

/** Handles click events for the add profile button. */
fun onAddProfileButtonClicked() {
addProfileListener.onAddProfileClicked()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.oppia.android.app.profile

import org.oppia.android.app.model.Profile

/** Listener for when a profile is clicked. */
interface ProfileClickListener {
/** Triggered when the profile is clicked. */
fun onProfileClicked(profile: Profile)
}
Original file line number Diff line number Diff line change
@@ -3,4 +3,15 @@ package org.oppia.android.app.profile
import org.oppia.android.app.model.Profile
import org.oppia.android.app.viewmodel.ObservableViewModel

class ProfileItemViewModel(val profile: Profile) : ObservableViewModel()
/** ViewModel for binding a profile data to the UI. */
class ProfileItemViewModel(
val profile: Profile,
val onProfileClicked: (Profile) -> Unit
) : ObservableViewModel() {

/** Called when a profile is clicked. */
// todo maybe remove
fun profileClicked() {
onProfileClicked(profile)
}
}
89 changes: 89 additions & 0 deletions app/src/main/java/org/oppia/android/app/profile/ProfileListView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package org.oppia.android.app.profile

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import org.oppia.android.app.recyclerview.BindableAdapter
import org.oppia.android.app.shim.ViewBindingShim
import org.oppia.android.app.view.ViewComponentFactory
import org.oppia.android.app.view.ViewComponentImpl
import javax.inject.Inject

/** A custom [RecyclerView] for displaying a list of profiles as a carousel. */
class ProfileListView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {

@Inject
lateinit var bindingInterface: ViewBindingShim

@Inject
lateinit var singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory

private lateinit var profileDataList: List<ProfileItemViewModel>

override fun onAttachedToWindow() {
super.onAttachedToWindow()
val viewComponentFactory = FragmentManager.findFragment<Fragment>(this) as ViewComponentFactory
val viewComponent = viewComponentFactory.createViewComponent(this) as ViewComponentImpl
viewComponent.inject(this)
maybeInitializeAdapter()
}

private fun maybeInitializeAdapter() {
if (::bindingInterface.isInitialized &&
::singleTypeBuilderFactory.isInitialized &&
::profileDataList.isInitialized
) {
bindDataToAdapter()
}
}

private fun bindDataToAdapter() {
// We manually set the data so we can first check for the adapter unlike when using an existing
// [RecyclerViewBindingAdapter].
// This ensures that the adapter will only be created once and correctly rebinds the data.
// For more context: https://github.com/oppia/oppia-android/pull/2246#pullrequestreview-565964462
if (adapter == null) {
adapter = createAdapter()
}

(adapter as BindableAdapter<*>).setDataUnchecked(profileDataList)
}

private fun createAdapter(): BindableAdapter<ProfileItemViewModel> {
return singleTypeBuilderFactory.create<ProfileItemViewModel>()
.registerViewBinder(
inflateView = { parent ->
bindingInterface.provideProfileItemInflatedView(
LayoutInflater.from(parent.context),
parent,
attachToParent = false
)
},
bindView = { view, viewModel ->
bindingInterface.provideProfileItemViewModel(
view,
viewModel
)
}
).build()
}

/**
* Sets the list of profiles that this view shows.
* @param newDataList the new list of profiles to present
*/

fun setProfileList(newDataList: List<ProfileItemViewModel>?) {
if (newDataList != null) {
profileDataList = newDataList
maybeInitializeAdapter()
}
}
}
Loading

0 comments on commit 2c7b33f

Please sign in to comment.