I have a TabLayout with Recyclerview so that when tabs are clicked then the Recyclerview is scrolled to a particular position.
I want the reverse procedure as well such that when the Recyclerview is scrolled to a particular position then the particular tab is highlighted.
For example: If there are 4 tabs in the TabLayout and when Recyclerview is scrolled to 5th position (item visible and below TabLayout) then 3rd tab should be highlighted.
Here when 'How it works' appears below TabLayout then tabs 'How it works' should be highlighted.
Try this
follow this steps
Add a ScrollListener to your RecyclerView
than find first visible item of your RecyclerView
set the select the tab in TabLayout as per position of your RecyclerView
SAMPLE CODE
myRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int itemPosition=linearLayoutManager.findFirstCompletelyVisibleItemPosition();
if(itemPosition==0){ // item position of uses
TabLayout.Tab tab = myTabLayout.getTabAt(Index);
tab.select();
}else if(itemPosition==1){// item position of side effects
TabLayout.Tab tab = myTabLayout.getTabAt(Index);
tab.select();
}else if(itemPosition==2){// item position of how it works
TabLayout.Tab tab = myTabLayout.getTabAt(Index);
tab.select();
}else if(itemPosition==3){// item position of precaution
TabLayout.Tab tab = myTabLayout.getTabAt(Index);
tab.select();
}
}
});
EDIT
public class MyActivity extends AppCompatActivity {
RecyclerView myRecyclerView;
TabLayout myTabLayout;
LinearLayoutManager linearLayoutManager;
ArrayList<String> arrayList = new ArrayList<>();
DataAdapter adapter;
private boolean isUserScrolling = false;
private boolean isListGoingUp = true;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
myTabLayout = findViewById(R.id.myTabLayout);
myRecyclerView = findViewById(R.id.myRecyclerView);
linearLayoutManager = new LinearLayoutManager(this);
myRecyclerView.setLayoutManager(linearLayoutManager);
myRecyclerView.setHasFixedSize(true);
for (int i = 0; i < 120; i++) {
arrayList.add("Item " + i);
}
adapter= new DataAdapter(this,arrayList);
myRecyclerView.setAdapter(adapter);
myTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
#Override
public void onTabSelected(TabLayout.Tab tab) {
isUserScrolling = false ;
int position = tab.getPosition();
if(position==0){
myRecyclerView.smoothScrollToPosition(0);
}else if(position==1){
myRecyclerView.smoothScrollToPosition(30);
}else if(position==2){
myRecyclerView.smoothScrollToPosition(60);
}else if(position==3){
myRecyclerView.smoothScrollToPosition(90);
}
}
#Override
public void onTabUnselected(TabLayout.Tab tab) {
}
#Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
myRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
isUserScrolling = true;
if (isListGoingUp) {
//my recycler view is actually inverted so I have to write this condition instead
if (linearLayoutManager.findLastCompletelyVisibleItemPosition() + 1 == arrayList.size()) {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
if (isListGoingUp) {
if (linearLayoutManager.findLastCompletelyVisibleItemPosition() + 1 == arrayList.size()) {
Toast.makeText(MyActivity.this, "exeute something", Toast.LENGTH_SHORT).show();
}
}
}
}, 50);
//waiting for 50ms because when scrolling down from top, the variable isListGoingUp is still true until the onScrolled method is executed
}
}
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int itemPosition = linearLayoutManager.findFirstVisibleItemPosition();
if(isUserScrolling){
if (itemPosition == 0) { // item position of uses
TabLayout.Tab tab = myTabLayout.getTabAt(0);
tab.select();
} else if (itemPosition == 30) {// item position of side effects
TabLayout.Tab tab = myTabLayout.getTabAt(1);
tab.select();
} else if (itemPosition == 60) {// item position of how it works
TabLayout.Tab tab = myTabLayout.getTabAt(2);
tab.select();
} else if (itemPosition == 90) {// item position of precaution
TabLayout.Tab tab = myTabLayout.getTabAt(3);
tab.select();
}
}
}
});
}
}
private fun syncTabWithRecyclerView() {
// Move recylerview to the position selected by user
menutablayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
if (!isUserScrolling) {
val position = tab.position
linearLayoutManager.scrollToPositionWithOffset(position, 0)
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
// Detect recyclerview position and select tab respectively.
menuRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
isUserScrolling = true
} else if (newState == RecyclerView.SCROLL_STATE_IDLE)
isUserScrolling = false
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (isUserScrolling) {
var itemPosition = 0
if (dy > 0) {
// scrolling up
itemPosition = linearLayoutManager.findLastVisibleItemPosition()
} else {
// scrolling down
itemPosition = linearLayoutManager.findFirstVisibleItemPosition()
}
val tab = menutablayout.getTabAt(itemPosition)
tab?.select()
}
}
})
}
I figured you don't even need those flags and it was enough to override RecyclerView's onScrolled and select tab and scroll to position when tab is selected and that Tab wasn't already selected:
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val llm = recyclerView.layoutManager as LinearLayoutManager
// depending on sections'heights one may want to add more logic
// on how to determine which section to scroll to
val firstCompletePos = llm.findFirstCompletelyVisibleItemPosition()
if (firstCompletePos != tabLayout.selectedTabPosition)
tabLayout.getTabAt(firstCompletePos)?.select()
}
Then I have a TextView which is set as customView to tabLayout:
tabLayout.addTab(newTab().also { tab ->
tab.customView = AppCompatTextView(context).apply {
// set layout params match_parent, so the entire section is clickable
// set style, gravity, text etc.
setOnClickListener {
tabLayout.selectTab(tab)
recyclerView.apply {
val scrollTo = tabLayout.selectedTabPosition
smoothScrollToPosition(scrollTo)
}
}
}
})
With this setup you have:
Tab selected when user both scrolls and flings
RecyclerView is scrolled when user clicks on the tab.
I use information from another answers but here is some omissions in code, that makes it not complete and bad working. My solution 100% work without lags. Photo of complete screen you may see in the end.
This is the code inside Fragment:
private val layoutManager get() = recyclerView?.layoutManager as? LinearLayoutManager
/**
* [SmoothScroller] need for smooth scrolling inside [tabListener] of [recyclerView]
* to top border of [RecyclerView.ViewHolder].
*/
private val smoothScroller: SmoothScroller by lazy {
object : LinearSmoothScroller(context) {
override fun getVerticalSnapPreference(): Int = SNAP_TO_START
}
}
/**
* Variable for prevent calling of [RecyclerView.OnScrollListener.onScrolled]
* inside [scrollListener], when user click on [TabLayout.Tab] and
* [tabListener] was called.
*
* Fake calls happens because of [tabListener] have smooth scrolling to position,
* and when [scrollListener] catch scrolling and call [TabLayout.Tab.select].
*/
private var isTabClicked = false
/**
* Variable for prevent calling of [TabLayout.OnTabSelectedListener.onTabSelected]
* inside [tabListener], when user scroll list and function
* [RecyclerView.OnScrollListener.onScrolled] was called inside [scrollListener].
*
* Fake calls happens because [scrollListener] contains call of [TabLayout.Tab.select],
* which in turn calling click handling inside [tabListener].
*/
private var isScrollSelect = false
private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
/**
* Reset [isTabClicked] key when user start scroll list.
*/
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
isTabClicked = false
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
/**
* Prevent scroll handling after tab click (see inside [tabListener]).
*/
if (isTabClicked) return
val commonIndex = commonIndex ?: return
val karaokeIndex = karaokeIndex ?: return
val socialIndex = socialIndex ?: return
val reviewIndex = reviewIndex ?: return
val addIndex = addIndex ?: return
when (layoutManager?.findFirstVisibleItemPosition() ?: return) {
in commonIndex until karaokeIndex -> selectTab(TabIndex.COMMON)
in karaokeIndex until socialIndex -> selectTab(TabIndex.KARAOKE)
in socialIndex until reviewIndex -> {
/**
* In case if [reviewIndex] can't reach top of the list,
* to become first visible item. Need check [addIndex]
* (last element of list) completely visible or not.
*/
if (layoutManager?.findLastCompletelyVisibleItemPosition() != addIndex) {
selectTab(TabIndex.CONTACTS)
} else {
selectTab(TabIndex.REVIEWS)
}
}
in reviewIndex until addIndex -> selectTab(TabIndex.REVIEWS)
}
}
/**
* It's very important to skip cases when [TabLayout.Tab] is checked like current,
* otherwise [tabLayout] will terribly lagging on [recyclerView] scroll.
*/
private fun selectTab(#TabIndex index: Int) {
val tab = tabLayout?.getTabAt(index) ?: return
if (!tab.isSelected) {
recyclerView?.post {
isScrollSelect = true
tab.select()
}
}
}
}
private val tabListener = object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) = scrollToPosition(tab)
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
/*
* If user click on tab again.
*/
override fun onTabReselected(tab: TabLayout.Tab?) = scrollToPosition(tab)
private fun scrollToPosition(tab: TabLayout.Tab?) {
/**
* Prevent scroll to position calling from [scrollListener].
*/
if (isScrollSelect) {
isScrollSelect = false
return
}
val position = when (tab?.position) {
TabIndex.COMMON -> commonIndex
TabIndex.KARAOKE -> karaokeIndex
TabIndex.CONTACTS -> socialIndex
TabIndex.REVIEWS -> reviewIndex
else -> null
}
if (position != null) {
isTabClicked = true
smoothScroller.targetPosition = position
layoutManager?.startSmoothScroll(smoothScroller)
}
}
}
private val commonIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.Info }
private val karaokeIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.Karaoke }
private val socialIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.Social }
private val reviewIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.ReviewHeader }
private val addIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.AddReview }
Extension:
private const val ND_INDEX = -1
fun <T> List<T>.validIndexOfFirst(predicate: (T) -> Boolean): Int? {
return indexOfFirst(predicate).takeIf { it != ND_INDEX }
}
TabIndex class for getting tab by position:
#IntDef(TabIndex.COMMON, TabIndex.KARAOKE, TabIndex.CONTACTS, TabIndex.REVIEWS)
private annotation class TabIndex {
companion object {
const val COMMON = 0
const val KARAOKE = 1
const val CONTACTS = 2
const val REVIEWS = 3
}
}
And it's how looks my ClubScreenItem:
sealed class ClubScreenItem {
class Info(val data: ClubItem): ClubScreenItem()
...
class Karaoke(...): ClubScreenItem()
class Social(...): ClubScreenItem()
...
class ReviewHeader(...): ClubScreenItem()
...
object AddReview : ClubScreenItem()
}
This is how looks screen:
Try this,
Simple Step :
Detect RecyclerView scroll state
Use findFirstVisibleItemPosition() to return the adapter position of the first visible view
Change the tab based on RecyclerView item position
Done
private fun syncTabWithRecyclerView() {
var isUserScrolling = false
val layoutManager = binding.recyclerViewGroup.layoutManager as LinearLayoutManager
val tabListener = object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val tabPosition = tab?.position
if (tabPosition != null) {
viewModel.setTabPosition(tabPosition)
// prevent RecyclerView to snap to its item start position while user scrolling,
// idk how to explain this XD
if (!isUserScrolling){
layoutManager.scrollToPositionWithOffset(tabPosition, 0)
}
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabReselected(tab: TabLayout.Tab?) {}
}
binding.tabLayout.addOnTabSelectedListener(tabListener)
// Detect recyclerview scroll state
val onScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
isUserScrolling = true
} else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
isUserScrolling = false
}
}
// this just represent my tab name using enum class ,
// and ordinal is just the index of its position in enum
val hardcase3D = CaseType.HARDCASE_3D.ordinal
val softcaseBlackmatte = CaseType.SOFTCASE_BLACKMATTE.ordinal
val softcaseTransparent = CaseType.SOFTCASE_TRANSPARENT.ordinal
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (isUserScrolling) {
when (layoutManager.findFirstVisibleItemPosition()) {
in hardcase3D until softcaseBlackmatte -> {
viewModel.setTabPosition(hardcase3D)
}
in softcaseBlackmatte until softcaseTransparent -> {
viewModel.setTabPosition(softcaseBlackmatte)
}
softcaseTransparent -> {
viewModel.setTabPosition(softcaseTransparent)
}
}
}
}
}
binding.recyclerViewGroup.addOnScrollListener(onScrollListener)
}
viewModel , you can simply use liveData if you want
private var _tabPosition = MutableStateFlow(CaseType.HARDCASE_3D)
val tabPostition : StateFlow<CaseType>
get() = _tabPosition
fun setTabPosition(position: Int){
_tabPosition.value = CaseType.values()[position]
}
Observer
lifecycleScope.launch(Dispatchers.Default) {
viewModel.tabPostition.collect { caseType ->
val positionIndex = CaseType.values().indexOf(caseType)
handleSelectedTab(positionIndex)
}
}
and handleSelectedTab
private fun handleSelectedTab(index: Int) {
val tab = binding.tabLayout.getTabAt(index)
tab?.select()
}
enum
enum class CaseType(val caseTypeName:String) {
HARDCASE_3D("Hardcase 3D"),
SOFTCASE_BLACKMATTE("Softcase Blackmatte"),
SOFTCASE_TRANSPARENT("Softcase Transparent")
}
Related
I have a recyclerview where view type is edittext and on value entered of the edittext, other edittext values should be added with current value.
value entered in edittext item 1 of recyclerview and value entered in edittext item 2 should be the sum in the edittext item 3 which is not editable.
So in below code, the type 'Edittext' is used to identify that these edittext have text watchers and on change of these editttext values, the values of calculations be set in else part.
How to update the data in else code when i am updating the text in editable edittext. this is my code below.
Anyone having solution,
// enter code here
class ProfileDataSubAdapter(
val context: Context,
var repeatedDataList: RealmList<EntityDetails>
) : RecyclerView.Adapter<ProfileDataSubAdapter.DataAdapterViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): DataAdapterViewHolder {
val layout = when (viewType) {
TYPE_EDITTEXT -> R.layout.item_edittext
TYPE_SPINNER -> R.layout.item_spinner
TYPE_CALCULATE -> R.layout.item_edittext
else -> throw IllegalArgumentException("Invalid view type")
}
val view = LayoutInflater
.from(context)
.inflate(layout, parent, false)
return DataAdapterViewHolder(view)
}
override fun onBindViewHolder(holder: DataAdapterViewHolder, position: Int) {
holder.bind(repeatedDataList[position], context, position)
}
fun getRepeatedList(): RealmList<EntityDetails>? {
repeatedDataList.forEach {
if (it.ValueType.equals("EditText") && it.UserEnterValue?.isNullOrEmpty()!!) {
Toast.makeText(context, "Field ${it.EntityName} is empty", Toast.LENGTH_LONG).show()
return null;
}
}
return repeatedDataList
}
override fun getItemCount(): Int = repeatedDataList.size
override fun getItemViewType(position: Int): Int {
if (repeatedDataList[position]?.ValueType == "EditText") {
return TYPE_EDITTEXT
} else if (repeatedDataList[position]?.ValueType == "RadioButton") {
return TYPE_SPINNER
} else
return TYPE_CALCULATE
}
companion object {
private const val TYPE_EDITTEXT = 0
private const val TYPE_SPINNER = 1
private const val TYPE_CALCULATE = 3
}
inner class DataAdapterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindEdittextData(item: EntityDetails, position: Int) {
//Do your view assignment here from the data model
itemView.findViewById<TextView>(R.id.tv_edit_text_label)?.text = item.EntityName
val content = itemView.findViewById<EditText>(R.id.et_content)
if (item.UserEnterValue?.isNotEmpty()!!) {
content.setText(item.UserEnterValue)
}
//if type is edittext then set text watchers here
if (item.ValueType == "EditText") {
content.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
repeatedDataList[position]?.UserEnterValue = p0.toString()
}
override fun afterTextChanged(p0: Editable?) {
}
})
}
// else if type is calculation set the total here
else {
content.setInputType(InputType.TYPE_NULL)
}
}
fun bindSpinnerData(item: EntityDetails, context: Context) {
//Do your view assignment here from the data model
itemView.findViewById<TextView>(R.id.tv_spinner_label)?.text = item.EntityName
val spinner = itemView.findViewById<Spinner>(R.id.spinner_content)
spinner.adapter = ArrayAdapter<String>(
context,
R.layout.row_spinner,
R.id.txt_content,
item.Values?.map { it.Value } as MutableList<String>
)
// spnr_house.setSelection(getIndexSpinner(spnr_house, noOfYearsInCurrentHouse?.trim()))
spinner.setSelection(item.Values!!.indexOfFirst {
it.IsSelected == true
}, false)
// spinner.setSelection()
spinner.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(view: AdapterView<*>?) {
}
override fun onItemSelected(
parentView: AdapterView<*>?,
selectedView: View?,
pos: Int,
id: Long
) {
val selectedItem =
spinner.getItemAtPosition(pos)
.toString()
if (selectedItem != "Select") {
item.Values?.find { it.Value == selectedItem }?.IsSelected =
true
val list = item.Values?.filter { it.Value != selectedItem }
list?.forEach { it.IsSelected = false }
}
}
}
}
fun bind(
dataModel: EntityDetails?,
context: Context,
position: Int
) {
if (dataModel?.ValueType == "EditText" || dataModel?.ValueType == "Calculation")
bindEdittextData(dataModel, position)
else if (dataModel?.ValueType == "RadioButton")
bindSpinnerData(dataModel, context)
}
}
}
//end code
I try connect TabLayout with ViewPager2 via TabLayoutMediator but items not displayed correctly
How i can fix it ?
Here is my code :
class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int) = SalesOfficesFragment()
}
private fun initViews() {
val pagerAdapter = ScreenSlidePagerAdapter(context as FragmentActivity)
binding.viewPager.adapter = pagerAdapter
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
}.attach()
}
You need to set the tab.text in the TabLayoutMediator:
private fun initViews() {
val pagerAdapter = ScreenSlidePagerAdapter(context as FragmentActivity)
binding.viewPager.adapter = pagerAdapter
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
// Set the title of the tabs
when (position) {
0 -> tab.text = "First element"
else -> tab.text = "Other elements"
}
}.attach()
}
Is there a way to combine two adapters that are inside a Callback from Model?
Like, I get data from two sources and I want to display both of them in the same recyclerView
I tried to load the method first then put both adapters on concatAdapter but I got a null from this concatAdapter and then my app crashed
here is my method in Java
private void fetchDataKabupaten(){
progressBar.setVisibility(View.VISIBLE);
token = getIntent().getStringExtra("token");
Call<KabupatenModel> kabupatenModelCall = RetrofitClient.getLoginInterface().getKabupatenData("Bearer "+token);
kabupatenModelCall.enqueue(new Callback<KabupatenModel>() {
#Override
public void onResponse(Call<KabupatenModel> call, Response<KabupatenModel> response) {
kabupatenList = response.body().getKabupaten();
adKabupaten = new KabupatenAdapter(MainActivity.this, kabupatenList);
progressBar.setVisibility(View.GONE);
}
}
private void fetchDataKecamatan(){
progressBar.setVisibility(View.VISIBLE);
token = getIntent().getStringExtra("token");
Call<KecamatanModel> kecamatanModelCall = RetrofitClient.getLoginInterface().getKecamatanData("Bearer " + token);
kecamatanModelCall.enqueue(new Callback<KecamatanModel>() {
#Override
public void onResponse(Call<KecamatanModel> call, Response<KecamatanModel> response) {
kecamatanList = response.body().getKecamatan();
adKecamatan = new KecamatanAdapter(MainActivity.this, kecamatanList);
progressBar.setVisibility(View.GONE);
}
}
I have tried to call both in MainActivity then setAdapter with concatAdapter like this:
fetchDataKabupaten();
fetchDataKecamatan();
concatAdapter = new ConcatAdapter(adKecamatan, adKabupaten);
rV.setAdapter(concatAdapter);
why is it null?
but, when I only set one Adapter inside the response method it works.
you can only use one adapter in recyclerview, but adapter can have multiple view.
for example I have adapter for chat, with right and left view
Adapter Chat Detail
class AdapterChatDetail(val datas: ArrayList<ChatMessage>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private lateinit var ctx: Context
companion object {
const val LEFT = 0
const val RIGHT = 1
}
inner class ViewHolderLeft(val binding: AdapterMyChatLeftBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(data: ChatMessage) {
}
}
inner class ViewHolderRight(val binding: AdapterMyChatRightBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(data: ChatMessage) {
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
ctx = parent.context
return when (viewType) {
LEFT -> ViewHolderLeft(
AdapterMyChatLeftBinding.inflate(
LayoutInflater.from(ctx),
parent,
false
)
)
RIGHT -> ViewHolderRight(
AdapterMyChatRightBinding.inflate(
LayoutInflater.from(ctx),
parent,
false
)
)
else -> ViewHolderLeft(
AdapterMyChatLeftBinding.inflate(
LayoutInflater.from(ctx),
parent,
false
)
)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getItemViewType(position)) {
LEFT -> (holder as ViewHolderLeft).bind(datas[position])
RIGHT -> (holder as ViewHolderRight).bind(datas[position])
}
}
override fun getItemCount() = datas.size
override fun getItemViewType(position: Int): Int {
val data = datas[position]
return if (data.position == "right") RIGHT else LEFT
}
}
App has bottom navigation menus and fragments. This is the fragment which requires swipe detection similar to Tinder:
class HomeFragment : Fragment() {
private lateinit var homeViewModel: HomeViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
homeViewModel =
ViewModelProvider(this).get(HomeViewModel::class.java)
val root = inflater.inflate(R.layout.fragment_home, container, false)
val textView: TextView = root.findViewById(R.id.text_home)
homeViewModel.text.observe(viewLifecycleOwner, {
textView.text = it
})
root.setOnTouchListener(object : OnSwipeTouchListener(requireContext()) {
override fun onSwipeRight() {
Toast.makeText(requireContext(), "Swiped right!", Toast.LENGTH_SHORT).show()
}
override fun onSwipeLeft() {
Toast.makeText(requireContext(), "Swiped left!", Toast.LENGTH_SHORT).show()
}
})
return root
}
}
The app needs Swipe features like Tinder.
Following is the Listener class:
open class OnSwipeTouchListener(ctx: Context) : OnTouchListener {
val gestureDetector: GestureDetector
companion object {
private val SWIPE_THRESHOLD = 100
private val SWIPE_VELOCITY_THRESHOLD = 100
}
init {
gestureDetector = GestureDetector(ctx, GestureListener())
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
var isTouch = false
if (gestureDetector != null && event != null) {
isTouch = gestureDetector.onTouchEvent(event)
} else {
isTouch = true
}
return isTouch
}
inner class GestureListener : SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
return false
}
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
var result = false
try {
val diffY = e1?.y?.let { e2?.y?.minus(it) }
val diffX = e1?.x?.let { e2?.x?.minus(it) }
if (diffX != null && diffY != null) {
if (abs(diffX) > abs(diffY)) {
if (abs(diffX) > SWIPE_THRESHOLD && abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
if (diffX > 0) {
onSwipeRight()
} else {
onSwipeLeft()
}
result = true
}
}
} else {
onSwipeRight()
result = true
}
} catch (exception: Exception) {
exception.printStackTrace()
}
return result
}
}
open fun onSwipeRight() {}
open fun onSwipeLeft() {}
open fun onSwipeTop() {}
open fun onSwipeBottom() {}
open fun onSwipeDown() {
}
}
The swipe feature or any sort of touch listeners are not working at all. Why is this happening? I'm new to Kotlin and mobile app development. So any help is very appreciated.
In Android, a gesture is defined as the beginning of ACTION_DOWN and ending with ACTION_UP. If you want your view to receive a gesture, you MUST return true for ACTION_DOWN, otherwise, you will got nothing.
Root cause
override fun onDown(e: MotionEvent): Boolean {
return false
}
Because in this callback, you return false, that means your view does not show interest to any gesture, it explains why all callback such as onFling(), onSwipeRight(), onSwipeLeft() is not called.
Solution
Return true for the ACTION_DOWN event.
override fun onDown(e: MotionEvent): Boolean {
return true
}
Here is a great article about the Android Touch System. Please take a look when you have time.
Hello I need to make the infinite scrolling (Pagination) with the recyclerview using the staggeredGridLayoutManager. Pagination is working but the problem is that onLoadMore() function is called so many times while scrolling which causing problems, here is my code:
newSearchAdapter = new NewSearchAdapter(getActivity(), gridData);
StaggeredGridLayoutManager mLayoutManager;
mLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
mLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
rv_NewProfilesGrid.setLayoutManager(mLayoutManager);
rv_NewProfilesGrid.setAdapter(newSearchAdapter);
rv_NewProfilesGrid.addOnScrollListener(new EndlessRecyclerOnScrollListenerStaggeredLayoutmanager(mLayoutManager) {
#Override
public void onLoadMore(int current_page) {
//calling the api
}
});
and here is my scroll listner
I think the problem here is with the getFirstVisibleItems() function because with GridLayoutManger or LinearLayoutManager it returns an integer but with StaggeredLayout it returns an int Array, so I did the following:
public abstract class EndlessRecyclerOnScrollListenerStaggeredLayoutmanager extends RecyclerView.OnScrollListener {
public static String TAG = EndlessRecyclerOnScrollListenerStaggeredLayoutmanager.class.getSimpleName();
private int scrolledDistance = 0;
private boolean controlsVisible = false;
private boolean loading = true; // True if we are still waiting for the last set of data to load.
private int visibleThreshold = 5; // The minimum amount of items to have below your current scroll position before loading more.
public static boolean loadOneTime = false;
private int pastVisibleItems, visibleItemCount, totalItemCount,previousTotal;
private int current_page = 1;
private StaggeredGridLayoutManager mStaggeredGridLayoutManager;
public EndlessRecyclerOnScrollListenerStaggeredLayoutmanager(StaggeredGridLayoutManager staggeredGridLayoutManager) {
this.mStaggeredGridLayoutManager = staggeredGridLayoutManager;
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
visibleItemCount = recyclerView.getChildCount();
totalItemCount = mStaggeredGridLayoutManager.getItemCount();
int[] firstVisibleItems = null;
firstVisibleItems = mStaggeredGridLayoutManager.findFirstVisibleItemPositions(firstVisibleItems);
if (firstVisibleItems != null && firstVisibleItems.length > 0) {
pastVisibleItems = firstVisibleItems[0];
}
if (loading) {
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
loading = false;
previousTotal = totalItemCount;
}
}
if (!loading && (totalItemCount - visibleItemCount)
<= (pastVisibleItems + visibleThreshold)) {
// End has been reached
// Do something
current_page++;
loadOneTime=false;
onLoadMore(current_page);
loading = true;
}
if (scrolledDistance > 1 && controlsVisible) {
controlsVisible = false;
scrolledDistance = 0;
} else if (scrolledDistance < -1 && !controlsVisible) {
controlsVisible = true;
scrolledDistance = 0;
}
if ((controlsVisible && dy > 0) || (!controlsVisible && dy < 0)) {
scrolledDistance += dy;
}
}
public abstract void onLoadMore(int current_page);
}
Try this. I borrowed it from a stack overflow answer and borrowed another answer and combine to of them I finally solve it.
import android.util.Log
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
abstract class PaginationScrollListener constructor() :
RecyclerView.OnScrollListener() {
private lateinit var mLayoutManager: RecyclerView.LayoutManager
constructor(layoutManager: GridLayoutManager) : this() {
this.mLayoutManager = layoutManager
}
constructor(layoutManager: StaggeredGridLayoutManager) : this() {
this.mLayoutManager = layoutManager
}
constructor(layoutManager: LinearLayoutManager) : this() {
this.mLayoutManager = layoutManager
}
/*
Method gets callback when user scroll the search list
*/
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount = mLayoutManager.childCount
val totalItemCount = mLayoutManager.itemCount
var firstVisibleItemPosition = 0
when (mLayoutManager) {
is StaggeredGridLayoutManager -> {
val firstVisibleItemPositions =
(mLayoutManager as StaggeredGridLayoutManager).findFirstVisibleItemPositions(null)
// get maximum element within the list
firstVisibleItemPosition = firstVisibleItemPositions[0]
}
is GridLayoutManager -> {
firstVisibleItemPosition =
(mLayoutManager as GridLayoutManager).findFirstVisibleItemPosition()
}
is LinearLayoutManager -> {
firstVisibleItemPosition =
(mLayoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
}
}
if (!isLoading && !isLastPage) {
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount
&& firstVisibleItemPosition >= 0
) {
Log.i(TAG, "Loading more items")
loadMoreItems()
}
}
}
protected abstract fun loadMoreItems()
abstract val isLastPage: Boolean
abstract val isLoading: Boolean
companion object {
private val TAG = PaginationScrollListener::class.java.simpleName
}
}
And use this in your recycler view in this way
rvCategoryProducts.addOnScrollListener(object :
PaginationScrollListener(layoutManager) {
override fun loadMoreItems() {
vm.isLoading = true
vm.currentPage++
GlobalScope.launch {
vm.getCategoryProductByCatId(vm.id, vm.currentPage)
}
}
override val isLastPage: Boolean
get() = vm.isLastPage
override val isLoading: Boolean
get() = vm.isLoading
})