Merge pull request #6456 from thundernest/swipe_action_text

Swipe actions: Display action name next to the icon
This commit is contained in:
cketti 2022-11-02 15:50:36 +01:00 committed by GitHub
commit 14693afd94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 220 additions and 72 deletions

View file

@ -463,6 +463,10 @@ class MessageListAdapter internal constructor(
item.messageUid == activeMessage.uid
}
fun isSelected(item: MessageListItem): Boolean {
return item.uniqueId in selected
}
fun toggleSelection(item: MessageListItem) {
if (messagesMap[item.uniqueId] == null) {
// MessageListItem is no longer in the list

View file

@ -264,7 +264,7 @@ class MessageListFragment :
val itemTouchHelper = ItemTouchHelper(
MessageListSwipeCallback(
resources,
requireContext(),
resourceProvider = SwipeResourceProvider(requireActivity().theme),
swipeActionSupportProvider,
swipeRightAction = K9.swipeRightAction,

View file

@ -1,10 +1,15 @@
package com.fsck.k9.ui.messagelist
import android.content.res.Resources
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.view.LayoutInflater
import android.view.View
import androidx.core.graphics.withSave
import android.view.View.MeasureSpec
import android.widget.ImageView
import android.widget.TextView
import androidx.core.graphics.withTranslation
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
@ -12,8 +17,9 @@ import com.fsck.k9.SwipeAction
import com.fsck.k9.ui.R
import kotlin.math.abs
@SuppressLint("InflateParams")
class MessageListSwipeCallback(
resources: Resources,
context: Context,
private val resourceProvider: SwipeResourceProvider,
private val swipeActionSupportProvider: SwipeActionSupportProvider,
private val swipeRightAction: SwipeAction,
@ -21,10 +27,19 @@ class MessageListSwipeCallback(
private val adapter: MessageListAdapter,
private val listener: MessageListSwipeListener
) : ItemTouchHelper.Callback() {
private val iconPadding = resources.getDimension(R.dimen.messageListSwipeIconPadding).toInt()
private val swipeThreshold = resources.getDimension(R.dimen.messageListSwipeThreshold)
private val swipeThreshold = context.resources.getDimension(R.dimen.messageListSwipeThreshold)
private val backgroundColorPaint = Paint()
private val swipeRightLayout: View
private val swipeLeftLayout: View
init {
val layoutInflater = LayoutInflater.from(context)
swipeRightLayout = layoutInflater.inflate(R.layout.swipe_right_action, null, false)
swipeLeftLayout = layoutInflater.inflate(R.layout.swipe_left_action, null, false)
}
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: ViewHolder): Int {
if (viewHolder !is MessageViewHolder) return 0
@ -82,89 +97,78 @@ class MessageListSwipeCallback(
actionState: Int,
isCurrentlyActive: Boolean
) {
canvas.withSave {
val view = viewHolder.itemView
val view = viewHolder.itemView
val viewWidth = view.width
val viewHeight = view.height
val holder = viewHolder as MessageViewHolder
val item = adapter.getItemById(holder.uniqueId) ?: return@withSave
val isViewAnimatingBack = !isCurrentlyActive && abs(dX).toInt() >= viewWidth
val swipeThreshold = recyclerView.width * getSwipeThreshold(holder)
val swipeThresholdReached = abs(dX) > swipeThreshold
if (swipeThresholdReached) {
val action = if (dX > 0) swipeRightAction else swipeLeftAction
val backgroundColor = resourceProvider.getBackgroundColor(action)
drawBackground(view, backgroundColor)
canvas.withTranslation(x = view.left.toFloat(), y = view.top.toFloat()) {
if (isViewAnimatingBack) {
drawBackground(dX, viewWidth, viewHeight)
} else {
val backgroundColor = resourceProvider.getBackgroundColor(SwipeAction.None)
drawBackground(view, backgroundColor)
}
// Stop drawing the icon when the view has been animated all the way off the screen by ItemTouchHelper.
// We do this so the icon doesn't switch state when RecyclerView's ItemAnimator animates the view back after
// a toggle action (mark as read/unread, add/remove star) was used.
if (isCurrentlyActive || abs(dX).toInt() < view.width) {
drawIcon(dX, view, item, swipeThresholdReached)
val holder = viewHolder as MessageViewHolder
drawLayout(dX, viewWidth, viewHeight, holder)
}
}
super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
private fun Canvas.drawBackground(view: View, color: Int) {
backgroundColorPaint.color = color
private fun Canvas.drawBackground(dX: Float, width: Int, height: Int) {
val swipeAction = if (dX > 0) swipeRightAction else swipeLeftAction
val backgroundColor = resourceProvider.getBackgroundColor(swipeAction)
backgroundColorPaint.color = backgroundColor
drawRect(
view.left.toFloat(),
view.top.toFloat(),
view.right.toFloat(),
view.bottom.toFloat(),
0F,
0F,
width.toFloat(),
height.toFloat(),
backgroundColorPaint
)
}
private fun Canvas.drawIcon(dX: Float, view: View, item: MessageListItem, swipeThresholdReached: Boolean) {
if (dX > 0) {
drawSwipeRightIcon(view, item, swipeThresholdReached)
private fun Canvas.drawLayout(dX: Float, width: Int, height: Int, viewHolder: MessageViewHolder) {
val item = adapter.getItemById(viewHolder.uniqueId) ?: return
val isSelected = adapter.isSelected(item)
val swipeRight = dX > 0
val swipeThresholdReached = abs(dX) > swipeThreshold
val swipeLayout = if (swipeRight) swipeRightLayout else swipeLeftLayout
val swipeAction = if (swipeRight) swipeRightAction else swipeLeftAction
val foregroundColor: Int
val backgroundColor: Int
if (swipeThresholdReached) {
foregroundColor = resourceProvider.iconTint
backgroundColor = resourceProvider.getBackgroundColor(swipeAction)
} else {
drawSwipeLeftIcon(view, item, swipeThresholdReached)
foregroundColor = resourceProvider.getBackgroundColor(swipeAction)
backgroundColor = resourceProvider.getBackgroundColor(SwipeAction.None)
}
}
private fun Canvas.drawSwipeRightIcon(view: View, item: MessageListItem, swipeThresholdReached: Boolean) {
resourceProvider.getIcon(item, swipeRightAction)?.let { icon ->
val iconLeft = iconPadding
val iconTop = view.top + ((view.height - icon.intrinsicHeight) / 2)
val iconRight = iconLeft + icon.intrinsicWidth
val iconBottom = iconTop + icon.intrinsicHeight
icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
swipeLayout.setBackgroundColor(backgroundColor)
val iconTint = if (swipeThresholdReached) {
resourceProvider.iconTint
} else {
resourceProvider.getBackgroundColor(swipeRightAction)
}
icon.setTint(iconTint)
val icon = resourceProvider.getIcon(item, swipeAction)
icon.setTint(foregroundColor)
icon.draw(this)
val iconView = swipeLayout.findViewById<ImageView>(R.id.swipe_action_icon)
iconView.setImageDrawable(icon)
val textView = swipeLayout.findViewById<TextView>(R.id.swipe_action_text)
textView.setTextColor(foregroundColor)
textView.text = resourceProvider.getActionName(item, swipeAction, isSelected)
if (swipeLayout.isDirty) {
val widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
val heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
swipeLayout.measure(widthMeasureSpec, heightMeasureSpec)
swipeLayout.layout(0, 0, width, height)
}
}
private fun Canvas.drawSwipeLeftIcon(view: View, item: MessageListItem, swipeThresholdReached: Boolean) {
resourceProvider.getIcon(item, swipeLeftAction)?.let { icon ->
val iconRight = view.right - iconPadding
val iconLeft = iconRight - icon.intrinsicWidth
val iconTop = view.top + ((view.height - icon.intrinsicHeight) / 2)
val iconBottom = iconTop + icon.intrinsicHeight
icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
val iconTint = if (swipeThresholdReached) {
resourceProvider.iconTint
} else {
resourceProvider.getBackgroundColor(swipeLeftAction)
}
icon.setTint(iconTint)
icon.draw(this)
}
swipeLayout.draw(this)
}
}

View file

@ -30,9 +30,20 @@ class SwipeResourceProvider(val theme: Theme) {
private val spamColor = theme.resolveColorAttribute(R.attr.messageListSwipeSpamBackgroundColor)
private val moveColor = theme.resolveColorAttribute(R.attr.messageListSwipeMoveBackgroundColor)
fun getIcon(item: MessageListItem, action: SwipeAction): Drawable? {
private val selectText = theme.resources.getString(R.string.swipe_action_select)
private val deselectText = theme.resources.getString(R.string.swipe_action_deselect)
private val markAsReadText = theme.resources.getString(R.string.swipe_action_mark_as_read)
private val markAsUnreadText = theme.resources.getString(R.string.swipe_action_mark_as_unread)
private val addStarText = theme.resources.getString(R.string.swipe_action_add_star)
private val removeStarText = theme.resources.getString(R.string.swipe_action_remove_star)
private val archiveText = theme.resources.getString(R.string.swipe_action_archive)
private val deleteText = theme.resources.getString(R.string.swipe_action_delete)
private val spamText = theme.resources.getString(R.string.swipe_action_spam)
private val moveText = theme.resources.getString(R.string.swipe_action_move)
fun getIcon(item: MessageListItem, action: SwipeAction): Drawable {
return when (action) {
SwipeAction.None -> null
SwipeAction.None -> error("action == SwipeAction.None")
SwipeAction.ToggleSelection -> selectIcon
SwipeAction.ToggleRead -> if (item.isRead) markAsUnreadIcon else markAsReadIcon
SwipeAction.ToggleStar -> if (item.isStarred) removeStarIcon else addStarIcon
@ -55,6 +66,19 @@ class SwipeResourceProvider(val theme: Theme) {
SwipeAction.Move -> moveColor
}
}
fun getActionName(item: MessageListItem, action: SwipeAction, isSelected: Boolean): String {
return when (action) {
SwipeAction.None -> error("action == SwipeAction.None")
SwipeAction.ToggleSelection -> if (isSelected) deselectText else selectText
SwipeAction.ToggleRead -> if (item.isRead) markAsUnreadText else markAsReadText
SwipeAction.ToggleStar -> if (item.isStarred) removeStarText else addStarText
SwipeAction.Archive -> archiveText
SwipeAction.Delete -> deleteText
SwipeAction.Spam -> spamText
SwipeAction.Move -> moveText
}
}
}
private fun Theme.loadDrawable(@AttrRes attributeId: Int): Drawable {

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:background="?attr/messageListSwipeSelectBackgroundColor"
tools:ignore="RtlHardcoded"
tools:layout_height="?android:listPreferredItemHeight">
<ImageView
android:id="@+id/swipe_action_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginRight="@dimen/messageListSwipeIconPadding"
android:layout_marginBottom="16dp"
android:contentDescription="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="?attr/messageListSwipeMarkAsReadIcon"
tools:tint="?attr/messageListSwipeIconTint" />
<TextView
android:id="@+id/swipe_action_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginRight="@dimen/messageListSwipeTextPadding"
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:gravity="right"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="?attr/messageListSwipeIconTint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/swipe_action_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/swipe_action_mark_as_read" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:background="?attr/messageListSwipeSelectBackgroundColor"
tools:ignore="RtlHardcoded"
tools:layout_height="?android:listPreferredItemHeight">
<ImageView
android:id="@+id/swipe_action_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/messageListSwipeIconPadding"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="?attr/messageListSwipeSelectIcon"
tools:tint="?attr/messageListSwipeIconTint" />
<TextView
android:id="@+id/swipe_action_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/messageListSwipeTextPadding"
android:layout_marginTop="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:gravity="left"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="?attr/messageListSwipeIconTint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toRightOf="@+id/swipe_action_icon"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/swipe_action_select" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -11,4 +11,5 @@
<dimen name="message_view_pager_page_margin">16dp</dimen>
<dimen name="messageListSwipeThreshold">72dp</dimen>
<dimen name="messageListSwipeIconPadding">24dp</dimen>
<dimen name="messageListSwipeTextPadding">12dp</dimen>
</resources>

View file

@ -1279,4 +1279,25 @@ You can keep this message and use it as a backup for your secret key. If you wan
<string name="push_info_configure_notification_action">Configure notification</string>
<string name="push_info_disable_push_text">If you don\'t need instant notifications about new messages, you should disable Push and use Polling. Polling checks for new mail at regular intervals and does not need the notification.</string>
<string name="push_info_disable_push_action">Disable Push</string>
<!-- Name of the swipe action to select a message. Try to keep it short. -->
<string name="swipe_action_select">Select</string>
<!-- Name of the swipe action to deselect a message. Try to keep it short. -->
<string name="swipe_action_deselect">Deselect</string>
<!-- Name of the swipe action to mark a message as read. Try to keep it short. -->
<string name="swipe_action_mark_as_read">Mark read</string>
<!-- Name of the swipe action to mark a message as unread. Try to keep it short. -->
<string name="swipe_action_mark_as_unread">Mark unread</string>
<!-- Name of the swipe action to add the star to a message. Try to keep it short. -->
<string name="swipe_action_add_star">Add star</string>
<!-- Name of the swipe action to remove the star from a message. Try to keep it short. -->
<string name="swipe_action_remove_star">Remove star</string>
<!-- Name of the swipe action to archive a message. Try to keep it short. -->
<string name="swipe_action_archive">Archive</string>
<!-- Name of the swipe action to delete a message. Try to keep it short. -->
<string name="swipe_action_delete">Delete</string>
<!-- Name of the swipe action to move a message to the Spam folder. Try to keep it short. -->
<string name="swipe_action_spam">Spam</string>
<!-- Name of the swipe action to move a message. The ellipsis (…) indicates that there is another step (selecting a folder) before the action is performed. Try to keep it short. -->
<string name="swipe_action_move">Move…</string>
</resources>

View file

@ -94,7 +94,7 @@
<item name="messageListSwipeIconTint">#ffffff</item>
<item name="messageListSwipeDisabledBackgroundColor">@color/material_gray_200</item>
<item name="messageListSwipeSelectIcon">@drawable/ic_import_status</item>
<item name="messageListSwipeSelectIcon">@drawable/ic_check_circle</item>
<item name="messageListSwipeSelectBackgroundColor">@color/material_blue_600</item>
<item name="messageListSwipeMarkAsReadIcon">?attr/iconActionMarkAsRead</item>
<item name="messageListSwipeMarkAsUnreadIcon">?attr/iconActionMarkAsUnread</item>
@ -238,7 +238,7 @@
<item name="messageListSwipeIconTint">#ffffff</item>
<item name="messageListSwipeDisabledBackgroundColor">@color/material_gray_900</item>
<item name="messageListSwipeSelectIcon">@drawable/ic_import_status</item>
<item name="messageListSwipeSelectIcon">@drawable/ic_check_circle</item>
<item name="messageListSwipeSelectBackgroundColor">@color/material_blue_700</item>
<item name="messageListSwipeMarkAsReadIcon">?attr/iconActionMarkAsRead</item>
<item name="messageListSwipeMarkAsUnreadIcon">?attr/iconActionMarkAsUnread</item>