Skip to content

Commit

Permalink
Fix #545: Low fi onboarding part 1 (#558)
Browse files Browse the repository at this point in the history
* Initial file introduction

* ViewModel amnd ViewPager introduction

* Basic implementation finished

* Added images

* New implementation

* Button click listener added

* Skip button functionality added

* Final implememntation

* Added test cases

* Self review nit changes

* Updated test cases- Not perfect

* Indicator dots implementation change and test cases updated

* Added TODO

* Nit change

* Changes as per Ben's suggestion

* Nit changes to OnboardingSlideViewModel

* Added contentDescription

* Updates image test cases

* All test cases fixed

* Nit change

* Merge branch 'develop' of https://github.com/oppia/oppia-android into material-cardview-issue-fix-approach-2

# Conflicts:
#	app/build.gradle

* nit

* nit

* change in package structure, and changed on-board to single word onboard, onboarding proto

*  changed on-boarded to single word onboarded

*  changed on-board to single word onboard in text class method names

*  changed on-board to single word onboard in comments

* nit

* Onboarding flow full lowfi implementation

* Updated test cases

Co-authored-by: nikitamarysolomanpvt <[email protected]>
  • Loading branch information
rt4914 and nikitamarysolomanpvt authored Feb 19, 2020
1 parent 4409ca7 commit d705580
Show file tree
Hide file tree
Showing 30 changed files with 1,438 additions and 28 deletions.
8 changes: 6 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
android:name=".mydownloads.MyDownloadsActivity"
android:screenOrientation="portrait"
android:theme="@style/OppiaThemeWithoutActionBar" />
<activity
android:name=".onboarding.OnboardingActivity"
android:screenOrientation="portrait"
android:theme="@style/OppiaThemeWithoutActionBar" />
<activity
android:name=".player.exploration.ExplorationActivity"
android:screenOrientation="portrait"
Expand Down Expand Up @@ -84,8 +88,10 @@
android:name=".story.testing.StoryFragmentTestActivity"
android:theme="@style/OppiaThemeWithoutActionBar" />
<activity android:name=".testing.BindableAdapterTestActivity" />
<activity android:name=".testing.ConceptCardFragmentTestActivity" />
<activity android:name=".testing.ContentCardTestActivity" />
<activity android:name=".testing.ContinuePlayingFragmentTestActivity" />
<activity android:name=".testing.ExplorationInjectionActivity" />
<activity android:name=".testing.ExplorationTestActivity" />
<activity android:name=".testing.HtmlParserTestActivity" />
<activity android:name=".testing.InputInteractionViewTestActivity" />
Expand All @@ -99,8 +105,6 @@
<activity
android:name=".testing.TopicTestActivityForStory"
android:theme="@style/OppiaThemeWithoutActionBar" />
<activity android:name=".testing.ExplorationInjectionActivity" />
<activity android:name=".testing.ConceptCardFragmentTestActivity" />
<activity
android:name=".topic.questionplayer.QuestionPlayerActivity"
android:screenOrientation="portrait" />
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/org/oppia/app/activity/ActivityComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.oppia.app.fragment.FragmentComponent
import org.oppia.app.help.HelpActivity
import org.oppia.app.home.HomeActivity
import org.oppia.app.home.continueplaying.ContinuePlayingActivity
import org.oppia.app.onboarding.OnboardingActivity
import org.oppia.app.mydownloads.MyDownloadsActivity
import org.oppia.app.player.exploration.ExplorationActivity
import org.oppia.app.profile.AddProfileActivity
Expand All @@ -18,6 +19,7 @@ import org.oppia.app.settings.profile.ProfileEditActivity
import org.oppia.app.settings.profile.ProfileListActivity
import org.oppia.app.settings.profile.ProfileRenameActivity
import org.oppia.app.settings.profile.ProfileResetPinActivity
import org.oppia.app.splash.SplashActivity
import org.oppia.app.story.StoryActivity
import org.oppia.app.story.testing.StoryFragmentTestActivity
import org.oppia.app.testing.AudioFragmentTestActivity
Expand Down Expand Up @@ -66,13 +68,15 @@ interface ActivityComponent {
fun inject(htmlParserTestActivity: HtmlParserTestActivity)
fun inject(myDownloadsActivity: MyDownloadsActivity)
fun inject(navigationDrawerTestActivity: NavigationDrawerTestActivity)
fun inject(onboardingActivity: OnboardingActivity)
fun inject(pinPasswordActivity: PinPasswordActivity)
fun inject(profileActivity: ProfileActivity)
fun inject(questionPlayerActivity: QuestionPlayerActivity)
fun inject(profileEditActivity: ProfileEditActivity)
fun inject(profileListActivity: ProfileListActivity)
fun inject(profileRenameActivity: ProfileRenameActivity)
fun inject(profileResetPinActivity: ProfileResetPinActivity)
fun inject(splashActivity: SplashActivity)
fun inject(storyActivity: StoryActivity)
fun inject(topicActivity: TopicActivity)
fun inject(topicTestActivity: TopicTestActivity)
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.oppia.app.drawer.NavigationDrawerFragment
import org.oppia.app.help.HelpFragment
import org.oppia.app.home.HomeFragment
import org.oppia.app.home.continueplaying.ContinuePlayingFragment
import org.oppia.app.onboarding.OnboardingFragment
import org.oppia.app.mydownloads.DownloadsTabFragment
import org.oppia.app.mydownloads.MyDownloadsFragment
import org.oppia.app.mydownloads.UpdatesTabFragment
Expand Down Expand Up @@ -54,6 +55,7 @@ interface FragmentComponent {
fun inject(homeFragment: HomeFragment)
fun inject(myDownloadsFragment: MyDownloadsFragment)
fun inject(navigationDrawerFragment: NavigationDrawerFragment)
fun inject(onboardingFragment: OnboardingFragment)
fun inject(profileChooserFragment: ProfileChooserFragment)
fun inject(questionPlayerFragment: QuestionPlayerFragment)
fun inject(resetPinDialogFragment: ResetPinDialogFragment)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.oppia.app.onboarding

import android.content.Context
import androidx.databinding.ObservableField
import androidx.lifecycle.ViewModel
import org.oppia.app.R
import org.oppia.app.viewmodel.ObservableViewModel

const val TOTAL_NUMBER_OF_SLIDES = 4

/** [ViewModel] for slide in onboarding flow. */
class OnboardingSlideViewModel(val context: Context, viewPagerSlide: ViewPagerSlide) : ObservableViewModel() {
val slideImage = ObservableField<Int>(R.drawable.ic_onboarding_0)
val contentDescription = ObservableField<String>(context.resources.getString(R.string.onboarding_slide_0_title))
val title = ObservableField<String>(context.resources.getString(R.string.onboarding_slide_0_title))
val description = ObservableField<String>(context.resources.getString(R.string.onboarding_slide_0_description))

init {
initializingSlide(viewPagerSlide)
}

private fun initializingSlide(viewPagerSlide: ViewPagerSlide) {
when (viewPagerSlide) {
ViewPagerSlide.SLIDE_0 -> {
slideImage.set(R.drawable.ic_onboarding_0)
contentDescription.set(context.resources.getString(R.string.onboarding_slide_0_title))
title.set(context.resources.getString(R.string.onboarding_slide_0_title))
description.set(context.resources.getString(R.string.onboarding_slide_0_description))
}
ViewPagerSlide.SLIDE_1 -> {
slideImage.set(R.drawable.ic_onboarding_1)
contentDescription.set(context.resources.getString(R.string.onboarding_slide_1_title))
title.set(context.resources.getString(R.string.onboarding_slide_1_title))
description.set(context.resources.getString(R.string.onboarding_slide_1_description))
}
ViewPagerSlide.SLIDE_2 -> {
slideImage.set(R.drawable.ic_onboarding_2)
contentDescription.set(context.resources.getString(R.string.onboarding_slide_2_title))
title.set(context.resources.getString(R.string.onboarding_slide_2_title))
description.set(context.resources.getString(R.string.onboarding_slide_2_description))
}
ViewPagerSlide.SLIDE_3 -> {
slideImage.set(R.drawable.ic_onboarding_3)
contentDescription.set(context.resources.getString(R.string.onboarding_slide_3_title))
title.set(context.resources.getString(R.string.onboarding_slide_3_title))
description.set(context.resources.getString(R.string.onboarding_slide_3_description))
}
}
}
}
16 changes: 16 additions & 0 deletions app/src/main/java/org/oppia/app/onboarding/OnboadingViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.oppia.app.onboarding

import androidx.databinding.ObservableField
import androidx.lifecycle.ViewModel
import org.oppia.app.viewmodel.ObservableViewModel
import javax.inject.Inject

/** [ViewModel] for [OnboardingFragment]. */
class OnboardingViewModel @Inject constructor() : ObservableViewModel() {
val slideNumber = ObservableField<Int>(0)
val totalNumberOfSlides = TOTAL_NUMBER_OF_SLIDES

fun slideChanged(viewPagerSlide: ViewPagerSlide) {
slideNumber.set(viewPagerSlide.ordinal)
}
}
32 changes: 32 additions & 0 deletions app/src/main/java/org/oppia/app/onboarding/OnboardingActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.oppia.app.onboarding

import android.content.Context
import android.content.Intent
import android.os.Bundle
import org.oppia.app.activity.InjectableAppCompatActivity
import org.oppia.app.profile.ProfileActivity
import javax.inject.Inject

/** Activity that contains the onboarding flow for learners. */
class OnboardingActivity : InjectableAppCompatActivity(), RouteToProfileListListener {
@Inject lateinit var onboardingActivityPresenter: OnboardingActivityPresenter

companion object {
fun createOnboardingActivity(context: Context): Intent {
val intent = Intent(context, OnboardingActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
return intent
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityComponent.inject(this)
onboardingActivityPresenter.handleOnCreate()
}

override fun routeToProfileList() {
startActivity(ProfileActivity.createProfileActivity(this))
finish()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.oppia.app.onboarding

import androidx.appcompat.app.AppCompatActivity
import org.oppia.app.R
import org.oppia.app.activity.ActivityScope
import javax.inject.Inject

/** The presenter for [OnboardingActivity]. */
@ActivityScope
class OnboardingActivityPresenter @Inject constructor(private val activity: AppCompatActivity) {
fun handleOnCreate() {
activity.setContentView(R.layout.onboarding_activity)
if (getOnboardingFragment() == null) {
activity.supportFragmentManager.beginTransaction().add(
R.id.onboarding_fragment_placeholder,
OnboardingFragment()
).commitNow()
}
}

private fun getOnboardingFragment(): OnboardingFragment? {
return activity.supportFragmentManager.findFragmentById(R.id.onboarding_fragment_placeholder) as OnboardingFragment?
}
}
23 changes: 23 additions & 0 deletions app/src/main/java/org/oppia/app/onboarding/OnboardingFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.oppia.app.onboarding

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.oppia.app.fragment.InjectableFragment
import javax.inject.Inject

/** Fragment that contains an onboarding flow of the app. */
class OnboardingFragment : InjectableFragment() {
@Inject lateinit var onboardingFragmentPresenter: OnboardingFragmentPresenter

override fun onAttach(context: Context) {
super.onAttach(context)
fragmentComponent.inject(this)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return onboardingFragmentPresenter.handleCreateView(inflater, container)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.oppia.app.onboarding

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewpager.widget.ViewPager
import org.oppia.app.R
import org.oppia.app.databinding.OnboardingFragmentBinding
import org.oppia.app.fragment.FragmentScope
import org.oppia.app.viewmodel.ViewModelProvider
import org.oppia.domain.onboarding.OnboardingFlowController
import javax.inject.Inject

/** The presenter for [OnboardingFragment]. */
@FragmentScope
class OnboardingFragmentPresenter @Inject constructor(
private val activity: AppCompatActivity,
private val fragment: Fragment,
private val onboardingFlowController: OnboardingFlowController,
private val viewModelProvider: ViewModelProvider<OnboardingViewModel>
) {
private val dotsList = ArrayList<ImageView>()
private lateinit var onboardingPagerAdapter: OnboardingPagerAdapter
private val routeToProfileListener = activity as RouteToProfileListListener
private lateinit var binding: OnboardingFragmentBinding

fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
binding = OnboardingFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)
// NB: Both the view model and lifecycle owner must be set in order to correctly bind LiveData elements to
// data-bound view models.
binding.let {
it.lifecycleOwner = fragment
it.presenter = this
it.viewModel = getOnboardingViewModel()
}
setUpViewPager()
addDots()
return binding.root
}

private fun setUpViewPager() {
onboardingPagerAdapter = OnboardingPagerAdapter(fragment.requireContext())
binding.onboardingSlideViewPager.adapter = onboardingPagerAdapter
binding.onboardingSlideViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
}

override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
}

override fun onPageSelected(position: Int) {
getOnboardingViewModel().slideChanged(ViewPagerSlide.getSlideForPosition(position))
selectDot(position)
}
})
}

fun clickOnGetStarted() {
onboardingFlowController.markOnboardingFlowCompleted()
routeToProfileListener.routeToProfileList()
}

fun clickOnSkip() {
getOnboardingViewModel().slideChanged(ViewPagerSlide.SLIDE_3)
binding.onboardingSlideViewPager.currentItem = ViewPagerSlide.SLIDE_3.ordinal
}

private fun getOnboardingViewModel(): OnboardingViewModel {
return viewModelProvider.getForFragment(fragment, OnboardingViewModel::class.java)
}

private fun addDots() {
val dotsLayout = binding.slideDotsContainer
val dotIdList = ArrayList<Int>()
dotIdList.add(R.id.onboarding_dot_0)
dotIdList.add(R.id.onboarding_dot_1)
dotIdList.add(R.id.onboarding_dot_2)
dotIdList.add(R.id.onboarding_dot_3)
for (index in 0 until TOTAL_NUMBER_OF_SLIDES) {
val dotView = ImageView(activity)
dotView.id = dotIdList[index]
dotView.setImageResource(R.drawable.onboarding_dot_active)

val params = LinearLayout.LayoutParams(
activity.resources.getDimensionPixelSize(R.dimen.dot_width_height),
activity.resources.getDimensionPixelSize(R.dimen.dot_width_height)
)
params.setMargins(
activity.resources.getDimensionPixelSize(R.dimen.dot_gap),
0,
activity.resources.getDimensionPixelSize(R.dimen.dot_gap),
0
)
dotsLayout.addView(dotView, params)
dotsList.add(dotView)
}
selectDot(0)
}

private fun selectDot(position: Int) {
for (index in 0 until TOTAL_NUMBER_OF_SLIDES) {
val alphaValue = if (index == position) 1.0F else 0.3F
dotsList[index].alpha = alphaValue
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.oppia.app.onboarding

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.viewpager.widget.PagerAdapter
import org.oppia.app.databinding.OnboardingSlideBinding

/** Adapter to control the slide details in onboarding flow. */
class OnboardingPagerAdapter(val context: Context) : PagerAdapter() {
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val binding = OnboardingSlideBinding.inflate(LayoutInflater.from(context), container, false)
val onboardingSlideViewModel = OnboardingSlideViewModel(context, ViewPagerSlide.getSlideForPosition(position))
binding.viewModel = onboardingSlideViewModel
container.addView(binding.root)
return binding.root
}

override fun getCount(): Int {
return TOTAL_NUMBER_OF_SLIDES
}

override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}

override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as ConstraintLayout)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.oppia.app.onboarding

/** Listener for when an activity should route to [ProfileActivity]. */
interface RouteToProfileListListener {
fun routeToProfileList()
}
18 changes: 18 additions & 0 deletions app/src/main/java/org/oppia/app/onboarding/ViewPagerSlide.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.oppia.app.onboarding

/** Enum to store the slides of [OnboardingFragment] and get slide by position. */
enum class ViewPagerSlide(private var position: Int) {
SLIDE_0(position = 0),
SLIDE_1(position = 1),
SLIDE_2(position = 2),
SLIDE_3(position = 3);

companion object {
fun getSlideForPosition(position: Int): ViewPagerSlide {
val ordinal = checkNotNull(values().map(ViewPagerSlide::position)[position]) {
"No tab corresponding to position: $position"
}
return values()[ordinal]
}
}
}
Loading

0 comments on commit d705580

Please sign in to comment.