diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-07-01 17:12:43 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-07-01 17:12:43 -0300 |
| commit | d8cbd703261fc60c0864264a0bd632f3823c723e (patch) | |
| tree | 6f2313fe3aad6af1973790011f3a5c5eb59cc9ca /android | |
| parent | 8cdfac15f26348da9b164716f1cad3c5287b3a80 (diff) | |
| parent | f70d2b0bf7ca89e5427144ce0cce46629da54d97 (diff) | |
| download | mullvadvpn-d8cbd703261fc60c0864264a0bd632f3823c723e.tar.xz mullvadvpn-d8cbd703261fc60c0864264a0bd632f3823c723e.zip | |
Merge branch 'animate-select-location-scrolling'
Diffstat (limited to 'android')
10 files changed, 415 insertions, 104 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListAdapter.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListAdapter.kt index 699aad1d66..a4f521be3d 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListAdapter.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListAdapter.kt @@ -52,11 +52,17 @@ class RelayListAdapter(private val resources: Resources) : Adapter<RelayItemHold override fun getItemCount() = relayList?.countries?.map { country -> country.visibleItemCount }?.sum() ?: 0 - fun onRelayListChange(relayList: RelayList, selectedItem: RelayItem?) { - this.relayList = relayList - this.selectedItem = selectedItem + fun onRelayListChange(newRelayList: RelayList, newSelectedItem: RelayItem?) { + val initializedRelayList = relayList == null - notifyDataSetChanged() + relayList = newRelayList + selectedItem = newSelectedItem + + if (initializedRelayList) { + notifyItemRangeInserted(0, getItemCount()) + } else { + notifyDataSetChanged() + } } fun selectItem(item: RelayItem?, holder: RelayItemHolder?) { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt index ec5a4549b1..dcde70b923 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt @@ -5,16 +5,16 @@ import android.view.View.OnLayoutChangeListener import android.view.ViewGroup.MarginLayoutParams import kotlin.properties.Delegates.observable import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.ui.widget.ListenableScrollView import net.mullvad.mullvadvpn.util.LinearInterpolation +import net.mullvad.mullvadvpn.util.ListenableScrollableView // In order to use this view controller, the parent view must contain four views with specific IDs: // -// 1. A `ListenableScrollView` with the ID `scroll_area`, which is used to animate the title based -// on the scroll offset. -// 2. A view inside the `scroll_area` with the ID `expanded_title`. This view is made invisible so +// 1. A scroll area `View` with the `scrollAreaId` that implements `ListenableScrollableView`, which +// is used to animate the title based on the scroll offset. +// 2. A view inside the scroll area with the ID `expanded_title`. This view is made invisible so // that it's not drawn, but it is used to measure the layout and the animation positions. -// 3. A view outside the `scroll_area` with the ID `collapsed_title`. This view is also made +// 3. A view outside the scroll area with the ID `collapsed_title`. This view is also made // invisible just like the `expanded_view`. // 4. A view with the ID `title`. This is the view that's actually drawn, and it's position and size // are interpolated from the expanded title to the collapsed title. This view should be placed @@ -23,8 +23,8 @@ import net.mullvad.mullvadvpn.util.LinearInterpolation // The animation interpolation is calculated based on the Y scroll offset of the scroll area. Once // the offset reaches a value that completely hides the expanded title inside the scroll view, the // animation finishes with the title being in the collapsed state. -class CollapsibleTitleController(val parentView: View) { - private inner class LayoutListener(val listener: () -> Unit) : OnLayoutChangeListener { +class CollapsibleTitleController(val parentView: View, scrollAreaId: Int = R.id.scroll_area) { + private inner class LayoutListener(val listener: (View) -> Unit) : OnLayoutChangeListener { override fun onLayoutChange( view: View, left: Int, @@ -36,7 +36,7 @@ class CollapsibleTitleController(val parentView: View) { oldRight: Int, oldBottom: Int ) { - listener.invoke() + listener.invoke(view) update() } } @@ -46,7 +46,7 @@ class CollapsibleTitleController(val parentView: View) { private val xOffsetInterpolation = LinearInterpolation() private val yOffsetInterpolation = LinearInterpolation() - private val collapsedTitleLayoutListener: LayoutListener = LayoutListener() { + private val collapsedTitleLayoutListener: LayoutListener = LayoutListener() { collapsedTitle -> val (x, y) = calculateViewCoordinates(collapsedTitle) collapsedTitleHeight = collapsedTitle.height.toFloat() @@ -56,12 +56,12 @@ class CollapsibleTitleController(val parentView: View) { yOffsetInterpolation.end = y } - private val collapsedTitle = parentView.findViewById<View>(R.id.collapsed_title).apply { + private val collapsedTitleView = parentView.findViewById<View>(R.id.collapsed_title).apply { addOnLayoutChangeListener(collapsedTitleLayoutListener) visibility = View.INVISIBLE } - private val expandedTitleLayoutListener: LayoutListener = LayoutListener() { + private val expandedTitleLayoutListener: LayoutListener = LayoutListener() { expandedTitle -> val (x, y) = calculateViewCoordinates(expandedTitle) val expandedTitleMarginTop = when (val layoutParams = expandedTitle.layoutParams) { @@ -78,12 +78,7 @@ class CollapsibleTitleController(val parentView: View) { scrollInterpolation.end = expandedTitleHeight + expandedTitleMarginTop } - private val expandedTitle = parentView.findViewById<View>(R.id.expanded_title).apply { - addOnLayoutChangeListener(expandedTitleLayoutListener) - visibility = View.INVISIBLE - } - - private val titleLayoutListener: LayoutListener = LayoutListener() { + private val titleLayoutListener: LayoutListener = LayoutListener() { title -> val (x, y) = calculateViewCoordinates(title) titleWidth = title.width.toFloat() @@ -95,7 +90,7 @@ class CollapsibleTitleController(val parentView: View) { yOffsetInterpolation.reference = y } - private val title = parentView.findViewById<View>(R.id.title).apply { + private val titleView = parentView.findViewById<View>(R.id.title).apply { addOnLayoutChangeListener(titleLayoutListener) // Setting the scale pivot point to the left corner simplifies the calculations @@ -104,16 +99,20 @@ class CollapsibleTitleController(val parentView: View) { } private val scrollAreaLayoutListener: LayoutListener = LayoutListener() { - scrollOffset = scrollArea.scrollY.toFloat() + scrollOffset = scrollArea.verticalScrollOffset.toFloat() } - private val scrollArea = parentView.findViewById<ListenableScrollView>(R.id.scroll_area).apply { - onScrollListener = { _, top, _, _ -> + private val scrollArea = parentView.findViewById<View>(scrollAreaId).let { view -> + val scrollableView = view as ListenableScrollableView + + view.addOnLayoutChangeListener(scrollAreaLayoutListener) + + scrollableView.onScrollListener = { _, top, _, _ -> scrollOffset = top.toFloat() update() } - addOnLayoutChangeListener(scrollAreaLayoutListener) + scrollableView } private var scrollOffsetUpdated = false @@ -140,17 +139,27 @@ class CollapsibleTitleController(val parentView: View) { val fullCollapseScrollOffset: Float get() = scrollInterpolation.end + var expandedTitleView by observable<View?>(null) { _, oldView, newView -> + oldView?.removeOnLayoutChangeListener(expandedTitleLayoutListener) + newView?.apply { + addOnLayoutChangeListener(expandedTitleLayoutListener) + expandedTitleLayoutListener.listener(this) + visibility = View.INVISIBLE + } + } + init { + expandedTitleView = parentView.findViewById<View>(R.id.expanded_title) update() } fun onDestroy() { scrollArea.onScrollListener = null - scrollArea.removeOnLayoutChangeListener(scrollAreaLayoutListener) + (scrollArea as View).removeOnLayoutChangeListener(scrollAreaLayoutListener) - collapsedTitle.removeOnLayoutChangeListener(collapsedTitleLayoutListener) - expandedTitle.removeOnLayoutChangeListener(expandedTitleLayoutListener) - title.removeOnLayoutChangeListener(titleLayoutListener) + collapsedTitleView.removeOnLayoutChangeListener(collapsedTitleLayoutListener) + expandedTitleView?.removeOnLayoutChangeListener(expandedTitleLayoutListener) + titleView.removeOnLayoutChangeListener(titleLayoutListener) } private fun update() { @@ -161,13 +170,17 @@ class CollapsibleTitleController(val parentView: View) { yOffsetInterpolation.updated if (shouldUpdate) { - val progress = maxOf(0.0f, minOf(1.0f, scrollInterpolation.progress(scrollOffset))) + val progress = if (expandedTitleView != null) { + maxOf(0.0f, minOf(1.0f, scrollInterpolation.progress(scrollOffset))) + } else { + 1.0f + } val scale = scaleInterpolation.interpolate(progress) val offsetX = xOffsetInterpolation.interpolate(progress) val offsetY = yOffsetInterpolation.interpolate(progress) - title.apply { + titleView.apply { scaleX = scale scaleY = scale translationX = offsetX 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 5b95c9d296..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,12 +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 android.widget.ViewSwitcher +import kotlinx.coroutines.CompletableDeferred import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.Constraint import net.mullvad.mullvadvpn.model.KeygenEvent @@ -19,10 +21,21 @@ 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 relayListContainer: ViewSwitcher + private lateinit var titleController: CollapsibleTitleController + + private var loadingSpinner = CompletableDeferred<View>() + private var relayListState = RelayListState.Initializing override fun onAttach(context: Context) { super.onAttach(context) @@ -50,37 +63,73 @@ class SelectLocationFragment : ServiceDependentFragment(OnNoService.GoToLaunchSc view.findViewById<ImageButton>(R.id.close).setOnClickListener { close() } - relayListContainer = view.findViewById<ViewSwitcher>(R.id.relay_list_container) - relayListContainer.showNext() + titleController = CollapsibleTitleController(view, R.id.relay_list) - configureRelayList(view.findViewById<RecyclerView>(R.id.relay_list)) + 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) + } + } + + addItemDecoration(RelayItemDividerDecoration(parentActivity)) + } return view } 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() { relayListListener.onRelayListChange = null } - fun close() { - activity?.onBackPressed() + override fun onDestroyView() { + super.onDestroyView() + titleController.onDestroy() } - private fun configureRelayList(relayList: RecyclerView) { - relayList.apply { - layoutManager = LinearLayoutManager(context!!) - adapter = relayListAdapter - - addItemDecoration(RelayItemDividerDecoration(context!!)) - } + fun close() { + activity?.onBackPressed() } private fun updateLocationConstraint(relayItem: RelayItem?) { @@ -92,12 +141,6 @@ class SelectLocationFragment : ServiceDependentFragment(OnNoService.GoToLaunchSc private fun updateRelayList(relayList: RelayList, selectedItem: RelayItem?) { relayListAdapter.onRelayListChange(relayList, selectedItem) - - if (relayList.countries.isEmpty()) { - relayListContainer.showPrevious() - } else if (relayListContainer.displayedChild == 0) { - relayListContainer.showNext() - } } private fun maybeConnect() { @@ -107,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/kotlin/net/mullvad/mullvadvpn/ui/widget/CustomRecyclerView.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/CustomRecyclerView.kt new file mode 100644 index 0000000000..710bb834d0 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/CustomRecyclerView.kt @@ -0,0 +1,38 @@ +package net.mullvad.mullvadvpn.ui.widget + +import android.content.Context +import android.support.v7.widget.RecyclerView +import android.util.AttributeSet +import net.mullvad.mullvadvpn.util.ListenableScrollableView + +class CustomRecyclerView : RecyclerView, ListenableScrollableView { + override var horizontalScrollOffset = 0 + override var verticalScrollOffset = 0 + + override var onScrollListener: ((Int, Int, Int, Int) -> Unit)? = null + + constructor(context: Context) : super(context) {} + + constructor(context: Context, attributes: AttributeSet) : super(context, attributes) {} + + constructor(context: Context, attributes: AttributeSet, defaultStyleAttribute: Int) : + super(context, attributes, defaultStyleAttribute) { + } + + override fun onScrolled(horizontalDelta: Int, verticalDelta: Int) { + super.onScrolled(horizontalDelta, verticalDelta) + + val oldHorizontalScrollOffset = horizontalScrollOffset + val oldVerticalScrollOffset = verticalScrollOffset + + horizontalScrollOffset += horizontalDelta + verticalScrollOffset += verticalDelta + + onScrollListener?.invoke( + horizontalScrollOffset, + verticalScrollOffset, + oldHorizontalScrollOffset, + oldVerticalScrollOffset + ) + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ListenableScrollView.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ListenableScrollView.kt index 303527c3d4..b436df903a 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ListenableScrollView.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ListenableScrollView.kt @@ -3,9 +3,15 @@ package net.mullvad.mullvadvpn.ui.widget import android.content.Context import android.util.AttributeSet import android.widget.ScrollView +import net.mullvad.mullvadvpn.util.ListenableScrollableView -class ListenableScrollView : ScrollView { - var onScrollListener: ((Int, Int, Int, Int) -> Unit)? = null +class ListenableScrollView : ScrollView, ListenableScrollableView { + override val horizontalScrollOffset + get() = scrollX + override val verticalScrollOffset + get() = scrollY + + override var onScrollListener: ((Int, Int, Int, Int) -> Unit)? = null constructor(context: Context) : super(context) {} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/util/AdapterWithHeader.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/AdapterWithHeader.kt new file mode 100644 index 0000000000..379d58f758 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/AdapterWithHeader.kt @@ -0,0 +1,122 @@ +package net.mullvad.mullvadvpn.util + +import android.support.v7.widget.RecyclerView.Adapter +import android.support.v7.widget.RecyclerView.AdapterDataObserver +import android.support.v7.widget.RecyclerView.ViewHolder +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import kotlin.properties.Delegates.observable + +class AdapterWithHeader<H : ViewHolder>( + val adapter: Adapter<H>, + val headerLayoutId: Int +) : Adapter<HeaderOrHolder<H>>() { + private val observer = object : AdapterDataObserver() { + override fun onChanged() { + notifyDataSetChanged() + } + + override fun onItemRangeChanged(start: Int, count: Int) { + notifyItemRangeChanged(start + 1, count) + } + + override fun onItemRangeChanged(start: Int, count: Int, payload: Any?) { + notifyItemRangeChanged(start + 1, count, payload) + } + + override fun onItemRangeInserted(start: Int, count: Int) { + notifyItemRangeInserted(start + 1, count) + } + + override fun onItemRangeMoved(from: Int, to: Int, count: Int) { + if (from == to) { + notifyItemRangeChanged(from + 1, count) + } else { + val sourceStart = from + 1 + val sourceEnd = sourceStart + count + val destinationStart = to + 1 + val destinationEnd = destinationStart + count + + val ascendingIndices = + (sourceStart..sourceEnd).zip(destinationStart..destinationEnd) + + val indices = if (from < to) { + ascendingIndices.asReversed() + } else { + ascendingIndices + } + + for ((source, destination) in indices) { + notifyItemMoved(source, destination) + } + } + } + + override fun onItemRangeRemoved(start: Int, count: Int) { + notifyItemRangeRemoved(start + 1, count) + } + } + + private var headerView: View? by observable<View?>(null) { _, _, newView -> + newView?.let { view -> onHeaderAvailable?.invoke(view) } + } + + var onHeaderAvailable by observable<((View) -> Unit)?>(null) { _, _, listener -> + headerView?.let { header -> listener?.invoke(header) } + } + + init { + adapter.registerAdapterDataObserver(observer) + } + + override fun getItemCount() = adapter.itemCount + 1 + + override fun getItemId(position: Int): Long { + if (position == 0) { + return 0L + } else { + return adapter.getItemId(position - 1) + 1 + } + } + + override fun getItemViewType(position: Int): Int { + if (position == 0) { + return 0 + } else { + return adapter.getItemViewType(position - 1) + 1 + } + } + + override fun onBindViewHolder(holder: HeaderOrHolder<H>, position: Int) { + when (holder) { + is HeaderOrHolder.Header -> { + if (position != 0) { + throw IllegalArgumentException("Adapter position is not for the header") + } + } + is HeaderOrHolder.Holder -> { + if (position > 0) { + adapter.onBindViewHolder(holder.holder, position - 1) + } else { + throw IllegalArgumentException("Adapter position is for the header") + } + } + } + } + + override fun onCreateViewHolder(parentView: ViewGroup, viewType: Int): HeaderOrHolder<H> { + if (viewType == 0) { + val inflater = LayoutInflater.from(parentView.context) + val view = inflater.inflate(headerLayoutId, parentView, false) + + headerView = view + + return HeaderOrHolder.Header(view) + } else { + val holder = adapter.onCreateViewHolder(parentView, viewType - 1) + + return HeaderOrHolder.Holder(holder) + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/util/HeaderOrHolder.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/HeaderOrHolder.kt new file mode 100644 index 0000000000..631d69100e --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/HeaderOrHolder.kt @@ -0,0 +1,9 @@ +package net.mullvad.mullvadvpn.util + +import android.support.v7.widget.RecyclerView.ViewHolder +import android.view.View + +sealed class HeaderOrHolder<H : ViewHolder>(itemView: View) : ViewHolder(itemView) { + class Header<H : ViewHolder>(headerView: View) : HeaderOrHolder<H>(headerView) + class Holder<H : ViewHolder>(val holder: H) : HeaderOrHolder<H>(holder.itemView) +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/util/ListenableScrollableView.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/ListenableScrollableView.kt new file mode 100644 index 0000000000..61deecacb2 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/ListenableScrollableView.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.util + +interface ListenableScrollableView { + val horizontalScrollOffset: Int + val verticalScrollOffset: Int + + var onScrollListener: ((Int, Int, Int, Int) -> Unit)? +} diff --git a/android/src/main/res/layout/select_location.xml b/android/src/main/res/layout/select_location.xml index 50c1d10643..cf3e905876 100644 --- a/android/src/main/res/layout/select_location.xml +++ b/android/src/main/res/layout/select_location.xml @@ -1,51 +1,40 @@ -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@color/darkBlue" - android:orientation="vertical" - android:gravity="left" - android:elevation="1dp"> - <ImageButton android:id="@+id/close" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_weight="0" - android:padding="12dp" - android:background="?android:attr/selectableItemBackground" - android:src="@drawable/icon_close" /> - <TextView android:layout_width="wrap_content" +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/darkBlue" + android:gravity="left" + android:elevation="1dp"> + <TextView android:id="@+id/title" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="0" - android:layout_marginVertical="4dp" - android:layout_marginHorizontal="24dp" android:textColor="@color/white" - android:textSize="32sp" + android:textSize="16sp" android:textStyle="bold" android:text="@string/select_location" /> - <TextView android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_weight="0" - android:layout_marginHorizontal="24dp" - android:layout_marginBottom="24dp" - android:textColor="@color/white60" - android:textSize="13sp" - android:text="@string/select_location_description" /> - <ViewSwitcher android:id="@+id/relay_list_container" - android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" - android:inAnimation="@anim/fade_in" - android:outAnimation="@anim/fade_out"> - <ProgressBar 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="invisible" /> - <android.support.v7.widget.RecyclerView android:id="@+id/relay_list" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:scrollbars="vertical" /> - </ViewSwitcher> -</LinearLayout> + <LinearLayout android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <FrameLayout android:layout_width="match_parent" + android:layout_height="wrap_content"> + <ImageButton android:id="@+id/close" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="12dp" + android:background="?android:attr/selectableItemBackground" + android:src="@drawable/icon_close" /> + <TextView android:id="@+id/collapsed_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginHorizontal="4dp" + android:layout_gravity="center" + android:textColor="@color/white" + android:textSize="16sp" + android:textStyle="bold" + android:text="@string/select_location" /> + </FrameLayout> + <net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView android:id="@+id/relay_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="vertical" /> + </LinearLayout> +</FrameLayout> diff --git a/android/src/main/res/layout/select_location_header.xml b/android/src/main/res/layout/select_location_header.xml new file mode 100644 index 0000000000..85b160ad9f --- /dev/null +++ b/android/src/main/res/layout/select_location_header.xml @@ -0,0 +1,33 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="left"> + <TextView android:id="@+id/expanded_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:layout_marginVertical="4dp" + android:layout_marginHorizontal="24dp" + android:textColor="@color/white" + android:textSize="32sp" + android:textStyle="bold" + android:text="@string/select_location" /> + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:layout_marginHorizontal="24dp" + android:layout_marginBottom="24dp" + 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> |
