Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 14693afd authored by cketti's avatar cketti Committed by GitHub
Browse files

Merge pull request #6456 from thundernest/swipe_action_text

Swipe actions: Display action name next to the icon
parents 154b526f 647340e1
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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
+1 −1
Original line number Diff line number Diff line
@@ -264,7 +264,7 @@ class MessageListFragment :

        val itemTouchHelper = ItemTouchHelper(
            MessageListSwipeCallback(
                resources,
                requireContext(),
                resourceProvider = SwipeResourceProvider(requireActivity().theme),
                swipeActionSupportProvider,
                swipeRightAction = K9.swipeRightAction,
+72 −68
Original line number Diff line number Diff line
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 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)
        } else {
            drawSwipeLeftIcon(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

    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)
        val swipeLayout = if (swipeRight) swipeRightLayout else swipeLeftLayout
        val swipeAction = if (swipeRight) swipeRightAction else swipeLeftAction

            val iconTint = if (swipeThresholdReached) {
                resourceProvider.iconTint
        val foregroundColor: Int
        val backgroundColor: Int
        if (swipeThresholdReached) {
            foregroundColor = resourceProvider.iconTint
            backgroundColor = resourceProvider.getBackgroundColor(swipeAction)
        } else {
                resourceProvider.getBackgroundColor(swipeRightAction)
            foregroundColor = resourceProvider.getBackgroundColor(swipeAction)
            backgroundColor = resourceProvider.getBackgroundColor(SwipeAction.None)
        }
            icon.setTint(iconTint)

            icon.draw(this)
        }
    }
        swipeLayout.setBackgroundColor(backgroundColor)

    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 icon = resourceProvider.getIcon(item, swipeAction)
        icon.setTint(foregroundColor)

            val iconTint = if (swipeThresholdReached) {
                resourceProvider.iconTint
            } else {
                resourceProvider.getBackgroundColor(swipeLeftAction)
            }
            icon.setTint(iconTint)
        val iconView = swipeLayout.findViewById<ImageView>(R.id.swipe_action_icon)
        iconView.setImageDrawable(icon)

            icon.draw(this)
        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)
        }

        swipeLayout.draw(this)
    }
}

+26 −2
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
<?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>
Loading