Merge pull request #6456 from thundernest/swipe_action_text
Swipe actions: Display action name next to the icon
This commit is contained in:
commit
14693afd94
9 changed files with 220 additions and 72 deletions
|
@ -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
|
||||
|
|
|
@ -264,7 +264,7 @@ class MessageListFragment :
|
|||
|
||||
val itemTouchHelper = ItemTouchHelper(
|
||||
MessageListSwipeCallback(
|
||||
resources,
|
||||
requireContext(),
|
||||
resourceProvider = SwipeResourceProvider(requireActivity().theme),
|
||||
swipeActionSupportProvider,
|
||||
swipeRightAction = K9.swipeRightAction,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
47
app/ui/legacy/src/main/res/layout/swipe_left_action.xml
Normal file
47
app/ui/legacy/src/main/res/layout/swipe_left_action.xml
Normal 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>
|
47
app/ui/legacy/src/main/res/layout/swipe_right_action.xml
Normal file
47
app/ui/legacy/src/main/res/layout/swipe_right_action.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue