Issue #2379 - Use LibrarySiteItemView in history

This commit is contained in:
Tiger Oakes 2019-08-01 16:44:52 -04:00 committed by Emily Kager
parent bac2e40eb2
commit 6e2ea0750e
8 changed files with 262 additions and 328 deletions

View File

@ -5,6 +5,7 @@
package org.mozilla.fenix.library
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageButton
import android.widget.ImageView
@ -17,9 +18,12 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.loadIntoView
class LibrarySiteItemView(
context: Context
) : ConstraintLayout(context) {
class LibrarySiteItemView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
val titleView: TextView get() = title

View File

@ -8,20 +8,19 @@ import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.jetbrains.anko.image
import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkFolderViewHolder
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkItemViewHolder
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkNodeViewHolder
import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkSeparatorViewHolder
class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteractor) :
RecyclerView.Adapter<BookmarkAdapter.BookmarkNodeViewHolder>() {
RecyclerView.Adapter<BookmarkNodeViewHolder>() {
private var tree: List<BookmarkNode> = listOf()
private var mode: BookmarkState.Mode = BookmarkState.Mode.Normal
@ -106,146 +105,6 @@ class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteracto
tree[position] in selected
)
}
abstract class BookmarkNodeViewHolder(
val view: LibrarySiteItemView,
val interactor: BookmarkViewInteractor
) :
RecyclerView.ViewHolder(view), LayoutContainer {
override val containerView get() = view
abstract fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean)
protected fun setupMenu(item: BookmarkNode) {
val bookmarkItemMenu = BookmarkItemMenu(view.context, item) {
when (it) {
is BookmarkItemMenu.Item.Edit -> interactor.edit(item)
is BookmarkItemMenu.Item.Select -> interactor.select(item)
is BookmarkItemMenu.Item.Copy -> interactor.copy(item)
is BookmarkItemMenu.Item.Share -> interactor.share(item)
is BookmarkItemMenu.Item.OpenInNewTab -> interactor.openInNewTab(item)
is BookmarkItemMenu.Item.OpenInPrivateTab -> interactor.openInPrivateTab(item)
is BookmarkItemMenu.Item.Delete -> interactor.delete(item)
}
}
view.overflowView.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(view.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
}
}
}
class BookmarkItemViewHolder(
view: LibrarySiteItemView,
interactor: BookmarkViewInteractor
) :
BookmarkNodeViewHolder(view, interactor) {
@Suppress("ComplexMethod")
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
view.displayAs(LibrarySiteItemView.ItemType.SITE)
setupMenu(item)
view.titleView.text = if (item.title.isNullOrBlank()) item.url else item.title
view.urlView.text = item.url
setClickListeners(mode, item, selected)
view.changeSelected(selected)
setColorsAndIcons(item.url)
}
private fun setColorsAndIcons(url: String?) {
if (url != null && url.startsWith("http")) {
view.loadFavicon(url)
} else {
view.iconView.setImageDrawable(null)
}
}
private fun setClickListeners(
mode: BookmarkState.Mode,
item: BookmarkNode,
selected: Boolean
) {
view.setOnClickListener {
when {
mode == BookmarkState.Mode.Normal -> interactor.open(item)
selected -> interactor.deselect(item)
else -> interactor.select(item)
}
}
view.setOnLongClickListener {
if (mode == BookmarkState.Mode.Normal) {
interactor.select(item)
true
} else false
}
}
}
class BookmarkFolderViewHolder(
view: LibrarySiteItemView,
interactor: BookmarkViewInteractor
) :
BookmarkNodeViewHolder(view, interactor) {
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
view.displayAs(LibrarySiteItemView.ItemType.FOLDER)
setClickListeners(mode, item, selected)
if (!item.inRoots()) {
setupMenu(item)
} else {
view.overflowView.visibility = View.GONE
}
view.changeSelected(selected)
view.iconView.image = view.context.getDrawable(R.drawable.ic_folder_icon)?.apply {
setTint(ContextCompat.getColor(view.context, R.color.primary_text_light_theme))
}
view.titleView.text = item.title
}
private fun setClickListeners(
mode: BookmarkState.Mode,
item: BookmarkNode,
selected: Boolean
) {
view.setOnClickListener {
when {
mode == BookmarkState.Mode.Normal -> interactor.expand(item)
selected -> interactor.deselect(item)
else -> interactor.select(item)
}
}
view.setOnLongClickListener {
if (mode == BookmarkState.Mode.Normal && !item.inRoots()) {
interactor.select(item)
true
} else false
}
}
}
class BookmarkSeparatorViewHolder(
view: LibrarySiteItemView,
interactor: BookmarkViewInteractor
) : BookmarkNodeViewHolder(view, interactor) {
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
view.displayAs(LibrarySiteItemView.ItemType.SEPARATOR)
setupMenu(item)
}
}
}
fun BookmarkNode.inRoots() = enumValues<BookmarkRoot>().any { it.id == guid }

View File

@ -0,0 +1,64 @@
/* 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.library.bookmarks.viewholders
import android.view.View
import androidx.core.content.ContextCompat
import mozilla.components.concept.storage.BookmarkNode
import org.jetbrains.anko.image
import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarkState
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
import org.mozilla.fenix.library.bookmarks.inRoots
/**
* Represents a folder with other bookmarks inside.
*/
class BookmarkFolderViewHolder(
view: LibrarySiteItemView,
interactor: BookmarkViewInteractor
) : BookmarkNodeViewHolder(view, interactor) {
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
containerView.displayAs(LibrarySiteItemView.ItemType.FOLDER)
setClickListeners(mode, item, selected)
if (!item.inRoots()) {
setupMenu(item)
} else {
containerView.overflowView.visibility = View.GONE
}
containerView.changeSelected(selected)
containerView.iconView.image = containerView.context.getDrawable(R.drawable.ic_folder_icon)?.apply {
setTint(ContextCompat.getColor(containerView.context, R.color.primary_text_light_theme))
}
containerView.titleView.text = item.title
}
private fun setClickListeners(
mode: BookmarkState.Mode,
item: BookmarkNode,
selected: Boolean
) {
containerView.setOnClickListener {
when {
mode == BookmarkState.Mode.Normal -> interactor.expand(item)
selected -> interactor.deselect(item)
else -> interactor.select(item)
}
}
containerView.setOnLongClickListener {
if (mode == BookmarkState.Mode.Normal && !item.inRoots()) {
interactor.select(item)
true
} else false
}
}
}

View File

@ -0,0 +1,61 @@
/* 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.library.bookmarks.viewholders
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarkState
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
/**
* Represents a bookmarked website in the bookmarks page.
*/
class BookmarkItemViewHolder(
view: LibrarySiteItemView,
interactor: BookmarkViewInteractor
) : BookmarkNodeViewHolder(view, interactor) {
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
containerView.displayAs(LibrarySiteItemView.ItemType.SITE)
setupMenu(item)
containerView.titleView.text = if (item.title.isNullOrBlank()) item.url else item.title
containerView.urlView.text = item.url
setClickListeners(mode, item, selected)
containerView.changeSelected(selected)
setColorsAndIcons(item.url)
}
private fun setColorsAndIcons(url: String?) {
if (url != null && url.startsWith("http")) {
containerView.loadFavicon(url)
} else {
containerView.iconView.setImageDrawable(null)
}
}
private fun setClickListeners(
mode: BookmarkState.Mode,
item: BookmarkNode,
selected: Boolean
) {
containerView.setOnClickListener {
when {
mode == BookmarkState.Mode.Normal -> interactor.open(item)
selected -> interactor.deselect(item)
else -> interactor.select(item)
}
}
containerView.setOnLongClickListener {
if (mode == BookmarkState.Mode.Normal) {
interactor.select(item)
true
} else false
}
}
}

View File

@ -0,0 +1,47 @@
/* 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.library.bookmarks.viewholders
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarkItemMenu
import org.mozilla.fenix.library.bookmarks.BookmarkState
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
/**
* Base class for bookmark node view holders.
*/
abstract class BookmarkNodeViewHolder(
override val containerView: LibrarySiteItemView,
val interactor: BookmarkViewInteractor
) :
RecyclerView.ViewHolder(containerView), LayoutContainer {
abstract fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean)
protected fun setupMenu(item: BookmarkNode) {
val bookmarkItemMenu = BookmarkItemMenu(containerView.context, item) {
when (it) {
BookmarkItemMenu.Item.Edit -> interactor.edit(item)
BookmarkItemMenu.Item.Select -> interactor.select(item)
BookmarkItemMenu.Item.Copy -> interactor.copy(item)
BookmarkItemMenu.Item.Share -> interactor.share(item)
BookmarkItemMenu.Item.OpenInNewTab -> interactor.openInNewTab(item)
BookmarkItemMenu.Item.OpenInPrivateTab -> interactor.openInPrivateTab(item)
BookmarkItemMenu.Item.Delete -> interactor.delete(item)
}
}
containerView.overflowView.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(containerView.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
}
}
}

View File

@ -0,0 +1,24 @@
/* 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.library.bookmarks.viewholders
import mozilla.components.concept.storage.BookmarkNode
import org.mozilla.fenix.library.LibrarySiteItemView
import org.mozilla.fenix.library.bookmarks.BookmarkState
import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor
/**
* Simple view holder for dividers in the bookmarks list.
*/
class BookmarkSeparatorViewHolder(
view: LibrarySiteItemView,
interactor: BookmarkViewInteractor
) : BookmarkNodeViewHolder(view, interactor) {
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
containerView.displayAs(LibrarySiteItemView.ItemType.SEPARATOR)
setupMenu(item)
}
}

View File

@ -5,14 +5,10 @@
package org.mozilla.fenix.library.history.viewholders
import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.history_list_item.view.*
import mozilla.components.browser.menu.BrowserMenu
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.library.history.HistoryInteractor
import org.mozilla.fenix.library.history.HistoryItem
import org.mozilla.fenix.library.history.HistoryItemMenu
@ -20,52 +16,29 @@ import org.mozilla.fenix.library.history.HistoryItemTimeGroup
import org.mozilla.fenix.library.history.HistoryState
class HistoryListItemViewHolder(
private val view: View,
view: View,
private val historyInteractor: HistoryInteractor
) : RecyclerView.ViewHolder(view) {
private val layout = view.history_layout
private val favicon = view.history_favicon
private val title = view.history_title
private val url = view.history_url
private val menuButton = view.history_item_overflow
private val headerWrapper = view.header_wrapper
private val headerTitle = view.header_title
private val deleteButtonWrapper = view.delete_button_wrapper
private val deleteButton = view.delete_button
private var item: HistoryItem? = null
private lateinit var historyMenu: HistoryItemMenu
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
init {
setupMenu()
layout.setOnLongClickListener {
item?.apply {
historyInteractor.onItemLongPress(this)
}
itemView.history_layout.setOnLongClickListener {
item?.also(historyInteractor::onItemLongPress)
true
}
menuButton.setOnClickListener {
historyMenu.menuBuilder.build(view.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
}
itemView.history_layout.setOnClickListener {
item?.also(historyInteractor::onItemPress)
}
deleteButton.setOnClickListener {
mode?.also {
when (it) {
is HistoryState.Mode.Normal -> historyInteractor.onDeleteAll()
is HistoryState.Mode.Editing -> historyInteractor.onDeleteSome(it.selectedItems)
}
itemView.delete_button.setOnClickListener {
when (val mode = this.mode) {
HistoryState.Mode.Normal -> historyInteractor.onDeleteAll()
is HistoryState.Mode.Editing -> historyInteractor.onDeleteSome(mode.selectedItems)
}
}
}
@ -73,76 +46,42 @@ class HistoryListItemViewHolder(
fun bind(
item: HistoryItem,
timeGroup: HistoryItemTimeGroup?,
showDeletebutton: Boolean,
showDeleteButton: Boolean,
mode: HistoryState.Mode
) {
this.item = item
this.mode = mode
title.text = item.title
url.text = item.url
itemView.history_layout.titleView.text = item.title
itemView.history_layout.urlView.text = item.url
toggleDeleteButton(showDeletebutton, mode)
toggleDeleteButton(showDeleteButton, mode)
val headerText = timeGroup?.let { it.humanReadable(view.context) }
val headerText = timeGroup?.humanReadable(itemView.context)
toggleHeader(headerText)
val selected = toggleSelected(mode, item)
if (mode is HistoryState.Mode.Editing) {
val backgroundTint =
if (selected) {
ThemeManager.resolveAttribute(R.attr.accentHighContrast, itemView.context)
} else {
ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
}
val backgroundTintList =
ContextCompat.getColorStateList(itemView.context, backgroundTint)
favicon.backgroundTintList = backgroundTintList
if (selected) {
favicon.setImageResource(R.drawable.mozac_ic_check)
} else {
updateFavIcon(item.url)
}
} else {
val backgroundTint = ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
val backgroundTintList =
ContextCompat.getColorStateList(itemView.context, backgroundTint)
favicon.backgroundTintList = backgroundTintList
updateFavIcon(item.url)
}
}
private fun toggleSelected(
mode: HistoryState.Mode,
item: HistoryItem
): Boolean {
return when (mode) {
is HistoryState.Mode.Editing -> mode.selectedItems.contains(item)
else -> false
}
itemView.history_layout.changeSelected(item in mode.selectedItems)
itemView.history_layout.loadFavicon(item.url)
}
private fun toggleHeader(text: String?) {
text?.also {
headerWrapper.visibility = View.VISIBLE
headerTitle.text = it
} ?: run {
headerWrapper.visibility = View.GONE
if (text != null) {
itemView.header_title.visibility = View.VISIBLE
itemView.header_title.text = text
} else {
itemView.header_title.visibility = View.GONE
}
}
private fun toggleDeleteButton(
showDeletebutton: Boolean,
showDeleteButton: Boolean,
mode: HistoryState.Mode
) {
if (showDeletebutton) {
deleteButtonWrapper.visibility = View.VISIBLE
if (showDeleteButton) {
itemView.delete_button.run {
visibility = View.VISIBLE
deleteButton.run {
val isDeleting = mode is HistoryState.Mode.Deleting
if (isDeleting || mode is HistoryState.Mode.Editing && mode.selectedItems.isNotEmpty()) {
if (mode === HistoryState.Mode.Deleting || mode.selectedItems.isNotEmpty()) {
isEnabled = false
alpha = DELETE_BUTTON_DISABLED_ALPHA
} else {
@ -151,22 +90,23 @@ class HistoryListItemViewHolder(
}
}
} else {
deleteButtonWrapper.visibility = View.GONE
itemView.delete_button.visibility = View.GONE
}
}
private fun setupMenu() {
this.historyMenu = HistoryItemMenu(itemView.context) {
val historyMenu = HistoryItemMenu(itemView.context) {
when (it) {
is HistoryItemMenu.Item.Delete -> {
item?.apply { historyInteractor.onDeleteOne(this) }
}
HistoryItemMenu.Item.Delete -> item?.also(historyInteractor::onDeleteOne)
}
}
}
private fun updateFavIcon(url: String) {
favicon.context.components.core.icons.loadIntoView(favicon, url)
itemView.history_layout.overflowView.setOnClickListener {
historyMenu.menuBuilder.build(itemView.context).show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
}
}
companion object {

View File

@ -3,103 +3,38 @@
- 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/. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/delete_button_wrapper"
android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:visibility="gone">
<com.google.android.material.button.MaterialButton
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_button"
style="@style/ThemeIndependentMaterialGreyButtonDestructive"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/history_delete_all"
app:rippleColor="?secondaryText" />
</FrameLayout>
app:rippleColor="?secondaryText"
android:visibility="gone" />
<FrameLayout
android:id="@+id/header_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:visibility="gone">
<TextView
<TextView
android:id="@+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="17sp"
android:textColor="?primaryText"/>
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/history_layout"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="4dp"
android:paddingStart="20dp"
android:paddingEnd="0dp">
<ImageButton
android:id="@+id/history_item_overflow"
android:layout_width="@dimen/glyph_button_width"
android:layout_height="@dimen/glyph_button_height"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/content_description_history_menu"
android:src="@drawable/ic_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/history_favicon"
android:layout_width="@dimen/history_favicon_width_height"
android:layout_height="@dimen/history_favicon_width_height"
android:background="@drawable/favicon_background"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/history_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?secondaryText"
android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@id/history_item_overflow"
app:layout_constraintStart_toEndOf="@id/history_favicon"
app:layout_constraintTop_toBottomOf="@id/history_title" />
<TextView
android:id="@+id/history_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?primaryText"
android:textSize="18sp"
android:layout_marginTop="2dp"
app:layout_constraintEnd_toStartOf="@id/history_item_overflow"
app:layout_constraintStart_toEndOf="@id/history_favicon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:paddingStart="20dp"
android:paddingEnd="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
tools:text="Header"
android:visibility="gone" />
<org.mozilla.fenix.library.LibrarySiteItemView
android:id="@+id/history_layout"
android:layout_width="match_parent"
android:layout_height="56dp" />
</LinearLayout>