├── example
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── drawable-nodpi
│ │ │ ├── may.jpg
│ │ │ ├── april.jpg
│ │ │ ├── august.jpg
│ │ │ ├── husky.jpg
│ │ │ ├── july.jpg
│ │ │ ├── june.jpg
│ │ │ ├── march.jpg
│ │ │ ├── december.jpg
│ │ │ ├── february.jpg
│ │ │ ├── january.jpg
│ │ │ ├── november.jpg
│ │ │ ├── october.jpg
│ │ │ ├── september.jpg
│ │ │ ├── ic_empty_list.png
│ │ │ └── gesture_recycler_big.png
│ │ ├── drawable-hdpi
│ │ │ ├── ic_ok.png
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_fingerprint.png
│ │ ├── drawable-mdpi
│ │ │ ├── ic_ok.png
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_fingerprint.png
│ │ ├── drawable-xhdpi
│ │ │ ├── ic_ok.png
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_fingerprint.png
│ │ ├── drawable-xxhdpi
│ │ │ ├── ic_ok.png
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_fingerprint.png
│ │ ├── values
│ │ │ ├── integers.xml
│ │ │ ├── colors.xml
│ │ │ ├── styles.xml
│ │ │ ├── strings.xml
│ │ │ └── dimens.xml
│ │ ├── drawable
│ │ │ └── rectangle_background.xml
│ │ ├── menu
│ │ │ ├── recycler_menu.xml
│ │ │ └── recycler_empty_menu.xml
│ │ └── layout
│ │ │ ├── activity_recycler.xml
│ │ │ ├── footer_item.xml
│ │ │ ├── header_item.xml
│ │ │ ├── first_background_item.xml
│ │ │ ├── second_background_item.xml
│ │ │ ├── month_header_item.xml
│ │ │ ├── fragment_recycler.xml
│ │ │ ├── grid_item.xml
│ │ │ ├── activity_start.xml
│ │ │ ├── linear_item.xml
│ │ │ └── linear_item_with_background.xml
│ │ ├── java
│ │ └── com
│ │ │ └── thesurix
│ │ │ └── example
│ │ │ └── gesturerecycler
│ │ │ ├── model
│ │ │ ├── MonthHeader.kt
│ │ │ ├── MonthItem.kt
│ │ │ └── Month.kt
│ │ │ ├── adapter
│ │ │ ├── GridItemViewHolder.kt
│ │ │ ├── MonthHeaderViewHolder.kt
│ │ │ ├── LinearItemViewHolder.kt
│ │ │ ├── LinearItemWithBackgroundViewHolder.kt
│ │ │ ├── MonthsAdapter.kt
│ │ │ └── BaseMonthViewHolder.kt
│ │ │ ├── callback
│ │ │ └── MonthDiffCallback.kt
│ │ │ ├── StartActivity.kt
│ │ │ ├── RecyclerActivity.kt
│ │ │ └── fragment
│ │ │ ├── BaseFragment.kt
│ │ │ ├── LinearRecyclerFragment.kt
│ │ │ ├── GridRecyclerFragment.kt
│ │ │ └── EmptyViewFragment.kt
│ │ └── AndroidManifest.xml
└── build.gradle
├── library
├── .gitignore
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── res
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── thesurix
│ │ │ └── gesturerecycler
│ │ │ ├── transactions
│ │ │ ├── Transaction.kt
│ │ │ ├── Transactional.kt
│ │ │ ├── RevertReorderTransaction.kt
│ │ │ ├── AddTransaction.kt
│ │ │ ├── RemoveTransaction.kt
│ │ │ ├── SwapTransaction.kt
│ │ │ ├── InsertTransaction.kt
│ │ │ └── MoveTransaction.kt
│ │ │ ├── util
│ │ │ ├── FixedSizeArrayDequeue.kt
│ │ │ └── extensions.kt
│ │ │ ├── GestureListener.kt
│ │ │ ├── DefaultItemClickListener.kt
│ │ │ ├── EmptyViewDataObserver.kt
│ │ │ ├── GestureViewHolder.kt
│ │ │ ├── LayoutFlags.kt
│ │ │ ├── RecyclerItemTouchListener.kt
│ │ │ ├── GestureTouchHelperCallback.kt
│ │ │ ├── GestureManager.kt
│ │ │ └── GestureAdapter.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── thesurix
│ │ └── gesturerecycler
│ │ ├── BaseTransactionTest.kt
│ │ ├── MoveTransactionTest.kt
│ │ ├── RevertReorderTransactionTest.kt
│ │ ├── AddTransactionTest.kt
│ │ ├── RemoveTransactionTest.kt
│ │ ├── InsertTransactionTest.kt
│ │ └── SwapTransactionTest.kt
├── lib-proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── LICENSE.txt
├── dependencies.gradle
├── gradle.properties
├── gradlew.bat
├── publish-mavencentral.gradle
├── gradlew
└── README.md
/example/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':example', ':library'
2 |
3 | project(':library').name = 'gesture-recycler'
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Gesture Recycler Library
3 |
4 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/may.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/may.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-hdpi/ic_ok.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-hdpi/ic_ok.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-mdpi/ic_ok.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-mdpi/ic_ok.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/april.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/april.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/august.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/august.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/husky.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/husky.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/july.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/july.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/june.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/june.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/march.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/march.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-xhdpi/ic_ok.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-xhdpi/ic_ok.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-xxhdpi/ic_ok.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-xxhdpi/ic_ok.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/december.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/december.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/february.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/february.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/january.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/january.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/november.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/november.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/october.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/october.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/september.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/september.jpg
--------------------------------------------------------------------------------
/example/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-hdpi/ic_fingerprint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-hdpi/ic_fingerprint.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-mdpi/ic_fingerprint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-mdpi/ic_fingerprint.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/ic_empty_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/ic_empty_list.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/values/integers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 250
4 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable-xhdpi/ic_fingerprint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-xhdpi/ic_fingerprint.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-xxhdpi/ic_fingerprint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-xxhdpi/ic_fingerprint.png
--------------------------------------------------------------------------------
/example/src/main/res/drawable-nodpi/gesture_recycler_big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thesurix/gesture-recycler/HEAD/example/src/main/res/drawable-nodpi/gesture_recycler_big.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Feb 17 23:18:17 CET 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/model/MonthHeader.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.model
2 |
3 | class MonthHeader(override val name: String) : MonthItem {
4 |
5 | override val type: MonthItem.MonthItemType
6 | get() = MonthItem.MonthItemType.HEADER
7 | }
8 |
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/model/MonthItem.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.model
2 |
3 | interface MonthItem {
4 |
5 | val type: MonthItemType
6 |
7 | val name: String
8 |
9 | enum class MonthItemType {
10 | HEADER, MONTH
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/rectangle_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/transactions/Transaction.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler.transactions
2 |
3 | /**
4 | * @author thesurix
5 | */
6 | interface Transaction {
7 | fun perform(transactional: Transactional): Boolean
8 | fun revert(transactional: Transactional): Boolean
9 | }
--------------------------------------------------------------------------------
/example/src/main/res/menu/recycler_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/model/Month.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.model
2 |
3 |
4 | import androidx.annotation.DrawableRes
5 |
6 | class Month(override val name: String, @param:DrawableRes val drawableId: Int) : MonthItem {
7 |
8 | override val type: MonthItem.MonthItemType
9 | get() = MonthItem.MonthItemType.MONTH
10 | }
11 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/activity_recycler.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/transactions/Transactional.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler.transactions
2 |
3 | /**
4 | * @author thesurix
5 | */
6 | interface Transactional {
7 | val data: MutableList
8 | fun notifyChanged(position: Int)
9 | fun notifyInserted(position: Int)
10 | fun notifyRemoved(position: Int)
11 | fun notifyMoved(fromPosition: Int, toPosition: Int)
12 | }
--------------------------------------------------------------------------------
/example/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 | #3f51b5
8 | #e91e63
9 | #f44336
10 | #4caf50
11 |
12 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/util/FixedSizeArrayDequeue.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler.util
2 |
3 | import java.util.*
4 |
5 | /**
6 | * @author thesurix
7 | */
8 | class FixedSizeArrayDequeue(private val maxSize: Int) : ArrayDeque(maxSize) {
9 |
10 | override fun offer(e: E): Boolean {
11 | if (size == maxSize) {
12 | removeFirst()
13 | }
14 |
15 | return super.offer(e)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/GestureListener.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 |
4 | import androidx.recyclerview.widget.ItemTouchHelper
5 |
6 | /**
7 | * Default gesture listener that handles manual spawned drag gestures.
8 | * @author thesurix
9 | */
10 | class GestureListener(private val touchHelper: ItemTouchHelper) : GestureAdapter.OnGestureListener {
11 |
12 | override fun onStartDrag(viewHolder: GestureViewHolder) {
13 | touchHelper.startDrag(viewHolder)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/footer_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/header_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 |
15 | # Gradle files
16 | .gradle/
17 | build/
18 |
19 | # Local configuration file (sdk path, etc)
20 | local.properties
21 |
22 | # Proguard folder generated by Eclipse
23 | proguard/
24 |
25 | # Log Files
26 | *.log
27 |
28 | # Android Studio Navigation editor temp files
29 | .navigation/
30 |
31 | # Android Studio captures folder
32 | captures/
33 |
34 |
35 | *.iml
36 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2020 thesurix
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/DefaultItemClickListener.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | /**
4 | * Default implementation of the [RecyclerItemTouchListener.ItemClickListener].
5 | * @author thesurix
6 | */
7 | open class DefaultItemClickListener : RecyclerItemTouchListener.ItemClickListener {
8 |
9 | override fun onItemClick(item: T, position: Int): Boolean {
10 | return false
11 | }
12 |
13 | override fun onItemLongPress(item: T, position: Int) {
14 | }
15 |
16 | override fun onDoubleTap(item: T, position: Int): Boolean {
17 | return false
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/util/extensions.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler.util
2 |
3 | /**
4 | * Swaps two items inside [MutableList]
5 | * @param firstIndex first index to swap
6 | * @param secondIndex second index to swap
7 | */
8 | fun MutableList.swap(firstIndex: Int, secondIndex: Int) {
9 | val tmp = this[firstIndex]
10 | this[firstIndex] = this[secondIndex]
11 | this[secondIndex] = tmp
12 | }
13 |
14 | /**
15 | * Returns data offset based on header state.
16 | * @param headerEnabled header flag
17 | * @return data offset
18 | */
19 | fun getDataOffset(headerEnabled: Boolean) = if (headerEnabled) -1 else 0
--------------------------------------------------------------------------------
/library/src/test/java/com/thesurix/gesturerecycler/BaseTransactionTest.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import com.thesurix.gesturerecycler.transactions.Transactional
4 | import org.junit.Before
5 | import org.junit.runner.RunWith
6 | import org.mockito.Mock
7 | import org.mockito.Mockito
8 | import org.mockito.junit.MockitoJUnitRunner
9 |
10 | @RunWith(MockitoJUnitRunner::class)
11 | abstract class BaseTransactionTest {
12 |
13 | @Mock
14 | lateinit var transactional: Transactional
15 |
16 | val data = mutableListOf("A", "B", "C", "D", "E")
17 |
18 | @Before
19 | fun setUp() {
20 | Mockito.`when`(transactional.data).thenReturn(data)
21 | }
22 | }
--------------------------------------------------------------------------------
/library/lib-proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /home/thesurix/Android/Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/adapter/GridItemViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.adapter
2 |
3 | import android.view.View
4 | import android.view.ViewStub
5 | import android.widget.ImageView
6 | import android.widget.TextView
7 | import com.thesurix.example.gesturerecycler.databinding.GridItemBinding
8 |
9 | class GridItemViewHolder (private val binding: GridItemBinding) : BaseMonthViewHolder(binding.root) {
10 | override val monthText: TextView
11 | get() = binding.monthText
12 | override val monthPicture: ImageView
13 | get() = binding.monthImage
14 | override val itemDrag: ImageView
15 | get() = binding.monthDrag
16 | override val foreground: View?
17 | get() = null
18 | }
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/adapter/MonthHeaderViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.adapter
2 |
3 | import android.widget.TextView
4 | import com.thesurix.example.gesturerecycler.databinding.MonthHeaderItemBinding
5 | import com.thesurix.example.gesturerecycler.model.MonthItem
6 | import com.thesurix.gesturerecycler.GestureViewHolder
7 |
8 |
9 | class MonthHeaderViewHolder(binding: MonthHeaderItemBinding) : GestureViewHolder(binding.root) {
10 |
11 | private val headerText: TextView = binding.headerText
12 |
13 | override fun canDrag() = false
14 |
15 | override fun canSwipe() = false
16 |
17 | override fun bind(item: MonthItem) {
18 | headerText.text = item.name
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/adapter/LinearItemViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.adapter
2 |
3 | import android.view.View
4 | import android.view.ViewStub
5 | import android.widget.ImageView
6 | import android.widget.TextView
7 | import com.thesurix.example.gesturerecycler.databinding.LinearItemBinding
8 |
9 | class LinearItemViewHolder (private val binding: LinearItemBinding) : BaseMonthViewHolder(binding.root) {
10 | override val monthText: TextView
11 | get() = binding.monthText
12 | override val monthPicture: ImageView
13 | get() = binding.monthImage
14 | override val itemDrag: ImageView
15 | get() = binding.monthDrag
16 | override val foreground: View?
17 | get() = null
18 | }
--------------------------------------------------------------------------------
/example/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/first_background_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/second_background_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
--------------------------------------------------------------------------------
/example/src/main/res/menu/recycler_empty_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/example/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Gesture Recycler
3 |
4 | Linear Recycler
5 | Linear Empty View
6 | Grid Recycler
7 | No data
8 | Undo
9 |
10 |
11 | Enable/Disable drag
12 | Add data
13 | Clear data
14 | Undo last
15 | Diff data
16 | Toggle footer/header
17 |
18 | HEADER
19 | FOOTER
20 |
21 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/transactions/RevertReorderTransaction.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler.transactions
2 |
3 |
4 | /**
5 | * @author thesurix
6 | */
7 | class RevertReorderTransaction(private val from: Int,
8 | private val to: Int,
9 | private val headerEnabled: Boolean) : Transaction {
10 |
11 | override fun perform(transactional: Transactional) = false
12 |
13 | override fun revert(transactional: Transactional): Boolean {
14 | return with(transactional.data) {
15 | val item = removeAt(to)
16 | item?.let {
17 | transactional.notifyRemoved(to + if (headerEnabled) 1 else 0)
18 | add(from, it)
19 | transactional.notifyInserted(from + if (headerEnabled) 1 else 0)
20 | true
21 | } ?: false
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/month_header_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
16 |
17 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/dependencies.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | junit_version = '4.13.2'
3 | mockito_version = '4.3.1'
4 |
5 | exampleDependencies = [
6 | glide : 'com.github.bumptech.glide:glide:4.13.0',
7 | glideCompiler : 'com.github.bumptech.glide:compiler:4.13.0'
8 | ]
9 |
10 | supportDependencies = [
11 | design : "com.google.android.material:material:1.5.0",
12 | recyclerView : "androidx.recyclerview:recyclerview:1.2.1",
13 | cardView : "androidx.cardview:cardview:1.0.0",
14 | appCompat : "androidx.appcompat:appcompat:1.4.1",
15 | ]
16 |
17 | langDependencies = [
18 | kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
19 | ]
20 |
21 | testDependencies = [
22 | junit : "junit:junit:$junit_version",
23 | mockito : "org.mockito:mockito-core:$mockito_version",
24 | junitKotlin : "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
25 | ]
26 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/transactions/AddTransaction.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler.transactions
2 |
3 |
4 | /**
5 | * @author thesurix
6 | */
7 | class AddTransaction(private val item: T,
8 | private val headerEnabled: Boolean) : Transaction {
9 |
10 | override fun perform(transactional: Transactional): Boolean {
11 | return with(transactional.data) {
12 | val success = add(item)
13 | if (success) {
14 | transactional.notifyInserted(if (headerEnabled) size else size - 1)
15 | }
16 | success
17 | }
18 | }
19 |
20 | override fun revert(transactional: Transactional): Boolean {
21 | return with(transactional.data) {
22 | val item = removeAt(size - 1)
23 | item?.let {
24 | transactional.notifyRemoved(if (headerEnabled) size + 1 else size)
25 | true
26 | } ?: false
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/example/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 |
5 | android {
6 | compileSdkVersion 31
7 | buildToolsVersion buildTools
8 |
9 | defaultConfig {
10 | applicationId "com.thesurix.example.gesturerecycler"
11 | minSdkVersion project.getProperty("minSdk") as Integer
12 | targetSdkVersion project.getProperty("targetSdk") as Integer
13 | versionCode 1
14 | versionName "1.0"
15 | }
16 | lintOptions {
17 | disable 'InvalidPackage'
18 | }
19 | dataBinding {
20 | enabled = true
21 | }
22 | }
23 |
24 | dependencies {
25 | implementation project(':gesture-recycler')
26 | implementation exampleDependencies.glide
27 | kapt exampleDependencies.glideCompiler
28 | implementation supportDependencies.cardView
29 | implementation supportDependencies.recyclerView
30 | implementation supportDependencies.appCompat
31 | implementation supportDependencies.design
32 | implementation langDependencies.kotlin
33 | }
34 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 | android.useAndroidX=true
20 | android.enableJetifier=true
21 |
22 | minSdk=15
23 | compileSdk=31
24 | targetSdk=31
25 | buildTools=31.0.0
26 | kotlinVersion=1.6.10
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/transactions/RemoveTransaction.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler.transactions
2 |
3 |
4 | /**
5 | * @author thesurix
6 | */
7 | class RemoveTransaction(private val position: Int,
8 | private val headerEnabled: Boolean) : Transaction {
9 | private var item: T? = null
10 |
11 | override fun perform(transactional: Transactional): Boolean {
12 | return with(transactional.data) {
13 | val removedItem = removeAt(position)
14 | removedItem?.let {
15 | item = it
16 | transactional.notifyRemoved(position + if(headerEnabled) 1 else 0)
17 | true
18 | } ?: false
19 | }
20 | }
21 |
22 | override fun revert(transactional: Transactional): Boolean {
23 | return with(transactional.data) {
24 | item?.let {
25 | add(position, it)
26 | transactional.notifyInserted(position + if(headerEnabled) 1 else 0)
27 | true
28 | } ?: false
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/transactions/SwapTransaction.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler.transactions
2 |
3 | import com.thesurix.gesturerecycler.util.swap
4 |
5 | /**
6 | * @author thesurix
7 | */
8 | class SwapTransaction(private val firstIndex: Int,
9 | private val secondIndex: Int,
10 | private val headerEnabled: Boolean) : Transaction {
11 |
12 | override fun perform(transactional: Transactional): Boolean {
13 | transactional.data.swap(firstIndex, secondIndex)
14 | notifyChanged(transactional)
15 | return true
16 | }
17 |
18 | override fun revert(transactional: Transactional): Boolean {
19 | transactional.data.swap(secondIndex, firstIndex)
20 | notifyChanged(transactional)
21 | return true
22 | }
23 |
24 | private fun notifyChanged(transactional: Transactional) {
25 | val changedOffset = + if(headerEnabled) 1 else 0
26 | transactional.notifyChanged(firstIndex + changedOffset)
27 | transactional.notifyChanged(secondIndex + changedOffset)
28 | }
29 | }
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/adapter/LinearItemWithBackgroundViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.adapter
2 |
3 | import android.view.View
4 | import android.view.ViewStub
5 | import android.widget.ImageView
6 | import android.widget.TextView
7 | import androidx.recyclerview.widget.ItemTouchHelper
8 | import com.thesurix.example.gesturerecycler.databinding.LinearItemWithBackgroundBinding
9 |
10 | class LinearItemWithBackgroundViewHolder(private val binding: LinearItemWithBackgroundBinding) : BaseMonthViewHolder(binding.root) {
11 |
12 | override val monthText: TextView
13 | get() = binding.monthText
14 | override val monthPicture: ImageView
15 | get() = binding.monthImage
16 | override val itemDrag: ImageView
17 | get() = binding.monthDrag
18 | override val foreground: View?
19 | get() = binding.foreground
20 |
21 | override fun getBackgroundView(direction: Int): View? {
22 | if (direction == ItemTouchHelper.RIGHT) {
23 | return binding.monthBackgroundTwo
24 | }
25 | return binding.monthBackgroundOne
26 | }
27 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/transactions/InsertTransaction.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler.transactions
2 |
3 |
4 | /**
5 | * @author thesurix
6 | */
7 | class InsertTransaction(private val item: T,
8 | private val position: Int,
9 | private val headerEnabled: Boolean) : Transaction {
10 |
11 | override fun perform(transactional: Transactional): Boolean {
12 | return with(transactional.data) {
13 | add(position, item)
14 | val insertedPosition = position + if (headerEnabled) 1 else 0
15 | transactional.notifyInserted(insertedPosition)
16 | true
17 | }
18 | }
19 |
20 | override fun revert(transactional: Transactional): Boolean {
21 | return with(transactional.data) {
22 | val item = removeAt(position)
23 | item?.let {
24 | val removedPosition = position + if (headerEnabled) 1 else 0
25 | transactional.notifyRemoved(removedPosition)
26 | true
27 | } ?: false
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/callback/MonthDiffCallback.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.callback
2 |
3 |
4 | import androidx.recyclerview.widget.DiffUtil
5 | import com.thesurix.example.gesturerecycler.model.MonthItem
6 |
7 | class MonthDiffCallback(private val mOldList: List, private val mNewList: List) : DiffUtil.Callback() {
8 |
9 | override fun getOldListSize(): Int {
10 | return mOldList.size
11 | }
12 |
13 | override fun getNewListSize(): Int {
14 | return mNewList.size
15 | }
16 |
17 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
18 | return areItemsEqual(oldItemPosition, newItemPosition)
19 | }
20 |
21 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
22 | return areItemsEqual(oldItemPosition, newItemPosition)
23 | }
24 |
25 | private fun areItemsEqual(oldItemPosition: Int, newItemPosition: Int): Boolean {
26 | val oldItem = mOldList[oldItemPosition]
27 | val newItem = mNewList[newItemPosition]
28 |
29 | return oldItem.name == newItem.name
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/example/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | ext {
6 | PUBLISH_GROUP_ID = 'com.github.thesurix'
7 | PUBLISH_VERSION = '1.17.0'
8 | PUBLISH_ARTIFACT_ID = 'gesture-recycler'
9 | }
10 |
11 | apply from: "${rootProject.projectDir}/publish-mavencentral.gradle"
12 |
13 | android {
14 | compileSdkVersion project.getProperty("compileSdk") as Integer
15 | buildToolsVersion buildTools
16 |
17 | defaultConfig {
18 | minSdkVersion project.getProperty("minSdk") as Integer
19 | targetSdkVersion project.getProperty("targetSdk") as Integer
20 | versionCode 1
21 | versionName version
22 | }
23 | buildTypes {
24 | release {
25 | consumerProguardFiles 'lib-proguard-rules.pro'
26 | }
27 | }
28 | }
29 |
30 | dependencies {
31 | implementation supportDependencies.recyclerView
32 | implementation supportDependencies.appCompat
33 | implementation langDependencies.kotlin
34 |
35 | testImplementation testDependencies.junit
36 | testImplementation testDependencies.mockito
37 | testImplementation testDependencies.junitKotlin
38 | }
39 |
40 | tasks.withType(Javadoc).all {
41 | enabled = false
42 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/transactions/MoveTransaction.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler.transactions
2 |
3 | /**
4 | * @author thesurix
5 | */
6 | class MoveTransaction(private val from: Int,
7 | private val to: Int,
8 | private val headerEnabled: Boolean) : Transaction {
9 |
10 | override fun perform(transactional: Transactional): Boolean {
11 | return with(transactional.data) {
12 | val removedItem = removeAt(from)
13 | removedItem?.let {
14 | add(to, it)
15 | val movedOffset = if (headerEnabled) 1 else 0
16 | transactional.notifyMoved(from + movedOffset, to + movedOffset)
17 | true
18 | } ?: false
19 | }
20 | }
21 |
22 | override fun revert(transactional: Transactional): Boolean {
23 | return with(transactional.data) {
24 | val removedItem = removeAt(to)
25 | removedItem?.let {
26 | add(from, it)
27 | val movedOffset = if (headerEnabled) 1 else 0
28 | transactional.notifyMoved(to + movedOffset, from + movedOffset)
29 | true
30 | } ?: false
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/StartActivity.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler
2 |
3 | import android.os.Bundle
4 |
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.databinding.DataBindingUtil
7 | import com.thesurix.example.gesturerecycler.databinding.ActivityStartBinding
8 |
9 | class StartActivity : AppCompatActivity() {
10 |
11 | private lateinit var binding: ActivityStartBinding
12 |
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 | binding = DataBindingUtil.setContentView(this, R.layout.activity_start)
16 |
17 | with(binding) {
18 | startGridBtn.setOnClickListener {
19 | startRecyclerActivity(RecyclerActivity.RecyclerOption.GRID)
20 | }
21 |
22 | startLinearBtn.setOnClickListener {
23 | startRecyclerActivity(RecyclerActivity.RecyclerOption.LINEAR)
24 | }
25 |
26 | startEmptyBtn.setOnClickListener {
27 | startRecyclerActivity(RecyclerActivity.RecyclerOption.EMPTY)
28 | }
29 | }
30 | }
31 |
32 | private fun startRecyclerActivity(option: RecyclerActivity.RecyclerOption) {
33 | startActivity(getIntent(this@StartActivity, option))
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/fragment_recycler.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
18 |
19 |
25 |
26 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/example/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 10dp
4 | 10dp
5 |
6 |
7 | 200dp
8 | 36sp
9 | 80dp
10 | 4dp
11 | 6dp
12 | 4dp
13 |
14 |
15 | 40dp
16 | 10dp
17 |
18 |
19 | 6dp
20 | 2dp
21 |
22 |
23 | 80dp
24 | 80dp
25 | 20dp
26 |
27 |
28 | 180dp
29 | 10dp
30 |
31 |
32 | 52dp
33 | 10dp
34 |
35 |
36 | 48dp
37 | 24sp
38 | 4dp
39 |
40 |
41 |
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/RecyclerActivity.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.appcompat.app.AppCompatActivity
7 | import com.thesurix.example.gesturerecycler.fragment.EmptyViewFragment
8 | import com.thesurix.example.gesturerecycler.fragment.GridRecyclerFragment
9 | import com.thesurix.example.gesturerecycler.fragment.LinearRecyclerFragment
10 |
11 | private const val EXTRA_RECYCLER_OPTION = "recycler_option"
12 |
13 | class RecyclerActivity : AppCompatActivity() {
14 |
15 | enum class RecyclerOption {
16 | LINEAR, GRID, EMPTY
17 | }
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | setContentView(R.layout.activity_recycler)
22 |
23 | val option = this.intent.getIntExtra(EXTRA_RECYCLER_OPTION, RecyclerOption.LINEAR.ordinal)
24 | val fragment = when (option) {
25 | RecyclerOption.GRID.ordinal -> GridRecyclerFragment()
26 | RecyclerOption.EMPTY.ordinal -> EmptyViewFragment()
27 | else -> LinearRecyclerFragment()
28 | }
29 |
30 | supportFragmentManager.beginTransaction().replace(R.id.main_placeholder, fragment).commit()
31 | }
32 | }
33 |
34 | fun getIntent(ctx: Context, option: RecyclerActivity.RecyclerOption): Intent {
35 | val intent = Intent(ctx, RecyclerActivity::class.java)
36 | intent.putExtra(EXTRA_RECYCLER_OPTION, option.ordinal)
37 |
38 | return intent
39 | }
40 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/EmptyViewDataObserver.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import android.view.View
4 | import androidx.recyclerview.widget.RecyclerView
5 |
6 | /**
7 | * Observer class for managing visibility of the adapter's empty view.
8 | */
9 | internal class EmptyViewDataObserver : RecyclerView.AdapterDataObserver() {
10 |
11 | var recyclerView: RecyclerView? = null
12 | set(value) {
13 | field = value
14 | updateEmptyViewState()
15 | }
16 | var emptyView: View? = null
17 | set(value) {
18 | field = value
19 | updateEmptyViewState()
20 | }
21 |
22 | var emptyViewVisibilityListener: EmptyViewVisibilityListener? = null
23 |
24 | override fun onChanged() {
25 | updateEmptyViewState()
26 | }
27 |
28 | override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
29 | updateEmptyViewState()
30 | }
31 |
32 | override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
33 | updateEmptyViewState()
34 | }
35 |
36 | private fun updateEmptyViewState() {
37 | recyclerView?.let {
38 | val noItems = it.adapter?.itemCount == 0
39 | emptyView?.visibility = if (noItems) View.VISIBLE else View.GONE
40 | recyclerView?.visibility = if (noItems) View.GONE else View.VISIBLE
41 | emptyViewVisibilityListener?.onVisibilityChanged(noItems)
42 | }
43 | }
44 | }
45 |
46 | interface EmptyViewVisibilityListener {
47 | fun onVisibilityChanged(visible: Boolean)
48 | }
49 |
--------------------------------------------------------------------------------
/library/src/test/java/com/thesurix/gesturerecycler/MoveTransactionTest.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import com.thesurix.gesturerecycler.transactions.MoveTransaction
4 | import org.junit.Test
5 | import org.mockito.Mockito
6 | import kotlin.test.assertEquals
7 | import kotlin.test.assertTrue
8 |
9 | class MoveTransactionTest : BaseTransactionTest() {
10 |
11 | @Test
12 | fun `move item in transaction without header`() {
13 | val item = transactional.data[1]
14 | val transaction = MoveTransaction(1, 4, false)
15 |
16 | assertTrue(transaction.perform(transactional))
17 | assertEquals(transactional.data[4], item)
18 | Mockito.verify(transactional).notifyMoved(1, 4)
19 | }
20 |
21 | @Test
22 | fun `revert move item in transaction without header`() {
23 | val item = transactional.data[4]
24 | val transaction = MoveTransaction(1, 4, false)
25 |
26 | assertTrue(transaction.revert(transactional))
27 | assertEquals(transactional.data[1], item)
28 | Mockito.verify(transactional).notifyMoved(4, 1)
29 | }
30 |
31 | @Test
32 | fun `move item in transaction with header`() {
33 | val item = transactional.data[1]
34 | val transaction = MoveTransaction(1, 4, true)
35 |
36 | assertTrue(transaction.perform(transactional))
37 | assertEquals(transactional.data[4], item)
38 | Mockito.verify(transactional).notifyMoved(2, 5)
39 | }
40 |
41 | @Test
42 | fun `revert move item in transaction with header`() {
43 | val item = transactional.data[4]
44 | val transaction = MoveTransaction(1, 4, true)
45 |
46 | assertTrue(transaction.revert(transactional))
47 | assertEquals(transactional.data[1], item)
48 | Mockito.verify(transactional).notifyMoved(5, 2)
49 | }
50 | }
--------------------------------------------------------------------------------
/library/src/test/java/com/thesurix/gesturerecycler/RevertReorderTransactionTest.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import com.thesurix.gesturerecycler.transactions.RevertReorderTransaction
4 | import org.junit.Test
5 | import org.mockito.Mockito
6 | import kotlin.test.assertEquals
7 | import kotlin.test.assertFalse
8 | import kotlin.test.assertTrue
9 |
10 | class RevertReorderTransactionTest : BaseTransactionTest() {
11 |
12 | @Test
13 | fun `reorder item in transaction without header`() {
14 | val transaction = RevertReorderTransaction(3, 4, false)
15 |
16 | assertFalse(transaction.perform(transactional))
17 | }
18 |
19 | @Test
20 | fun `revert reorder item in transaction without header`() {
21 | val item1 = transactional.data[3]
22 | val item2 = transactional.data[4]
23 | val transaction = RevertReorderTransaction(3, 4, false)
24 |
25 | assertTrue(transaction.revert(transactional))
26 | assertEquals(transactional.data[3], item2)
27 | assertEquals(transactional.data[4], item1)
28 | Mockito.verify(transactional).notifyRemoved(4)
29 | Mockito.verify(transactional).notifyInserted(3)
30 | }
31 |
32 | @Test
33 | fun `reorder item in transaction with header`() {
34 | val transaction = RevertReorderTransaction(3, 4, true)
35 |
36 | assertFalse(transaction.perform(transactional))
37 | }
38 |
39 | @Test
40 | fun `revert reorder item in transaction with header`() {
41 | val item1 = transactional.data[3]
42 | val item2 = transactional.data[4]
43 | val transaction = RevertReorderTransaction(3, 4, true)
44 |
45 | assertTrue(transaction.revert(transactional))
46 | assertEquals(transactional.data[3], item2)
47 | assertEquals(transactional.data[4], item1)
48 | Mockito.verify(transactional).notifyRemoved(5)
49 | Mockito.verify(transactional).notifyInserted(4)
50 | }
51 | }
--------------------------------------------------------------------------------
/example/src/main/res/layout/grid_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
13 |
14 |
17 |
18 |
22 |
23 |
31 |
32 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/library/src/test/java/com/thesurix/gesturerecycler/AddTransactionTest.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import com.thesurix.gesturerecycler.transactions.AddTransaction
4 | import org.junit.Test
5 | import org.mockito.Mockito
6 | import kotlin.test.assertEquals
7 | import kotlin.test.assertTrue
8 |
9 | class AddTransactionTest : BaseTransactionTest() {
10 |
11 | @Test
12 | fun `add item in transaction without header`() {
13 | val transaction = AddTransaction("F", false)
14 |
15 | assertTrue(transaction.perform(transactional))
16 | assertEquals(transactional.data.last(), "F")
17 | Mockito.verify(transactional).notifyInserted(data.size - 1)
18 | }
19 |
20 | @Test
21 | fun `revert add item in transaction without header`() {
22 | val lastElement = transactional.data.last()
23 | val transaction = AddTransaction("F", false)
24 | transaction.perform(transactional)
25 |
26 | assertEquals(transactional.data.last(), "F")
27 | assertTrue(transaction.revert(transactional))
28 | assertEquals(transactional.data.last(), lastElement)
29 | Mockito.verify(transactional).notifyRemoved(data.size)
30 | }
31 |
32 | @Test
33 | fun `add item in transaction with header`() {
34 | val transaction = AddTransaction("F", true)
35 |
36 | assertTrue(transaction.perform(transactional))
37 | assertEquals(transactional.data.last(), "F")
38 | Mockito.verify(transactional).notifyInserted(data.size)
39 | }
40 |
41 | @Test
42 | fun `revert add item in transaction with header`() {
43 | val lastElement = transactional.data.last()
44 | val transaction = AddTransaction("F", true)
45 | transaction.perform(transactional)
46 |
47 | assertEquals(transactional.data.last(), "F")
48 | assertTrue(transaction.revert(transactional))
49 | assertEquals(transactional.data.last(), lastElement)
50 | Mockito.verify(transactional).notifyRemoved(data.size + 1)
51 | }
52 | }
--------------------------------------------------------------------------------
/example/src/main/res/layout/activity_start.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
18 |
19 |
26 |
27 |
32 |
33 |
38 |
39 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/library/src/test/java/com/thesurix/gesturerecycler/RemoveTransactionTest.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import com.thesurix.gesturerecycler.transactions.RemoveTransaction
4 | import org.junit.Test
5 | import org.mockito.Mockito
6 | import kotlin.test.assertFalse
7 | import kotlin.test.assertTrue
8 |
9 | class RemoveTransactionTest : BaseTransactionTest() {
10 |
11 | @Test
12 | fun `remove item in transaction without header`() {
13 | val item = transactional.data[3]
14 | val transaction = RemoveTransaction(3, false)
15 |
16 | assertTrue(transaction.perform(transactional))
17 | assertFalse(transactional.data.contains(item))
18 | Mockito.verify(transactional).notifyRemoved(3)
19 | }
20 |
21 | @Test
22 | fun `revert remove item in transaction without header`() {
23 | val item = transactional.data[3]
24 | val transaction = RemoveTransaction(3, false)
25 | transaction.perform(transactional)
26 |
27 | assertFalse(transactional.data.contains(item))
28 | assertTrue(transaction.revert(transactional))
29 | assertTrue(transactional.data.contains(item))
30 | Mockito.verify(transactional).notifyInserted(3)
31 | }
32 |
33 | @Test
34 | fun `remove item in transaction with header`() {
35 | val item = transactional.data[3]
36 | val transaction = RemoveTransaction(3, true)
37 |
38 | assertTrue(transaction.perform(transactional))
39 | assertFalse(transactional.data.contains(item))
40 | Mockito.verify(transactional).notifyRemoved(4)
41 | }
42 |
43 | @Test
44 | fun `revert remove item in transaction with header`() {
45 | val item = transactional.data[3]
46 | val transaction = RemoveTransaction(3, true)
47 | transaction.perform(transactional)
48 |
49 | assertFalse(transactional.data.contains(item))
50 | assertTrue(transaction.revert(transactional))
51 | assertTrue(transactional.data.contains(item))
52 | Mockito.verify(transactional).notifyInserted(4)
53 | }
54 | }
--------------------------------------------------------------------------------
/library/src/test/java/com/thesurix/gesturerecycler/InsertTransactionTest.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import com.thesurix.gesturerecycler.transactions.InsertTransaction
4 | import org.junit.Test
5 | import org.mockito.Mockito
6 | import kotlin.test.assertEquals
7 | import kotlin.test.assertTrue
8 |
9 | class InsertTransactionTest : BaseTransactionTest() {
10 |
11 | @Test
12 | fun `insert item in transaction without header`() {
13 | val itemToInsert = "X"
14 | val transaction = InsertTransaction(itemToInsert, 4, false)
15 |
16 | assertTrue(transaction.perform(transactional))
17 | assertEquals(transactional.data[4], itemToInsert)
18 | Mockito.verify(transactional).notifyInserted(4)
19 | }
20 |
21 | @Test
22 | fun `revert insert item in transaction without header`() {
23 | val itemToInsert = "X"
24 | val itemBeforeInsert = transactional.data[4]
25 | val transaction = InsertTransaction(itemToInsert, 4, false)
26 | transaction.perform(transactional)
27 |
28 | assertEquals(transactional.data[4], itemToInsert)
29 | assertTrue(transaction.revert(transactional))
30 | assertEquals(transactional.data[4], itemBeforeInsert)
31 | Mockito.verify(transactional).notifyRemoved(4)
32 | }
33 |
34 | @Test
35 | fun `insert item in transaction with header`() {
36 | val itemToInsert = "X"
37 | val transaction = InsertTransaction(itemToInsert, 4, true)
38 |
39 | assertTrue(transaction.perform(transactional))
40 | assertEquals(transactional.data[4], itemToInsert)
41 | Mockito.verify(transactional).notifyInserted(5)
42 | }
43 |
44 | @Test
45 | fun `revert insert item in transaction with header`() {
46 | val itemToInsert = "X"
47 | val itemBeforeInsert = transactional.data[4]
48 | val transaction = InsertTransaction(itemToInsert, 4, true)
49 | transaction.perform(transactional)
50 |
51 | assertEquals(transactional.data[4], itemToInsert)
52 | assertTrue(transaction.revert(transactional))
53 | assertEquals(transactional.data[4], itemBeforeInsert)
54 | Mockito.verify(transactional).notifyRemoved(5)
55 | }
56 | }
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/fragment/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.*
5 | import androidx.fragment.app.Fragment
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.thesurix.example.gesturerecycler.R
8 | import com.thesurix.example.gesturerecycler.model.Month
9 | import com.thesurix.example.gesturerecycler.model.MonthHeader
10 | import com.thesurix.example.gesturerecycler.model.MonthItem
11 | import com.thesurix.gesturerecycler.GestureManager
12 |
13 | open class BaseFragment : Fragment() {
14 |
15 | protected lateinit var recyclerView: RecyclerView
16 | protected var gestureManager: GestureManager? = null
17 |
18 | protected open val months: MutableList
19 | get() {
20 | return mutableListOf(MonthHeader("First quarter"),
21 | Month("JAN", R.drawable.january),
22 | Month("FEB", R.drawable.february),
23 | Month("MAR", R.drawable.march),
24 | MonthHeader("Second quarter"),
25 | Month("APR", R.drawable.april),
26 | Month("MAY", R.drawable.may),
27 | Month("JUN", R.drawable.june),
28 | MonthHeader("Third quarter"),
29 | Month("JUL", R.drawable.july),
30 | Month("AUG", R.drawable.august),
31 | Month("SEP", R.drawable.september),
32 | MonthHeader("Fourth quarter"),
33 | Month("OCT", R.drawable.october),
34 | Month("NOV", R.drawable.november),
35 | Month("DEC", R.drawable.december))
36 | }
37 |
38 | override fun onCreate(savedInstanceState: Bundle?) {
39 | super.onCreate(savedInstanceState)
40 | setHasOptionsMenu(true)
41 | }
42 |
43 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
44 | recyclerView = RecyclerView(activity!!)
45 | return recyclerView
46 | }
47 |
48 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
49 | inflater.inflate(R.menu.recycler_menu, menu)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/linear_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
14 |
15 |
19 |
20 |
29 |
30 |
33 |
34 |
38 |
39 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/library/src/test/java/com/thesurix/gesturerecycler/SwapTransactionTest.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import com.thesurix.gesturerecycler.transactions.SwapTransaction
4 | import org.junit.Test
5 | import org.mockito.Mockito
6 | import kotlin.test.assertEquals
7 | import kotlin.test.assertTrue
8 |
9 | class SwapTransactionTest : BaseTransactionTest() {
10 |
11 | @Test
12 | fun `move item in transaction without header`() {
13 | val firstItem = transactional.data[1]
14 | val secondItem = transactional.data[3]
15 | val transaction = SwapTransaction(1, 3, false)
16 |
17 | assertTrue(transaction.perform(transactional))
18 | assertEquals(transactional.data[1], secondItem)
19 | assertEquals(transactional.data[3], firstItem)
20 | Mockito.verify(transactional).notifyChanged(1)
21 | Mockito.verify(transactional).notifyChanged(3)
22 | }
23 |
24 | @Test
25 | fun `revert move item in transaction without header`() {
26 | val firstItem = transactional.data[1]
27 | val secondItem = transactional.data[3]
28 | val transaction = SwapTransaction(1, 3, false)
29 |
30 | assertTrue(transaction.revert(transactional))
31 | assertEquals(transactional.data[1], secondItem)
32 | assertEquals(transactional.data[3], firstItem)
33 | Mockito.verify(transactional).notifyChanged(3)
34 | Mockito.verify(transactional).notifyChanged(1)
35 | }
36 |
37 | @Test
38 | fun `move item in transaction with header`() {
39 | val firstItem = transactional.data[1]
40 | val secondItem = transactional.data[3]
41 | val transaction = SwapTransaction(1, 3, true)
42 |
43 | assertTrue(transaction.perform(transactional))
44 | assertEquals(transactional.data[1], secondItem)
45 | assertEquals(transactional.data[3], firstItem)
46 | Mockito.verify(transactional).notifyChanged(2)
47 | Mockito.verify(transactional).notifyChanged(4)
48 | }
49 |
50 | @Test
51 | fun `revert move item in transaction with header`() {
52 | val firstItem = transactional.data[1]
53 | val secondItem = transactional.data[3]
54 | val transaction = SwapTransaction(1, 3, true)
55 |
56 | assertTrue(transaction.revert(transactional))
57 | assertEquals(transactional.data[1], secondItem)
58 | assertEquals(transactional.data[3], firstItem)
59 | Mockito.verify(transactional).notifyChanged(4)
60 | Mockito.verify(transactional).notifyChanged(2)
61 | }
62 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/GestureViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import android.view.View
4 | import androidx.recyclerview.widget.RecyclerView
5 |
6 | /**
7 | * Base view holder class for gesture compatible items.
8 | * @author thesurix
9 | */
10 | abstract class GestureViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
11 |
12 | /**
13 | * Returns view that can spawn drag gesture. If there is no view simply return null.
14 | * @return view that can spawn drag gesture
15 | */
16 | open val draggableView: View?
17 | get() = null
18 |
19 | /**
20 | * Returns top visible view (originally root view of the item),
21 | * override this method to use background view feature in case of swipe gestures.
22 | * @return top view
23 | */
24 | open val foregroundView: View
25 | get() = itemView
26 |
27 | /**
28 | * Returns background view which is visible when foreground view is partially or fully swiped.
29 | * @return background view
30 | */
31 | open val backgroundView: View?
32 | get() = null
33 |
34 | /**
35 | * Method that shows view for manual drag gestures.
36 | * Called only when getDraggableView() returns valid view.
37 | */
38 | fun showDraggableView() {
39 | draggableView?.visibility = View.VISIBLE
40 | }
41 |
42 | /**
43 | * Method that hides view for manual drag gestures.
44 | * Called only when getDraggableView() returns valid view.
45 | */
46 | fun hideDraggableView() {
47 | draggableView?.visibility = View.GONE
48 | }
49 |
50 | /**
51 | * This method delegates bind logic into your ViewHolder.
52 | * @param item model from adapter's data collection
53 | * */
54 | open fun bind(item: T) {}
55 |
56 | /**
57 | * Indicates that ViewHolder is ready to recycle itself.
58 | */
59 | open fun recycle() {}
60 |
61 | /**
62 | * Indicates that view is selected.
63 | */
64 | open fun onItemSelect() {}
65 |
66 | /**
67 | * Indicates that view has no selection.
68 | */
69 | open fun onItemClear() {}
70 |
71 | open fun getBackgroundView(direction: Int): View? {
72 | return backgroundView
73 | }
74 |
75 | /**
76 | * Returns information if we can drag this view.
77 | * @return true if draggable, false otherwise
78 | */
79 | abstract fun canDrag(): Boolean
80 |
81 | /**
82 | * Returns information if we can swipe this view.
83 | * @return true if swipeable, false otherwise
84 | */
85 | abstract fun canSwipe(): Boolean
86 | }
87 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/adapter/MonthsAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.adapter
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.annotation.LayoutRes
8 | import com.thesurix.example.gesturerecycler.R
9 | import com.thesurix.example.gesturerecycler.databinding.*
10 | import com.thesurix.example.gesturerecycler.model.MonthItem
11 | import com.thesurix.gesturerecycler.GestureAdapter
12 | import com.thesurix.gesturerecycler.GestureViewHolder
13 | import com.thesurix.gesturerecycler.TYPE_FOOTER_ITEM
14 | import com.thesurix.gesturerecycler.TYPE_HEADER_ITEM
15 |
16 | class MonthsAdapter(@param:LayoutRes private val mItemResId: Int) : GestureAdapter>() {
17 |
18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GestureViewHolder {
19 | return if (viewType == MonthItem.MonthItemType.MONTH.ordinal) {
20 | when (mItemResId) {
21 | R.layout.linear_item -> LinearItemViewHolder(LinearItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
22 | R.layout.linear_item_with_background -> LinearItemWithBackgroundViewHolder(
23 | LinearItemWithBackgroundBinding.inflate(LayoutInflater.from(parent.context), parent, false))
24 | R.layout.grid_item -> GridItemViewHolder(GridItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
25 | else -> throw UnsupportedOperationException("Unsupported resource")
26 | }
27 | } else if (viewType == TYPE_HEADER_ITEM) {
28 | createHeaderOrFooterViewHolder(parent.context, parent, R.layout.header_item)
29 | } else if (viewType == TYPE_FOOTER_ITEM) {
30 | createHeaderOrFooterViewHolder(parent.context, parent, R.layout.footer_item)
31 | } else {
32 | MonthHeaderViewHolder(MonthHeaderItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
33 | }
34 | }
35 |
36 | override fun getItemViewType(viewPosition: Int): Int {
37 | val handledType = super.getItemViewType(viewPosition)
38 | if (handledType > 0) {
39 | return handledType
40 | }
41 | return getItemByViewPosition(viewPosition).type.ordinal
42 | }
43 |
44 | fun createHeaderOrFooterViewHolder(context: Context, parent: ViewGroup, @LayoutRes layout: Int): GestureViewHolder {
45 | val view = LayoutInflater.from(context).inflate(layout, parent, false)
46 | return object : GestureViewHolder(view) {
47 | override fun canDrag() = false
48 |
49 | override fun canSwipe() = false
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/LayoutFlags.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 |
4 | import androidx.recyclerview.widget.*
5 |
6 | /**
7 | * Enum with predefined gesture flags for various layout managers, see [RecyclerView.LayoutManager]
8 | * @author thesurix
9 | */
10 | internal enum class LayoutFlags {
11 | LINEAR {
12 | override fun getDragFlags(layout: RecyclerView.LayoutManager): Int {
13 | val linearLayout = layout as LinearLayoutManager
14 | return when(linearLayout.orientation) {
15 | LinearLayoutManager.HORIZONTAL -> ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
16 | else -> ItemTouchHelper.UP or ItemTouchHelper.DOWN
17 | }
18 | }
19 |
20 | override fun getSwipeFlags(layout: RecyclerView.LayoutManager): Int {
21 | val linearLayout = layout as LinearLayoutManager
22 | return when(linearLayout.orientation) {
23 | LinearLayoutManager.HORIZONTAL -> ItemTouchHelper.UP
24 | else -> ItemTouchHelper.RIGHT
25 | }
26 | }
27 | },
28 | GRID {
29 | override fun getDragFlags(layout: RecyclerView.LayoutManager): Int {
30 | return ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
31 | }
32 |
33 | override fun getSwipeFlags(layout: RecyclerView.LayoutManager): Int {
34 | val gridLayout = layout as GridLayoutManager
35 | return when(gridLayout.orientation) {
36 | GridLayoutManager.HORIZONTAL -> ItemTouchHelper.UP or ItemTouchHelper.DOWN
37 | else -> ItemTouchHelper.RIGHT
38 | }
39 | }
40 | },
41 | STAGGERED {
42 | override fun getDragFlags(layout: RecyclerView.LayoutManager): Int {
43 | return ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
44 | }
45 |
46 | override fun getSwipeFlags(layout: RecyclerView.LayoutManager): Int {
47 | val staggeredGridLayout = layout as StaggeredGridLayoutManager
48 | return when(staggeredGridLayout.orientation) {
49 | StaggeredGridLayoutManager.HORIZONTAL -> ItemTouchHelper.UP or ItemTouchHelper.DOWN
50 | else -> ItemTouchHelper.RIGHT
51 | }
52 | }
53 | };
54 |
55 | /**
56 | * Returns drag flags for the given layout manager.
57 | * @param layout layout manager instance
58 | * @return drag flags
59 | */
60 | internal abstract fun getDragFlags(layout: RecyclerView.LayoutManager): Int
61 |
62 | /**
63 | * Returns swipe flags for the given layout manager.
64 | * @param layout layout manager instance
65 | * @return swipe flags
66 | */
67 | internal abstract fun getSwipeFlags(layout: RecyclerView.LayoutManager): Int
68 | }
69 |
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/fragment/LinearRecyclerFragment.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.MenuItem
5 | import android.view.View
6 | import androidx.recyclerview.widget.ItemTouchHelper
7 | import androidx.recyclerview.widget.LinearLayoutManager
8 | import com.google.android.material.snackbar.Snackbar
9 | import com.thesurix.example.gesturerecycler.R
10 | import com.thesurix.example.gesturerecycler.adapter.MonthsAdapter
11 | import com.thesurix.example.gesturerecycler.model.MonthItem
12 | import com.thesurix.gesturerecycler.DefaultItemClickListener
13 | import com.thesurix.gesturerecycler.GestureAdapter
14 | import com.thesurix.gesturerecycler.GestureManager
15 | import com.thesurix.gesturerecycler.RecyclerItemTouchListener
16 |
17 | class LinearRecyclerFragment : BaseFragment() {
18 |
19 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
20 | super.onViewCreated(view, savedInstanceState)
21 |
22 | val manager = LinearLayoutManager(context)
23 | recyclerView.setHasFixedSize(true)
24 | recyclerView.layoutManager = manager
25 |
26 | val adapter = MonthsAdapter(R.layout.linear_item)
27 | adapter.data = months
28 |
29 | recyclerView.adapter = adapter
30 | recyclerView.addOnItemTouchListener(RecyclerItemTouchListener(object : DefaultItemClickListener() {
31 |
32 | override fun onItemClick(item: MonthItem, position: Int): Boolean {
33 | Snackbar.make(view, "Click event on the $position position", Snackbar.LENGTH_SHORT).show()
34 | return true
35 | }
36 |
37 | override fun onItemLongPress(item: MonthItem, position: Int) {
38 | Snackbar.make(view, "Long press event on the $position position", Snackbar.LENGTH_SHORT).show()
39 | }
40 |
41 | override fun onDoubleTap(item: MonthItem, position: Int): Boolean {
42 | Snackbar.make(view, "Double tap event on the $position position", Snackbar.LENGTH_SHORT).show()
43 | return true
44 | }
45 | }))
46 |
47 | gestureManager = GestureManager.Builder(recyclerView)
48 | .setSwipeEnabled(true)
49 | .setLongPressDragEnabled(true)
50 | .setSwipeFlags(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
51 | .setDragFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN)
52 | .build()
53 |
54 | adapter.setDataChangeListener(object : GestureAdapter.OnDataChangeListener {
55 | override fun onItemRemoved(item: MonthItem, position: Int, direction: Int) {
56 | Snackbar.make(view, "Month removed from position $position ", Snackbar.LENGTH_SHORT).show()
57 | }
58 |
59 | override fun onItemReorder(item: MonthItem, fromPos: Int, toPos: Int) {
60 | Snackbar.make(view, "Month moved from position $fromPos to $toPos", Snackbar.LENGTH_SHORT).show()
61 | }
62 | })
63 | }
64 |
65 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
66 | if (item.itemId == R.id.recycler_drag_menu) {
67 | gestureManager?.isManualDragEnabled = !gestureManager!!.isManualDragEnabled
68 | }
69 | return super.onOptionsItemSelected(item)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/linear_item_with_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
14 |
15 |
16 |
17 |
23 |
24 |
30 |
31 |
36 |
37 |
46 |
47 |
50 |
51 |
55 |
56 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/RecyclerItemTouchListener.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import android.view.GestureDetector
4 | import android.view.MotionEvent
5 | import androidx.recyclerview.widget.RecyclerView
6 |
7 | /**
8 | * Class that is responsible for handling item touch events.
9 | * Constructs [RecyclerView] touch listener.
10 | * @param listener listener for item's click events
11 | * @author thesurix
12 | */
13 | class RecyclerItemTouchListener(listener: ItemClickListener) : RecyclerView.SimpleOnItemTouchListener() {
14 |
15 | private var gestureDetector: GestureDetector? = null
16 |
17 | /**
18 | * The listener that is used to notify when a tap, long press or double tap occur.
19 | */
20 | interface ItemClickListener {
21 |
22 | /**
23 | * Called when a tap occurs on a specified item.
24 | * @param item pressed item
25 | * @param position item's position
26 | * @return true if the event is consumed, else false
27 | */
28 | fun onItemClick(item: T, position: Int): Boolean
29 |
30 | /**
31 | * Called when a long press occurs on a specified item.
32 | * @param item pressed item
33 | * @param position item's position
34 | */
35 | fun onItemLongPress(item: T, position: Int)
36 |
37 | /**
38 | * Called when a double tap occurs on a specified item.
39 | * @param item tapped item
40 | * @param position item's position
41 | * @return true if the event is consumed, else false
42 | */
43 | fun onDoubleTap(item: T, position: Int): Boolean
44 | }
45 |
46 | private val gestureClickListener = GestureClickListener(listener)
47 |
48 | override fun onInterceptTouchEvent(view: RecyclerView, e: MotionEvent): Boolean {
49 |
50 | val childView = view.findChildViewUnder(e.x, e.y) ?: return false
51 | val childPosition = view.getChildAdapterPosition(childView)
52 | if (childPosition == RecyclerView.NO_POSITION) {
53 | return false
54 | }
55 |
56 | val adapter = view.adapter
57 | if (adapter is GestureAdapter<*, *>) {
58 | val gestureAdapter = adapter as GestureAdapter
59 | gestureClickListener.setTouchedItem(gestureAdapter.getItem(childPosition), childPosition)
60 | }
61 |
62 | if (gestureDetector == null) {
63 | gestureDetector = GestureDetector(view.context, gestureClickListener)
64 | }
65 |
66 | return gestureDetector?.onTouchEvent(e) ?: false
67 | }
68 | }
69 |
70 | private class GestureClickListener internal constructor(private val listener: RecyclerItemTouchListener.ItemClickListener)
71 | : GestureDetector.SimpleOnGestureListener() {
72 |
73 | private var item: T? = null
74 | private var viewPosition = 0
75 |
76 | override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
77 | return item?.let { listener.onItemClick(it, viewPosition) } ?: false
78 | }
79 |
80 | override fun onLongPress(e: MotionEvent) {
81 | item?.let { listener.onItemLongPress(it, viewPosition) }
82 | }
83 |
84 | override fun onDoubleTap(e: MotionEvent): Boolean {
85 | return item?.let { listener.onDoubleTap(it, viewPosition) } ?: false
86 | }
87 |
88 | internal fun setTouchedItem(item: T, viewPosition: Int) {
89 | this.item = item
90 | this.viewPosition = viewPosition
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/fragment/GridRecyclerFragment.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.MenuItem
5 | import android.view.View
6 | import androidx.recyclerview.widget.GridLayoutManager
7 | import com.google.android.material.snackbar.Snackbar
8 | import com.thesurix.example.gesturerecycler.R
9 | import com.thesurix.example.gesturerecycler.adapter.MonthsAdapter
10 | import com.thesurix.example.gesturerecycler.model.MonthItem
11 | import com.thesurix.gesturerecycler.DefaultItemClickListener
12 | import com.thesurix.gesturerecycler.GestureAdapter
13 | import com.thesurix.gesturerecycler.GestureManager
14 | import com.thesurix.gesturerecycler.RecyclerItemTouchListener
15 |
16 | class GridRecyclerFragment : BaseFragment() {
17 |
18 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
19 | super.onViewCreated(view, savedInstanceState)
20 |
21 | val manager = GridLayoutManager(context, 2)
22 | recyclerView.setHasFixedSize(true)
23 | recyclerView.layoutManager = manager
24 |
25 | val adapter = MonthsAdapter(R.layout.grid_item)
26 | adapter.data = months
27 | recyclerView.adapter = adapter
28 | recyclerView.addOnItemTouchListener(RecyclerItemTouchListener(object : DefaultItemClickListener() {
29 |
30 | override fun onItemClick(item: MonthItem, position: Int): Boolean {
31 | Snackbar.make(view, "Click event on the $position position", Snackbar.LENGTH_SHORT).show()
32 | return true
33 | }
34 |
35 | override fun onItemLongPress(item: MonthItem, position: Int) {
36 | Snackbar.make(view, "Long press event on the $position position", Snackbar.LENGTH_SHORT).show()
37 | }
38 |
39 | override fun onDoubleTap(item: MonthItem, position: Int): Boolean {
40 | Snackbar.make(view, "Double tap event on the $position position", Snackbar.LENGTH_SHORT).show()
41 | return true
42 | }
43 | }))
44 |
45 | manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
46 | override fun getSpanSize(position: Int): Int {
47 | val item = adapter.getItem(position)
48 | return if (item.type === MonthItem.MonthItemType.HEADER) manager.spanCount else 1
49 | }
50 | }
51 |
52 | gestureManager = GestureManager.Builder(recyclerView)
53 | .setSwipeEnabled(true)
54 | .setLongPressDragEnabled(true)
55 | .build()
56 |
57 | adapter.setDataChangeListener(object : GestureAdapter.OnDataChangeListener {
58 | override fun onItemRemoved(item: MonthItem, position: Int, direction: Int) {
59 | Snackbar.make(view, "Month removed from position $position", Snackbar.LENGTH_SHORT).show()
60 | }
61 |
62 | override fun onItemReorder(item: MonthItem, fromPos: Int, toPos: Int) {
63 | Snackbar.make(view, "Month moved from position $fromPos to $toPos", Snackbar.LENGTH_SHORT).show()
64 | }
65 | })
66 | }
67 |
68 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
69 | if (item.itemId == R.id.recycler_drag_menu) {
70 | gestureManager?.isManualDragEnabled = !gestureManager!!.isManualDragEnabled
71 | }
72 | return super.onOptionsItemSelected(item)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/publish-mavencentral.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven-publish'
2 | apply plugin: 'signing'
3 |
4 | task androidSourcesJar(type: Jar) {
5 | archiveClassifier.set('sources')
6 | if (project.plugins.findPlugin("com.android.library")) {
7 | // For Android libraries
8 | from android.sourceSets.main.java.srcDirs
9 | from android.sourceSets.main.kotlin.srcDirs
10 | } else {
11 | // For pure Kotlin libraries, in case you have them
12 | from sourceSets.main.java.srcDirs
13 | from sourceSets.main.kotlin.srcDirs
14 | }
15 | }
16 |
17 | artifacts {
18 | archives androidSourcesJar
19 | }
20 |
21 | group = PUBLISH_GROUP_ID
22 | version = PUBLISH_VERSION
23 |
24 | ext["signing.keyId"] = ''
25 | ext["signing.password"] = ''
26 | ext["signing.secretKeyRingFile"] = ''
27 | ext["ossrhUsername"] = ''
28 | ext["ossrhPassword"] = ''
29 | ext["sonatypeStagingProfileId"] = ''
30 |
31 | File secretPropsFile = project.rootProject.file('local.properties')
32 | if (secretPropsFile.exists()) {
33 | Properties p = new Properties()
34 | new FileInputStream(secretPropsFile).withCloseable { is ->
35 | p.load(is)
36 | }
37 |
38 | p.each { name, value ->
39 | ext[name] = value
40 | }
41 | }
42 |
43 | publishing {
44 | publications {
45 | release(MavenPublication) {
46 |
47 | groupId PUBLISH_GROUP_ID
48 | artifactId PUBLISH_ARTIFACT_ID
49 | version PUBLISH_VERSION
50 |
51 | // Two artifacts, the `aar` (or `jar`) and the sources
52 | if (project.plugins.findPlugin("com.android.library")) {
53 | artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
54 | } else {
55 | artifact("$buildDir/libs/${project.getName()}-${version}.jar")
56 | }
57 |
58 | artifact androidSourcesJar
59 |
60 | pom {
61 | name = PUBLISH_ARTIFACT_ID
62 | description = 'This library provides swipe & drag and drop support for RecyclerView.'
63 | url = 'https://github.com/thesurix/gesture-recycler'
64 | licenses {
65 | license {
66 | name = 'Gesture Recycler License'
67 | url = 'https://github.com/thesurix/gesture-recycler/blob/master/LICENSE.txt'
68 | }
69 | }
70 |
71 | developers {
72 | developer {
73 | id = 'thesurix'
74 | name = 'Pawel Surowka'
75 | email = 'thesurix@gmail.com'
76 | }
77 | }
78 |
79 | scm {
80 | connection = 'scm:git:github.com/thesurix/gesture-recycler.git'
81 | developerConnection = 'scm:git:ssh://github.com/thesurix/gesture-recycler.git'
82 | url = 'https://github.com/thesurix/gesture-recycler/tree/main'
83 | }
84 |
85 | // A slightly hacky fix so that your POM will include any transitive dependencies
86 | // that your library builds upon
87 | withXml {
88 | def dependenciesNode = asNode().appendNode('dependencies')
89 | project.configurations.implementation.allDependencies.each {
90 | def dependencyNode = dependenciesNode.appendNode('dependency')
91 | dependencyNode.appendNode('groupId', it.group)
92 | dependencyNode.appendNode('artifactId', it.name)
93 | dependencyNode.appendNode('version', it.version)
94 | }
95 | }
96 | }
97 | }
98 | }
99 |
100 | repositories {
101 | maven {
102 | name = "sonatype"
103 | url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
104 | credentials {
105 | username ossrhUsername
106 | password ossrhPassword
107 | }
108 | }
109 | }
110 | }
111 |
112 | signing {
113 | sign publishing.publications
114 | }
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/adapter/BaseMonthViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.adapter
2 |
3 | import android.animation.ArgbEvaluator
4 | import android.animation.ValueAnimator
5 | import android.view.View
6 | import android.view.ViewStub
7 | import android.widget.ImageView
8 | import android.widget.TextView
9 | import androidx.core.content.ContextCompat
10 | import com.bumptech.glide.Glide
11 | import com.bumptech.glide.request.RequestOptions
12 | import com.thesurix.example.gesturerecycler.R
13 | import com.thesurix.example.gesturerecycler.model.Month
14 | import com.thesurix.example.gesturerecycler.model.MonthItem
15 | import com.thesurix.gesturerecycler.GestureViewHolder
16 |
17 | abstract class BaseMonthViewHolder(rootView: View) : GestureViewHolder(rootView) {
18 | protected abstract val monthText: TextView
19 | protected abstract val monthPicture: ImageView
20 | protected abstract val itemDrag: ImageView
21 | protected abstract val foreground: View?
22 |
23 | override val draggableView: View?
24 | get() = itemDrag
25 |
26 | override val foregroundView: View
27 | get() = foreground ?: super.foregroundView
28 |
29 | override fun bind(item: MonthItem) {
30 | if (item is Month) {
31 | monthText.text = item.name
32 | Glide.with(itemView.context)
33 | .load(item.drawableId)
34 | .apply(RequestOptions.centerCropTransform())
35 | .into(monthPicture)
36 | }
37 | }
38 |
39 | override fun onItemSelect() {
40 | val textColorFrom = ContextCompat.getColor(itemView.context, android.R.color.white)
41 | val textColorTo = ContextCompat.getColor(itemView.context, R.color.indigo_500)
42 | ValueAnimator.ofObject(ArgbEvaluator(), textColorFrom, textColorTo).apply {
43 | duration = itemView.context.resources.getInteger(R.integer.animation_speed_ms).toLong()
44 | addUpdateListener(getTextAnimatorListener(monthText, this))
45 | start()
46 | }
47 |
48 |
49 | val backgroundColorFrom = ContextCompat.getColor(itemView.context, R.color.indigo_500)
50 | val backgroundColorTo = ContextCompat.getColor(itemView.context, android.R.color.white)
51 | ValueAnimator.ofObject(ArgbEvaluator(), backgroundColorFrom, backgroundColorTo).apply {
52 | duration = itemView.context.resources.getInteger(R.integer.animation_speed_ms).toLong()
53 | addUpdateListener(getBackgroundAnimatorListener(monthText, this))
54 | start()
55 | }
56 | }
57 |
58 | override fun onItemClear() {
59 | val textColorFrom = ContextCompat.getColor(itemView.context, R.color.indigo_500)
60 | val textColorTo = ContextCompat.getColor(itemView.context, android.R.color.white)
61 | ValueAnimator.ofObject(ArgbEvaluator(), textColorFrom, textColorTo).apply {
62 | duration = itemView.context.resources.getInteger(R.integer.animation_speed_ms).toLong()
63 | addUpdateListener(getTextAnimatorListener(monthText, this))
64 | start()
65 | }
66 |
67 |
68 | val backgroundColorFrom = ContextCompat.getColor(itemView.context, android.R.color.white)
69 | val backgroundColorTo = ContextCompat.getColor(itemView.context, R.color.indigo_500)
70 | ValueAnimator.ofObject(ArgbEvaluator(), backgroundColorFrom, backgroundColorTo).apply {
71 | duration = itemView.context.resources.getInteger(R.integer.animation_speed_ms).toLong()
72 | addUpdateListener(getBackgroundAnimatorListener(monthText, this))
73 | start()
74 | }
75 | }
76 |
77 | override fun canDrag() = true
78 |
79 | override fun canSwipe() = true
80 |
81 | private fun getBackgroundAnimatorListener(view: TextView, animator: ValueAnimator): ValueAnimator.AnimatorUpdateListener {
82 | return ValueAnimator.AnimatorUpdateListener { view.setBackgroundColor(animator.animatedValue as Int) }
83 | }
84 |
85 | private fun getTextAnimatorListener(view: TextView, animator: ValueAnimator): ValueAnimator.AnimatorUpdateListener {
86 | return ValueAnimator.AnimatorUpdateListener { view.setTextColor(animator.animatedValue as Int) }
87 | }
88 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/GestureTouchHelperCallback.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import android.graphics.Canvas
4 | import android.util.Log
5 | import android.view.View
6 | import androidx.recyclerview.widget.*
7 | import com.thesurix.gesturerecycler.LayoutFlags.*
8 |
9 | /**
10 | * Touch helper callback that handles different RecycleView gestures.
11 | * Constructs callback object based on passed adapter.
12 | * @param adapter adapter
13 | * @author thesurix
14 | */
15 | private val DIRECTIONS = listOf(ItemTouchHelper.LEFT, ItemTouchHelper.RIGHT, ItemTouchHelper.UP, ItemTouchHelper.DOWN)
16 | class GestureTouchHelperCallback(private val gestureAdapter: GestureAdapter<*, *>) : ItemTouchHelper.Callback() {
17 |
18 | /** Flag that enables or disables swipe gesture */
19 | var swipeEnabled = false
20 | /** Flag that enables or disables manual drag gesture */
21 | var manualDragEnabled = false
22 | set(enabled) {
23 | field = enabled
24 | gestureAdapter.allowManualDrag(manualDragEnabled)
25 | }
26 | /** Flag that enables long press drag gesture */
27 | var longPressDragEnabled = false
28 |
29 | /** Flags for drag gesture */
30 | var dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
31 | /** Flags for swipe gesture */
32 | var swipeFlags = ItemTouchHelper.RIGHT
33 |
34 | override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
35 | val holder = viewHolder as GestureViewHolder<*>
36 | return makeMovementFlags(if (holder.canDrag()) dragFlags else 0, if (holder.canSwipe()) swipeFlags else 0)
37 | }
38 |
39 | override fun onMove(recyclerView: RecyclerView, source: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
40 | return gestureAdapter.onItemMove(source.adapterPosition, target.adapterPosition)
41 | }
42 |
43 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
44 | gestureAdapter.onItemDismissed(viewHolder.adapterPosition, direction)
45 | }
46 |
47 | override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
48 | super.onSelectedChanged(viewHolder, actionState)
49 | if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && viewHolder is GestureViewHolder<*>) {
50 | viewHolder.onItemSelect()
51 | }
52 | }
53 |
54 | override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float,
55 | actionState: Int, isCurrentlyActive: Boolean) {
56 | when(actionState) {
57 | ItemTouchHelper.ACTION_STATE_SWIPE -> {
58 | val direction = when {
59 | dX.compareTo(0f) == 0 -> if (dY < 0) ItemTouchHelper.UP else ItemTouchHelper.DOWN
60 | dY.compareTo(0f) == 0 -> if (dX < 0) ItemTouchHelper.LEFT else ItemTouchHelper.RIGHT
61 | else -> -1
62 | }
63 |
64 | val gestureViewHolder = (viewHolder as GestureViewHolder<*>)
65 | hideBackgroundViews(gestureViewHolder)
66 |
67 | if (direction != -1) {
68 | val backgroundView = gestureViewHolder.getBackgroundView(direction)
69 | backgroundView?.let {
70 | if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && backgroundView.visibility == View.GONE) {
71 | backgroundView.visibility = View.VISIBLE
72 | }
73 | }
74 | }
75 |
76 | val foregroundView = gestureViewHolder.foregroundView
77 | getDefaultUIUtil().onDraw(c, recyclerView, foregroundView, dX, dY, actionState, isCurrentlyActive)
78 | }
79 | else -> super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
80 | }
81 | }
82 |
83 | override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
84 | super.clearView(recyclerView, viewHolder)
85 | gestureAdapter.onItemMoved()
86 | if (viewHolder is GestureViewHolder<*>) {
87 | viewHolder.onItemClear()
88 | hideBackgroundViews(viewHolder)
89 |
90 | val foregroundView = viewHolder.foregroundView
91 | getDefaultUIUtil().clearView(foregroundView)
92 | }
93 | }
94 |
95 | private fun hideBackgroundViews(viewHolder: GestureViewHolder<*>) {
96 | DIRECTIONS.forEach {
97 | viewHolder.getBackgroundView(it)?.visibility = View.GONE
98 | }
99 | }
100 |
101 | override fun isLongPressDragEnabled(): Boolean {
102 | return longPressDragEnabled
103 | }
104 |
105 | override fun isItemViewSwipeEnabled(): Boolean {
106 | return swipeEnabled
107 | }
108 |
109 | /**
110 | * Sets predefined drag flags for RecyclerView layout.
111 | * @param layout type of the RecyclerView layout
112 | */
113 | fun setDragFlagsForLayout(layout: RecyclerView.LayoutManager) {
114 | dragFlags = when (layout) {
115 | is GridLayoutManager -> GRID.getDragFlags(layout)
116 | is LinearLayoutManager -> LINEAR.getDragFlags(layout)
117 | is StaggeredGridLayoutManager -> STAGGERED.getDragFlags(layout)
118 | else -> throw IllegalArgumentException("Unsupported layout type.")
119 | }
120 | }
121 |
122 | /**
123 | * Sets predefined swipe flags for RecyclerView layout.
124 | * @param layout type of the RecyclerView layout
125 | */
126 | fun setSwipeFlagsForLayout(layout: RecyclerView.LayoutManager) {
127 | swipeFlags = when (layout) {
128 | is GridLayoutManager -> GRID.getSwipeFlags(layout)
129 | is LinearLayoutManager -> LINEAR.getSwipeFlags(layout)
130 | is StaggeredGridLayoutManager -> STAGGERED.getSwipeFlags(layout)
131 | else -> throw IllegalArgumentException("Unsupported layout type.")
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/example/src/main/java/com/thesurix/example/gesturerecycler/fragment/EmptyViewFragment.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.example.gesturerecycler.fragment
2 |
3 | import android.animation.FloatEvaluator
4 | import android.animation.ValueAnimator
5 | import android.os.Bundle
6 | import android.view.*
7 | import androidx.recyclerview.widget.ItemTouchHelper
8 | import androidx.recyclerview.widget.LinearLayoutManager
9 | import androidx.recyclerview.widget.RecyclerView
10 | import com.google.android.material.snackbar.Snackbar
11 | import com.thesurix.example.gesturerecycler.R
12 | import com.thesurix.example.gesturerecycler.adapter.MonthsAdapter
13 | import com.thesurix.example.gesturerecycler.callback.MonthDiffCallback
14 | import com.thesurix.example.gesturerecycler.model.Month
15 | import com.thesurix.example.gesturerecycler.model.MonthItem
16 | import com.thesurix.gesturerecycler.EmptyViewVisibilityListener
17 | import com.thesurix.gesturerecycler.GestureAdapter
18 | import com.thesurix.gesturerecycler.GestureManager
19 |
20 | class EmptyViewFragment : BaseFragment() {
21 |
22 | private var adapter: MonthsAdapter? = null
23 |
24 | private var headerFooterState = false
25 | override val months: MutableList
26 | get() {
27 | return mutableListOf(Month("JAN", R.drawable.january),
28 | Month("FEB", R.drawable.february),
29 | Month("MAR", R.drawable.march),
30 | Month("APR", R.drawable.april),
31 | Month("MAY", R.drawable.may),
32 | Month("JUN", R.drawable.june))
33 | }
34 |
35 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
36 | val rootView = inflater.inflate(R.layout.fragment_recycler, container, false)
37 | recyclerView = rootView.findViewById(R.id.recycler_view)
38 | return rootView
39 | }
40 |
41 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
42 | super.onViewCreated(view, savedInstanceState)
43 | val manager = LinearLayoutManager(context)
44 | recyclerView.setHasFixedSize(true)
45 | recyclerView.layoutManager = manager
46 |
47 | adapter = MonthsAdapter(R.layout.linear_item_with_background).apply {
48 | data = months
49 | setUndoSize(2)
50 | setDataChangeListener(object : GestureAdapter.OnDataChangeListener {
51 | override fun onItemRemoved(item: MonthItem, position: Int, direction: Int) {
52 | val undoSnack = Snackbar.make(view, "Month removed from position $position", Snackbar.LENGTH_SHORT)
53 | undoSnack.setAction(R.string.undo_text) { adapter?.undoLast() }
54 | undoSnack.show()
55 | }
56 |
57 | override fun onItemReorder(item: MonthItem, fromPos: Int, toPos: Int) {
58 | val undoSnack = Snackbar.make(view, "Month moved from position $fromPos to $toPos", Snackbar.LENGTH_SHORT)
59 | undoSnack.setAction(R.string.undo_text) { adapter?.undoLast() }
60 | undoSnack.show()
61 | }
62 | })
63 |
64 | val emptyView = view.findViewById(R.id.empty_root)
65 | setEmptyViewVisibilityListener(object : EmptyViewVisibilityListener {
66 | override fun onVisibilityChanged(visible: Boolean) {
67 | if (visible) {
68 | emptyView.visibility = View.VISIBLE
69 | runFadeInAnimation(emptyView)
70 | } else {
71 | runFadeOutAnimation(emptyView)
72 | }
73 | }
74 | })
75 | }
76 |
77 | recyclerView.adapter = adapter
78 |
79 | gestureManager = GestureManager.Builder(recyclerView)
80 | .setSwipeEnabled(true)
81 | .setSwipeFlags(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
82 | .setLongPressDragEnabled(true)
83 | .setHeaderEnabled(headerFooterState)
84 | .setFooterEnabled(headerFooterState)
85 | .build()
86 | }
87 |
88 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
89 | inflater.inflate(R.menu.recycler_empty_menu, menu)
90 | }
91 |
92 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
93 | when (item.itemId) {
94 | R.id.recycler_undo_menu -> adapter?.undoLast()
95 | R.id.recycler_clear_menu -> adapter?.clearData()
96 | R.id.recycler_generate_menu -> {
97 | val months = months
98 | val month = (Math.random() * months.size).toInt()
99 | adapter?.insert(months[month], 0)
100 | recyclerView.scrollToPosition(0)
101 | }
102 | R.id.recycler_diff_menu -> {
103 | val diffMonths = months
104 | diffMonths.shuffle()
105 | adapter?.setData(diffMonths, MonthDiffCallback(adapter!!.data, diffMonths))
106 | recyclerView.scrollToPosition(0)
107 | }
108 | R.id.add -> {
109 | val months = months
110 | val month = (Math.random() * months.size).toInt()
111 | adapter?.add(months[month])
112 | }
113 | R.id.toggle_hf -> {
114 | headerFooterState = !headerFooterState
115 | adapter?.setHeaderEnabled(headerFooterState)
116 | adapter?.setFooterEnabled(headerFooterState)
117 | }
118 | }
119 | return super.onOptionsItemSelected(item)
120 | }
121 |
122 | private fun runFadeInAnimation(emptyView: View) {
123 | val fadeInAnimation = ValueAnimator.ofObject(FloatEvaluator(), 0f, 1f)
124 | fadeInAnimation.duration = resources.getInteger(R.integer.animation_speed_ms).toLong()
125 | fadeInAnimation.addUpdateListener { animation ->
126 | val alpha = animation.animatedValue as Float
127 | emptyView.alpha = alpha
128 | }
129 | fadeInAnimation.start()
130 | }
131 |
132 | private fun runFadeOutAnimation(emptyView: View) {
133 | val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1f, 0f)
134 | fadeOutAnimation.duration = resources.getInteger(R.integer.animation_speed_ms).toLong()
135 | fadeOutAnimation.addUpdateListener { animation ->
136 | val alpha = animation.animatedValue as Float
137 | emptyView.alpha = alpha
138 |
139 | if (alpha < 0.01f) {
140 | emptyView.visibility = View.GONE
141 | }
142 | }
143 |
144 | fadeOutAnimation.start()
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/GestureManager.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import androidx.recyclerview.widget.ItemTouchHelper
4 | import androidx.recyclerview.widget.RecyclerView
5 |
6 | private const val INVALID_FLAG = -1
7 |
8 | /**
9 | * Class that is responsible for gesture management for [RecyclerView] widget.
10 | * @author thesurix
11 | */
12 | class GestureManager {
13 |
14 | private val touchHelperCallback: GestureTouchHelperCallback
15 |
16 | private constructor(builder: Builder) {
17 | val adapter = builder.recyclerView.adapter as GestureAdapter
18 | touchHelperCallback = GestureTouchHelperCallback(adapter).apply {
19 | swipeEnabled = builder.isSwipeEnabled
20 | longPressDragEnabled = builder.isDragEnabled
21 | manualDragEnabled = builder.isManualDragEnabled
22 | }
23 |
24 | val touchHelper = ItemTouchHelper(touchHelperCallback)
25 | touchHelper.attachToRecyclerView(builder.recyclerView)
26 | adapter.setGestureListener(GestureListener(touchHelper))
27 |
28 | if (builder.swipeFlags == INVALID_FLAG) {
29 | touchHelperCallback.setSwipeFlagsForLayout(builder.recyclerView.layoutManager!!)
30 | } else {
31 | touchHelperCallback.swipeFlags = builder.swipeFlags
32 | }
33 |
34 | if (builder.dragFlags == INVALID_FLAG) {
35 | touchHelperCallback.setDragFlagsForLayout(builder.recyclerView.layoutManager!!)
36 | } else {
37 | touchHelperCallback.dragFlags = builder.dragFlags
38 | }
39 |
40 | adapter.setHeaderEnabled(builder.isHeaderEnabled)
41 | adapter.setFooterEnabled(builder.isFooterEnabled)
42 | }
43 |
44 | /**
45 | * Returns true if swipe is enabled, false if swipe is disabled.
46 | * @return swipe state
47 | */
48 | /**
49 | * Sets swipe gesture enabled or disabled.
50 | * @param enabled true to enable, false to disable
51 | */
52 | var isSwipeEnabled: Boolean
53 | get() = touchHelperCallback.isItemViewSwipeEnabled
54 | set(enabled) {
55 | touchHelperCallback.swipeEnabled = enabled
56 | }
57 |
58 | /**
59 | * Returns true if long press drag is enabled, false if long press drag is disabled.
60 | * @return long press drag state
61 | */
62 | /**
63 | * Sets long press drag gesture enabled or disabled.
64 | * @param enabled true to enable, false to disable
65 | */
66 | var isLongPressDragEnabled: Boolean
67 | get() = touchHelperCallback.isLongPressDragEnabled
68 | set(enabled) {
69 | touchHelperCallback.longPressDragEnabled = enabled
70 | }
71 |
72 | /**
73 | * Returns true if manual drag is enabled, false if manual drag is disabled.
74 | * @return manual drag state
75 | */
76 | /**
77 | * Sets manual drag gesture enabled or disabled.
78 | * @param enabled true to enable, false to disable
79 | */
80 | var isManualDragEnabled: Boolean
81 | get() = touchHelperCallback.manualDragEnabled
82 | set(enabled) {
83 | touchHelperCallback.manualDragEnabled = enabled
84 | }
85 |
86 | /**
87 | * Class that builds [GestureManager] instance.
88 | * Constructs [GestureManager] for the given RecyclerView.
89 | * @param recyclerView RecyclerView instance
90 | */
91 | class Builder(val recyclerView: RecyclerView) {
92 | internal var swipeFlags = INVALID_FLAG
93 | private set
94 | internal var dragFlags = INVALID_FLAG
95 | private set
96 | internal var isSwipeEnabled = false
97 | private set
98 | internal var isDragEnabled = false
99 | private set
100 | internal var isManualDragEnabled = false
101 | private set
102 | internal var isHeaderEnabled = false
103 | private set
104 | internal var isFooterEnabled = false
105 | private set
106 |
107 | /**
108 | * Sets swipe gesture enabled or disabled.
109 | * Swipe is disabled by default.
110 | * @param enabled true to enable, false to disable
111 | * @return returns builder instance
112 | */
113 | fun setSwipeEnabled(enabled: Boolean): Builder {
114 | isSwipeEnabled = enabled
115 | return this
116 | }
117 |
118 | /**
119 | * Sets long press drag gesture enabled or disabled.
120 | * Long press drag is disabled by default.
121 | * @param enabled true to enable, false to disable
122 | * @return returns builder instance
123 | */
124 | fun setLongPressDragEnabled(enabled: Boolean): Builder {
125 | isDragEnabled = enabled
126 | return this
127 | }
128 |
129 | /**
130 | * Sets manual drag gesture enabled or disabled.
131 | * Manual drag is disabled by default.
132 | * @param enabled true to enable, false to disable
133 | * @return returns builder instance
134 | */
135 | fun setManualDragEnabled(enabled: Boolean): Builder {
136 | isManualDragEnabled = enabled
137 | return this
138 | }
139 |
140 | /**
141 | * Sets flags for swipe and drag gesture. Do not set this flags if you want predefined flags for RecyclerView layout manager.
142 | * See [ItemTouchHelper] flags.
143 | *
144 | * This method is deprecated, use [.setDragFlags] or [.setSwipeFlags].
145 | * @param swipeFlags flags for swipe gesture
146 | * @param dragFlags flags for drag gesture
147 | * @return returns builder instance
148 | */
149 | @Deprecated("Use setSwipeFlags() and setDragFlags() methods.")
150 | fun setGestureFlags(swipeFlags: Int, dragFlags: Int): Builder {
151 | this.swipeFlags = swipeFlags
152 | this.dragFlags = dragFlags
153 | return this
154 | }
155 |
156 | /**
157 | * Sets flags for swipe gesture. Do not set this flags if you want predefined flags for RecyclerView layout manager.
158 | * See [ItemTouchHelper] flags.
159 | * @param flags flags for swipe gesture
160 | * @return returns builder instance
161 | */
162 | fun setSwipeFlags(flags: Int): Builder {
163 | swipeFlags = flags
164 | return this
165 | }
166 |
167 | /**
168 | * Sets flags for drag gesture. Do not set this flags if you want predefined flags for RecyclerView layout manager.
169 | * See [ItemTouchHelper] flags.
170 | * @param flags flags for drag gesture
171 | * @return returns builder instance
172 | */
173 | fun setDragFlags(flags: Int): Builder {
174 | dragFlags = flags
175 | return this
176 | }
177 |
178 |
179 | /**
180 | * Sets header item enabled or disabled. If enabled then [RecyclerView.Adapter.onCreateViewHolder]
181 | * will get [TYPE_HEADER_ITEM] as a viewType argument.
182 | * Header is disabled by default.
183 | * @param enabled true to enable, false to disable
184 | * @return returns builder instance
185 | */
186 | fun setHeaderEnabled(enabled: Boolean): Builder {
187 | isHeaderEnabled = enabled
188 | return this
189 | }
190 |
191 | /**
192 | * Sets footer item enabled or disabled. If enabled then [RecyclerView.Adapter.onCreateViewHolder]
193 | * will get [TYPE_FOOTER_ITEM] as a viewType argument.
194 | * Footer is disabled by default.
195 | * @param enabled true to enable, false to disable
196 | * @return returns builder instance
197 | */
198 | fun setFooterEnabled(enabled: Boolean): Builder {
199 | isFooterEnabled = enabled
200 | return this
201 | }
202 |
203 | /**
204 | * Builds [GestureManager] instance.
205 | * @return returns GestureManager instance
206 | */
207 | fun build(): GestureManager {
208 | validateBuilder()
209 | return GestureManager(this)
210 | }
211 |
212 | private fun validateBuilder() {
213 | val hasAdapter = recyclerView.adapter is GestureAdapter<*, *>
214 | if (!hasAdapter) {
215 | throw IllegalArgumentException("RecyclerView does not have adapter that extends " + GestureAdapter::class.java.name)
216 | }
217 |
218 | if (swipeFlags == INVALID_FLAG || dragFlags == INVALID_FLAG) {
219 | if (recyclerView.layoutManager == null) {
220 | throw IllegalArgumentException("No layout manager for RecyclerView. Provide custom flags or attach layout manager to RecyclerView.")
221 | }
222 | }
223 | }
224 | }
225 |
226 | }
227 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://android-arsenal.com/api?level=15)
3 | [](https://android-arsenal.com/details/1/3317)
4 |
5 | # Gesture Recycler
6 | This library provides swipe & drag and drop support for RecyclerView. Based on great example from [Android-ItemTouchHelper-Demo](https://github.com/iPaulPro/Android-ItemTouchHelper-Demo).
7 |
8 | # Demo
9 | 
10 |
11 | # Features
12 | * item click/long press/double tap listener
13 | * background views for swipeable items
14 | * empty view
15 | * undo
16 | * swipe
17 | * long press drag
18 | * manual mode drag
19 | * support for different layout managers
20 | * predefined drag & swipe flags for RecyclerView's layout managers
21 | * DiffUtil feature
22 | * header/footer
23 |
24 | # Dependency
25 |
26 | To use this library in your android project, just simply add the following dependency into your build.gradle
27 |
28 | ```sh
29 | repositories {
30 | mavenCentral()
31 | }
32 |
33 | dependencies {
34 | implementation "com.github.thesurix:gesture-recycler:1.17.0"
35 | }
36 | ```
37 |
38 | # How to use?
39 |
40 | ```kotlin
41 | // Define your RecyclerView and adapter as usually
42 | val manager = LinearLayoutManager(context)
43 | recyclerView.setHasFixedSize(true)
44 | recyclerView.layoutManager = manager
45 |
46 | // Extend GestureAdapter and write your own
47 | // ViewHolder items must extend GestureViewHolder
48 | val adapter = MonthsAdapter(R.layout.linear_item)
49 | adapter.data = months
50 | recyclerView.adapter = adapter
51 | ```
52 | ### Swipe and drag & drop support:
53 | ```kotlin
54 | val gestureManager = GestureManager.Builder(recyclerView)
55 | // Enable swipe
56 | .setSwipeEnabled(true)
57 | // Enable long press drag and drop
58 | .setLongPressDragEnabled(true)
59 | // Enable manual drag from the beginning, you need to provide View inside your GestureViewHolder
60 | .setManualDragEnabled(true)
61 | // Use custom gesture flags
62 | // Do not use those methods if you want predefined flags for RecyclerView layout manager
63 | .setSwipeFlags(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
64 | .setDragFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN)
65 | .build()
66 | ```
67 | ### Background view for swipeable items:
68 | ```xml
69 |
73 |
74 |
75 |
81 |
82 |
87 |
88 |
89 |
90 | ```
91 | ```kotlin
92 | // Override foregroundView, backgroundView() variables in ViewHolder to provide top and bottom view
93 | open val foregroundView: View
94 | get() = foreground
95 |
96 | open val backgroundView: View?
97 | get() = background
98 | ```
99 | ### Different background views:
100 | ```xml
101 |
105 |
106 |
107 |
113 |
114 |
120 |
121 |
126 |
127 |
128 |
129 | ```
130 | ```kotlin
131 | // Override foregroundView variable and getBackgroundView(direction: Int) method in ViewHolder to provide top and bottom views
132 | open val foregroundView: View
133 | get() = foreground
134 |
135 | override fun getBackgroundView(direction: Int): View? {
136 | //direction can be ItemTouchHelper.LEFT, ItemTouchHelper.RIGHT, ItemTouchHelper.UP, ItemTouchHelper.DOWN
137 | if (direction == ItemTouchHelper.RIGHT) {
138 | return firstBackgroundView
139 | }
140 | return secondBackgroundView
141 | }
142 | ```
143 | ### Data callbacks:
144 | ```kotlin
145 | adapter.setDataChangeListener(object : GestureAdapter.OnDataChangeListener {
146 | override fun onItemRemoved(item: MonthItem, position: Int, direction: Int) {
147 | }
148 |
149 | override fun onItemReorder(item: MonthItem, fromPos: Int, toPos: Int) {
150 | }
151 | })
152 | ```
153 | ### Data animations:
154 | ```kotlin
155 | // Support for data animations
156 | adapter.add(month)
157 | adapter.insert(month, 5)
158 | adapter.remove(5)
159 | adapter.swap(2, 5)
160 |
161 | // or
162 | adapter.setData(months, diffUtilCallback)
163 |
164 | // This will interrupt pending animations
165 | adapter.data = months
166 |
167 | ```
168 | ### Item click events:
169 |
170 | ```kotlin
171 | // Attach DefaultItemClickListener or implement RecyclerItemTouchListener.ItemClickListener
172 | recyclerView.addOnItemTouchListener(RecyclerItemTouchListener(object : DefaultItemClickListener() {
173 |
174 | override fun onItemClick(item: MonthItem, position: Int): Boolean {
175 | // return true if the event is consumed
176 | return true
177 | }
178 |
179 | override fun onItemLongPress(item: MonthItem, position: Int) {
180 | }
181 |
182 | override fun onDoubleTap(item: MonthItem, position: Int): Boolean {
183 | // return true if the event is consumed
184 | return true
185 | }
186 | }))
187 | ```
188 | ### Empty view:
189 | ```xml
190 |
194 |
195 |
199 |
200 |
201 |
207 |
208 | ```
209 | ```kotlin
210 | // Pass null to disable empty view
211 | val emptyView = view.findViewById(R.id.empty_view)
212 | adapter.setEmptyView(emptyView)
213 |
214 | // or use callback
215 | adapter.setEmptyViewVisibilityListener(object : EmptyViewVisibilityListener {
216 | override fun onVisibilityChanged(visible: Boolean) {
217 | // show/hide emptyView with animation
218 | }
219 | })
220 | ```
221 | ### Undo:
222 | ```kotlin
223 | // Undo last data transaction (add, insert, remove, swipe, reorder)
224 | adapter.undoLast()
225 |
226 | // Set undo stack size
227 | adapter.setUndoSize(2)
228 | ```
229 |
230 | ### Header/Footer:
231 | ```kotlin
232 | // Enabled, disable header/footer by builder
233 | GestureManager.Builder(recyclerView)
234 | .setHeaderEnabled(state)
235 | .setFooterEnabled(state)
236 |
237 | // or directly by adapter
238 | adaper.setHeaderEnabled(state)
239 | adaper.setHeaderEnabled(state)
240 |
241 | // if header or footer is enabled then library will pass viewType (TYPE_HEADER_ITEM, TYPE_FOOTER_ITEM)
242 | // to onCreateViewHolder(parent: ViewGroup, viewType: Int)
243 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GestureViewHolder {
244 | return when (viewType) {
245 | TYPE_HEADER_ITEM -> {
246 | // return header view holder
247 | }
248 | TYPE_FOOTER_ITEM -> {
249 | // return footer view holder
250 | }
251 | else -> {
252 | // return regular view holder
253 | }
254 | }
255 | }
256 |
257 | // if getItemViewType(viewPosition: Int) is used in your adapter then
258 | // firstly call super.getItemViewType() and check if library wants to handle incoming view type
259 | override fun getItemViewType(viewPosition: Int): Int {
260 | val handledType = super.getItemViewType(viewPosition)
261 | if (handledType > 0) {
262 | // library wants to handle this case, simply return
263 | return handledType
264 | }
265 | return yourTypes
266 | }
267 | ```
268 |
269 | # Help
270 | See examples.
271 |
272 | # To do
273 | * examples with data binding
274 | * tests
275 | * different layouts for different swipe directions
276 |
277 | # Licence
278 |
279 | ```
280 | Copyright 2022 thesurix
281 |
282 | Licensed under the Apache License, Version 2.0 (the "License");
283 | you may not use this file except in compliance with the License.
284 | You may obtain a copy of the License at
285 |
286 | http://www.apache.org/licenses/LICENSE-2.0
287 |
288 | Unless required by applicable law or agreed to in writing, software
289 | distributed under the License is distributed on an "AS IS" BASIS,
290 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
291 | See the License for the specific language governing permissions and
292 | limitations under the License.
293 | ```
294 |
295 |
--------------------------------------------------------------------------------
/library/src/main/java/com/thesurix/gesturerecycler/GestureAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.thesurix.gesturerecycler
2 |
3 | import android.view.MotionEvent
4 | import android.view.View
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.thesurix.gesturerecycler.transactions.*
8 | import com.thesurix.gesturerecycler.util.FixedSizeArrayDequeue
9 | import com.thesurix.gesturerecycler.util.getDataOffset
10 | import java.util.*
11 | import kotlin.math.abs
12 |
13 | /**
14 | * Base adapter for gesture recognition, extends this to provide own implementation. T is the data type, K is the ViewHolder type.
15 | * @author thesurix
16 | */
17 | private const val INVALID_DRAG_POS = -1
18 | const val TYPE_HEADER_ITEM = 456789
19 | const val TYPE_FOOTER_ITEM = TYPE_HEADER_ITEM + 1
20 | abstract class GestureAdapter> : RecyclerView.Adapter(), Transactional {
21 |
22 | /** Temp item for swap action */
23 | private var swappedItem: T? = null
24 | /** Start position of the drag action */
25 | private var startDragPos = 0
26 | /** Stop position of the drag action */
27 | private var stopDragPos = INVALID_DRAG_POS
28 | /** Flag that defines if adapter allows manual dragging */
29 | private var manualDragAllowed = false
30 | /** Flag that defines if header item is enabled */
31 | private var headerEnabled = false
32 | /** Flag that defines if footer item is enabled */
33 | private var footerEnabled = false
34 | /** This variable holds stack of data transactions for undo purposes */
35 | private var transactions = FixedSizeArrayDequeue>(1)
36 |
37 | private var gestureListener: OnGestureListener? = null
38 | private var dataChangeListener: OnDataChangeListener? = null
39 | private val emptyViewDataObserver = EmptyViewDataObserver()
40 | private val attachListener = object : View.OnAttachStateChangeListener {
41 |
42 | private var registered = false
43 |
44 | override fun onViewAttachedToWindow(v: View) {
45 | if (!registered) {
46 | registered = true
47 | registerAdapterDataObserver(emptyViewDataObserver)
48 | }
49 | }
50 |
51 | override fun onViewDetachedFromWindow(v: View) {
52 | if (registered) {
53 | registered = false
54 | unregisterAdapterDataObserver(emptyViewDataObserver)
55 | }
56 | resetTransactions()
57 | }
58 | }
59 |
60 | /** Collection for adapter's data */
61 | private val _data = mutableListOf()
62 |
63 | /**
64 | * Returns adapter's data.
65 | * @return adapter's data
66 | */
67 | /**
68 | * Sets adapter data. This method will interrupt pending animations.
69 | * Use [.add], [.remove] or [.insert] or [.setData] to achieve smooth animations.
70 | * @param data data to show
71 | */
72 |
73 | override var data: MutableList
74 | get() = _data
75 | set(data) = setData(data, null)
76 |
77 | /** Listener for data changes inside adapter */
78 | interface OnDataChangeListener {
79 |
80 | /**
81 | * Called when item has been removed by swipe gesture.
82 | * @param item removed item
83 | * @param position removed position
84 | * @param direction the direction to which the ViewHolder is swiped. See [androidx.recyclerview.widget.ItemTouchHelper]
85 | */
86 | fun onItemRemoved(item: T, position: Int, direction: Int)
87 |
88 | /**
89 | * Called when item has been reordered by drag gesture.
90 | * @param item reordered item
91 | * @param fromPos reorder start position
92 | * @param toPos reorder end position
93 | */
94 | fun onItemReorder(item: T, fromPos: Int, toPos: Int)
95 | }
96 |
97 | /** Listener for gestures */
98 | internal interface OnGestureListener {
99 |
100 | /**
101 | * Called when view holder item has pending drag gesture.
102 | * @param viewHolder dragged view holder item
103 | */
104 | fun onStartDrag(viewHolder: GestureViewHolder)
105 | }
106 |
107 | override fun getItemViewType(viewPosition: Int): Int {
108 | if (headerEnabled && viewPosition == 0) {
109 | return TYPE_HEADER_ITEM
110 | }
111 |
112 | val dataSize = if (headerEnabled) _data.size + 1 else _data.size
113 | if (footerEnabled && viewPosition == dataSize) {
114 | return TYPE_FOOTER_ITEM
115 | }
116 | return super.getItemViewType(viewPosition)
117 | }
118 |
119 | override fun onBindViewHolder(holder: K, position: Int, payloads: MutableList) {
120 | val viewType = getItemViewType(position)
121 | if (viewType == TYPE_HEADER_ITEM || viewType == TYPE_FOOTER_ITEM) {
122 | return
123 | }
124 |
125 | holder.bind(getItemByViewPosition(position))
126 | super.onBindViewHolder(holder, position, payloads)
127 | }
128 |
129 | override fun onBindViewHolder(holder: K, position: Int) {
130 | holder.draggableView?.let {
131 | if (manualDragAllowed && holder.canDrag()) {
132 | holder.showDraggableView()
133 | holder.draggableView?.setOnTouchListener { _, motionEvent ->
134 | if (motionEvent.action == MotionEvent.ACTION_DOWN) {
135 | gestureListener?.onStartDrag(holder)
136 | }
137 |
138 | false
139 | }
140 | } else {
141 | holder.hideDraggableView()
142 | }
143 | }
144 | }
145 |
146 | override fun onViewRecycled(holder: K) {
147 | if (holder.isRecyclable) {
148 | holder.recycle()
149 | }
150 | }
151 |
152 | override fun getItemCount(): Int {
153 | return when {
154 | headerEnabled && footerEnabled -> _data.size + 2
155 | headerEnabled || footerEnabled -> _data.size + 1
156 | else -> _data.size
157 | }
158 | }
159 |
160 | override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
161 | super.onAttachedToRecyclerView(recyclerView)
162 | emptyViewDataObserver.recyclerView = recyclerView
163 | recyclerView.addOnAttachStateChangeListener(attachListener)
164 | }
165 |
166 | override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
167 | super.onDetachedFromRecyclerView(recyclerView)
168 | emptyViewDataObserver.recyclerView = null
169 | recyclerView.removeOnAttachStateChangeListener(attachListener)
170 | resetTransactions()
171 | }
172 |
173 | override fun notifyChanged(position: Int) {
174 | notifyItemChanged(position)
175 | }
176 |
177 | override fun notifyInserted(position: Int) {
178 | notifyItemInserted(position)
179 | }
180 |
181 | override fun notifyRemoved(position: Int) {
182 | notifyItemRemoved(position)
183 | }
184 |
185 | override fun notifyMoved(fromPosition: Int, toPosition: Int) {
186 | notifyItemMoved(fromPosition, toPosition)
187 | }
188 |
189 | /**
190 | * Sets adapter data with [DiffUtil.Callback] to achieve smooth animations.
191 | * @param data data to show
192 | * @param diffCallback diff callback to manage internal data changes
193 | */
194 | fun setData(data: List, diffCallback: DiffUtil.Callback?) {
195 | when(diffCallback) {
196 | null -> {
197 | setNewData(data)
198 | notifyDataSetChanged()
199 | }
200 | else -> {
201 | val diffResult = DiffUtil.calculateDiff(diffCallback)
202 | setNewData(data)
203 | diffResult.dispatchUpdatesTo(this)
204 | }
205 | }
206 | resetTransactions()
207 | }
208 |
209 | /**
210 | * Clears data.
211 | */
212 | fun clearData() {
213 | _data.clear()
214 | notifyDataSetChanged()
215 |
216 | resetTransactions()
217 | }
218 |
219 | /**
220 | * Returns item for the given position
221 | * @param position item's position
222 | * @return item
223 | */
224 | fun getItem(position: Int): T {
225 | return _data[position]
226 | }
227 |
228 | /**
229 | * Returns item for the given view position.
230 | * @param position view position
231 | * @return item
232 | */
233 | fun getItemByViewPosition(position: Int): T {
234 | val dataPosition = position + getDataOffset(headerEnabled)
235 | return _data[dataPosition]
236 | }
237 |
238 | /**
239 | * Adds item to the adapter.
240 | * @param item item to add
241 | * @return true if added, false otherwise
242 | */
243 | fun add(item: T): Boolean {
244 | val addTransaction = AddTransaction(item, headerEnabled)
245 | val success = addTransaction.perform(this)
246 |
247 | transactions.offer(addTransaction)
248 | return success
249 | }
250 |
251 | /**
252 | * Removes item from the given position.
253 | * @param position item's position
254 | * @return true if removed, false otherwise
255 | */
256 | fun remove(position: Int): Boolean {
257 | val removeTransaction = RemoveTransaction(position, headerEnabled)
258 | val success = removeTransaction.perform(this)
259 |
260 | transactions.offer(removeTransaction)
261 | return success
262 | }
263 |
264 | /**
265 | * Inserts item in the given position.
266 | * @param item item to insert
267 | * @param position position for the item
268 | */
269 | fun insert(item: T, position: Int) {
270 | val insertTransaction = InsertTransaction(item, position, headerEnabled)
271 | insertTransaction.perform(this)
272 |
273 | transactions.offer(insertTransaction)
274 | }
275 |
276 | /**
277 | * Moves item from one position to another.
278 | * @param fromPosition item's old position
279 | * @param toPosition item's new position
280 | * @return true if moved, false otherwise
281 | */
282 | fun move(fromPosition: Int, toPosition: Int): Boolean {
283 | val moveTransaction = MoveTransaction(fromPosition, toPosition, headerEnabled)
284 | val success = moveTransaction.perform(this)
285 |
286 | transactions.offer(moveTransaction)
287 | return success
288 | }
289 |
290 | /**
291 | * Swap items in given positions.
292 | * @param firstPosition first item position
293 | * @param secondPosition second item position
294 | * @return true if swapped, false otherwise
295 | */
296 | fun swap(firstPosition: Int, secondPosition: Int): Boolean {
297 | val swapTransaction = SwapTransaction(firstPosition, secondPosition, headerEnabled)
298 | val success = swapTransaction.perform(this)
299 |
300 | transactions.offer(swapTransaction)
301 | return success
302 | }
303 |
304 | /**
305 | * Sets empty view. Empty view is used when adapter has no data.
306 | * Pass null to disable empty view feature.
307 | * @param emptyView view to show
308 | */
309 | fun setEmptyView(emptyView: View) {
310 | emptyViewDataObserver.emptyView = emptyView
311 | }
312 |
313 | /**
314 | * Sets empty view visibility listener.
315 | * @param listener empty view visibility listener
316 | */
317 | fun setEmptyViewVisibilityListener(listener: EmptyViewVisibilityListener) {
318 | emptyViewDataObserver.emptyViewVisibilityListener = listener
319 | }
320 |
321 | /**
322 | * Sets undo stack size. If undo stack is full, the oldest action will be removed (default size is 1).
323 | * @param size undo actions size
324 | */
325 | fun setUndoSize(size: Int) {
326 | require(size >= 0) { "Stack can not have negative size." }
327 | transactions = FixedSizeArrayDequeue(size)
328 | }
329 |
330 | /**
331 | * Reverts last data transaction like [.add], [.remove],
332 | * [.insert]. It supports also reverting swipe and drag & drop actions.
333 | *
334 | * @return true for successful undo action, false otherwise
335 | */
336 | fun undoLast(): Boolean {
337 | return transactions.isNotEmpty() && transactions.pollLast().revert(this)
338 | }
339 |
340 | /**
341 | * Sets adapter data change listener.
342 | * @param listener data change listener
343 | */
344 | fun setDataChangeListener(listener: OnDataChangeListener) {
345 | dataChangeListener = listener
346 | }
347 |
348 | /**
349 | * Sets header item state.
350 | * @param enabled true to enable, false to disable
351 | */
352 | fun setHeaderEnabled(enabled: Boolean) {
353 | if (headerEnabled != enabled) {
354 | headerEnabled = enabled
355 | notifyDataSetChanged()
356 | }
357 | }
358 |
359 | /**
360 | * Sets footer item state.
361 | * @param enabled true to enable, false to disable
362 | */
363 | fun setFooterEnabled(enabled: Boolean) {
364 | if (footerEnabled != enabled) {
365 | footerEnabled = enabled
366 | notifyDataSetChanged()
367 | }
368 | }
369 |
370 | /**
371 | * Defines if move to position toPosition is allowed. E.g. it can restrict moves within group
372 | * or deny move over some element that cannot be declared neither header nor footer.
373 | * @param fromPosition view start position
374 | * @param toPosition view end position
375 | * @return returns true if transition is allowed
376 | */
377 | open fun isItemMoveAllowed(fromPosition: Int, toPosition: Int): Boolean = true
378 |
379 | /**
380 | * Sets adapter gesture listener.
381 | * @param listener gesture listener
382 | */
383 | internal fun setGestureListener(listener: OnGestureListener) {
384 | gestureListener = listener
385 | }
386 |
387 | /**
388 | * Dismisses item from the given position.
389 | * @param viewPosition dismissed item position
390 | * @param direction the direction to which the ViewHolder is swiped
391 | */
392 | internal fun onItemDismissed(viewPosition: Int, direction: Int) {
393 | val dataRemovePosition = viewPosition + getDataOffset(headerEnabled)
394 | val removedItem = _data[dataRemovePosition]
395 | val wasRemoved = remove(dataRemovePosition)
396 | if (wasRemoved) {
397 | dataChangeListener?.onItemRemoved(removedItem, dataRemovePosition, direction)
398 | }
399 | }
400 |
401 | /**
402 | * Moves item from one position to another.
403 | * @param fromPosition view start position
404 | * @param toPosition view end position
405 | * @return returns true if transition is successful
406 | */
407 | internal fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
408 | if (!isItemMoveAllowed(fromPosition, toPosition)) {
409 | return false
410 | }
411 |
412 | val viewType = getItemViewType(toPosition)
413 | if (viewType == TYPE_HEADER_ITEM || viewType == TYPE_FOOTER_ITEM) {
414 | return false
415 | }
416 |
417 | val dataFromPosition = fromPosition + getDataOffset(headerEnabled)
418 | val dataToPosition = toPosition + getDataOffset(headerEnabled)
419 | if (swappedItem == null) {
420 | startDragPos = dataFromPosition
421 | swappedItem = _data[dataFromPosition]
422 | }
423 | stopDragPos = dataToPosition
424 |
425 | // Steps bigger than one we have to swap manually in right order
426 | val jumpSize = abs(toPosition - fromPosition)
427 | if (jumpSize > 1) {
428 | val sign = Integer.signum(toPosition - fromPosition)
429 | var startPos = dataFromPosition
430 | for (i in 0 until jumpSize) {
431 | val endPos = startPos + sign
432 | Collections.swap(_data, startPos, endPos)
433 | startPos += sign
434 | }
435 | } else {
436 | Collections.swap(_data, dataFromPosition, dataToPosition)
437 | }
438 | notifyItemMoved(fromPosition, toPosition)
439 | return true
440 | }
441 |
442 | /**
443 | * Called when item has been moved.
444 | */
445 | internal fun onItemMoved() {
446 | swappedItem?.let {
447 | if (stopDragPos != INVALID_DRAG_POS) {
448 | dataChangeListener?.onItemReorder(it, startDragPos, stopDragPos)
449 |
450 | val revertReorderTransaction = RevertReorderTransaction(startDragPos, stopDragPos, headerEnabled)
451 | transactions.offer(revertReorderTransaction)
452 | swappedItem = null
453 | stopDragPos = INVALID_DRAG_POS
454 | }
455 | }
456 | }
457 |
458 | /**
459 | * Enables or disables manual drag actions on items. Manual dragging is disabled by default.
460 | * To allow manual drags provide draggable view, see [GestureViewHolder].
461 | * @param allowState true to enable, false to disable
462 | */
463 | internal fun allowManualDrag(allowState: Boolean) {
464 | manualDragAllowed = allowState
465 | notifyDataSetChanged()
466 | }
467 |
468 | private fun setNewData(data: List) {
469 | _data.clear()
470 | _data.addAll(data)
471 | }
472 |
473 | private fun resetTransactions() {
474 | transactions.clear()
475 | }
476 | }
477 |
478 |
--------------------------------------------------------------------------------