Switch message list from ListView
to RecyclerView
This commit is contained in:
parent
fbe8eca814
commit
2edf42f9f8
12 changed files with 256 additions and 188 deletions
|
@ -1,9 +1,8 @@
|
|||
package com.fsck.k9.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Resources
|
||||
import android.content.res.Resources.Theme
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Spannable
|
||||
|
@ -14,12 +13,15 @@ import android.text.style.StyleSpan
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.View.OnLongClickListener
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
|
||||
import com.fsck.k9.FontSizes
|
||||
import com.fsck.k9.contacts.ContactPictureLoader
|
||||
import com.fsck.k9.controller.MessageReference
|
||||
|
@ -32,8 +34,12 @@ import com.fsck.k9.ui.resolveColorAttribute
|
|||
import com.fsck.k9.ui.resolveDrawableAttribute
|
||||
import kotlin.math.max
|
||||
|
||||
private const val FOOTER_ID = 1L
|
||||
|
||||
private const val TYPE_MESSAGE = 0
|
||||
private const val TYPE_FOOTER = 1
|
||||
|
||||
class MessageListAdapter internal constructor(
|
||||
private val context: Context,
|
||||
theme: Theme,
|
||||
private val res: Resources,
|
||||
private val layoutInflater: LayoutInflater,
|
||||
|
@ -41,7 +47,7 @@ class MessageListAdapter internal constructor(
|
|||
private val listItemListener: MessageListItemActionListener,
|
||||
private val appearance: MessageListAppearance,
|
||||
private val relativeDateTimeFormatter: RelativeDateTimeFormatter
|
||||
) : BaseAdapter() {
|
||||
) : RecyclerView.Adapter<MessageListViewHolder>() {
|
||||
|
||||
private val forwardedIcon: Drawable = theme.resolveDrawableAttribute(R.attr.messageListForwarded)
|
||||
private val answeredIcon: Drawable = theme.resolveDrawableAttribute(R.attr.messageListAnswered)
|
||||
|
@ -49,11 +55,15 @@ class MessageListAdapter internal constructor(
|
|||
private val previewTextColor: Int = theme.resolveColorAttribute(R.attr.messageListPreviewTextColor)
|
||||
private val activeItemBackgroundColor: Int = theme.resolveColorAttribute(R.attr.messageListActiveItemBackgroundColor)
|
||||
private val selectedItemBackgroundColor: Int = theme.resolveColorAttribute(R.attr.messageListSelectedBackgroundColor)
|
||||
private val regularItemBackgroundColor: Int = theme.resolveColorAttribute(R.attr.messageListUnreadItemBackgroundColor)
|
||||
private val readItemBackgroundColor: Int = theme.resolveColorAttribute(R.attr.messageListReadItemBackgroundColor)
|
||||
private val unreadItemBackgroundColor: Int = theme.resolveColorAttribute(R.attr.messageListUnreadItemBackgroundColor)
|
||||
|
||||
var messages: List<MessageListItem> = emptyList()
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
set(value) {
|
||||
val oldMessageList = field
|
||||
|
||||
field = value
|
||||
messagesMap = value.associateBy { it.uniqueId }
|
||||
|
||||
|
@ -62,18 +72,57 @@ class MessageListAdapter internal constructor(
|
|||
selected = selected.intersect(uniqueIds)
|
||||
}
|
||||
|
||||
notifyDataSetChanged()
|
||||
if (oldMessageList.isEmpty()) {
|
||||
// While loading, only the footer view is showing. If we used DiffUtil, the footer view would be used as
|
||||
// anchor element and the updated list would be scrolled all the way down.
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
val diffResult = DiffUtil.calculateDiff(
|
||||
MessageListDiffCallback(oldMessageList = oldMessageList, newMessageList = value)
|
||||
)
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
}
|
||||
|
||||
private var messagesMap = emptyMap<Long, MessageListItem>()
|
||||
|
||||
var activeMessage: MessageReference? = null
|
||||
set(value) {
|
||||
if (value == field) return
|
||||
|
||||
val oldPosition = getPosition(field)
|
||||
val newPosition = getPosition(value)
|
||||
|
||||
field = value
|
||||
|
||||
oldPosition?.let { position -> notifyItemChanged(position) }
|
||||
newPosition?.let { position -> notifyItemChanged(position) }
|
||||
}
|
||||
|
||||
var selected: Set<Long> = emptySet()
|
||||
private set(value) {
|
||||
if (value == field) return
|
||||
|
||||
// Selection removed
|
||||
field.asSequence()
|
||||
.filter { uniqueId -> uniqueId !in value }
|
||||
.mapNotNull { uniqueId -> messagesMap[uniqueId] }
|
||||
.mapNotNull { messageListItem -> getPosition(messageListItem) }
|
||||
.forEach { position ->
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
// Selection added
|
||||
value.asSequence()
|
||||
.filter { uniqueId -> uniqueId !in field }
|
||||
.mapNotNull { uniqueId -> messagesMap[uniqueId] }
|
||||
.mapNotNull { messageListItem -> getPosition(messageListItem) }
|
||||
.forEach { position ->
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
field = value
|
||||
selectedCount = calculateSelectionCount()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
val selectedMessages: List<MessageListItem>
|
||||
|
@ -85,6 +134,29 @@ class MessageListAdapter internal constructor(
|
|||
var selectedCount: Int = 0
|
||||
private set
|
||||
|
||||
var footerText: String? = null
|
||||
set(value) {
|
||||
if (field == value) return
|
||||
|
||||
val hadFooterText = field != null
|
||||
field = value
|
||||
|
||||
if (hadFooterText) {
|
||||
notifyItemChanged(footerPosition)
|
||||
} else {
|
||||
notifyItemInserted(footerPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private val hasFooter: Boolean
|
||||
get() = footerText != null
|
||||
|
||||
private val lastMessagePosition: Int
|
||||
get() = messages.lastIndex
|
||||
|
||||
private val footerPosition: Int
|
||||
get() = if (hasFooter) lastMessagePosition + 1 else NO_POSITION
|
||||
|
||||
private inline val subjectViewFontSize: Int
|
||||
get() = if (appearance.senderAboveSubject) {
|
||||
appearance.fontSizes.messageListSender
|
||||
|
@ -92,34 +164,59 @@ class MessageListAdapter internal constructor(
|
|||
appearance.fontSizes.messageListSubject
|
||||
}
|
||||
|
||||
private val messageClickedListener = OnClickListener { view: View ->
|
||||
val messageListItem = getItemFromView(view)
|
||||
listItemListener.onMessageClicked(messageListItem)
|
||||
}
|
||||
|
||||
private val messageLongClickedListener = OnLongClickListener { view: View ->
|
||||
val messageListItem = getItemFromView(view)
|
||||
listItemListener.onToggleMessageSelection(messageListItem)
|
||||
true
|
||||
}
|
||||
|
||||
private val footerClickListener = OnClickListener {
|
||||
listItemListener.onFooterClicked()
|
||||
}
|
||||
|
||||
private val flagClickListener = OnClickListener { view: View ->
|
||||
val messageViewHolder = view.tag as MessageViewHolder
|
||||
val messageListItem = getItemById(messageViewHolder.uniqueId)
|
||||
val messageListItem = getItemFromView(view)
|
||||
listItemListener.onToggleMessageFlag(messageListItem)
|
||||
}
|
||||
|
||||
private val contactPictureClickListener = OnClickListener { view: View ->
|
||||
val parentView = view.parent.parent as View
|
||||
val messageViewHolder = parentView.tag as MessageViewHolder
|
||||
val messageListItem = getItemById(messageViewHolder.uniqueId)
|
||||
val messageListItem = getItemFromView(parentView)
|
||||
listItemListener.onToggleMessageSelection(messageListItem)
|
||||
}
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
private fun recipientSigil(toMe: Boolean, ccMe: Boolean) = when {
|
||||
toMe -> res.getString(R.string.messagelist_sent_to_me_sigil) + " "
|
||||
ccMe -> res.getString(R.string.messagelist_sent_cc_me_sigil) + " "
|
||||
else -> ""
|
||||
}
|
||||
|
||||
override fun hasStableIds(): Boolean = true
|
||||
override fun getItemCount(): Int = messages.size + if (hasFooter) 1 else 0
|
||||
|
||||
override fun getCount(): Int = messages.size
|
||||
override fun getItemId(position: Int): Long {
|
||||
return if (position <= lastMessagePosition) {
|
||||
messages[position].uniqueId
|
||||
} else {
|
||||
FOOTER_ID
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long = messages[position].uniqueId
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position <= lastMessagePosition) TYPE_MESSAGE else TYPE_FOOTER
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): MessageListItem = messages[position]
|
||||
private fun getItem(position: Int): MessageListItem = messages[position]
|
||||
|
||||
private fun getItemById(uniqueId: Long): MessageListItem {
|
||||
fun getItemById(uniqueId: Long): MessageListItem {
|
||||
return messagesMap[uniqueId]!!
|
||||
}
|
||||
|
||||
|
@ -135,16 +232,26 @@ class MessageListAdapter internal constructor(
|
|||
return messages.indexOf(messageListItem).takeIf { it != -1 }
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
val message = getItem(position)
|
||||
val view: View = convertView ?: newView(parent)
|
||||
bindView(view, context, message)
|
||||
private fun getPosition(messageReference: MessageReference?): Int? {
|
||||
if (messageReference == null) return null
|
||||
|
||||
return view
|
||||
return messages.indexOfFirst {
|
||||
messageReference.equals(it.account.uuid, it.folderId, it.messageUid)
|
||||
}.takeIf { it != -1 }
|
||||
}
|
||||
|
||||
private fun newView(parent: ViewGroup?): View {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageListViewHolder {
|
||||
return when (viewType) {
|
||||
TYPE_MESSAGE -> createMessageViewHolder(parent)
|
||||
TYPE_FOOTER -> createFooterViewHolder(parent)
|
||||
else -> error("Unsupported type: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMessageViewHolder(parent: ViewGroup?): MessageViewHolder {
|
||||
val view = layoutInflater.inflate(R.layout.message_list_item, parent, false)
|
||||
view.setOnClickListener(messageClickedListener)
|
||||
view.setOnLongClickListener(messageLongClickedListener)
|
||||
|
||||
val holder = MessageViewHolder(view)
|
||||
|
||||
|
@ -168,14 +275,33 @@ class MessageListAdapter internal constructor(
|
|||
|
||||
view.tag = holder
|
||||
|
||||
return view
|
||||
return holder
|
||||
}
|
||||
|
||||
private fun bindView(view: View, context: Context, message: MessageListItem) {
|
||||
val isSelected = selected.contains(message.uniqueId)
|
||||
val isActive = isActiveMessage(message)
|
||||
private fun createFooterViewHolder(parent: ViewGroup): MessageListViewHolder {
|
||||
val view = layoutInflater.inflate(R.layout.message_list_item_footer, parent, false)
|
||||
view.setOnClickListener(footerClickListener)
|
||||
return FooterViewHolder(view)
|
||||
}
|
||||
|
||||
val holder = view.tag as MessageViewHolder
|
||||
override fun onBindViewHolder(holder: MessageListViewHolder, position: Int) {
|
||||
when (val viewType = getItemViewType(position)) {
|
||||
TYPE_MESSAGE -> {
|
||||
val messageListItem = getItem(position)
|
||||
bindMessageViewHolder(holder as MessageViewHolder, messageListItem)
|
||||
}
|
||||
TYPE_FOOTER -> {
|
||||
bindFooterViewHolder(holder as FooterViewHolder)
|
||||
}
|
||||
else -> {
|
||||
error("Unsupported type: $viewType")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindMessageViewHolder(holder: MessageViewHolder, messageListItem: MessageListItem) {
|
||||
val isSelected = selected.contains(messageListItem.uniqueId)
|
||||
val isActive = isActiveMessage(messageListItem)
|
||||
|
||||
if (appearance.showContactPicture) {
|
||||
if (isSelected) {
|
||||
|
@ -187,7 +313,7 @@ class MessageListAdapter internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
with(message) {
|
||||
with(messageListItem) {
|
||||
val maybeBoldTypeface = if (isRead) Typeface.NORMAL else Typeface.BOLD
|
||||
val displayDate = relativeDateTimeFormatter.formatDate(messageDate)
|
||||
val displayThreadCount = if (appearance.showingThreadedList) threadCount else 0
|
||||
|
@ -206,7 +332,7 @@ class MessageListAdapter internal constructor(
|
|||
if (appearance.showContactPicture && holder.contactPicture.isVisible) {
|
||||
setContactPicture(holder.contactPicture, displayAddress)
|
||||
}
|
||||
setBackgroundColor(view, isSelected, isRead, isActive)
|
||||
setBackgroundColor(holder.itemView, isSelected, isRead, isActive)
|
||||
updateWithThreadCount(holder, displayThreadCount)
|
||||
val beforePreviewText = if (appearance.senderAboveSubject) subject else displayName
|
||||
val sigil = recipientSigil(toMe, ccMe)
|
||||
|
@ -240,6 +366,10 @@ class MessageListAdapter internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun bindFooterViewHolder(holder: FooterViewHolder) {
|
||||
holder.text.text = footerText
|
||||
}
|
||||
|
||||
private fun formatPreviewText(
|
||||
preview: TextView,
|
||||
beforePreviewText: CharSequence,
|
||||
|
@ -304,7 +434,7 @@ class MessageListAdapter internal constructor(
|
|||
selected -> selectedItemBackgroundColor
|
||||
backGroundAsReadIndicator && read -> readItemBackgroundColor
|
||||
backGroundAsReadIndicator && !read -> unreadItemBackgroundColor
|
||||
else -> Color.TRANSPARENT
|
||||
else -> regularItemBackgroundColor
|
||||
}
|
||||
|
||||
view.setBackgroundColor(backgroundColor)
|
||||
|
@ -388,9 +518,33 @@ class MessageListAdapter internal constructor(
|
|||
.filter { it.uniqueId in selected }
|
||||
.sumOf { it.threadCount.coerceAtLeast(1) }
|
||||
}
|
||||
|
||||
private fun getItemFromView(view: View): MessageListItem {
|
||||
val messageViewHolder = view.tag as MessageViewHolder
|
||||
return getItemById(messageViewHolder.uniqueId)
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageListDiffCallback(
|
||||
private val oldMessageList: List<MessageListItem>,
|
||||
private val newMessageList: List<MessageListItem>
|
||||
) : DiffUtil.Callback() {
|
||||
override fun getOldListSize(): Int = oldMessageList.size
|
||||
|
||||
override fun getNewListSize(): Int = newMessageList.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldMessageList[oldItemPosition].uniqueId == newMessageList[newItemPosition].uniqueId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldMessageList[oldItemPosition] == newMessageList[newItemPosition]
|
||||
}
|
||||
}
|
||||
|
||||
interface MessageListItemActionListener {
|
||||
fun onMessageClicked(messageListItem: MessageListItem)
|
||||
fun onToggleMessageSelection(item: MessageListItem)
|
||||
fun onToggleMessageFlag(item: MessageListItem)
|
||||
fun onFooterClicked()
|
||||
}
|
||||
|
|
|
@ -11,15 +11,14 @@ import android.view.Menu
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import android.widget.AdapterView.OnItemLongClickListener
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Account.Expunge
|
||||
|
@ -63,8 +62,6 @@ private const val MAXIMUM_MESSAGE_SORT_OVERRIDES = 3
|
|||
|
||||
class MessageListFragment :
|
||||
Fragment(),
|
||||
OnItemClickListener,
|
||||
OnItemLongClickListener,
|
||||
ConfirmationDialogFragmentListener,
|
||||
MessageListItemActionListener {
|
||||
|
||||
|
@ -82,12 +79,9 @@ class MessageListFragment :
|
|||
|
||||
private lateinit var fragmentListener: MessageListFragmentListener
|
||||
|
||||
private lateinit var listView: ListView
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||
private lateinit var adapter: MessageListAdapter
|
||||
private var footerView: View? = null
|
||||
|
||||
private var savedListState: Parcelable? = null
|
||||
|
||||
private lateinit var accountUuids: Array<String>
|
||||
private var account: Account? = null
|
||||
|
@ -118,6 +112,7 @@ class MessageListFragment :
|
|||
private set
|
||||
private var isSingleFolderMode = false
|
||||
private var isRemoteSearch = false
|
||||
private var initialMessageListLoad = true
|
||||
|
||||
private val isUnifiedInbox: Boolean
|
||||
get() = localSearch.id == SearchAccount.UNIFIED_INBOX
|
||||
|
@ -173,7 +168,6 @@ class MessageListFragment :
|
|||
activeMessages = savedInstanceState.getStringArray(STATE_ACTIVE_MESSAGES)?.map { MessageReference.parse(it)!! }
|
||||
restoreSelectedMessages(savedInstanceState)
|
||||
isRemoteSearch = savedInstanceState.getBoolean(STATE_REMOTE_SEARCH_PERFORMED)
|
||||
savedListState = savedInstanceState.getParcelable(STATE_MESSAGE_LIST)
|
||||
val messageReferenceString = savedInstanceState.getString(STATE_ACTIVE_MESSAGE)
|
||||
activeMessage = MessageReference.parse(messageReferenceString)
|
||||
}
|
||||
|
@ -183,7 +177,7 @@ class MessageListFragment :
|
|||
}
|
||||
|
||||
fun restoreListState(savedListState: Parcelable) {
|
||||
listView.onRestoreInstanceState(savedListState)
|
||||
recyclerView.layoutManager?.onRestoreInstanceState(savedListState)
|
||||
}
|
||||
|
||||
private fun decodeArguments(): MessageListFragment? {
|
||||
|
@ -223,7 +217,7 @@ class MessageListFragment :
|
|||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.message_list_fragment, container, false).apply {
|
||||
initializeSwipeRefreshLayout(this)
|
||||
initializeListView(this)
|
||||
initializeRecyclerView(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,16 +234,13 @@ class MessageListFragment :
|
|||
swipeRefreshLayout.isEnabled = false
|
||||
}
|
||||
|
||||
private fun initializeListView(view: View) {
|
||||
listView = view.findViewById(R.id.message_list)
|
||||
with(listView) {
|
||||
scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
|
||||
isLongClickable = true
|
||||
isFastScrollEnabled = true
|
||||
isVerticalFadingEdgeEnabled = false
|
||||
isScrollingCacheEnabled = false
|
||||
onItemClickListener = this@MessageListFragment
|
||||
onItemLongClickListener = this@MessageListFragment
|
||||
private fun initializeRecyclerView(view: View) {
|
||||
recyclerView = view.findViewById(R.id.message_list)
|
||||
|
||||
val itemDecoration = DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)
|
||||
recyclerView.addItemDecoration(itemDecoration)
|
||||
recyclerView.itemAnimator = DefaultItemAnimator().apply {
|
||||
supportsChangeAnimations = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -265,7 +256,6 @@ class MessageListFragment :
|
|||
|
||||
private fun initializeMessageList() {
|
||||
adapter = MessageListAdapter(
|
||||
context = requireContext(),
|
||||
theme = requireActivity().theme,
|
||||
res = resources,
|
||||
layoutInflater = layoutInflater,
|
||||
|
@ -277,12 +267,7 @@ class MessageListFragment :
|
|||
|
||||
adapter.activeMessage = activeMessage
|
||||
|
||||
if (isSingleFolderMode) {
|
||||
listView.addFooterView(getFooterView(listView))
|
||||
updateFooter(null)
|
||||
}
|
||||
|
||||
listView.adapter = adapter
|
||||
recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
private fun initializeSortSettings() {
|
||||
|
@ -315,10 +300,9 @@ class MessageListFragment :
|
|||
currentFolder?.let {
|
||||
if (it.databaseId == folderId) {
|
||||
it.loading = loading
|
||||
updateFooterText()
|
||||
}
|
||||
}
|
||||
|
||||
updateFooterView()
|
||||
}
|
||||
|
||||
fun updateTitle() {
|
||||
|
@ -372,16 +356,7 @@ class MessageListFragment :
|
|||
fragmentListener.setMessageListProgressEnabled(progress)
|
||||
}
|
||||
|
||||
override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||
if (view === footerView) {
|
||||
handleFooterClick()
|
||||
} else {
|
||||
val messageListItem = adapter.getItem(position)
|
||||
handleListItemClick(messageListItem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFooterClick() {
|
||||
override fun onFooterClicked() {
|
||||
val currentFolder = this.currentFolder ?: return
|
||||
|
||||
if (currentFolder.moreMessages && !localSearch.isManualSearch) {
|
||||
|
@ -400,7 +375,7 @@ class MessageListFragment :
|
|||
} else {
|
||||
extraSearchResults = null
|
||||
loadSearchResults = additionalSearchResults
|
||||
updateFooter(null)
|
||||
updateFooterText(null)
|
||||
}
|
||||
|
||||
messagingController.loadSearchResults(
|
||||
|
@ -412,7 +387,7 @@ class MessageListFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleListItemClick(messageListItem: MessageListItem) {
|
||||
override fun onMessageClicked(messageListItem: MessageListItem) {
|
||||
if (adapter.selectedCount > 0) {
|
||||
toggleMessageSelect(messageListItem)
|
||||
} else {
|
||||
|
@ -424,28 +399,17 @@ class MessageListFragment :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(parent: AdapterView<*>?, view: View, position: Int, id: Long): Boolean {
|
||||
if (view === footerView) return false
|
||||
|
||||
val messageListItem = adapter.getItem(position)
|
||||
toggleMessageSelect(messageListItem)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
if (isNewMessagesView && !requireActivity().isChangingConfigurations) {
|
||||
messagingController.clearNewMessages(account)
|
||||
}
|
||||
|
||||
savedListState = listView.onSaveInstanceState()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
saveListState(outState)
|
||||
outState.putLongArray(STATE_SELECTED_MESSAGES, adapter.selected.toLongArray())
|
||||
outState.putBoolean(STATE_REMOTE_SEARCH_PERFORMED, isRemoteSearch)
|
||||
outState.putStringArray(
|
||||
|
@ -457,15 +421,6 @@ class MessageListFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun saveListState(outState: Bundle) {
|
||||
if (savedListState != null) {
|
||||
// The previously saved state was never restored, so just use that.
|
||||
outState.putParcelable(STATE_MESSAGE_LIST, savedListState)
|
||||
} else {
|
||||
outState.putParcelable(STATE_MESSAGE_LIST, listView.onSaveInstanceState())
|
||||
}
|
||||
}
|
||||
|
||||
private val messageListAppearance: MessageListAppearance
|
||||
get() = MessageListAppearance(
|
||||
fontSizes = K9.fontSizes,
|
||||
|
@ -789,26 +744,15 @@ class MessageListFragment :
|
|||
messagingController.sendPendingMessages(account, null)
|
||||
}
|
||||
|
||||
private fun getFooterView(parent: ViewGroup?): View? {
|
||||
return footerView ?: createFooterView(parent).also { footerView = it }
|
||||
}
|
||||
|
||||
private fun createFooterView(parent: ViewGroup?): View {
|
||||
return layoutInflater.inflate(R.layout.message_list_item_footer, parent, false).apply {
|
||||
tag = FooterViewHolder(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFooterView() {
|
||||
private fun updateFooterText() {
|
||||
val currentFolder = this.currentFolder
|
||||
val account = this.account
|
||||
|
||||
if (localSearch.isManualSearch || currentFolder == null || account == null) {
|
||||
updateFooter(null)
|
||||
return
|
||||
}
|
||||
|
||||
val footerText = if (currentFolder.loading) {
|
||||
val footerText = if (initialMessageListLoad) {
|
||||
null
|
||||
} else if (localSearch.isManualSearch || currentFolder == null || account == null) {
|
||||
null
|
||||
} else if (currentFolder.loading) {
|
||||
getString(R.string.status_loading_more)
|
||||
} else if (!currentFolder.moreMessages) {
|
||||
null
|
||||
|
@ -818,24 +762,11 @@ class MessageListFragment :
|
|||
getString(R.string.load_more_messages_fmt, account.displayCount)
|
||||
}
|
||||
|
||||
updateFooter(footerText)
|
||||
updateFooterText(footerText)
|
||||
}
|
||||
|
||||
fun updateFooter(text: String?) {
|
||||
val footerView = this.footerView ?: return
|
||||
|
||||
val shouldHideFooter = text == null
|
||||
if (shouldHideFooter) {
|
||||
listView.removeFooterView(footerView)
|
||||
} else {
|
||||
val isFooterViewAddedToListView = listView.footerViewsCount > 0
|
||||
if (!isFooterViewAddedToListView) {
|
||||
listView.addFooterView(footerView)
|
||||
}
|
||||
}
|
||||
|
||||
val holder = footerView.tag as FooterViewHolder
|
||||
holder.main.text = text
|
||||
fun updateFooterText(text: String?) {
|
||||
adapter.footerText = text
|
||||
}
|
||||
|
||||
private fun selectAll() {
|
||||
|
@ -1193,25 +1124,11 @@ class MessageListFragment :
|
|||
}
|
||||
|
||||
fun onMoveUp() {
|
||||
var currentPosition = listView.selectedItemPosition
|
||||
if (currentPosition == AdapterView.INVALID_POSITION || listView.isInTouchMode) {
|
||||
currentPosition = listView.firstVisiblePosition
|
||||
}
|
||||
|
||||
if (currentPosition > 0) {
|
||||
listView.setSelection(currentPosition - 1)
|
||||
}
|
||||
// FIXME
|
||||
}
|
||||
|
||||
fun onMoveDown() {
|
||||
var currentPosition = listView.selectedItemPosition
|
||||
if (currentPosition == AdapterView.INVALID_POSITION || listView.isInTouchMode) {
|
||||
currentPosition = listView.firstVisiblePosition
|
||||
}
|
||||
|
||||
if (currentPosition < listView.count) {
|
||||
listView.setSelection(currentPosition + 1)
|
||||
}
|
||||
// FIXME
|
||||
}
|
||||
|
||||
fun openMessage(messageReference: MessageReference) {
|
||||
|
@ -1227,8 +1144,9 @@ class MessageListFragment :
|
|||
|
||||
private val selectedMessageListItem: MessageListItem?
|
||||
get() {
|
||||
val position = listView.selectedItemPosition
|
||||
return if (position !in 0 until adapter.count) null else adapter.getItem(position)
|
||||
val focusedView = recyclerView.focusedChild ?: return null
|
||||
val viewHolder = recyclerView.findContainingViewHolder(focusedView) as? MessageViewHolder ?: return null
|
||||
return adapter.getItemById(viewHolder.uniqueId)
|
||||
}
|
||||
|
||||
private val selectedMessages: List<MessageReference>
|
||||
|
@ -1365,16 +1283,13 @@ class MessageListFragment :
|
|||
resetActionMode()
|
||||
computeBatchDirection()
|
||||
|
||||
if (savedListState != null) {
|
||||
handler.restoreListPosition(savedListState)
|
||||
savedListState = null
|
||||
}
|
||||
|
||||
invalidateMenu()
|
||||
|
||||
initialMessageListLoad = false
|
||||
|
||||
currentFolder?.let { currentFolder ->
|
||||
currentFolder.moreMessages = messageListInfo.hasMoreMessages
|
||||
updateFooterView()
|
||||
updateFooterText()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1416,7 +1331,6 @@ class MessageListFragment :
|
|||
// Redraw list immediately
|
||||
if (::adapter.isInitialized) {
|
||||
adapter.activeMessage = activeMessage
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
if (messageReference != null) {
|
||||
scrollToMessage(messageReference)
|
||||
|
@ -1459,8 +1373,11 @@ class MessageListFragment :
|
|||
val messageListItem = adapter.getItem(messageReference) ?: return
|
||||
val position = adapter.getPosition(messageListItem) ?: return
|
||||
|
||||
if (position <= listView.firstVisiblePosition || position >= listView.lastVisiblePosition) {
|
||||
listView.smoothScrollToPosition(position)
|
||||
val linearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
val firstVisiblePosition = linearLayoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
val lastVisiblePosition = linearLayoutManager.findLastCompletelyVisibleItemPosition()
|
||||
if (position !in firstVisiblePosition..lastVisiblePosition) {
|
||||
recyclerView.smoothScrollToPosition(position)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1836,10 +1753,6 @@ class MessageListFragment :
|
|||
}
|
||||
}
|
||||
|
||||
internal class FooterViewHolder(view: View) {
|
||||
val main: TextView = view.findViewById(R.id.main_text)
|
||||
}
|
||||
|
||||
private enum class FolderOperation {
|
||||
COPY, MOVE
|
||||
}
|
||||
|
@ -1873,7 +1786,6 @@ class MessageListFragment :
|
|||
private const val STATE_ACTIVE_MESSAGES = "activeMessages"
|
||||
private const val STATE_ACTIVE_MESSAGE = "activeMessage"
|
||||
private const val STATE_REMOTE_SEARCH_PERFORMED = "remoteSearchPerformed"
|
||||
private const val STATE_MESSAGE_LIST = "listState"
|
||||
|
||||
fun newInstance(search: LocalSearch, isThreadDisplay: Boolean, threadedList: Boolean): MessageListFragment {
|
||||
return MessageListFragment().apply {
|
||||
|
|
|
@ -57,7 +57,7 @@ public class MessageListHandler extends Handler {
|
|||
public void run() {
|
||||
MessageListFragment fragment = mFragment.get();
|
||||
if (fragment != null) {
|
||||
fragment.updateFooter(message);
|
||||
fragment.updateFooterText(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,9 +4,12 @@ import android.view.View
|
|||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.fsck.k9.ui.R
|
||||
|
||||
class MessageViewHolder(view: View) {
|
||||
sealed class MessageListViewHolder(view: View) : ViewHolder(view)
|
||||
|
||||
class MessageViewHolder(view: View) : MessageListViewHolder(view) {
|
||||
var uniqueId: Long = -1L
|
||||
|
||||
val selected: View = view.findViewById(R.id.selected)
|
||||
|
@ -20,3 +23,7 @@ class MessageViewHolder(view: View) {
|
|||
val attachment: ImageView = view.findViewById(R.id.attachment)
|
||||
val status: ImageView = view.findViewById(R.id.status)
|
||||
}
|
||||
|
||||
class FooterViewHolder(view: View) : MessageListViewHolder(view) {
|
||||
val text: TextView = view.findViewById(R.id.main_text)
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_window_focused="false" android:state_selected="true"
|
||||
android:drawable="@color/message_list_item_footer_background" />
|
||||
<item android:state_selected="true"
|
||||
android:drawable="@android:color/transparent" />
|
||||
<item android:state_pressed="true" android:state_selected="false"
|
||||
android:drawable="@android:color/transparent" />
|
||||
<item android:state_selected="false"
|
||||
android:drawable="@android:color/transparent"
|
||||
/>
|
||||
</selector>
|
|
@ -1,18 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
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/swiperefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="com.fsck.k9.fragment.MessageListFragment">
|
||||
|
||||
<ListView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/message_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="5"
|
||||
android:scrollbars="vertical"
|
||||
android:scrollbarStyle="insideOverlay"
|
||||
android:fadingEdge="none"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/message_list_item"/>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="center_vertical"
|
||||
tools:layout_height="?android:attr/listPreferredItemHeight"
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
android:background="@drawable/message_list_item_footer_background"
|
||||
android:background="?attr/messageListRegularItemBackgroundColor"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<attr name="textColorSecondaryRecipientDropdown" format="reference" />
|
||||
<attr name="backgroundColorChooseAccountHeader" format="color" />
|
||||
<attr name="messageListSelectedBackgroundColor" format="reference|color"/>
|
||||
<attr name="messageListRegularItemBackgroundColor" format="reference|color"/>
|
||||
<attr name="messageListReadItemBackgroundColor" format="reference|color"/>
|
||||
<attr name="messageListUnreadItemBackgroundColor" format="reference|color"/>
|
||||
<attr name="messageListThreadCountForegroundColor" format="reference|color"/>
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
|
||||
<color name="light_black">#444444</color>
|
||||
|
||||
<color name="message_list_item_footer_background">#eeeeee</color>
|
||||
|
||||
<color name="status_todo_chevron">#888</color>
|
||||
<color name="status_error_cross">#f44336</color>
|
||||
<color name="status_ok_checkmark">#7bad45</color>
|
||||
|
|
|
@ -77,9 +77,10 @@
|
|||
<item name="iconSettingsImportStatus">@drawable/ic_import_status</item>
|
||||
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_light</item>
|
||||
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_light</item>
|
||||
<item name="messageListSelectedBackgroundColor">#8038B8E2</item>
|
||||
<item name="messageListReadItemBackgroundColor">#c0cdcdcd</item>
|
||||
<item name="messageListUnreadItemBackgroundColor">#00ffffff</item>
|
||||
<item name="messageListSelectedBackgroundColor">#ff99d9ee</item>
|
||||
<item name="messageListRegularItemBackgroundColor">?android:attr/windowBackground</item>
|
||||
<item name="messageListReadItemBackgroundColor">#ffd8d8d8</item>
|
||||
<item name="messageListUnreadItemBackgroundColor">?attr/messageListRegularItemBackgroundColor</item>
|
||||
<item name="messageListThreadCountForegroundColor">?android:attr/colorBackground</item>
|
||||
<item name="messageListThreadCountBackground">@drawable/thread_count_box_light</item>
|
||||
<item name="messageListActiveItemBackgroundColor">#ff2ea7d1</item>
|
||||
|
@ -196,9 +197,10 @@
|
|||
<item name="iconSettingsImportStatus">@drawable/ic_import_status</item>
|
||||
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_dark</item>
|
||||
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_dark</item>
|
||||
<item name="messageListSelectedBackgroundColor">#8038B8E2</item>
|
||||
<item name="messageListReadItemBackgroundColor">#00000000</item>
|
||||
<item name="messageListUnreadItemBackgroundColor">#c05a5a5a</item>
|
||||
<item name="messageListSelectedBackgroundColor">#ff347489</item>
|
||||
<item name="messageListRegularItemBackgroundColor">?android:attr/windowBackground</item>
|
||||
<item name="messageListReadItemBackgroundColor">?attr/messageListRegularItemBackgroundColor</item>
|
||||
<item name="messageListUnreadItemBackgroundColor">#ff505050</item>
|
||||
<item name="messageListThreadCountForegroundColor">?android:attr/colorBackground</item>
|
||||
<item name="messageListThreadCountBackground">@drawable/thread_count_box_dark</item>
|
||||
<item name="messageListActiveItemBackgroundColor">#ff33b5e5</item>
|
||||
|
|
|
@ -457,7 +457,6 @@ class MessageListAdapterTest : RobolectricTest() {
|
|||
)
|
||||
|
||||
return MessageListAdapter(
|
||||
context = context,
|
||||
theme = context.theme,
|
||||
res = context.resources,
|
||||
layoutInflater = LayoutInflater.from(context),
|
||||
|
@ -518,7 +517,9 @@ class MessageListAdapterTest : RobolectricTest() {
|
|||
|
||||
fun MessageListAdapter.createAndBindView(item: MessageListItem = createMessageListItem()): View {
|
||||
messages = listOf(item)
|
||||
return getView(0, null, LinearLayout(context))
|
||||
val holder = onCreateViewHolder(LinearLayout(context), 0)
|
||||
onBindViewHolder(holder, 0)
|
||||
return holder.itemView
|
||||
}
|
||||
|
||||
fun secondLine(senderOrSubject: String, preview: String) = "$senderOrSubject $preview"
|
||||
|
|
Loading…
Reference in a new issue