From fa306915837346edebb755f00668cd16f0625d0c Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 16 Dec 2021 15:20:20 +0000 Subject: [PATCH 1/7] adding automatic rotation of the onboarding carousel items - items change every 5 seconds - uses fake dragging to control the page transition speed, by default it's too fast --- .../im/vector/app/core/extensions/Integer.kt | 19 +++++++ .../vector/app/core/extensions/ViewPager2.kt | 57 +++++++++++++++++++ .../FtueAuthSplashCarouselFragment.kt | 19 ++++++- 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 vector/src/main/java/im/vector/app/core/extensions/Integer.kt create mode 100644 vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt diff --git a/vector/src/main/java/im/vector/app/core/extensions/Integer.kt b/vector/src/main/java/im/vector/app/core/extensions/Integer.kt new file mode 100644 index 0000000000..70c834af89 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/Integer.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.extensions + +fun Int.incrementAndWrap(max: Int, min: Int = 0, amount: Int = 1) = if (this == max) min else this + amount diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt new file mode 100644 index 0000000000..b3b54bc192 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.extensions + +import android.animation.Animator +import android.animation.TimeInterpolator +import android.animation.ValueAnimator +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.viewpager2.widget.ViewPager2 + +fun ViewPager2.setCurrentItem( + item: Int, + duration: Long, + interpolator: TimeInterpolator = AccelerateDecelerateInterpolator(), + pagePxWidth: Int = width // Default value taken from getWidth() from ViewPager2 view +) { + val pxToDrag: Int = pagePxWidth * (item - currentItem) + val animator = ValueAnimator.ofInt(0, pxToDrag) + var previousValue = 0 + animator.addUpdateListener { valueAnimator -> + val currentValue = valueAnimator.animatedValue as Int + val currentPxToDrag = (currentValue - previousValue).toFloat() + kotlin.runCatching { + fakeDragBy(-currentPxToDrag) + previousValue = currentValue + }.onFailure { animator.cancel() } + } + animator.addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(animation: Animator?) { + beginFakeDrag() + } + + override fun onAnimationEnd(animation: Animator?) { + endFakeDrag() + } + + override fun onAnimationCancel(animation: Animator?) = Unit + override fun onAnimationRepeat(animation: Animator?) = Unit + }) + animator.interpolator = interpolator + animator.duration = duration + animator.start() +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt index f89ae36eb6..1520ec45b2 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt @@ -22,20 +22,29 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayoutMediator import im.vector.app.BuildConfig import im.vector.app.R +import im.vector.app.core.extensions.incrementAndWrap +import im.vector.app.core.extensions.setCurrentItem +import im.vector.app.core.flow.tickerFlow import im.vector.app.databinding.FragmentFtueSplashCarouselBinding import im.vector.app.features.VectorFeatures import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingFlow import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.failure.Failure import java.net.UnknownHostException import javax.inject.Inject +private const val CAROUSEL_ROTATION_DELAY_MS = 5000L +private const val CAROUSEL_TRANSITION_TIME_MS = 500L + class FtueAuthSplashCarouselFragment @Inject constructor( private val vectorPreferences: VectorPreferences, private val vectorFeatures: VectorFeatures, @@ -52,7 +61,8 @@ class FtueAuthSplashCarouselFragment @Inject constructor( } private fun setupViews() { - views.splashCarousel.adapter = carouselController.adapter + val carouselAdapter = carouselController.adapter + views.splashCarousel.adapter = carouselAdapter TabLayoutMediator(views.carouselIndicator, views.splashCarousel) { _, _ -> }.attach() carouselController.setData(SplashCarouselState()) @@ -69,6 +79,13 @@ class FtueAuthSplashCarouselFragment @Inject constructor( "Branch: ${BuildConfig.GIT_BRANCH_NAME}" views.loginSplashVersion.debouncedClicks { navigator.openDebug(requireContext()) } } + + views.splashCarousel.apply { + val itemCount = carouselAdapter.itemCount + tickerFlow(lifecycleScope, delayMillis = CAROUSEL_ROTATION_DELAY_MS) + .onEach { setCurrentItem(currentItem.incrementAndWrap(max = itemCount - 1), duration = CAROUSEL_TRANSITION_TIME_MS) } + .launchIn(lifecycleScope) + } } private fun getStarted() { From 486671f385580a6989ad6bab910f8a486e619836 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 5 Jan 2022 10:53:50 +0000 Subject: [PATCH 2/7] making the incrementing helper specific to increments of 1 and incrementing first before returning the minimum value --- .../main/java/im/vector/app/core/extensions/Integer.kt | 9 ++++++++- .../ftueauth/FtueAuthSplashCarouselFragment.kt | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/extensions/Integer.kt b/vector/src/main/java/im/vector/app/core/extensions/Integer.kt index 70c834af89..2940c16fa2 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Integer.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Integer.kt @@ -16,4 +16,11 @@ package im.vector.app.core.extensions -fun Int.incrementAndWrap(max: Int, min: Int = 0, amount: Int = 1) = if (this == max) min else this + amount +fun Int.incrementByOneAndWrap(max: Int, min: Int = 0): Int { + val incrementedValue = this + 1 + return if (incrementedValue > max) { + min + } else { + incrementedValue + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt index 1520ec45b2..5ba7358112 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt @@ -28,7 +28,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayoutMediator import im.vector.app.BuildConfig import im.vector.app.R -import im.vector.app.core.extensions.incrementAndWrap +import im.vector.app.core.extensions.incrementByOneAndWrap import im.vector.app.core.extensions.setCurrentItem import im.vector.app.core.flow.tickerFlow import im.vector.app.databinding.FragmentFtueSplashCarouselBinding @@ -83,7 +83,7 @@ class FtueAuthSplashCarouselFragment @Inject constructor( views.splashCarousel.apply { val itemCount = carouselAdapter.itemCount tickerFlow(lifecycleScope, delayMillis = CAROUSEL_ROTATION_DELAY_MS) - .onEach { setCurrentItem(currentItem.incrementAndWrap(max = itemCount - 1), duration = CAROUSEL_TRANSITION_TIME_MS) } + .onEach { setCurrentItem(currentItem.incrementByOneAndWrap(max = itemCount - 1), duration = CAROUSEL_TRANSITION_TIME_MS) } .launchIn(lifecycleScope) } } From 0d2ad2d85d9a41a1168148871f16528927c24e4d Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 7 Jan 2022 13:47:14 +0000 Subject: [PATCH 3/7] adding back debug option to enable carousel (was rebased away) --- .../app/features/debug/features/DebugFeaturesStateFactory.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 87d7e36ed5..6ddbb53134 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -38,6 +38,11 @@ class DebugFeaturesStateFactory @Inject constructor( label = "FTUE Splash - I already have an account", factory = VectorFeatures::isAlreadyHaveAccountSplashEnabled, key = DebugFeatureKeys.alreadyHaveAnAccount + ), + createBooleanFeature( + label = "FTUE Splash - Carousel", + factory = VectorFeatures::isSplashCarouselEnabled, + key = DebugFeatureKeys.splashCarousel ) )) } From 5de76380ad95e79172f8b6103cdcd8897992c9df Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 7 Jan 2022 14:26:49 +0000 Subject: [PATCH 4/7] supporting rtl dragging in the viewpager setCurrentItem --- .../main/java/im/vector/app/core/extensions/ViewPager2.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt index b3b54bc192..2054152f91 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt @@ -19,6 +19,7 @@ package im.vector.app.core.extensions import android.animation.Animator import android.animation.TimeInterpolator import android.animation.ValueAnimator +import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import androidx.viewpager2.widget.ViewPager2 @@ -31,11 +32,16 @@ fun ViewPager2.setCurrentItem( val pxToDrag: Int = pagePxWidth * (item - currentItem) val animator = ValueAnimator.ofInt(0, pxToDrag) var previousValue = 0 + val isRtl = this.layoutDirection == View.LAYOUT_DIRECTION_RTL + animator.addUpdateListener { valueAnimator -> val currentValue = valueAnimator.animatedValue as Int val currentPxToDrag = (currentValue - previousValue).toFloat() kotlin.runCatching { - fakeDragBy(-currentPxToDrag) + when { + isRtl -> fakeDragBy(currentPxToDrag) + else -> fakeDragBy(-currentPxToDrag) + } previousValue = currentValue }.onFailure { animator.cancel() } } From bdb41b253dae6dc2f44df4ac758340d04a557c91 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 7 Jan 2022 14:32:28 +0000 Subject: [PATCH 5/7] flipping the gradient ftue background for rtl locales, fixes the gradient starting from the wrong side --- library/ui-styles/src/main/res/drawable/bg_carousel_page_1.xml | 1 + library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml | 1 + library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml | 1 + library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml | 1 + 4 files changed, 4 insertions(+) diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_1.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_1.xml index bff828fb22..fa3aea4cab 100644 --- a/library/ui-styles/src/main/res/drawable/bg_carousel_page_1.xml +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_1.xml @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml index 54e5286ded..f696823a6e 100644 --- a/library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_2.xml @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml index c31c70c078..b114f9c804 100644 --- a/library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_3.xml @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml b/library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml index 56989688af..e8ee364431 100644 --- a/library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml +++ b/library/ui-styles/src/main/res/drawable/bg_carousel_page_4.xml @@ -1,6 +1,7 @@ \ No newline at end of file From a0bda0282498daeb2ee731dba36a6a982accedc4 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 7 Jan 2022 15:28:05 +0000 Subject: [PATCH 6/7] disabling user input whilst the automatic animation is taking place, fixes crashes when user input is attempted at the same time --- .../src/main/java/im/vector/app/core/extensions/ViewPager2.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt index 2054152f91..ff3f02e55c 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/ViewPager2.kt @@ -27,7 +27,7 @@ fun ViewPager2.setCurrentItem( item: Int, duration: Long, interpolator: TimeInterpolator = AccelerateDecelerateInterpolator(), - pagePxWidth: Int = width // Default value taken from getWidth() from ViewPager2 view + pagePxWidth: Int = width, ) { val pxToDrag: Int = pagePxWidth * (item - currentItem) val animator = ValueAnimator.ofInt(0, pxToDrag) @@ -47,10 +47,12 @@ fun ViewPager2.setCurrentItem( } animator.addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator?) { + isUserInputEnabled = false beginFakeDrag() } override fun onAnimationEnd(animation: Animator?) { + isUserInputEnabled = true endFakeDrag() } From 70c82443ee235811b4e29a08f7fe20ddf2f42f15 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Fri, 7 Jan 2022 15:51:03 +0000 Subject: [PATCH 7/7] simplifying the automatic transitions and matching iOS by scheduling the next transition once the page settles - means there's always a 5 second delay when manually skipping toa page --- .../FtueAuthSplashCarouselFragment.kt | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt index 5ba7358112..152754f241 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt @@ -23,6 +23,7 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope +import androidx.viewpager2.widget.ViewPager2 import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayoutMediator @@ -30,14 +31,14 @@ import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.incrementByOneAndWrap import im.vector.app.core.extensions.setCurrentItem -import im.vector.app.core.flow.tickerFlow import im.vector.app.databinding.FragmentFtueSplashCarouselBinding import im.vector.app.features.VectorFeatures import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingFlow import im.vector.app.features.settings.VectorPreferences -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.failure.Failure import java.net.UnknownHostException import javax.inject.Inject @@ -81,10 +82,22 @@ class FtueAuthSplashCarouselFragment @Inject constructor( } views.splashCarousel.apply { - val itemCount = carouselAdapter.itemCount - tickerFlow(lifecycleScope, delayMillis = CAROUSEL_ROTATION_DELAY_MS) - .onEach { setCurrentItem(currentItem.incrementByOneAndWrap(max = itemCount - 1), duration = CAROUSEL_TRANSITION_TIME_MS) } - .launchIn(lifecycleScope) + var scheduledTransition: Job? = null + registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + scheduledTransition?.cancel() + scheduledTransition = scheduleCarouselTransition() + } + }) + scheduledTransition = scheduleCarouselTransition() + } + } + + private fun ViewPager2.scheduleCarouselTransition(): Job { + val itemCount = adapter?.itemCount ?: throw IllegalStateException("An adapter must be set") + return lifecycleScope.launch { + delay(CAROUSEL_ROTATION_DELAY_MS) + setCurrentItem(currentItem.incrementByOneAndWrap(max = itemCount - 1), duration = CAROUSEL_TRANSITION_TIME_MS) } }