diff options
| -rw-r--r-- | android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt | 96 | ||||
| -rw-r--r-- | android/src/main/res/layout/select_location_header.xml | 9 |
2 files changed, 101 insertions, 4 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt index ca0e007025..1ba8948375 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SelectLocationFragment.kt @@ -3,11 +3,14 @@ package net.mullvad.mullvadvpn.ui import android.content.Context import android.os.Bundle import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.Animation.AnimationListener +import android.view.animation.AnimationUtils import android.widget.ImageButton +import kotlinx.coroutines.CompletableDeferred import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.Constraint import net.mullvad.mullvadvpn.model.KeygenEvent @@ -18,12 +21,22 @@ import net.mullvad.mullvadvpn.relaylist.RelayItem import net.mullvad.mullvadvpn.relaylist.RelayItemDividerDecoration import net.mullvad.mullvadvpn.relaylist.RelayList import net.mullvad.mullvadvpn.relaylist.RelayListAdapter +import net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView import net.mullvad.mullvadvpn.util.AdapterWithHeader class SelectLocationFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) { + private enum class RelayListState { + Initializing, + Loading, + Visible, + } + private lateinit var relayListAdapter: RelayListAdapter private lateinit var titleController: CollapsibleTitleController + private var loadingSpinner = CompletableDeferred<View>() + private var relayListState = RelayListState.Initializing + override fun onAttach(context: Context) { super.onAttach(context) @@ -52,11 +65,12 @@ class SelectLocationFragment : ServiceDependentFragment(OnNoService.GoToLaunchSc titleController = CollapsibleTitleController(view, R.id.relay_list) - view.findViewById<RecyclerView>(R.id.relay_list).apply { + view.findViewById<CustomRecyclerView>(R.id.relay_list).apply { layoutManager = LinearLayoutManager(parentActivity) adapter = AdapterWithHeader(relayListAdapter, R.layout.select_location_header).apply { onHeaderAvailable = { headerView -> + initializeLoadingSpinner(headerView) titleController.expandedTitleView = headerView.findViewById(R.id.expanded_title) } } @@ -68,11 +82,41 @@ class SelectLocationFragment : ServiceDependentFragment(OnNoService.GoToLaunchSc } override fun onSafelyResume() { + // If the relay list is immediately available, setting the listener will cause it to be + // called right away, while the state is still Initializing. In that case we can skip + // showing the spinner animation and go directly to the Visible state. + // + // If it's not immediately available, then when the listener is called later the state will + // have changed to Loading, and an animation from the spinner to the new relay items will be + // shown. + // + // If the state is ready, it means that the relay list has already been shown, and we can + // update it in place. relayListListener.onRelayListChange = { relayList, selectedItem -> - jobTracker.newUiJob("updateRelayList") { - updateRelayList(relayList, selectedItem) + when (relayListState) { + RelayListState.Initializing -> { + jobTracker.newUiJob("updateRelayList") { + updateRelayList(relayList, selectedItem) + } + + relayListState = RelayListState.Visible + } + RelayListState.Loading -> { + jobTracker.newUiJob("updateRelayList") { + animateRelayListInitialization(relayList, selectedItem) + } + } + RelayListState.Visible -> { + jobTracker.newUiJob("updateRelayList") { + updateRelayList(relayList, selectedItem) + } + } } } + + if (relayListState == RelayListState.Initializing) { + relayListState = RelayListState.Loading + } } override fun onSafelyPause() { @@ -106,4 +150,48 @@ class SelectLocationFragment : ServiceDependentFragment(OnNoService.GoToLaunchSc connectionProxy.connect() } } + + private fun initializeLoadingSpinner(parentView: View) { + val spinner = parentView.findViewById<View>(R.id.loading_spinner) + + if (relayListState == RelayListState.Visible) { + // Because this method is executed inside a layout pass, hiding the spinner needs to be + // done in a new job so that it is executed after the layout pass finishes and can + // therefore schedule a new layout + jobTracker.newUiJob("hideLoadingSpinner") { + spinner.visibility = View.GONE + } + } + + loadingSpinner.complete(spinner) + } + + // Smoothly fade out the spinner before showing the relay list items. + private suspend fun animateRelayListInitialization( + relayList: RelayList, + selectedItem: RelayItem? + ) { + val animationFinished = CompletableDeferred<Unit>() + val animationListener = object : AnimationListener { + override fun onAnimationEnd(animation: Animation) { + animationFinished.complete(Unit) + } + + override fun onAnimationStart(animation: Animation) {} + override fun onAnimationRepeat(animation: Animation) {} + } + + val fadeOut = AnimationUtils.loadAnimation(parentActivity, R.anim.fade_out).apply { + setAnimationListener(animationListener) + } + + loadingSpinner.await().let { spinner -> + spinner.startAnimation(fadeOut) + + animationFinished.await() + + spinner.visibility = View.GONE + updateRelayList(relayList, selectedItem) + } + } } diff --git a/android/src/main/res/layout/select_location_header.xml b/android/src/main/res/layout/select_location_header.xml index 8182db4be0..85b160ad9f 100644 --- a/android/src/main/res/layout/select_location_header.xml +++ b/android/src/main/res/layout/select_location_header.xml @@ -21,4 +21,13 @@ android:textColor="@color/white60" android:textSize="13sp" android:text="@string/select_location_description" /> + <ProgressBar android:id="@+id/loading_spinner" + android:layout_width="60dp" + android:layout_height="60dp" + android:layout_gravity="center" + android:indeterminate="true" + android:indeterminateOnly="true" + android:indeterminateDuration="600" + android:indeterminateDrawable="@drawable/icon_spinner" + android:visibility="visible" /> </LinearLayout> |
