├── .gitignore ├── .idea ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README.md ├── README_images └── recyclerview_bindings.gif ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── fueled │ │ └── recyclerviewbindings │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── fueled │ │ │ └── recyclerviewbindings │ │ │ ├── LoginModel.kt │ │ │ ├── MainActivity.kt │ │ │ ├── adapter │ │ │ └── RVAdapter.kt │ │ │ ├── databinding │ │ │ ├── BindingAdapter.kt │ │ │ └── BindingComponent.kt │ │ │ ├── entity │ │ │ └── User.kt │ │ │ ├── model │ │ │ ├── MainModel.kt │ │ │ └── UserModel.kt │ │ │ ├── mvp │ │ │ ├── MainContract.kt │ │ │ ├── MainContractImpl.kt │ │ │ └── MainPresenterImpl.kt │ │ │ ├── util │ │ │ ├── Extension.kt │ │ │ ├── Mapper.kt │ │ │ └── ViewUtil.kt │ │ │ └── widget │ │ │ ├── ScrollAwareFABBehavior.kt │ │ │ ├── decoration │ │ │ └── SpaceItemDecoration.kt │ │ │ ├── drag │ │ │ ├── DragHandler.java │ │ │ └── DragItemTouchHelperCallback.kt │ │ │ ├── scroll │ │ │ ├── LayoutManagerType.kt │ │ │ ├── RecyclerViewScrollCallback.kt │ │ │ └── RecyclerViewUtil.kt │ │ │ └── swipe │ │ │ ├── SwipeHandler.java │ │ │ └── SwipeItemTouchHelperCallback.kt │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── item_main.xml │ │ └── item_progress.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── fueled │ └── recyclerviewbindings │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Chetan Sachdeva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## RecyclerView Bindings 2 | 3 | RecyclerViewBindings provides a wrapper class [RecyclerViewScrollCallback](./app/src/main/java/com/fueled/recyclerviewbindings/widget/scroll/RecyclerViewScrollCallback.kt) which can be used to add Scroll to Bottom (Endless Scroll) and Pull to Refresh capability to your RecyclerView. You can make use of DataBinding to bind it via XML. 4 | 5 | 6 | 7 | ## How to Use 8 | 9 | ```kotlin 10 | val callback = RecyclerViewScrollCallback 11 | .Builder(visibleThreshold, recyclerView.layoutManager) 12 | .resetLoadingState(resetLoadingState) 13 | .onScrolledToBottom(onScrolledToBottom) 14 | .build() 15 | 16 | recyclerView.clearOnScrollListeners() 17 | recyclerView.addOnScrollListener(callback) 18 | ``` 19 | 20 | ## How to Bind 21 | 22 | In your `Gradle` 23 | 24 | ```groovy 25 | dataBinding { 26 | enabled = true 27 | } 28 | ``` 29 | 30 | In your `BindingAdapter` 31 | 32 | ```kotlin 33 | /** 34 | * @param recyclerView RecyclerView to bind to RecyclerViewScrollCallback 35 | * @param visibleThreshold The minimum number of items to have below your current scroll position before loading more. 36 | * @param resetLoadingState Reset endless scroll listener when performing a new search 37 | * @param onScrolledToBottom OnScrolledListener for RecyclerView scrolled 38 | */ 39 | @BindingAdapter(value = *arrayOf("visibleThreshold", "resetLoadingState", "onScrolledToBottom"), requireAll = false) 40 | fun setRecyclerViewScrollCallback(recyclerView: RecyclerView, visibleThreshold: Int, resetLoadingState: Boolean, 41 | onScrolledToBottom: RecyclerViewScrollCallback.OnScrolledListener) { 42 | 43 | ... // add addOnScrollListener to RecyclerView using OnScrolledListener as above 44 | } 45 | 46 | /** 47 | * @param swipeRefreshLayout Bind swipeRefreshLayout with OnRefreshListener 48 | * @param onRefresh Listener for onRefresh when swiped 49 | */ 50 | @BindingAdapter("onPulledToRefresh") 51 | fun setOnSwipeRefreshListener(swipeRefreshLayout: SwipeRefreshLayout, onPulledToRefresh: Runnable) { 52 | swipeRefreshLayout.setOnRefreshListener { onPulledToRefresh.run() } 53 | } 54 | ``` 55 | 56 | In your `XML` file 57 | 58 | ```xml 59 | 64 | 65 | 76 | 77 | 78 | ``` 79 | 80 | ## Pagination using RxJava (using Subjects) 81 | 82 | ```kotlin 83 | /** 84 | * initialize all resources 85 | * set current page to 1 86 | * create paginator and subscribe to events 87 | */ 88 | override fun initialize() { 89 | currentPage = 1 // set page = 1 90 | paginator = PublishProcessor.create() // create PublishProcessor 91 | 92 | val d = paginator.onBackpressureDrop() 93 | .filter { !loading } // return if it is still loading 94 | .doOnNext { loading = view.showProgress() } // loading = true 95 | .concatMap { contract.getUsersFromServer(it) } // API call 96 | .observeOn(AndroidSchedulers.mainThread()) 97 | .subscribe({ 98 | loading = view.hideProgress() // loading = false 99 | view.showItems(it) // show items 100 | currentPage++ // increment page 101 | }, { 102 | loading = view.hideProgress() // loading = false 103 | view.showError(it.localizedMessage) // show error 104 | }) 105 | 106 | disposables.add(d) 107 | 108 | onLoadMore(currentPage) 109 | } 110 | 111 | /** 112 | * called when list is scrolled to its bottom 113 | * @param page current page (not used) 114 | */ 115 | override fun onLoadMore(page: Int) { 116 | paginator.onNext(currentPage) // increment page if not loading 117 | } 118 | ``` 119 | 120 | ## Library used 121 | 122 | Add Android Support Design, RxJava and RxAndroid dependency to your gradle file. 123 | 124 | ```groovy 125 | dependencies { 126 | compile 'com.android.support:design:{latest_version}' 127 | compile 'io.reactivex.rxjava2:rxandroid:{latest_version}' 128 | compile 'io.reactivex.rxjava2:rxjava:{latest_version}' 129 | } 130 | ``` 131 | 132 | ## Also try 133 | 134 | - [Swipeable RecyclerView](https://github.com/chetdeva/swipeablerecyclerview) 135 | - [Draggable RecyclerView](https://github.com/chetdeva/draggablerecyclerview) 136 | 137 | ## Reference 138 | 139 | - [Endless Scrolling with AdapterViews and RecyclerView, CodePath](https://github.com/codepath/android_guides/wiki/Endless-Scrolling-with-AdapterViews-and-RecyclerView) 140 | - [Pagination with Rx (using Subjects), Kaushik Gopal](https://github.com/kaushikgopal/RxJava-Android-Samples#14-pagination-with-rx-using-subjects) -------------------------------------------------------------------------------- /README_images/recyclerview_bindings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chetdeva/recyclerview-bindings/c42050f7f38738d23cae14d44b3745c26127e099/README_images/recyclerview_bindings.gif -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | android { 6 | compileSdkVersion 27 7 | defaultConfig { 8 | applicationId "com.fueled.recyclerviewbindings" 9 | minSdkVersion 19 10 | targetSdkVersion 27 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | 22 | dataBinding { 23 | enabled = true 24 | } 25 | } 26 | 27 | kapt { 28 | generateStubs = true 29 | } 30 | 31 | dependencies { 32 | def googleSupportLibraryVersion = "27.1.1" 33 | 34 | implementation fileTree(dir: 'libs', include: ['*.jar']) 35 | androidTestImplementation ('com.android.support.test.espresso:espresso-core:3.0.1', { 36 | exclude group: 'com.android.support', module: 'support-annotations' 37 | }) 38 | 39 | implementation "com.android.support:appcompat-v7:${googleSupportLibraryVersion}" 40 | implementation "com.android.support:design:${googleSupportLibraryVersion}" 41 | implementation "com.android.support.constraint:constraint-layout:1.1.0" 42 | 43 | testImplementation 'junit:junit:4.12' 44 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 45 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' 46 | implementation 'io.reactivex.rxjava2:rxjava:2.1.13' 47 | } 48 | repositories { 49 | mavenCentral() 50 | } 51 | -------------------------------------------------------------------------------- /app/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 /Users/chetansachdeva/Library/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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/fueled/recyclerviewbindings/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.fueled.recyclerviewbindings", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/LoginModel.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings 2 | 3 | import android.databinding.BaseObservable 4 | import android.databinding.Bindable 5 | import android.text.TextUtils 6 | 7 | /** 8 | * Copyright (c) 2017 Fueled. All rights reserved. 9 | * 10 | * @author chetansachdeva on 13/10/17 11 | */ 12 | 13 | class LoginModel : BaseObservable() { 14 | 15 | @get:Bindable 16 | var email: String? = null 17 | set(email) { 18 | field = email 19 | notifyPropertyChanged(BR.email) 20 | isLoginEnabled = isEmailAndPasswordSet 21 | } 22 | @get:Bindable 23 | var password: String? = null 24 | set(password) { 25 | field = password 26 | notifyPropertyChanged(BR.password) 27 | isLoginEnabled = isEmailAndPasswordSet 28 | } 29 | @get:Bindable 30 | var isLoginEnabled: Boolean = false 31 | set(loginEnabled) { 32 | field = loginEnabled 33 | notifyPropertyChanged(BR.loginEnabled) 34 | } 35 | 36 | /** 37 | * checks if email and password fields are set 38 | * 39 | * @return isEmailAndPasswordSet 40 | */ 41 | private val isEmailAndPasswordSet: Boolean 42 | get() = !TextUtils.isEmpty(email) && !TextUtils.isEmpty(password) 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings 2 | 3 | import android.databinding.DataBindingUtil 4 | import android.support.v7.app.AppCompatActivity 5 | import android.os.Bundle 6 | import android.widget.Toast 7 | import com.fueled.recyclerviewbindings.databinding.BindingComponent 8 | 9 | import com.fueled.recyclerviewbindings.adapter.RVAdapter 10 | import com.fueled.recyclerviewbindings.databinding.ActivityMainBinding 11 | import com.fueled.recyclerviewbindings.entity.User 12 | import com.fueled.recyclerviewbindings.model.MainModel 13 | import com.fueled.recyclerviewbindings.model.UserModel 14 | import com.fueled.recyclerviewbindings.mvp.MainContract 15 | import com.fueled.recyclerviewbindings.mvp.MainPresenterImpl 16 | import com.fueled.recyclerviewbindings.util.Mapper 17 | import com.fueled.recyclerviewbindings.util.toast 18 | 19 | class MainActivity : AppCompatActivity(), MainContract.View { 20 | 21 | private lateinit var binding: ActivityMainBinding 22 | private lateinit var model: MainModel 23 | 24 | private lateinit var presenter: MainContract.Presenter 25 | 26 | private lateinit var adapter: RVAdapter 27 | private val list = arrayListOf() 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | binding = DataBindingUtil.setContentView(this, R.layout.activity_main, BindingComponent()) 32 | setBindings() 33 | setRecyclerView() 34 | 35 | presenter = MainPresenterImpl(this) 36 | binding.presenter = presenter 37 | } 38 | 39 | /** 40 | * set bindings for model and handler 41 | */ 42 | private fun setBindings() { 43 | model = MainModel() 44 | model.visibleThreshold = 7 45 | binding.model = model 46 | } 47 | 48 | /** 49 | * set RVAdapter to RecyclerView 50 | */ 51 | private fun setRecyclerView() { 52 | adapter = RVAdapter(list) { toast(it?.name) } 53 | binding.rv.adapter = adapter 54 | } 55 | 56 | /** 57 | * show progress loader at bottom of list 58 | */ 59 | override fun showProgress(): Boolean { 60 | binding.rv.post { adapter.add(UserModel().apply { id = -1 }) } // add progress loader (UserModel with id = -1) at bottom 61 | return true 62 | } 63 | 64 | /** 65 | * remove progress loader at bottom of list 66 | * if list is refreshing, clear the list 67 | */ 68 | override fun hideProgress(): Boolean { 69 | if (list.size > 0 && list[list.size - 1].id == -1) { 70 | adapter.remove(list.size - 1) // remove progress loader (UserModel with id = -1) from bottom 71 | } 72 | if (binding.srl.isRefreshing) { 73 | adapter.clear() // clear list 74 | binding.srl.isRefreshing = false // hide pull to refresh 75 | model.resetLoadingState = true // reset loading state and callback 76 | } 77 | return false 78 | } 79 | 80 | /** 81 | * show items and add them to list 82 | */ 83 | override fun showItems(items: List) { 84 | val mappedItems = arrayListOf() 85 | items.map { mappedItems.add(Mapper.mapToUserModel(it)) } 86 | adapter.addAll(mappedItems) 87 | } 88 | 89 | /** 90 | * show error message 91 | */ 92 | override fun showError(message: String) { 93 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 94 | } 95 | 96 | /** 97 | * terminate presenter 98 | */ 99 | override fun onDestroy() { 100 | presenter.terminate() 101 | super.onDestroy() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/adapter/RVAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.adapter 2 | 3 | import android.databinding.DataBindingUtil 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.View 6 | import android.view.ViewGroup 7 | 8 | import com.fueled.recyclerviewbindings.R 9 | import com.fueled.recyclerviewbindings.databinding.ItemMainBinding 10 | import com.fueled.recyclerviewbindings.databinding.ItemProgressBinding 11 | import com.fueled.recyclerviewbindings.model.UserModel 12 | import com.fueled.recyclerviewbindings.util.inflate 13 | 14 | /** 15 | * @author chetansachdeva on 04/06/17 16 | */ 17 | 18 | class RVAdapter(private val list: MutableList, private val listener: (UserModel) -> Unit) : 19 | RecyclerView.Adapter() { 20 | 21 | override fun getItemViewType(position: Int): Int { 22 | return if (list[position].id == -1) ITEM_PROGRESS 23 | else super.getItemViewType(position) 24 | } 25 | 26 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 27 | return if (viewType == ITEM_PROGRESS) VHProgress(parent.inflate(R.layout.item_progress)) 28 | else VHUser(parent.inflate(R.layout.item_main)) 29 | } 30 | 31 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 32 | if (holder is VHUser) holder.bind(list[position], listener) 33 | else (holder as VHProgress).bind(true) 34 | } 35 | 36 | override fun getItemCount() = list.size 37 | 38 | /** 39 | * add list of items and notify 40 | */ 41 | fun addAll(users: List) { 42 | list.addAll(users) 43 | notifyItemRangeChanged(users.size, list.size - 1) 44 | } 45 | 46 | /** 47 | * add an item and notify 48 | */ 49 | fun add(user: UserModel) { 50 | list.add(user) 51 | notifyItemInserted(list.size - 1) 52 | } 53 | 54 | /** 55 | * remove an item and notify 56 | */ 57 | fun remove(position: Int) { 58 | list.removeAt(position) 59 | notifyItemRemoved(list.size) 60 | } 61 | 62 | /** 63 | * clear all items and notify 64 | */ 65 | fun clear() { 66 | list.clear() 67 | notifyDataSetChanged() 68 | } 69 | 70 | /** 71 | * UserModel ViewHolder 72 | */ 73 | internal class VHUser(itemView: View) : RecyclerView.ViewHolder(itemView) { 74 | private val binding: ItemMainBinding = DataBindingUtil.bind(itemView)!! 75 | fun bind(user: UserModel, listener: (UserModel) -> Unit) { 76 | binding.user = user 77 | itemView.setOnClickListener { listener(user) } 78 | } 79 | } 80 | 81 | /** 82 | * Progress ViewHolder 83 | */ 84 | internal class VHProgress(itemView: View) : RecyclerView.ViewHolder(itemView) { 85 | private val binding: ItemProgressBinding = DataBindingUtil.bind(itemView)!! 86 | fun bind(isIndeterminate: Boolean) { 87 | binding.pb.isIndeterminate = isIndeterminate 88 | } 89 | } 90 | 91 | companion object { 92 | private val ITEM_PROGRESS = -1 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/databinding/BindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.databinding 2 | 3 | import android.databinding.BindingAdapter 4 | import android.graphics.drawable.Drawable 5 | import android.support.v4.widget.SwipeRefreshLayout 6 | import android.support.v7.widget.* 7 | import android.support.v7.widget.helper.ItemTouchHelper 8 | import com.fueled.recyclerviewbindings.core.SpaceItemDecoration 9 | import com.fueled.recyclerviewbindings.widget.drag.DragItemTouchHelperCallback 10 | import com.fueled.recyclerviewbindings.widget.scroll.RecyclerViewScrollCallback 11 | import com.fueled.recyclerviewbindings.widget.swipe.SwipeItemTouchHelperCallback 12 | 13 | /** 14 | * Copyright (c) 2017 Fueled. All rights reserved. 15 | 16 | * @author chetansachdeva on 15/09/17 17 | */ 18 | 19 | class BindingAdapter { 20 | 21 | /** 22 | * @param recyclerView RecyclerView to bind to RecyclerViewScrollCallback 23 | * @param visibleThreshold The minimum number of items to have below your current scroll position before loading more. 24 | * @param resetLoadingState Reset endless scroll listener when performing a new search 25 | * @param onScrolledListener OnScrolledListener for RecyclerView scrolled 26 | */ 27 | @BindingAdapter(value = *arrayOf("visibleThreshold", "resetLoadingState", "onScrolledToBottom"), requireAll = false) 28 | fun setRecyclerViewScrollCallback(recyclerView: RecyclerView, visibleThreshold: Int, resetLoadingState: Boolean, 29 | onScrolledListener: RecyclerViewScrollCallback.OnScrolledListener) { 30 | 31 | val callback = RecyclerViewScrollCallback 32 | .Builder(recyclerView.layoutManager) 33 | .visibleThreshold(visibleThreshold) 34 | .onScrolledListener(onScrolledListener) 35 | .resetLoadingState(resetLoadingState) 36 | .build() 37 | 38 | recyclerView.clearOnScrollListeners() 39 | recyclerView.addOnScrollListener(callback) 40 | } 41 | 42 | /** 43 | * @param recyclerView RecyclerView to bind to SpaceItemDecoration 44 | * @param spaceInPx space in pixels 45 | */ 46 | @BindingAdapter("spaceItemDecoration") 47 | fun setSpaceItemDecoration(recyclerView: RecyclerView, spaceInPx: Float) { 48 | if (spaceInPx != 0f) { 49 | val itemDecoration = SpaceItemDecoration(spaceInPx.toInt(), true, false) 50 | recyclerView.addItemDecoration(itemDecoration) 51 | } else { 52 | recyclerView.addItemDecoration(null) 53 | } 54 | } 55 | 56 | /** 57 | * @param recyclerView RecyclerView to bind to DividerItemDecoration 58 | * @param orientation 0 for LinearLayout.HORIZONTAL and 1 for LinearLayout.VERTICAL 59 | */ 60 | @BindingAdapter("dividerItemDecoration") 61 | fun setDividerItemDecoration(recyclerView: RecyclerView, orientation: Int) { 62 | val itemDecoration = DividerItemDecoration(recyclerView.context, orientation) 63 | recyclerView.addItemDecoration(itemDecoration) 64 | } 65 | 66 | /** 67 | * Bind ItemTouchHelper.SimpleCallback with RecyclerView 68 | 69 | * @param recyclerView RecyclerView to bind to SwipeItemTouchHelperCallback 70 | * @param swipeEnabled enable/disable swipe 71 | * @param drawableLeft drawable shown when swiped left 72 | * @param drawableRight drawable shown when swiped right 73 | * @param bgColorSwipeLeft background color when swiped left 74 | * @param bgColorSwipeRight background color when swiped right 75 | * @param onItemSwipeLeft OnItemSwipeListener for Item swiped left 76 | * @param onItemSwipeRight OnItemSwipeListener for Item swiped right 77 | */ 78 | @BindingAdapter(value = *arrayOf("swipeEnabled", "drawableSwipeLeft", "drawableSwipeRight", "bgColorSwipeLeft", "bgColorSwipeRight", "onItemSwipeLeft", "onItemSwipeRight"), requireAll = false) 79 | fun setItemSwipeToRecyclerView(recyclerView: RecyclerView, swipeEnabled: Boolean, drawableLeft: Drawable, drawableRight: Drawable, bgColorSwipeLeft: Int, bgColorSwipeRight: Int, 80 | onItemSwipeLeft: SwipeItemTouchHelperCallback.OnItemSwipeListener, onItemSwipeRight: SwipeItemTouchHelperCallback.OnItemSwipeListener) { 81 | 82 | val swipeCallback = SwipeItemTouchHelperCallback 83 | .Builder(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) 84 | .bgColorSwipeLeft(bgColorSwipeLeft) 85 | .bgColorSwipeRight(bgColorSwipeRight) 86 | .drawableLeft(drawableLeft) 87 | .drawableRight(drawableRight) 88 | .swipeEnabled(swipeEnabled) 89 | .onItemSwipeLeftListener(onItemSwipeLeft) 90 | .onItemSwipeRightListener(onItemSwipeRight) 91 | .build() 92 | 93 | val itemTouchHelper = ItemTouchHelper(swipeCallback) 94 | itemTouchHelper.attachToRecyclerView(recyclerView) 95 | } 96 | 97 | /** 98 | * Bind ItemTouchHelper.SimpleCallback with RecyclerView 99 | 100 | * @param recyclerView RecyclerView to bind to DragItemTouchHelperCallback 101 | * @param dragEnabled enable/disable drag 102 | * @param onItemDrag OnItemDragListener for Item dragged 103 | */ 104 | @BindingAdapter(value = *arrayOf("dragEnabled", "onItemDrag"), requireAll = false) 105 | fun setItemDragToRecyclerView(recyclerView: RecyclerView, dragEnabled: Boolean, 106 | onItemDrag: DragItemTouchHelperCallback.OnItemDragListener) { 107 | 108 | val dragCallback = DragItemTouchHelperCallback 109 | .Builder(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) 110 | .dragEnabled(dragEnabled) 111 | .onItemDragListener(onItemDrag) 112 | .build() 113 | 114 | val itemTouchHelper = ItemTouchHelper(dragCallback) 115 | itemTouchHelper.attachToRecyclerView(recyclerView) 116 | } 117 | 118 | /** 119 | * @param swipeRefreshLayout Bind swipeRefreshLayout with OnRefreshListener 120 | * @param onRefresh Listener for onRefresh when swiped 121 | */ 122 | @BindingAdapter("onPulledToRefresh") 123 | fun setOnSwipeRefreshListener(swipeRefreshLayout: SwipeRefreshLayout, onPulledToRefresh: Runnable) { 124 | swipeRefreshLayout.setOnRefreshListener { onPulledToRefresh.run() } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/databinding/BindingComponent.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.databinding 2 | 3 | import android.databinding.DataBindingComponent 4 | 5 | class BindingComponent : DataBindingComponent { 6 | override fun getBindingAdapter() = BindingAdapter() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/entity/User.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.entity 2 | 3 | /** 4 | * Copyright (c) 2017 Fueled. All rights reserved. 5 | * @author chetansachdeva on 24/09/17 6 | */ 7 | data class User(val id: Int, val name: String) -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/model/MainModel.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.model 2 | 3 | import android.databinding.BaseObservable 4 | import android.databinding.Bindable 5 | 6 | import com.fueled.recyclerviewbindings.BR 7 | 8 | /** 9 | * Copyright (c) 2017 Fueled. All rights reserved. 10 | * 11 | * @author chetansachdeva on 24/09/17 12 | */ 13 | 14 | class MainModel : BaseObservable() { 15 | 16 | @get:Bindable 17 | var resetLoadingState: Boolean = false 18 | set(resetLoadingState) { 19 | field = resetLoadingState 20 | notifyPropertyChanged(BR.resetLoadingState) 21 | } 22 | 23 | @get:Bindable 24 | var visibleThreshold: Int = 0 25 | set(visibleThreshold) { 26 | field = visibleThreshold 27 | notifyPropertyChanged(BR.visibleThreshold) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/model/UserModel.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.model 2 | 3 | import android.databinding.BaseObservable 4 | import android.databinding.Bindable 5 | import com.fueled.recyclerviewbindings.BR 6 | 7 | /** 8 | * Copyright (c) 2017 Fueled. All rights reserved. 9 | 10 | * @author chetansachdeva on 15/09/17 11 | */ 12 | 13 | class UserModel : BaseObservable() { 14 | 15 | @get:Bindable 16 | var id: Int = 0 17 | set(id) { 18 | field = id 19 | notifyPropertyChanged(BR.id) 20 | } 21 | 22 | @get:Bindable 23 | var name: String = "" 24 | set(name) { 25 | field = name 26 | notifyPropertyChanged(BR.name) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/mvp/MainContract.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.mvp 2 | 3 | import com.fueled.recyclerviewbindings.entity.User 4 | import io.reactivex.Flowable 5 | 6 | /** 7 | * Copyright (c) 2017 Fueled. All rights reserved. 8 | 9 | * @author chetansachdeva on 02/09/17 10 | */ 11 | 12 | interface MainContract { 13 | 14 | fun getUsersFromServer(page: Int): Flowable> 15 | 16 | interface Presenter { 17 | fun initialize() 18 | 19 | fun onLoadMore(page: Int) 20 | 21 | fun terminate() 22 | } 23 | 24 | interface View { 25 | fun showProgress(): Boolean 26 | 27 | fun hideProgress(): Boolean 28 | 29 | fun showItems(items: List) 30 | 31 | fun showError(message: String) 32 | } 33 | 34 | companion object { 35 | const val LOAD_DELAY_IN_MILLISECONDS: Long = 1000 36 | const val PAGE_SIZE: Int = 10 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/mvp/MainContractImpl.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.mvp 2 | 3 | import com.fueled.recyclerviewbindings.entity.User 4 | import java.util.concurrent.TimeUnit 5 | import io.reactivex.Flowable 6 | import io.reactivex.android.schedulers.AndroidSchedulers 7 | import io.reactivex.schedulers.Schedulers 8 | 9 | /** 10 | * Copyright (c) 2017 Fueled. All rights reserved. 11 | 12 | * @author chetansachdeva on 02/09/17 13 | */ 14 | 15 | class MainContractImpl : MainContract { 16 | 17 | /** 18 | * get items from server with a delay of MainContract.LOAD_DELAY_IN_MILLISECONDS 19 | */ 20 | override fun getUsersFromServer(page: Int): Flowable> { 21 | return Flowable.just(page) 22 | .delay(MainContract.LOAD_DELAY_IN_MILLISECONDS, TimeUnit.MILLISECONDS, Schedulers.computation()) 23 | .observeOn(AndroidSchedulers.mainThread()) 24 | .map { p -> getItems(p) } 25 | } 26 | 27 | /** 28 | * iterate from start to end where end = page * page_size 29 | */ 30 | private fun getItems(page: Int): List { 31 | return (getStartIndex(page) until page * MainContract.PAGE_SIZE) 32 | .map { makeUser(it) } 33 | } 34 | 35 | /** 36 | * make user from id 37 | */ 38 | private fun makeUser(id: Int) = User(id, "User " + id) 39 | 40 | /** 41 | * deduce start index of page from page number 42 | */ 43 | private fun getStartIndex(page: Int) = (page - 1) * MainContract.PAGE_SIZE 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/mvp/MainPresenterImpl.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.mvp 2 | 3 | import io.reactivex.android.schedulers.AndroidSchedulers 4 | import io.reactivex.disposables.CompositeDisposable 5 | import io.reactivex.functions.Predicate 6 | import io.reactivex.processors.PublishProcessor 7 | 8 | /** 9 | * Copyright (c) 2017 Fueled. All rights reserved. 10 | 11 | * @author chetansachdeva on 02/09/17 12 | */ 13 | 14 | class MainPresenterImpl(private val view: MainContract.View) : MainContract.Presenter { 15 | 16 | private val contract: MainContract 17 | private var currentPage: Int = 0 18 | private val disposables: CompositeDisposable 19 | private lateinit var paginator: PublishProcessor 20 | private var loading: Boolean = false 21 | 22 | init { 23 | contract = MainContractImpl() 24 | disposables = CompositeDisposable() 25 | initialize() 26 | } 27 | 28 | /** 29 | * initialize all resources 30 | * set current page to 1 31 | * create paginator and subscribe to events 32 | */ 33 | override fun initialize() { 34 | currentPage = 1 // set page = 1 35 | paginator = PublishProcessor.create() // create PublishProcessor 36 | 37 | val d = paginator.onBackpressureDrop() 38 | .filter { !loading } // return if it is still loading 39 | .doOnNext { loading = view.showProgress() } // loading = true 40 | .concatMap { contract.getUsersFromServer(it) } // API call 41 | .observeOn(AndroidSchedulers.mainThread()) 42 | .subscribe({ 43 | loading = view.hideProgress() // loading = false 44 | view.showItems(it) // show items 45 | currentPage++ // increment page 46 | }, { 47 | loading = view.hideProgress() // loading = false 48 | view.showError(it.localizedMessage) // show error 49 | }) 50 | 51 | disposables.add(d) 52 | 53 | onLoadMore(currentPage) 54 | } 55 | 56 | /** 57 | * called when list is scrolled to its bottom 58 | * @param page current page (not used) 59 | */ 60 | override fun onLoadMore(page: Int) { 61 | paginator.onNext(currentPage) // increment page if not loading 62 | } 63 | 64 | /** 65 | * terminate presenter and dispose subscriptions 66 | */ 67 | override fun terminate() { 68 | disposables.clear() // clear disposable onDestroy 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/util/Extension.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.util 2 | 3 | import android.app.Activity 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.Toast 8 | 9 | /** 10 | * Copyright (c) 2017 Fueled. All rights reserved. 11 | * @author chetansachdeva on 14/09/17 12 | */ 13 | 14 | fun ViewGroup.inflate(layoutRes: Int): View { 15 | return LayoutInflater.from(context).inflate(layoutRes, this, false) 16 | } 17 | 18 | fun Activity.toast(text: String?, duration: Int = Toast.LENGTH_SHORT) { 19 | text?.let { Toast.makeText(this, text, duration).show() } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/util/Mapper.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.util 2 | 3 | import com.fueled.recyclerviewbindings.entity.User 4 | import com.fueled.recyclerviewbindings.model.UserModel 5 | 6 | /** 7 | * Copyright (c) 2017 Fueled. All rights reserved. 8 | * @author chetansachdeva on 24/09/17 9 | */ 10 | 11 | object Mapper { 12 | 13 | /** 14 | * map user with id 15 | */ 16 | fun mapToUserModel(user: User): UserModel { 17 | val userModel = UserModel() 18 | userModel.id = user.id 19 | userModel.name = user.name 20 | return userModel 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/util/ViewUtil.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.util 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.drawable.BitmapDrawable 6 | import android.graphics.drawable.Drawable 7 | 8 | /** 9 | * Utility class pertaining to Views 10 | 11 | * @author chetansachdeva on 25/07/17 12 | */ 13 | 14 | object ViewUtil { 15 | 16 | /** 17 | * converts drawable to bitmap 18 | 19 | * @param drawable 20 | * * 21 | * @return bitmap 22 | */ 23 | fun getBitmap(drawable: Drawable): Bitmap { 24 | val bitmap: Bitmap 25 | if (drawable is BitmapDrawable) { 26 | val bitmapDrawable = drawable 27 | if (bitmapDrawable.bitmap != null) { 28 | return bitmapDrawable.bitmap 29 | } 30 | } 31 | 32 | if (drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) { 33 | bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) // Single color bitmap will be created of 1x1 pixel 34 | } else { 35 | bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) 36 | } 37 | 38 | val canvas = Canvas(bitmap) 39 | drawable.setBounds(0, 0, canvas.width, canvas.height) 40 | drawable.draw(canvas) 41 | return bitmap 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/widget/ScrollAwareFABBehavior.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.widget 2 | 3 | import android.content.Context 4 | import android.support.design.widget.CoordinatorLayout 5 | import android.support.design.widget.FloatingActionButton 6 | import android.util.AttributeSet 7 | import android.view.View 8 | 9 | /** 10 | * ScrollAwareFABBehavior makes fab show or hide based on RecyclerView scroll events. 11 | */ 12 | 13 | class ScrollAwareFABBehavior(context: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior() { 14 | 15 | override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: FloatingActionButton, 16 | directTargetChild: View, target: View, nestedScrollAxes: Int): Boolean { 17 | return true 18 | } 19 | 20 | override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, 21 | child: FloatingActionButton, 22 | target: View, dxConsumed: Int, dyConsumed: Int, 23 | dxUnconsumed: Int, dyUnconsumed: Int) { 24 | super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed) 25 | // if scrolling up else scrolling down 26 | if (dyConsumed > 0 && child.visibility == View.VISIBLE) { 27 | // We cannot do fab.hide() from API 25 on while using ScrollAwareFABBehavior as it doesn't work as desired. 28 | child.hide(object : FloatingActionButton.OnVisibilityChangedListener() { 29 | override fun onHidden(fab: FloatingActionButton?) { 30 | super.onHidden(fab) 31 | fab!!.visibility = View.INVISIBLE 32 | } 33 | }) 34 | } else if (dyConsumed < 0 && child!!.visibility != View.VISIBLE) { 35 | child.show() 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/widget/decoration/SpaceItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.core 2 | 3 | /** 4 | * Available `ItemDecoration` of `RecyclerView` doesn't allow option of show / hide 5 | * top / bottom dividers. It also doesn't allow us to specify height / width of divider if it's not a drawable 6 | * and just space. 7 | * 8 | * 9 | * Created to have control over item decoration. 10 | * Ability to show / not show divider on top bottom. 11 | * Created by garima-fueled on 21/07/17. 12 | */ 13 | 14 | import android.content.Context 15 | import android.graphics.Rect 16 | import android.support.v7.widget.LinearLayoutManager 17 | import android.support.v7.widget.RecyclerView 18 | import android.view.View 19 | 20 | class SpaceItemDecoration : RecyclerView.ItemDecoration { 21 | 22 | private val space: Int 23 | private var showFirstDivider = false 24 | private var showLastDivider = false 25 | 26 | internal var orientation = -1 27 | 28 | constructor(spaceInPx: Int) { 29 | space = spaceInPx 30 | } 31 | 32 | constructor(spaceInPx: Int, showFirstDivider: Boolean, 33 | showLastDivider: Boolean) : this(spaceInPx) { 34 | this.showFirstDivider = showFirstDivider 35 | this.showLastDivider = showLastDivider 36 | } 37 | 38 | constructor(ctx: Context, resId: Int) { 39 | space = ctx.resources.getDimensionPixelSize(resId) 40 | } 41 | 42 | constructor(ctx: Context, resId: Int, showFirstDivider: Boolean, 43 | showLastDivider: Boolean) : this(ctx, resId) { 44 | this.showFirstDivider = showFirstDivider 45 | this.showLastDivider = showLastDivider 46 | } 47 | 48 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, 49 | state: RecyclerView.State?) { 50 | if (space == 0) { 51 | return 52 | } 53 | 54 | if (orientation == -1) { 55 | getOrientation(parent) 56 | } 57 | 58 | val position = parent.getChildAdapterPosition(view) 59 | if (position == RecyclerView.NO_POSITION || position == 0 && !showFirstDivider) { 60 | return 61 | } 62 | 63 | if (orientation == LinearLayoutManager.VERTICAL) { 64 | outRect.top = space 65 | if (showLastDivider && position == state!!.itemCount - 1) { 66 | outRect.bottom = outRect.top 67 | } 68 | } else { 69 | outRect.left = space 70 | if (showLastDivider && position == state!!.itemCount - 1) { 71 | outRect.right = outRect.left 72 | } 73 | } 74 | } 75 | 76 | private fun getOrientation(parent: RecyclerView): Int { 77 | if (orientation == -1) { 78 | if (parent.layoutManager is LinearLayoutManager) { 79 | val layoutManager = parent.layoutManager as LinearLayoutManager 80 | orientation = layoutManager.orientation 81 | } else { 82 | throw IllegalStateException( 83 | "SpaceItemDecoration can only be used with a LinearLayoutManager.") 84 | } 85 | } 86 | return orientation 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/widget/drag/DragHandler.java: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.widget.drag; 2 | 3 | /** 4 | * @author chetansachdeva on 27/07/17 5 | */ 6 | 7 | public interface DragHandler { 8 | void onItemDragged(int indexFrom, int indexTo); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/widget/drag/DragItemTouchHelperCallback.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.widget.drag 2 | 3 | import android.graphics.Color 4 | import android.support.v7.widget.RecyclerView 5 | import android.support.v7.widget.helper.ItemTouchHelper 6 | 7 | /** 8 | * Reference @link {https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf} 9 | 10 | * @author chetansachdeva on 26/07/17 11 | */ 12 | 13 | class DragItemTouchHelperCallback private constructor(dragDirs: Int, swipeDirs: Int) : ItemTouchHelper.SimpleCallback(dragDirs, swipeDirs) { 14 | 15 | private var dragEnabled: Boolean = false 16 | private lateinit var onItemDragListener: OnItemDragListener 17 | 18 | private constructor(builder: Builder) : this(builder.dragDirs, builder.swipeDirs) { 19 | dragEnabled = builder.dragEnabled 20 | onItemDragListener = builder.onItemDragListener 21 | } 22 | 23 | override fun isLongPressDragEnabled() = dragEnabled 24 | 25 | override fun onMove(recyclerView: RecyclerView, source: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { 26 | if (source.itemViewType != target.itemViewType) { 27 | return false 28 | } 29 | // Notify the adapter of the move 30 | onItemDragListener.onItemDragged(source.adapterPosition, target.adapterPosition) 31 | return true 32 | } 33 | 34 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { 35 | 36 | } 37 | 38 | override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { 39 | // We only want the active item to change 40 | if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { 41 | viewHolder!!.itemView.alpha = ALPHA_FULL / 2 42 | viewHolder.itemView.setBackgroundColor(Color.LTGRAY) 43 | } 44 | super.onSelectedChanged(viewHolder, actionState) 45 | } 46 | 47 | override fun clearView(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder) { 48 | viewHolder.itemView.alpha = ALPHA_FULL 49 | viewHolder.itemView.setBackgroundColor(0) 50 | super.clearView(recyclerView, viewHolder) 51 | } 52 | 53 | interface OnItemDragListener { 54 | fun onItemDragged(indexFrom: Int, indexTo: Int) 55 | } 56 | 57 | class Builder(internal val dragDirs: Int, internal val swipeDirs: Int) { 58 | internal lateinit var onItemDragListener: OnItemDragListener 59 | internal var dragEnabled: Boolean = false 60 | 61 | fun onItemDragListener(value: OnItemDragListener): Builder { 62 | onItemDragListener = value 63 | return this 64 | } 65 | 66 | fun dragEnabled(value: Boolean): Builder { 67 | dragEnabled = value 68 | return this 69 | } 70 | 71 | fun build() = DragItemTouchHelperCallback(this) 72 | } 73 | 74 | companion object { 75 | val ALPHA_FULL = 1.0f 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/widget/scroll/LayoutManagerType.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.widget.scroll 2 | 3 | enum class LayoutManagerType { 4 | DEFAULT, LINEAR, GRID, STAGGERED_GRID 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/widget/scroll/RecyclerViewScrollCallback.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.widget.scroll 2 | 3 | import android.support.v7.widget.RecyclerView 4 | 5 | class RecyclerViewScrollCallback(private val visibleThreshold: Int, private val layoutManager: RecyclerView.LayoutManager) 6 | : RecyclerView.OnScrollListener() { 7 | // The minimum amount of items to have below your current scroll position 8 | // before loading more. 9 | // The current offset index of data you have loaded 10 | private var currentPage = 0 11 | // The total number of items in the dataset after the last load 12 | private var previousTotalItemCount = 0 13 | // True if we are still waiting for the last set of data to load. 14 | private var loading = true 15 | // Sets the starting page index 16 | private val startingPageIndex = 0 17 | 18 | lateinit var layoutManagerType: LayoutManagerType 19 | lateinit var onScrolledListener: OnScrolledListener 20 | 21 | constructor(builder: Builder) : this(builder.visibleThreshold, builder.layoutManager) { 22 | this.layoutManagerType = builder.layoutManagerType 23 | this.onScrolledListener = builder.onScrolledListener 24 | if (builder.resetLoadingState) { 25 | resetState() 26 | } 27 | } 28 | 29 | // This happens many times a second during a scroll, so be wary of the code you place here. 30 | // We are given a few useful parameters to help us work out if we need to load some more data, 31 | // but first we check if we are waiting for the previous load to finish. 32 | override fun onScrolled(view: RecyclerView?, dx: Int, dy: Int) { 33 | val lastVisibleItemPosition = RecyclerViewUtil.getLastVisibleItemPosition(layoutManager, layoutManagerType) 34 | val totalItemCount = layoutManager.itemCount 35 | 36 | // If the total item count is zero and the previous isn't, assume the 37 | // list is invalidated and should be reset back to initial state 38 | if (totalItemCount < previousTotalItemCount) { 39 | this.currentPage = this.startingPageIndex 40 | this.previousTotalItemCount = totalItemCount 41 | if (totalItemCount == 0) { 42 | this.loading = true 43 | } 44 | } 45 | // If it’s still loading, we check to see if the dataset count has 46 | // changed, if so we conclude it has finished loading and update the current page 47 | // number and total item count. 48 | if (loading && totalItemCount > previousTotalItemCount) { 49 | loading = false 50 | previousTotalItemCount = totalItemCount 51 | } 52 | 53 | // If it isn’t currently loading, we check to see if we have breached 54 | // the visibleThreshold and need to reload more data. 55 | // If we do need to reload some more data, we execute onLoadMore to fetch the data. 56 | // threshold should reflect how many total columns there are too 57 | if (!loading && lastVisibleItemPosition + visibleThreshold > totalItemCount) { 58 | currentPage++ 59 | onScrolledListener.onScrolledToBottom(currentPage) 60 | loading = true 61 | } 62 | } 63 | 64 | // Call this method whenever performing new searches 65 | private fun resetState() { 66 | this.currentPage = this.startingPageIndex 67 | this.previousTotalItemCount = 0 68 | this.loading = true 69 | } 70 | 71 | interface OnScrolledListener { 72 | fun onScrolledToBottom(page: Int) 73 | } 74 | 75 | class Builder(internal val layoutManager: RecyclerView.LayoutManager) { 76 | internal var visibleThreshold = 7 77 | internal var layoutManagerType = LayoutManagerType.LINEAR 78 | internal lateinit var onScrolledListener: OnScrolledListener 79 | internal var resetLoadingState: Boolean = false 80 | 81 | fun visibleThreshold(value: Int): Builder { 82 | visibleThreshold = value 83 | return this 84 | } 85 | 86 | fun onScrolledListener(value: OnScrolledListener): Builder { 87 | onScrolledListener = value 88 | return this 89 | } 90 | 91 | fun resetLoadingState(value: Boolean): Builder { 92 | resetLoadingState = value 93 | return this 94 | } 95 | 96 | fun build(): RecyclerViewScrollCallback { 97 | layoutManagerType = RecyclerViewUtil.computeLayoutManagerType(layoutManager) 98 | visibleThreshold = RecyclerViewUtil.computeVisibleThreshold( 99 | layoutManager, layoutManagerType, visibleThreshold) 100 | return RecyclerViewScrollCallback(this) 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/widget/scroll/RecyclerViewUtil.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.widget.scroll 2 | 3 | import android.support.v7.widget.GridLayoutManager 4 | import android.support.v7.widget.LinearLayoutManager 5 | import android.support.v7.widget.RecyclerView 6 | import android.support.v7.widget.StaggeredGridLayoutManager 7 | 8 | /** 9 | * Copyright (c) 2017 Fueled. All rights reserved. 10 | * 11 | * @author chetansachdeva on 28/11/17 12 | */ 13 | 14 | object RecyclerViewUtil { 15 | 16 | fun computeLayoutManagerType(layoutManager: RecyclerView.LayoutManager): LayoutManagerType { 17 | return when (layoutManager) { 18 | is GridLayoutManager -> LayoutManagerType.GRID 19 | is LinearLayoutManager -> LayoutManagerType.LINEAR 20 | is StaggeredGridLayoutManager -> LayoutManagerType.STAGGERED_GRID 21 | else -> LayoutManagerType.DEFAULT 22 | } 23 | } 24 | 25 | fun computeVisibleThreshold(layoutManager: RecyclerView.LayoutManager, 26 | layoutManagerType: LayoutManagerType, visibleThreshold: Int): Int = 27 | when (layoutManagerType) { 28 | LayoutManagerType.GRID -> (layoutManager as GridLayoutManager).spanCount * visibleThreshold 29 | LayoutManagerType.STAGGERED_GRID -> (layoutManager as StaggeredGridLayoutManager).spanCount * visibleThreshold 30 | LayoutManagerType.LINEAR, LayoutManagerType.DEFAULT -> visibleThreshold 31 | } 32 | 33 | fun getLastVisibleItemPosition(layoutManager: RecyclerView.LayoutManager, 34 | layoutManagerType: LayoutManagerType): Int = 35 | when (layoutManagerType) { 36 | LayoutManagerType.LINEAR, LayoutManagerType.GRID -> (layoutManager as LinearLayoutManager).findLastVisibleItemPosition() 37 | LayoutManagerType.STAGGERED_GRID -> { 38 | val lastVisibleItemPositions = (layoutManager as StaggeredGridLayoutManager).findLastVisibleItemPositions(null) 39 | getStaggeredLayoutLastVisibleItem(lastVisibleItemPositions) 40 | } 41 | LayoutManagerType.DEFAULT -> 0 42 | } 43 | 44 | private fun getStaggeredLayoutLastVisibleItem(lastVisibleItemPositions: IntArray): Int { 45 | var maxSize = 0 46 | for (i in lastVisibleItemPositions.indices) { 47 | if (i == 0) { 48 | maxSize = lastVisibleItemPositions[i] 49 | } else if (lastVisibleItemPositions[i] > maxSize) { 50 | maxSize = lastVisibleItemPositions[i] 51 | } 52 | } 53 | return maxSize 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/widget/swipe/SwipeHandler.java: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.widget.swipe; 2 | 3 | /** 4 | * @author chetansachdeva on 27/07/17 5 | */ 6 | 7 | public interface SwipeHandler { 8 | 9 | void onItemSwipedLeft(int position); 10 | 11 | void onItemSwipedRight(int position); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/fueled/recyclerviewbindings/widget/swipe/SwipeItemTouchHelperCallback.kt: -------------------------------------------------------------------------------- 1 | package com.fueled.recyclerviewbindings.widget.swipe 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.graphics.RectF 6 | import android.graphics.drawable.Drawable 7 | import android.support.v7.widget.RecyclerView 8 | import android.support.v7.widget.helper.ItemTouchHelper 9 | 10 | import com.fueled.recyclerviewbindings.util.ViewUtil 11 | 12 | /** 13 | * Reference @link {https://www.learn2crack.com/2016/02/custom-swipe-recyclerview.html} 14 | 15 | * @author chetansachdeva on 26/07/17 16 | */ 17 | 18 | class SwipeItemTouchHelperCallback private constructor(dragDirs: Int, swipeDirs: Int) : ItemTouchHelper.SimpleCallback(dragDirs, swipeDirs) { 19 | 20 | private lateinit var drawableLeft: Drawable 21 | private lateinit var drawableRight: Drawable 22 | private val paintLeft: Paint = Paint(Paint.ANTI_ALIAS_FLAG) 23 | private val paintRight: Paint = Paint(Paint.ANTI_ALIAS_FLAG) 24 | private lateinit var onItemSwipeLeftListener: OnItemSwipeListener 25 | private lateinit var onItemSwipeRightListener: OnItemSwipeListener 26 | private var swipeEnabled: Boolean = false 27 | 28 | private constructor(builder: Builder) : this(builder.dragDirs, builder.swipeDirs) { 29 | setPaintColor(paintLeft, builder.bgColorSwipeLeft) 30 | setPaintColor(paintRight, builder.bgColorSwipeRight) 31 | drawableLeft = builder.drawableLeft 32 | drawableRight = builder.drawableRight 33 | swipeEnabled = builder.swipeEnabled 34 | onItemSwipeLeftListener = builder.onItemSwipeLeftListener 35 | onItemSwipeRightListener = builder.onItemSwipeRightListener 36 | } 37 | 38 | private fun setPaintColor(paint: Paint, color: Int) { 39 | paint.color = color 40 | } 41 | 42 | override fun isItemViewSwipeEnabled() = swipeEnabled 43 | 44 | override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = false 45 | 46 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { 47 | val position = viewHolder.adapterPosition 48 | if (direction == ItemTouchHelper.LEFT) { 49 | onItemSwipeLeftListener.onItemSwiped(position) 50 | } else if (direction == ItemTouchHelper.RIGHT) { 51 | onItemSwipeRightListener.onItemSwiped(position) 52 | } 53 | } 54 | 55 | override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { 56 | 57 | if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { 58 | 59 | val itemView = viewHolder.itemView 60 | val height = itemView.bottom.toFloat() - itemView.top.toFloat() 61 | val width = height / 3 62 | 63 | if (dX > 0) { 64 | val background = RectF(itemView.left.toFloat(), itemView.top.toFloat(), dX, itemView.bottom.toFloat()) 65 | val iconDest = RectF(itemView.left.toFloat() + width, itemView.top.toFloat() + width, itemView.left.toFloat() + 2 * width, itemView.bottom.toFloat() - width) 66 | c.drawRect(background, paintLeft) 67 | c.drawBitmap(ViewUtil.getBitmap(drawableLeft), null, iconDest, paintLeft) 68 | } else { 69 | val background = RectF(itemView.right.toFloat() + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat()) 70 | val iconDest = RectF(itemView.right.toFloat() - 2 * width, itemView.top.toFloat() + width, itemView.right.toFloat() - width, itemView.bottom.toFloat() - width) 71 | c.drawRect(background, paintRight) 72 | c.drawBitmap(ViewUtil.getBitmap(drawableRight), null, iconDest, paintRight) 73 | } 74 | } 75 | super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) 76 | } 77 | 78 | interface OnItemSwipeListener { 79 | fun onItemSwiped(position: Int) 80 | } 81 | 82 | class Builder(internal val dragDirs: Int, internal val swipeDirs: Int) { 83 | internal lateinit var drawableLeft: Drawable 84 | internal lateinit var drawableRight: Drawable 85 | internal var bgColorSwipeLeft: Int = 0 86 | internal var bgColorSwipeRight: Int = 0 87 | internal lateinit var onItemSwipeLeftListener: OnItemSwipeListener 88 | internal lateinit var onItemSwipeRightListener: OnItemSwipeListener 89 | internal var swipeEnabled: Boolean = false 90 | 91 | fun drawableLeft(value: Drawable): Builder { 92 | drawableLeft = value 93 | return this 94 | } 95 | 96 | fun drawableRight(value: Drawable): Builder { 97 | drawableRight = value 98 | return this 99 | } 100 | 101 | fun bgColorSwipeLeft(value: Int): Builder { 102 | bgColorSwipeLeft = value 103 | return this 104 | } 105 | 106 | fun bgColorSwipeRight(value: Int): Builder { 107 | bgColorSwipeRight = value 108 | return this 109 | } 110 | 111 | fun onItemSwipeLeftListener(value: OnItemSwipeListener): Builder { 112 | onItemSwipeLeftListener = value 113 | return this 114 | } 115 | 116 | fun onItemSwipeRightListener(value: OnItemSwipeListener): Builder { 117 | onItemSwipeRightListener = value 118 | return this 119 | } 120 | 121 | fun swipeEnabled(value: Boolean): Builder { 122 | swipeEnabled = value 123 | return this 124 | } 125 | 126 | fun build() = SwipeItemTouchHelperCallback(this) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 18 | 22 | 23 | 28 | 29 | 40 | 41 | 42 | 43 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chetdeva/recyclerview-bindings/c42050f7f38738d23cae14d44b3745c26127e099/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chetdeva/recyclerview-bindings/c42050f7f38738d23cae14d44b3745c26127e099/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chetdeva/recyclerview-bindings/c42050f7f38738d23cae14d44b3745c26127e099/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chetdeva/recyclerview-bindings/c42050f7f38738d23cae14d44b3745c26127e099/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chetdeva/recyclerview-bindings/c42050f7f38738d23cae14d44b3745c26127e099/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chetdeva/recyclerview-bindings/c42050f7f38738d23cae14d44b3745c26127e099/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chetdeva/recyclerview-bindings/c42050f7f38738d23cae14d44b3745c26127e099/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chetdeva/recyclerview-bindings/c42050f7f38738d23cae14d44b3745c26127e099/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chetdeva/recyclerview-bindings/c42050f7f38738d23cae14d44b3745c26127e099/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chetdeva/recyclerview-bindings/c42050f7f38738d23cae14d44b3745c26127e099/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | #EFEFEF 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2dp 5 | 4dp 6 | 8dp 7 | 10dp 8 | 12dp 9 | 16dp 10 | 20dp 11 | 24dp 12 | 32dp 13 | 16dp 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RecyclerViewBindings 3 | Main2Activity 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |