Merge pull request #6462 from thundernest/limit_swipe_distance
Swipe actions: Limit how far list items can be dragged
This commit is contained in:
commit
e5f5744186
10 changed files with 2712 additions and 21 deletions
|
@ -1,12 +1,12 @@
|
||||||
package com.fsck.k9
|
package com.fsck.k9
|
||||||
|
|
||||||
enum class SwipeAction {
|
enum class SwipeAction(val removesItem: Boolean) {
|
||||||
None,
|
None(removesItem = false),
|
||||||
ToggleSelection,
|
ToggleSelection(removesItem = false),
|
||||||
ToggleRead,
|
ToggleRead(removesItem = false),
|
||||||
ToggleStar,
|
ToggleStar(removesItem = false),
|
||||||
Archive,
|
Archive(removesItem = true),
|
||||||
Delete,
|
Delete(removesItem = true),
|
||||||
Spam,
|
Spam(removesItem = true),
|
||||||
Move
|
Move(removesItem = true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ dependencies {
|
||||||
implementation "com.takisoft.preferencex:preferencex-colorpicker:${versions.preferencesFix}"
|
implementation "com.takisoft.preferencex:preferencex-colorpicker:${versions.preferencesFix}"
|
||||||
implementation "androidx.recyclerview:recyclerview:${versions.androidxRecyclerView}"
|
implementation "androidx.recyclerview:recyclerview:${versions.androidxRecyclerView}"
|
||||||
implementation project(':ui-utils:LinearLayoutManager')
|
implementation project(':ui-utils:LinearLayoutManager')
|
||||||
|
implementation project(':ui-utils:ItemTouchHelper')
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidxLifecycle}"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.androidxLifecycle}"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidxLifecycle}"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidxLifecycle}"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}"
|
||||||
|
|
|
@ -15,9 +15,9 @@ import android.widget.Toast
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import app.k9mail.ui.utils.itemtouchhelper.ItemTouchHelper
|
||||||
import app.k9mail.ui.utils.linearlayoutmanager.LinearLayoutManager
|
import app.k9mail.ui.utils.linearlayoutmanager.LinearLayoutManager
|
||||||
import com.fsck.k9.Account
|
import com.fsck.k9.Account
|
||||||
import com.fsck.k9.Account.Expunge
|
import com.fsck.k9.Account.Expunge
|
||||||
|
|
|
@ -10,9 +10,9 @@ import android.view.View.MeasureSpec
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.graphics.withTranslation
|
import androidx.core.graphics.withTranslation
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
import app.k9mail.ui.utils.itemtouchhelper.ItemTouchHelper
|
||||||
import com.fsck.k9.SwipeAction
|
import com.fsck.k9.SwipeAction
|
||||||
import com.fsck.k9.ui.R
|
import com.fsck.k9.ui.R
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -27,12 +27,16 @@ class MessageListSwipeCallback(
|
||||||
private val adapter: MessageListAdapter,
|
private val adapter: MessageListAdapter,
|
||||||
private val listener: MessageListSwipeListener
|
private val listener: MessageListSwipeListener
|
||||||
) : ItemTouchHelper.Callback() {
|
) : ItemTouchHelper.Callback() {
|
||||||
|
private val swipePadding = context.resources.getDimension(R.dimen.messageListSwipeIconPadding).toInt()
|
||||||
private val swipeThreshold = context.resources.getDimension(R.dimen.messageListSwipeThreshold)
|
private val swipeThreshold = context.resources.getDimension(R.dimen.messageListSwipeThreshold)
|
||||||
private val backgroundColorPaint = Paint()
|
private val backgroundColorPaint = Paint()
|
||||||
|
|
||||||
private val swipeRightLayout: View
|
private val swipeRightLayout: View
|
||||||
private val swipeLeftLayout: View
|
private val swipeLeftLayout: View
|
||||||
|
|
||||||
|
private var maxSwipeRightDistance: Int = -1
|
||||||
|
private var maxSwipeLeftDistance: Int = -1
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val layoutInflater = LayoutInflater.from(context)
|
val layoutInflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
|
@ -101,14 +105,16 @@ class MessageListSwipeCallback(
|
||||||
val viewWidth = view.width
|
val viewWidth = view.width
|
||||||
val viewHeight = view.height
|
val viewHeight = view.height
|
||||||
|
|
||||||
val isViewAnimatingBack = !isCurrentlyActive && abs(dX).toInt() >= viewWidth
|
val isViewAnimatingBack = !isCurrentlyActive
|
||||||
|
|
||||||
canvas.withTranslation(x = view.left.toFloat(), y = view.top.toFloat()) {
|
if (dX != 0F) {
|
||||||
if (isViewAnimatingBack) {
|
canvas.withTranslation(x = view.left.toFloat(), y = view.top.toFloat()) {
|
||||||
drawBackground(dX, viewWidth, viewHeight)
|
if (isViewAnimatingBack) {
|
||||||
} else {
|
drawBackground(dX, viewWidth, viewHeight)
|
||||||
val holder = viewHolder as MessageViewHolder
|
} else {
|
||||||
drawLayout(dX, viewWidth, viewHeight, holder)
|
val holder = viewHolder as MessageViewHolder
|
||||||
|
drawLayout(dX, viewWidth, viewHeight, holder)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,10 +172,42 @@ class MessageListSwipeCallback(
|
||||||
val heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
val heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
||||||
swipeLayout.measure(widthMeasureSpec, heightMeasureSpec)
|
swipeLayout.measure(widthMeasureSpec, heightMeasureSpec)
|
||||||
swipeLayout.layout(0, 0, width, height)
|
swipeLayout.layout(0, 0, width, height)
|
||||||
|
|
||||||
|
if (swipeRight) {
|
||||||
|
maxSwipeRightDistance = textView.right + swipePadding
|
||||||
|
} else {
|
||||||
|
maxSwipeLeftDistance = swipeLayout.width - textView.left + swipePadding
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
swipeLayout.draw(this)
|
swipeLayout.draw(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getMaxSwipeDistance(recyclerView: RecyclerView, direction: Int): Int {
|
||||||
|
return when (direction) {
|
||||||
|
ItemTouchHelper.RIGHT -> if (maxSwipeRightDistance > 0) maxSwipeRightDistance else recyclerView.width
|
||||||
|
ItemTouchHelper.LEFT -> if (maxSwipeLeftDistance > 0) maxSwipeLeftDistance else recyclerView.width
|
||||||
|
else -> recyclerView.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldAnimateOut(direction: Int): Boolean {
|
||||||
|
return when (direction) {
|
||||||
|
ItemTouchHelper.RIGHT -> swipeRightAction.removesItem
|
||||||
|
ItemTouchHelper.LEFT -> swipeLeftAction.removesItem
|
||||||
|
else -> error("Unsupported direction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAnimationDuration(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
animationType: Int,
|
||||||
|
animateDx: Float,
|
||||||
|
animateDy: Float
|
||||||
|
): Long {
|
||||||
|
val percentage = abs(animateDx) / recyclerView.width
|
||||||
|
return (super.getAnimationDuration(recyclerView, animationType, animateDx, animateDy) * percentage).toLong()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun interface SwipeActionSupportProvider {
|
fun interface SwipeActionSupportProvider {
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/swipe_action_text"
|
android:id="@+id/swipe_action_text"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="16dp"
|
android:layout_marginLeft="16dp"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
|
@ -37,8 +37,9 @@
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
android:textColor="?attr/messageListSwipeIconTint"
|
android:textColor="?attr/messageListSwipeIconTint"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="1.0"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toLeftOf="@+id/swipe_action_icon"
|
app:layout_constraintRight_toLeftOf="@+id/swipe_action_icon"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/swipe_action_text"
|
android:id="@+id/swipe_action_text"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginLeft="@dimen/messageListSwipeTextPadding"
|
android:layout_marginLeft="@dimen/messageListSwipeTextPadding"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
|
@ -37,6 +37,7 @@
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
android:textColor="?attr/messageListSwipeIconTint"
|
android:textColor="?attr/messageListSwipeIconTint"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
app:layout_constraintLeft_toRightOf="@+id/swipe_action_icon"
|
app:layout_constraintLeft_toRightOf="@+id/swipe_action_icon"
|
||||||
|
|
|
@ -13,6 +13,7 @@ include ':app:autodiscovery:srvrecords'
|
||||||
include ':app:autodiscovery:thunderbird'
|
include ':app:autodiscovery:thunderbird'
|
||||||
include ':app:html-cleaner'
|
include ':app:html-cleaner'
|
||||||
include ':ui-utils:LinearLayoutManager'
|
include ':ui-utils:LinearLayoutManager'
|
||||||
|
include ':ui-utils:ItemTouchHelper'
|
||||||
include ':mail:common'
|
include ':mail:common'
|
||||||
include ':mail:testing'
|
include ':mail:testing'
|
||||||
include ':mail:protocols:imap'
|
include ':mail:protocols:imap'
|
||||||
|
|
27
ui-utils/ItemTouchHelper/build.gradle
Normal file
27
ui-utils/ItemTouchHelper/build.gradle
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api "androidx.recyclerview:recyclerview:${versions.androidxRecyclerView}"
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'app.k9mail.ui.utils.itemtouchhelper'
|
||||||
|
|
||||||
|
compileSdkVersion buildConfig.compileSdk
|
||||||
|
buildToolsVersion buildConfig.buildTools
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion buildConfig.minSdk
|
||||||
|
targetSdkVersion buildConfig.robolectricSdk
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
lintConfig file("$rootProject.projectDir/config/lint/lint.xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility javaVersion
|
||||||
|
targetCompatibility javaVersion
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package app.k9mail.ui.utils.itemtouchhelper;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.recyclerview.R;
|
||||||
|
import androidx.recyclerview.widget.ItemTouchUIUtil;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Package private class to keep implementations. Putting them inside ItemTouchUIUtil makes them
|
||||||
|
* public API, which is not desired in this case.
|
||||||
|
*/
|
||||||
|
class ItemTouchUIUtilImpl implements ItemTouchUIUtil {
|
||||||
|
static final ItemTouchUIUtil INSTANCE = new ItemTouchUIUtilImpl();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
|
||||||
|
int actionState, boolean isCurrentlyActive) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
if (isCurrentlyActive) {
|
||||||
|
Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
|
||||||
|
if (originalElevation == null) {
|
||||||
|
originalElevation = ViewCompat.getElevation(view);
|
||||||
|
float newElevation = 1f + findMaxElevation(recyclerView, view);
|
||||||
|
ViewCompat.setElevation(view, newElevation);
|
||||||
|
view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.setTranslationX(dX);
|
||||||
|
view.setTranslationY(dY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float findMaxElevation(RecyclerView recyclerView, View itemView) {
|
||||||
|
final int childCount = recyclerView.getChildCount();
|
||||||
|
float max = 0;
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
final View child = recyclerView.getChildAt(i);
|
||||||
|
if (child == itemView) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final float elevation = ViewCompat.getElevation(child);
|
||||||
|
if (elevation > max) {
|
||||||
|
max = elevation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDrawOver(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
|
||||||
|
int actionState, boolean isCurrentlyActive) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearView(View view) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);
|
||||||
|
if (tag instanceof Float) {
|
||||||
|
ViewCompat.setElevation(view, (Float) tag);
|
||||||
|
}
|
||||||
|
view.setTag(R.id.item_touch_helper_previous_elevation, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
view.setTranslationX(0f);
|
||||||
|
view.setTranslationY(0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSelected(View view) {
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue