mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-03 23:15:31 +00:00
[fenix] Issue https://github.com/mozilla-mobile/fenix/issues/19178: Apply new styling to Synced Tabs list
This commit is contained in:
parent
3b5e51d2ee
commit
b26989607f
@ -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…
Reference in New Issue
Block a user