Scenario:
I am using Spinner for date range selection, which is working perfectly. The issue is, there is an option "Custom Range" on which I have to open a custom date range picker. Although it is working fine for the first time.
The issue is:
When users click it again, it doesn't call onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) (which is a functionality of Spinner). But from the user point of view, it's a bug.
Is there any way, we can get any selection event on an already selected item of Spinner?
To call an event on the already selected item in Spinner You can do it in this way:
Create Your own Spinner class by extending AppCompatSpinner:
import android.content.Context
import android.util.AttributeSet
import android.util.Log
class MySpinner(context: Context, attrs: AttributeSet?) : androidx.appcompat.widget.AppCompatSpinner(
context,
attrs
)
{
var listener: OnItemSelectedListener? = null
override fun setSelection(position: Int)
{
super.setSelection(position)
if (position == selectedItemPosition)
{
listener!!.onItemSelected(this, selectedView, position, selectedItemId)
}
}
override fun setOnItemSelectedListener(listener: OnItemSelectedListener?)
{
this.listener = listener
}
}
Use it in Your layout:
<com.yourcompany.kotlintest.MySpinner
android:id="#+id/spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
When creating a layout do this:
class MainActivity : AppCompatActivity()
{
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val spinner = findViewById<MySpinner>(R.id.spinner)
spinner.adapter = ArrayAdapter(
this,
android.R.layout.simple_spinner_dropdown_item,
arrayListOf("One", "Two", "Three")
)
spinner.onItemSelectedListener = object :
AdapterView.OnItemSelectedListener
{
override fun onItemSelected(
parent: AdapterView<*>,
view: View, position: Int, id: Long
)
{
Log.d("MyTag", "Click item at pos $position")
}
override fun onNothingSelected(parent: AdapterView<*>)
{
Log.d("MyTag", "Nothing selected")
}
}
}
}
Now when You reselect the same item function will be executed
did you use
spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) {
// your code here with selected **item position**
}
#Override
public void onNothingSelected(AdapterView<?> parentView) {
// your code here
}
});
Related
I have a view model I have added a click event listener. How do I navigate from that ViewModel into a Fragment
The ViewModel Code
class PromoAdapter(var promo: ArrayList<PromoModal>) :
RecyclerView.Adapter<PromoAdapter.UserViewHolder>() {
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(promo[position])
holder.promoRow.setOnClickListener() {
// Navigate to Fragment with name UserFragment
};
}
}
User Fragment Code
class UserFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val view: View = LayoutInflater.from(container!!.context)
.inflate(R.layout.user_page_fragment, container, false)
}
}
I have tried writing the following code still not working
Inside the ViewModel Click Event
val intent = Intent(itemView.context, UserFragment::class.java)
itemView.context?.startActivity(intent)
I cant find something working any help will be appreciated
It's better to move the click listener to a fragment
In Adapter:
class PromoAdapter(
var promo: ArrayList<PromoModal>,
val openFragment: (promoModal: PromoModal) -> Unit
) :
RecyclerView.Adapter<PromoAdapter.UserViewHolder>() {
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(promo[position])
val currentItem = promo.getOrNull(position) ?: return
holder.promoRow.setOnClickListener {
openFragment(currentItem)
};
}
}
In Fragment:
val mAdapter = PromoAdapter(
promo = promoList,
openFragment = { promoModal ->
// Navigate to Fragment with name UserFragment
}
)
I'm trying to build an Android application using the new ViewPager2. I've added two ViewPagers, separated by a view, and when you swipe, both viewpagers should move. Both view pagers are moving correctly but upon completion of the gesture, the swiped view flashes and the non swiped view reloads, as demonstrated by the attached gif. Here is my code for the Activity, ViewPagerAdapter, and Fragment. Any help is appreciated
public class MainActivity extends FragmentActivity {
ActivityMainBinding viewBinding;
MyPager adapter1, adapter2;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
List<String> data = populateData();
adapter1 = new MyPager(this, data);
adapter2 = new MyPager(this, data);
viewBinding.viewPager.setAdapter(adapter1);
viewBinding.viewPager2.setAdapter(adapter2);
viewBinding.viewPager2.setOffscreenPageLimit(data.size());
viewBinding.viewPager.setOffscreenPageLimit(data.size());
viewBinding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
viewBinding.viewPager2.scrollTo(positionOffsetPixels, 0);
}
#Override
public void onPageScrollStateChanged(final int state) {
viewBinding.viewPager2.setCurrentItem(viewBinding.viewPager.getCurrentItem(), true);
}
});
viewBinding.viewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
viewBinding.viewPager.scrollTo(positionOffsetPixels, 0);
}
#Override
public void onPageScrollStateChanged(final int state) {
viewBinding.viewPager.setCurrentItem(viewBinding.viewPager2.getCurrentItem(), true);
}
});
}
private List<String> populateData() {
List<String> data = new ArrayList<>();
for (int x = 0; x < 10; x++) {
String derril = "derril " + x;
data.add(derril);
}
return data;
}
}
public class MyPager extends FragmentStateAdapter {
List<String> data;
public MyPager(#NonNull FragmentActivity fragmentActivity, List<String> data) {
super(fragmentActivity);
this.data = data;
}
#NonNull
#Override
public Fragment createFragment(int position) {
return DerrilFragment.newInstance(data.get(position));
}
#Override
public int getItemCount() {
return data.size();
}
}
public class DerrilFragment extends Fragment {
PizzaBinding viewBinding;
String data;
private DerrilFragment(String data) {
this.data = data;
}
public static DerrilFragment newInstance(String data) {
return new DerrilFragment(data);
}
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
viewBinding = DataBindingUtil.inflate(inflater, R.layout.pizza, container, false);
viewBinding.text.setText(data);
return viewBinding.getRoot();
}
}
You can implement by RecyclerView.
class MainActivity : AppCompatActivity() {
private var isFirstFocused = false
private val helper: SnapHelper = LinearSnapHelper()
private val helper2: SnapHelper = LinearSnapHelper()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recycler1.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (isFirstFocused) recycler2.scrollBy(dx, dy)
}
})
recycler2.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (!isFirstFocused) recycler1.scrollBy(dx, dy)
}
})
recycler2.setOnTouchListener { v, event ->
if (event.action != null && event.action == MotionEvent.ACTION_DOWN ) {
isFirstFocused = false
return#setOnTouchListener true
}
return#setOnTouchListener false
}
recycler1.setOnTouchListener { v, event ->
if (event.action != null && event.action == MotionEvent.ACTION_DOWN ) {
isFirstFocused = true
return#setOnTouchListener true
}
return#setOnTouchListener false
}
helper.attachToRecyclerView(recycler1)
helper2.attachToRecyclerView(recycler2)
val manager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
val manager2 = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
recycler1.layoutManager = manager
recycler2.layoutManager = manager2
recycler2.adapter = Adapter(items)
recycler1.adapter = Adapter(items)
}
private val items = listOf("One", "Two", "Three")
}
class Adapter(val list: List<String>) : RecyclerView.Adapter<Adapter.Holder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val view = LayoutInflater.from(parent.context).inflate(
R.layout.activity_listview,
parent,
false
)
return Holder(view)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.itemView.text_view.text = list[position]
}
override fun getItemCount() = list.size
class Holder(view: View) : RecyclerView.ViewHolder(view)
}
You can try to use adapter.setOffscreenPageLimit = n. It sets the number of pages that should be retained to either side of the currently visible page(s). Pages beyond this limit will be recreated from the adapter when needed If you have a limited number of pages.
More details are here.
In your setCurrentItem() usages (under onPageScrollStateChanged()), set the second parameter to false
This will disable animation (not permanently) in the ViewPager when you set a page programatically, which is the reason you see the flash-like animation.
To know why:
Consider two ViewPagers A & B
You scroll from one page to another in A, which induces the same scroll in ViewPager B. Once you completely scroll from one page to another in A, the currentItem is internally set by A, but not for B.
Now, you inform B about the page change by calling setCurrentItem([index], true), which, irrespective of your scroll, will animate the page change introducing the flash (your induced scroll is reset to normal, and page animation for one page to another is started)
this looks a bit suspicious that you are calling setCurrentItem inside onPageScrollStateChanged for every passed state... try to add some if and setCurrentItem only when state==ViewPager2.SCROLL_STATE_IDLE
list of states:
ViewPager2.SCROLL_STATE_IDLE
ViewPager2.SCROLL_STATE_DRAGGING
ViewPager2.SCROLL_STATE_SETTLING
ViewPager2.SCROLL_STATE_DRAGGING state is handled in onPageScrolled method (sharing position offset aka. dragging by finger)
ViewPager2.SCROLL_STATE_SETTLING is auto-scroll (snap) of ViewPager2 to closest page - in here I'm not shure that ViewPager2 dragged "by code" (sharing position with scrollTo) will also enter in this state causing same snap behavior (so also onPageScrolled?) ... worth testing, put some logs in there
also: you are dragging one ViewPager2 and in onPageScrolled you are calling scrollTo on second ViewPager2. this may lead to call onPageScrolled call on this second listener, which is calling first ViewPager2 scrollTo method and so on... looped. consider dynamic unregistering listener when user will start drag one ViewPager2 and reregistering when movement ends (SCROLL_STATE_IDLE state)
I think the problem is that in 'onPageScrolled' method of both ViewPagers you are scrolling other pager. So, both pages might be scrolling each other at the same. For example, if you are scrolling ViewPager_1 then its onPageScrolled method will scroll ViewPager_2 and as ViewPager_2 is now scrolled then its onPageScrolled method will be called and that will scroll ViewPager_1 and vice versa.
So, You may need to consider applying the check that if touch is holded ny ViewPager_1 then code inside onPageScrolled method of ViewPager_2 should not be executed. Or if you want to avoid those checks. Then registerOnPageChangeCallback/unregisterOnPageChangeCallback the listener of other ViewPager when one holds the touch.
#Override
public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
viewBinding.viewPager.scrollTo(positionOffsetPixels, 0);
}
I have a recycleView containing n reviews. Each review has a nickname, body and a button that gives the opportunity to report my review if something is wrong. Problem is I don't know how to retrieve these info on click. Can someone show me some code draft maybe?
On your RecyclerView class override the following:
#Override
public void onViewAttachedToWindow(#NonNull MyViewHolder holder) {
super.onViewAttachedToWindow(holder);
//assuming your 'body' is TextView:
TextView tv = holder.body; //body should have been set in your ViewHolder inner class
Button button = holder.button; //assume button was defined in your ViewHolder inner class
//now you have a reference to your objects!
}
A clean way to do that is making a interface, here i put an example.
class NewsAdapter(private val noticias: List<Noticia>, private val layout: Int, private val activity: Activity, private val listener: OnItemClickListener) : RecyclerView.Adapter<NewsAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(layout, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(noticias[position], listener)
}
override fun getItemCount() = noticias.size
// ViewHolder
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(noticia: Noticia, listener: OnItemClickListener) {
itemView.textViewNewsTitulo.text = noticia.titulo
Glide.with(activity)
.load(noticia.foto_url)
.fitCenter()
.placeholder(R.drawable.placeholder)
.into(itemView.imageViewNews)
itemView.textViewNewsDesc.text = noticia.desc
// bind the click listener with the itemView
itemView.setOnClickListener { listener.onItemClick(noticia, adapterPosition) }
}
}
// Interface onClick define in adapter adapter
interface OnItemClickListener {
fun onItemClick(noticia: Noticia?, posicion: Int)
}
}
For use in your activity you need to implement the interface like this.
class HomeActivity : BaseActivity(), NewsAdapter.OnItemClickListener
The interface force you to implement the methods in the activity.
override fun onItemClick(noticia: Noticia?, posicion: Int) {
// do your logic on click
}
What I would like to do
I would like to acquire data that the user selected on dependent spinners.
On my current code, it was succeeded to acquire the data from the first spinner but I cannot do it from the second spinner.
Problem
How should I fix my code to acquire appropriate data of a user's selection?
In this case, the second textView should be BMW but it is Audi, which is the first candidate of the second spinner.
The first textView is changed following a user's selection. In this case, it changed to mobile from motor, but again, the second textView was remained as the first the first candidate of the second spinner.
Current Code
MainActivity.kt
package com.example.spinner
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.app.Activity
import android.content.Intent
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.activity_main.*
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.Toast
class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener {
var spinner1: Spinner? = null
var spinner2: Spinner? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
spinner1 = findViewById(R.id.spinner1) as Spinner
spinner2 = findViewById(R.id.spinner2) as Spinner
val adapter1 = ArrayAdapter.createFromResource(
this,
R.array.array1, android.R.layout.simple_spinner_item
)
spinner1?.setAdapter(adapter1)
spinner1?.setOnItemSelectedListener(this)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the main; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
val id = item.getItemId()
return if (id == R.id.action_settings) {
true
} else super.onOptionsItemSelected(item)
}
override fun onItemSelected(
parent: AdapterView<*>, view: View, position: Int,
id: Long
) {
if (spinner1?.getSelectedItem() == "mobile") {
Toast.makeText(
applicationContext, "Mobil dipilih",
Toast.LENGTH_SHORT
).show()
val adapter2 = ArrayAdapter.createFromResource(
this,
R.array.mobile_array, android.R.layout.simple_spinner_item
)
spinner2?.setAdapter(adapter2)
spinner2?.setOnItemSelectedListener(this)
} else {
val adapter2 = ArrayAdapter.createFromResource(
this,
R.array.motor_array, android.R.layout.simple_spinner_item
)
spinner2?.setAdapter(adapter2)
spinner2?.setOnItemSelectedListener(this)
}
// here I try to put the selected contents to variables
val spinner1_content = spinner1?.getSelectedItem() as String
textView1.text = spinner1_content
val spinner2_content = spinner2?.getSelectedItem() as String
textView2.text = spinner2_content
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context="com.example.stackspinner.MainActivity" >
<Spinner
android:id="#+id/spinner1"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Spinner
android:id="#+id/spinner2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/spinner1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="#+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#00f"
android:layout_marginTop="100dp"
android:textSize="40sp" />
<TextView
android:id="#+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#00f"
android:layout_marginTop="100dp"
android:textSize="40sp" />
</LinearLayout>
</RelativeLayout>
strings.xml
<resources>
<string name="app_name">Application Name</string>
<string name="hello_world">Hello world!</string>
<string name="action_settings">Settings</string>
<string-array name="array1">
<item>mobile</item>
<item>motor</item>
</string-array>
<string-array name="mobile_array">
<item>Android</item>
<item>blackberry</item>
<item>apple</item>
</string-array>
<string-array name="motor_array">
<item>Audi</item>
<item>BMW</item>
<item>unicorn</item>
</string-array>
</resources>
current code after hearing an answer
MainActivity.kt
package com.example.spinner
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.app.Activity
import android.content.Intent
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.activity_main.*
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.Toast
class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener {
var spinner1: Spinner? = null
var spinner2: Spinner? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
spinner1 = findViewById(R.id.spinner1) as Spinner
spinner2 = findViewById(R.id.spinner2) as Spinner
val adapter1 = ArrayAdapter.createFromResource(
this,
R.array.array1, android.R.layout.simple_spinner_item
)
spinner1?.setAdapter(adapter1)
spinner1?.setOnItemSelectedListener(this)
val adapter2 = ArrayAdapter.createFromResource(
this,
R.array.array1, android.R.layout.simple_spinner_item
)
spinner2?.setAdapter(adapter2)
spinner2?.setOnItemSelectedListener(this)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the main; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
val id = item.getItemId()
return if (id == R.id.action_settings) {
true
} else super.onOptionsItemSelected(item)
}
override fun onItemSelected(
parent: AdapterView<*>, view: View, position: Int,
id: Long
) {
if (spinner1?.getSelectedItem() == "mobile") {
Toast.makeText(
applicationContext, "Mobil dipilih",
Toast.LENGTH_SHORT
).show()
val adapter2 = ArrayAdapter.createFromResource(
this,
R.array.mobile_array, android.R.layout.simple_spinner_item
)
spinner2?.setAdapter(adapter2)
} else {
val adapter2 = ArrayAdapter.createFromResource(
this,
R.array.motor_array, android.R.layout.simple_spinner_item
)
spinner2?.setAdapter(adapter2)
}
// here I try to put the selected contents to variables
val spinner1_content = spinner1?.getSelectedItem() as String
textView1.text = spinner1_content
val spinner2_content = spinner2?.getSelectedItem() as String
textView2.text = spinner2_content
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
Take note that every time you select a mobile (o a motor) you recreate your spinner2. You should use different listeners for each spinner:
spinner1.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
//your spinner1 listener code
}
}
spinner2.dothesame.....
I put the entire onCreate for better understanding:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
spinner1 = findViewById(R.id.spinner1) as Spinner
spinner2 = findViewById(R.id.spinner2) as Spinner
textView1 = findViewById(R.id.textView1) as TextView
textView2 = findViewById(R.id.textView2) as TextView
val adapter1 = ArrayAdapter.createFromResource(
this,
R.array.array1, android.R.layout.simple_spinner_item
)
spinner1?.setAdapter(adapter1)
spinner1?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
var spinner2Adapter: ArrayAdapter<CharSequence>? = null
//select the right adapter for de Spinner2
if (spinner1?.getSelectedItem() == "mobile") {
spinner2Adapter = ArrayAdapter.createFromResource(
applicationContext,
R.array.mobile_array, android.R.layout.simple_spinner_item
)
} else {
spinner2Adapter = ArrayAdapter.createFromResource(
applicationContext,
R.array.motor_array, android.R.layout.simple_spinner_item
)
}
//set the adapter and the listener for spinner2
spinner2?.setAdapter(spinner2Adapter)
spinner2?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val spinner2_content = spinner2?.getSelectedItem() as String
textView2?.text = spinner2_content
}
}
// here I try to put the selected contents to variables
val spinner1_content = spinner1?.getSelectedItem() as String
textView1?.text = spinner1_content
}
}
}
I am working in a chat application for Android and I am using RecyclerView for listing the messages.
I have written the adapter, but I am having a problem with detecting when an element(TextView in this case) inside the layout is clicked.
This is my adapter:
public class ChatRoomThreadAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private class ViewHolder extends RecyclerView.ViewHolder {
TextView message, timestamp;
private ViewHolder(View view) {
super(view);
message = (TextView) itemView.findViewById(R.id.message);
timestamp = (TextView) itemView.findViewById(R.id.timestamp);
}
}
public ChatRoomThreadAdapter(Context mContext, ArrayList<Message> messageArrayList, String userId) {
this.mContext = mContext;
this.messageArrayList = messageArrayList;
this.userId = userId;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView;
itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.chat, parent, false);
return new ViewHolder(itemView);
}
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
((ViewHolder) holder).message.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (((ViewHolder) holder).timestamp.getVisibility() == View.GONE) {
((ViewHolder) holder).timestamp.setVisibility(View.VISIBLE);
} else {
((ViewHolder) holder).timestamp.setVisibility(View.GONE);
}
}
});
}
#Override
public int getItemCount() {
return messageArrayList.size();
}
}
The current onClick works but I have to click twice on the message in order for the onClick to trigger. I have been searching and trying endless solutions for 3 days in order to fix this, but none of the solutions on the internet have worked so far.
Make sure you have both focusableInTouchMode & focusable disabled on the button. The first click will get the focus and the second click executes the onClickListener.
.
After scroll, recycler view items are not clickable and the issue is still open https://issuetracker.google.com/issues/66996774
Found a way to force click if the scroll state is still SCROLL_STATE_SETTLING
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.recyclerview.widget.RecyclerView
class WrapperRecyclerView(context: Context, attributeSet: AttributeSet?, defStyle: Int) :
RecyclerView(context, attributeSet, defStyle) {
constructor(context: Context) : this(context, null, 0)
constructor(context: Context, attributeSet: AttributeSet) : this(context, attributeSet, 0)
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
val eventConsumed = super.onInterceptTouchEvent(event)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> if (scrollState == SCROLL_STATE_SETTLING) {
parent.requestDisallowInterceptTouchEvent(false)
// only if it touched the top or the bottom.
if (!canScrollVertically(-1) || !canScrollVertically(1)) {
// stop scroll to enable child view to get the touch event
stopScroll()
// do not consume the event
return false
}
}
}
return eventConsumed
}
}
The following class will fix this issue :
class WrapperRecyclerView(context: Context, attributeSet: AttributeSet?, defStyle: Int) :
RecyclerView(context, attributeSet, defStyle) {
constructor(context: Context) : this(context, null, 0)
constructor(context: Context, attributeSet: AttributeSet) : this(context, attributeSet, 0)
override fun onScrollStateChanged(state: Int) {
super.onScrollStateChanged(state)
if (state == RecyclerView.SCROLL_STATE_SETTLING) {
this.stopScroll();
}
}
}
For me focusableInTouchMode & focusable doesn't work.
Add this line to RecyclerView :
android:nestedScrollingEnabled="false"
Enjoy¡
A tip for those trying to debug this kind of scrolling state problem in recycler view. Create your own MyRecyclerView extending RecyclerView, and override fun onInterceptTouchEvent there just to add logging:
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
Log.d(TAG, "MyRecyclerView intercepted a touch event: ${event!!.action}")
Log.d(TAG, "MyRecyclerView is in state: ${scrollState}")
return super.onInterceptTouchEvent(event)
}
It helped me to see that my recycler view was stuck in SCROLL_STATE_DRAGGING.
I believe, for child views to receive clicks coming from recycler view, the recycler view has to be in SCROLL_STATE_IDLE state.
So my solution to the problem was based on what #Rocky suggested,
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
val eventConsumed = super.onInterceptTouchEvent(event)
/*
* Fix the following problem.
*
* When the recycler view received a click, it was still in the DRAGGING mode.
* It consumed the click, instead of passing it down to the child view.
* The user had to click a child view again, to be able to interact with it.
*/
when (event!!.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (scrollState == SCROLL_STATE_DRAGGING) {
stopScroll()
return false
}
}
}
return eventConsumed
}