Issue #19178: Apply new styling to Synced Tabs list

upstream-sync
Jonathan Almeida 3 years ago committed by Jonathan Almeida
parent 5729137ddb
commit f06e3a6493

@ -0,0 +1,77 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
/**
* Adds an [ItemDecoration] to the device name of each Synced Tab group.
*/
class SyncedTabsTitleDecoration(
context: Context,
private val style: Style = Style(
height = 1.dpToPx(context.resources.displayMetrics),
color = run {
val a = context.obtainStyledAttributes(intArrayOf(R.attr.toolbarDivider))
val color = a.getDrawable(0)!!
a.recycle()
color
}
)
) : ItemDecoration() {
/**
* A class for holding various customizations.
*/
data class Style(val height: Int, val color: Drawable)
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val viewHolder = parent.getChildViewHolder(view)
val position = viewHolder.bindingAdapterPosition
val viewType = viewHolder.itemViewType
// Only add offsets on the device title that is not the first.
if (viewType == DeviceViewHolder.LAYOUT_ID && position != 0) {
outRect.set(0, style.height, 0, 0)
return
}
outRect.setEmpty()
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
for (i in 0 until parent.childCount) {
val view = parent.getChildAt(i)
val viewHolder = parent.getChildViewHolder(view)
val position = viewHolder.bindingAdapterPosition
val viewType = viewHolder.itemViewType
// Only draw on the device title that is not the first.
if (viewType == DeviceViewHolder.LAYOUT_ID && position != 0) {
style.color.setBounds(
view.left,
view.top - style.height,
view.right,
view.top
)
style.color.draw(c)
}
}
}
}

@ -9,17 +9,18 @@ import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.sync_tabs_error_row.view.*
import kotlinx.android.synthetic.main.sync_tabs_list_item.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_group.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_title.view.*
import mozilla.components.concept.sync.DeviceType
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem
/**
@ -44,6 +45,8 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
val active = tab.tab.active()
itemView.synced_tab_item_title.text = active.title
itemView.synced_tab_item_url.text = active.url
.toShortUrl(itemView.context.components.publicSuffixList)
.take(MAX_URI_LENGTH)
}
companion object {
@ -55,7 +58,6 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
val errorItem = item as AdapterItem.Error
setErrorMargins()
itemView.sync_tabs_error_description.text =
itemView.context.getString(errorItem.descriptionResId)
@ -81,18 +83,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
}
private fun bindHeader(device: AdapterItem.Device) {
val deviceLogoDrawable = when (device.device.deviceType) {
DeviceType.DESKTOP -> R.drawable.mozac_ic_device_desktop
else -> R.drawable.mozac_ic_device_mobile
}
itemView.synced_tabs_group_name.text = device.device.displayName
itemView.synced_tabs_group_name.setCompoundDrawablesRelativeWithIntrinsicBounds(
deviceLogoDrawable,
0,
0,
0
)
}
companion object {
@ -129,14 +120,4 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
const val LAYOUT_ID = R.layout.view_synced_tabs_title
}
}
internal fun setErrorMargins() {
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
val margin = itemView.resources.getDimensionPixelSize(R.dimen.synced_tabs_error_margin)
lp.setMargins(margin, margin, margin, 0)
itemView.layoutParams = lp
}
}

@ -22,6 +22,7 @@ import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.sync.SyncedTabsTitleDecoration
import org.mozilla.fenix.sync.ext.toAdapterItem
import org.mozilla.fenix.sync.ext.toStringRes
import org.mozilla.fenix.tabstray.TabsTrayAction
@ -64,6 +65,12 @@ class SyncedTabsTrayLayout @JvmOverloads constructor(
override var listener: SyncedTabsView.Listener? = null
override fun onFinishInflate() {
synced_tabs_list.addItemDecoration(SyncedTabsTitleDecoration(context))
super.onFinishInflate()
}
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
coroutineScope.launch {
(synced_tabs_list.adapter as SyncedTabsAdapter).updateData(syncedTabs)

@ -4,21 +4,10 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsTrayLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/synced_tabs_tray_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/sync_tabs_progress_bar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:indeterminate="true"
android:layout_width="match_parent"
android:layout_height="8dp"
android:translationY="-3dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/synced_tabs_list"

@ -6,7 +6,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/empty_session_control_background"
android:layout_marginBottom="12dp"
android:layout_margin="@dimen/synced_tabs_error_margin"
android:padding="16dp"
android:orientation="vertical"
android:layout_width="match_parent"

@ -6,8 +6,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:paddingTop="6dp"
android:paddingBottom="8dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground">
@ -16,8 +16,8 @@
android:id="@+id/synced_tab_item_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginEnd="48dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="@color/primary_text_normal_theme"
@ -31,8 +31,8 @@
android:id="@+id/synced_tab_item_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginEnd="48dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="2dp"
android:singleLine="true"
android:textAlignment="viewStart"

@ -8,36 +8,24 @@
android:id="@+id/synced_tabs_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:paddingTop="16dp"
android:paddingBottom="7dp">
<TextView
android:id="@+id/synced_tabs_group_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:drawablePadding="32dp"
android:gravity="center_vertical|start"
android:textAppearance="@style/Header14TextStyle"
android:textColor="@color/primary_text_normal_theme"
android:textSize="12sp"
android:letterSpacing="0.04"
android:textDirection="locale"
app:drawableStartCompat="@drawable/mozac_ic_device_desktop"
app:drawableTint="@color/primary_text_normal_theme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Header" />
<View
android:id="@+id/synced_tabs_group_separator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="6dp"
android:background="?syncedTabsSeparator"
android:importantForAccessibility="no"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@+id/synced_tabs_group_name" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -5,13 +5,13 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="24dp">
android:paddingBottom="7dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="7dp"
android:singleLine="true"
android:text="@string/synced_tabs_no_open_tabs"
android:textAlignment="viewStart"

@ -0,0 +1,101 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import io.mockk.Called
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.sync.SyncedTabsTitleDecoration.Style
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder
class SyncedTabsTitleDecorationTest {
private val recyclerView: RecyclerView = mockk(relaxed = true)
private val canvas: Canvas = mockk(relaxed = true)
private val viewHolder: RecyclerView.ViewHolder = mockk(relaxed = true)
private val state: RecyclerView.State = mockk(relaxed = true)
private val view: View = mockk(relaxed = true)
// Mocking these classes so we don't have to use the (slow) Android test runner.
private val rect: Rect = mockk(relaxed = true)
private val colorDrawable: Drawable = mockk(relaxed = true)
private val style = Style(5, colorDrawable)
@Before
fun setup() {
every { recyclerView.getChildViewHolder(any()) }.returns(viewHolder)
every { recyclerView.childCount }.returns(1)
every { recyclerView.getChildAt(any()) }.returns(view)
every { view.left }.returns(5)
every { view.top }.returns(5)
every { view.right }.returns(5)
every { view.bottom }.returns(5)
}
@Test
fun `WHEN device title and not first item THEN add offset to the layout rect`() {
val decoration = SyncedTabsTitleDecoration(mockk(), style)
every { viewHolder.itemViewType }.answers { DeviceViewHolder.LAYOUT_ID }
every { viewHolder.bindingAdapterPosition }.answers { 1 }
decoration.getItemOffsets(rect, mockk(), recyclerView, state)
verify { rect.set(0, 5, 0, 0) }
}
@Test
fun `WHEN not device title and first position THEN do not add offsets`() {
val decoration = SyncedTabsTitleDecoration(mockk(), style)
every { viewHolder.itemViewType }.answers { ErrorViewHolder.LAYOUT_ID }
every { viewHolder.bindingAdapterPosition }
.answers { 1 }
.andThen { 0 }
decoration.getItemOffsets(rect, mockk(), recyclerView, state)
decoration.getItemOffsets(rect, mockk(), recyclerView, state)
verify(exactly = 2) { rect.setEmpty() }
}
@Test
fun `WHEN device title and not first THEN draw`() {
val decoration = SyncedTabsTitleDecoration(mockk(), style)
every { viewHolder.itemViewType }.answers { DeviceViewHolder.LAYOUT_ID }
every { viewHolder.bindingAdapterPosition }.answers { 1 }
decoration.onDraw(canvas, recyclerView, state)
verify { colorDrawable.setBounds(5, 0, 5, 5) }
verify { colorDrawable.draw(canvas) }
}
@Test
fun `WHEN not device title and not first THEN do not draw`() {
val decoration = SyncedTabsTitleDecoration(mockk(), style)
every { viewHolder.itemViewType }.answers { ErrorViewHolder.LAYOUT_ID }
every { viewHolder.bindingAdapterPosition }
.answers { 1 }
.andThen { 0 }
decoration.onDraw(canvas, recyclerView, state)
verify { colorDrawable wasNot Called }
}
}

@ -13,7 +13,6 @@ import io.mockk.mockk
import io.mockk.verify
import kotlinx.android.synthetic.main.sync_tabs_list_item.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_group.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_title.view.*
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.concept.sync.Device
@ -45,8 +44,8 @@ class SyncedTabsViewHolderTest {
mockk(),
TabEntry(
title = "Firefox",
url = "https://firefox.com",
iconUrl = "https://firefox.com/favicon.ico"
url = "https://mozilla.org/mobile",
iconUrl = "https://mozilla.org/favicon.ico"
),
mockk()
),
@ -79,7 +78,7 @@ class SyncedTabsViewHolderTest {
tabViewHolder.bind(SyncedTabsAdapter.AdapterItem.Tab(tab), mockk())
assertEquals("Firefox", tabView.synced_tab_item_title.text)
assertEquals("https://firefox.com", tabView.synced_tab_item_url.text)
assertEquals("mozilla.org", tabView.synced_tab_item_url.text)
}
@Test
@ -100,11 +99,6 @@ class SyncedTabsViewHolderTest {
deviceViewHolder.bind(SyncedTabsAdapter.AdapterItem.Device(device), mockk())
verify { deviceViewGroupName.text = "Charcoal" }
verify {
deviceViewGroupName.setCompoundDrawablesRelativeWithIntrinsicBounds(
R.drawable.mozac_ic_device_desktop, 0, 0, 0
)
}
}
@Test
@ -116,11 +110,6 @@ class SyncedTabsViewHolderTest {
deviceViewHolder.bind(SyncedTabsAdapter.AdapterItem.Device(device), mockk())
verify { deviceViewGroupName.text = "Emerald" }
verify {
deviceViewGroupName.setCompoundDrawablesRelativeWithIntrinsicBounds(
R.drawable.mozac_ic_device_mobile, 0, 0, 0
)
}
}
@Test

Loading…
Cancel
Save