From 2edf42f9f8179d248feef3609fa16f946eabade3 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 12 Sep 2022 20:41:34 +0200 Subject: [PATCH] Switch message list from `ListView` to `RecyclerView` --- .../fsck/k9/fragment/MessageListAdapter.kt | 214 +++++++++++++++--- .../fsck/k9/fragment/MessageListFragment.kt | 172 ++++---------- .../fsck/k9/fragment/MessageListHandler.java | 2 +- .../com/fsck/k9/fragment/MessageViewHolder.kt | 9 +- .../message_list_item_footer_background.xml | 12 - .../main/res/layout/message_list_fragment.xml | 9 +- .../src/main/res/layout/message_list_item.xml | 1 + .../res/layout/message_list_item_footer.xml | 3 +- app/ui/legacy/src/main/res/values/attrs.xml | 1 + app/ui/legacy/src/main/res/values/colors.xml | 2 - app/ui/legacy/src/main/res/values/themes.xml | 14 +- .../k9/fragment/MessageListAdapterTest.kt | 5 +- 12 files changed, 256 insertions(+), 188 deletions(-) delete mode 100644 app/ui/legacy/src/main/res/drawable/message_list_item_footer_background.xml diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt index 30cd1418b..0704a1fd0 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt @@ -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() { 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 = 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() 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 = 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 @@ -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, + private val newMessageList: List +) : 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() } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt index 061bd58d9..b4f80984b 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt @@ -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 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 @@ -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 { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListHandler.java b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListHandler.java index d62e7b138..72252cf15 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListHandler.java +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListHandler.java @@ -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); } } }); diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageViewHolder.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageViewHolder.kt index d6487ab9b..5e436a3c7 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageViewHolder.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageViewHolder.kt @@ -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) +} diff --git a/app/ui/legacy/src/main/res/drawable/message_list_item_footer_background.xml b/app/ui/legacy/src/main/res/drawable/message_list_item_footer_background.xml deleted file mode 100644 index 6606c0bb5..000000000 --- a/app/ui/legacy/src/main/res/drawable/message_list_item_footer_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/app/ui/legacy/src/main/res/layout/message_list_fragment.xml b/app/ui/legacy/src/main/res/layout/message_list_fragment.xml index f4baa830a..d5329bcc3 100644 --- a/app/ui/legacy/src/main/res/layout/message_list_fragment.xml +++ b/app/ui/legacy/src/main/res/layout/message_list_fragment.xml @@ -1,18 +1,21 @@ - - diff --git a/app/ui/legacy/src/main/res/layout/message_list_item.xml b/app/ui/legacy/src/main/res/layout/message_list_item.xml index a6d1bce89..8b4fa3034 100644 --- a/app/ui/legacy/src/main/res/layout/message_list_item.xml +++ b/app/ui/legacy/src/main/res/layout/message_list_item.xml @@ -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" diff --git a/app/ui/legacy/src/main/res/layout/message_list_item_footer.xml b/app/ui/legacy/src/main/res/layout/message_list_item_footer.xml index bb118bf78..365aebdb6 100644 --- a/app/ui/legacy/src/main/res/layout/message_list_item_footer.xml +++ b/app/ui/legacy/src/main/res/layout/message_list_item_footer.xml @@ -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"> + diff --git a/app/ui/legacy/src/main/res/values/colors.xml b/app/ui/legacy/src/main/res/values/colors.xml index 5228d7681..a569d49d6 100644 --- a/app/ui/legacy/src/main/res/values/colors.xml +++ b/app/ui/legacy/src/main/res/values/colors.xml @@ -4,8 +4,6 @@ #444444 - #eeeeee - #888 #f44336 #7bad45 diff --git a/app/ui/legacy/src/main/res/values/themes.xml b/app/ui/legacy/src/main/res/values/themes.xml index 0500a0236..9c3016a43 100644 --- a/app/ui/legacy/src/main/res/values/themes.xml +++ b/app/ui/legacy/src/main/res/values/themes.xml @@ -77,9 +77,10 @@ @drawable/ic_import_status @android:color/primary_text_light @android:color/secondary_text_light - #8038B8E2 - #c0cdcdcd - #00ffffff + #ff99d9ee + ?android:attr/windowBackground + #ffd8d8d8 + ?attr/messageListRegularItemBackgroundColor ?android:attr/colorBackground @drawable/thread_count_box_light #ff2ea7d1 @@ -196,9 +197,10 @@ @drawable/ic_import_status @android:color/primary_text_dark @android:color/secondary_text_dark - #8038B8E2 - #00000000 - #c05a5a5a + #ff347489 + ?android:attr/windowBackground + ?attr/messageListRegularItemBackgroundColor + #ff505050 ?android:attr/colorBackground @drawable/thread_count_box_dark #ff33b5e5 diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt index 168f2bc68..095eb6815 100644 --- a/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt +++ b/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt @@ -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"