RecyclerView is drawn before its data set is updated - java

I have a fragment in which I have a RecyclerView:
public class AlarmListFragment extends Fragment{
public AlarmListAdapter alarmListAdapter;
RecyclerView recyclerViewAlarms;
public AlarmListFragment() { }
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_alarm_list, container, false);
recyclerViewAlarms = (RecyclerView) rootView.findViewById(R.id.fragment_alarm_list_card_list);
recyclerViewAlarms.setHasFixedSize(true); // performance!
LinearLayoutManager llm = new LinearLayoutManager(getActivity());
recyclerViewAlarms.setLayoutManager(llm);
alarmListAdapter = new AlarmListAdapter(getActivity());
recyclerViewAlarms.setAdapter(alarmListAdapter);
return rootView;
}
It uses the following Adapter:
public class AlarmListAdapter extends RecyclerView.Adapter<AlarmListViewHolder> {
private List<AlarmModel> alarmModelDataSet;
private Context context;
public AlarmListAdapter(Context context) {
this.alarmModelDataSet = AlarmModel.listAll(AlarmModel.class);
this.context = context;
}
#Override
public AlarmListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(R.layout.alarm_list_card, parent, false);
return new AlarmListViewHolder(view);
}
#Override
public int getItemCount() {
return (int) AlarmModel.count(AlarmModel.class, null, null);
}
#Override
public void onBindViewHolder(AlarmListViewHolder holder, int position) {
// todo wait here until alarmModel is added to DataSet
AlarmModel alarmModel = alarmModelDataSet.get(position);
*** update GUI stuff ***
if (alarmModel.isEnabled()) {
*** update GUI stuff ***
}
if (alarmModel.isRepeatWeekly()) {
*** update GUI stuff ***
} else {
*** update GUI stuff ***
}
if (!alarmModel.isEnabled()){
*** update GUI stuff ***
}
}
public List<AlarmModel> getDataSet(){
return alarmModelDataSet;
}
public int getIndex(AlarmModel alarmModel){
for (AlarmModel _item : alarmModelDataSet){
if (_item.getId().equals(alarmModel.getId())){
return alarmModelDataSet.indexOf(_item);
}
}
return -1;
}
public void addOrUpdateAlarm(AlarmModel alarmModel){
int position = getIndex(alarmModel);
if (position >= 0) {
updateAlarm(alarmModel, position);
} else {
addAlarm(alarmModel);
}
}
private void addAlarm(AlarmModel alarmModel){
alarmModelDataSet.add(alarmModel);
notifyItemInserted(alarmModelDataSet.size() - 1);
}
private void updateAlarm(AlarmModel alarmModel, int position){
alarmModelDataSet.set(getIndex(alarmModel), alarmModel);
notifyItemChanged(position);
}
public void deleteAlarm(AlarmModel alarmModel) {
alarmModel.setIsEnabled(false);
AlarmManagerBroadcastReceiver.setAlarms(context);
int position = getIndex(alarmModel);
alarmModelDataSet.remove(position); // deletes out of class internal List
notifyItemRemoved(position); // notifies list fragment of deletion
}
public void enableOrDisableAlarm(int position){
AlarmModel alarmModel = alarmModelDataSet.get(position);
alarmModel.setIsEnabled(!alarmModel.isEnabled());
notifyItemChanged(position);
}
Every alarmModel is saved in a database which is working fine thus I removed the code about the database.
The fragment from the first code snippet uses startActivityForResult to open up an activity that allows the creation of a new alarm that is stored in another alarmModel. This new alarmModel is saved to the data base and its ID is returned to my fragment by this code
Intent intent = new Intent();
intent.putExtra("id", alarmDetails.getId());
intent.putExtra("delete", false);
setResult(RESULT_OK, intent);
supportFinishAfterTransition();
This result is received by the onActivityResult method of the fragment:
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE_ADD_ALARM_ID) {
long id = data.getLongExtra("id", -7777);
if (data.getBooleanExtra("delete", false)) { // delete
alarmListAdapter.deleteAlarm(AlarmModel.findById(AlarmModel.class, id));
} else { // add or update
alarmListAdapter.addOrUpdateAlarm(AlarmModel.findById(AlarmModel.class, id));
}
}
}
After all this code I finally am able to describe my problem:
Before the code even reaches the addOrUpdate method, the adapter's onBindViewHolder method is called and produces an IndexOutOfBoundsException because AlarmModel alarmModel = alarmModelDataSet.get(position); the just added alarmModel wants to get drawn but is not yet added to the DataSet.
Everything works fine (although the app becomes pretty slow) when the RecyclerView has ~10+ items since onBindViewHolder is called for every other item first and after calculating 9 other items the addOrUpdate method has finished.
Is there a fatal mistake I made or a method I did not find yet, which could help me? I thought about using two threads and letting one wait until the other one has finished but am unsure how to do that since I allready know that you should never block the Ui-Thread.
Edit 1
In an attempt to make the add, update and delete method in the adapter static I removed the List alarmModelDataSetcompletely and queried the data base every time instead. Although my initial plan did not work out, I noticed that it solved my problem by making the methods so slow that everything worked fine again. But since this is not a real solution but simply bad coding that works slowly I am not really satisfied with it...
Thanks to everybody reading this long question/problem
Tafelbomber

Related

Shared Element Transition is not exiting properly

I have fragment from which I'm launching activity with shared element transition that has viewpager in it, the enter transition works fine but when i scroll in view pager and finish transition the shared image comes from left side which is not desired it should reposition itself to where it was launched, here is my code:
Intent myIntent = new Intent(getActivity(), EnlargeActivity.class);
ActivityOptionsCompat options = ActivityOptionsCompat.
makeSceneTransitionAnimation(getActivity(),
imageView,
ViewCompat.getTransitionName(imageView));
startActivity(myIntent, options.toBundle());
I'm updating view and its name in activity that contains viewpager when finishing activity, but its going with blink:
public void finishAfterTransition() {
setEnterSharedElementCallback(new SharedElementCallback() {
#Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
// Clear all current shared views and names
names.clear();
sharedElements.clear();
ViewGroup viewGroup = (ViewGroup) viewPagerDetail.getAdapter()
.instantiateItem(viewPagerDetail, viewPagerDetail.getCurrentItem());
if (viewGroup == null) {
return;
}
// Map the first shared element name to the child ImageView.
sharedElements.put(viewGroup.findViewById(R.id.img).getTransitionName(), viewGroup.findViewById(R.id.img));
// setExitSharedElementCallback((SharedElementCallback) this);
}
});
super.finishAfterTransition();
Basically, Android start the transition with your pre-defined View and transitionName and automatically use the same properties for the return transition. When you change your focused View in ViewPager, Android doesn't know about that and keep the transition on the previous one on its way back. So you need to inform Android about the changes:
Remap the transition properties: Use setEnterSharedElementCallback to change the transitionName and View to the new one before returning from Activity2.
Wait for the Activity1 to finish rendering addOnPreDrawListener.
It's a bit complex in the final implementation. But you can look at my sample code https://github.com/tamhuynhit/PhotoGallery. I try to implement the shared-element-transition from many simple to complex sections.
Your problem appeared from Level 3 and solved in Level 4.
I am writing a tutorial about this but it's not in English so hope the code can help
UPDATE 1: Work flow
Here is how I implement it in my code:
Override finishAfterTransition in Activity2 and call setEnterSharedElementCallback method to re-map the current selected item in ViewPager. Also, call setResult to pass the new selected index back to previous activity here.
#Override
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void finishAfterTransition() {
setEnterSharedElementCallback(new SharedElementCallback() {
#Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
View selectedView = getSelectedView();
if (selectedView == null)
return;
// Clear all current shared views and names
names.clear();
sharedElements.clear();
// Store new selected view and name
String transitionName = ViewCompat.getTransitionName(selectedView);
names.add(transitionName);
sharedElements.put(transitionName, selectedView);
setExitSharedElementCallback((SharedElementCallback) null);
}
});
Intent intent = new Intent();
intent.putExtra(PHOTO_FOCUSED_INDEX, mCurrentIndex);
setResult(RESULT_PHOTO_CLOSED, intent);
super.finishAfterTransition();
}
Write a custom ShareElementCallback so I can set the callback before knowing which View is going to be used.
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static class CustomSharedElementCallback extends SharedElementCallback {
private View mView;
/**
* Set the transtion View to the callback, this should be called before starting the transition so the View is not null
*/
public void setView(View view) {
mView = view;
}
#Override
public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
// Clear all current shared views and names
names.clear();
sharedElements.clear();
// Store new selected view and name
String transitionName = ViewCompat.getTransitionName(mView);
names.add(transitionName);
sharedElements.put(transitionName, mView);
}
}
Override onActivityReenter in Activity1, get the selected index from the result Intent. Set setExitSharedElementCallback to re-map new selected View when the transition begins.Call supportPostponeEnterTransition to delay a bit because your new View may not be rendered at this point. Use getViewTreeObserver().addOnPreDrawListener to listen for the layout changes, find the right View by the selected index and continue the transition supportStartPostponedEnterTransition.
#Override
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onActivityReenter(int resultCode, Intent data) {
if (resultCode != LevelFourFullPhotoActivity.RESULT_PHOTO_CLOSED || data == null)
return;
final int selectedIndex = data.getIntExtra(LevelFourFullPhotoActivity.PHOTO_FOCUSED_INDEX, -1);
if (selectedIndex == -1)
return;
// Scroll to the new selected view in case it's not currently visible on the screen
mPhotoList.scrollToPosition(selectedIndex);
final CustomSharedElementCallback callback = new CustomSharedElementCallback();
getActivity().setExitSharedElementCallback(callback);
// Listen for the transition end and clear all registered callback
getActivity().getWindow().getSharedElementExitTransition().addListener(new Transition.TransitionListener() {
#Override
public void onTransitionStart(Transition transition) {}
#Override
public void onTransitionPause(Transition transition) {}
#Override
public void onTransitionResume(Transition transition) {}
#Override
public void onTransitionEnd(Transition transition) {
removeCallback();
}
#Override
public void onTransitionCancel(Transition transition) {
removeCallback();
}
private void removeCallback() {
if (getActivity() != null) {
getActivity().getWindow().getSharedElementExitTransition().removeListener(this);
getActivity().setExitSharedElementCallback((SharedElementCallback) null);
}
}
});
// Pause transition until the selected view is fully drawn
getActivity().supportPostponeEnterTransition();
// Listen for the RecyclerView pre draw to make sure the selected view is visible,
// and findViewHolderForAdapterPosition will return a non null ViewHolder
mPhotoList.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
mPhotoList.getViewTreeObserver().removeOnPreDrawListener(this);
RecyclerView.ViewHolder holder = mPhotoList.findViewHolderForAdapterPosition(selectedIndex);
if (holder instanceof ViewHolder) {
callback.setView(((ViewHolder) holder).mPhotoImg);
}
// Continue the transition
getActivity().supportStartPostponedEnterTransition();
return true;
}
});
}
UPDATE 2: getSelectedItem
To get selected View from the ViewPager, don't use getChildAt or you get the wrong View, use findViewWithTag instead
In the PagerAdapter.instantiateItem, use position as tag for each View:
#Override
public View instantiateItem(ViewGroup container, int position) {
// Create the View
view.setTag(position)
// ...
}
Listen to onPageSelected event to get the selected index:
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected(int position) {
mSelectedIndex = position;
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
Call getSelectedView to get the current view by the selected index
private View getSelectedView() {
try {
return mPhotoViewPager.findViewWithTag(mSelectedIndex);
} catch (IndexOutOfBoundsException | NullPointerException ex) {
return null;
}
}
This is actually a default behavior, I was struggling SharedElementTransitions a lot, but I have nested fragments. I got my solution from an article (very recent article), it shows an implementation with a RecyclerView, which I assume you have. In short, the solution is to override onLayoutChange :
recyclerView.addOnLayoutChangeListener(
new OnLayoutChangeListener() {
#Override
public void onLayoutChange(View view,
int left,
int top,
int right,
int bottom,
int oldLeft,
int oldTop,
int oldRight,
int oldBottom) {
recyclerView.removeOnLayoutChangeListener(this);
final RecyclerView.LayoutManager layoutManager =
recyclerView.getLayoutManager();
View viewAtPosition =
layoutManager.findViewByPosition(MainActivity.currentPosition);
// Scroll to position if the view for the current position is null (not
// currently part of layout manager children), or it's not completely
// visible.
if (viewAtPosition == null
|| layoutManager.isViewPartiallyVisible(viewAtPosition, false, true)){
recyclerView.post(()
-> layoutManager.scrollToPosition(MainActivity.currentPosition));
}
}
});
Here is the article, and you will also find the project on GitHub.

Load more data when FragmentStatePagerAdapter reaches the end of items

Question from Noob android developer
Issue Defitition :
I'm trying to achieve endless scrolling implement functionality of loading more data via network request when the FragmentStatePagerAdapter reaches last item, currently i'm setting static number for getCount to 10//, what i'd like to do is trigger a network request as soon as it hits 7th item to get 10 more items and refresh the list, keeping the cycle going and potentially end up with more than 100 items hence why i'm using FragmentStatePagerAdapter, also store/cache the data so to support left to right & right to left swipe
Here's what i've tried so far
Read this article infinite viewpager however it only works for
limited set of fragments perhaps a static number, what i'm trying to
do is more dynamic as such I dont have a fixed getCount.
Read the article endless scrolling adapters, i'm not trying to use
recycler view as viewpager works just fine for what i'm trying to do
Few more pageradapter implementations
what i've learnt so far
need to override getcount to return the maximum possible value
public int getCount() {
#Override
return Integer.MAX_VALUE;
}
*I'm not sure if should also override getItemPosition or implement some kind on pageListener there are many examples available online using pagerAdapter/fragmentPageradapter i'm getting confused as to which ones are related to FragmentStatePagerAdapter and which ones are not
*
// My Framgent class
public class ScreenSlidePageFragment extends Fragment {
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.activity_screen_slide_page_fragment,container,false);
return rootView;
}
public static ScreenSlidePageFragment newInstance (String url){
ScreenSlidePageFragment newFragment = new ScreenSlidePageFragment();
Bundle args = new Bundle();
args.putString("imagePathUrl", url);
newFragment.setArguments(args);
return newFragment;
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ImageView mImageView = (ImageView) getView().findViewById(R.id.imageView);
String imageUrl = getArguments().getString("imagePathUrl");
if (imageUrl==null){
Log.i("ScreenSlidePageFragment","no data passed");
}else {
Glide.with(this).load(imageUrl).into(mImageView);
}
}
}
// My adapter implementation
public class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
List<Resource> mResources;
String url;
Context mContext;
private int NUM_PAGES = 5;
private int currentPosition = 0;
public ScreenSlidePagerAdapter(FragmentManager fm, List<Resource> res,
Context context) {
super(fm);
mResources = res;
mContext = context;
}
#Override
public Fragment getItem(int position) {
if (mResources != null & mResources.size() > 0){
url = mResources.get(position).getUrl();
Log.i("url",url);
return ScreenSlidePageFragment.newInstance(url);
}else {
Toast.makeText(mContext,"no results returned",Toast.LENGTH_LONG).show();
return null;
}
}
#Override
public int getItemPosition(Object object) {
return super.getItemPosition(object);
}
#Override
public int getCount() {
return NUM_PAGES;
}
}
any help will be highly appreciated
Use FragmentStatePagerAdapter (support.v13) and implement getItemPosition like this:
public int getItemPosition(Object object) {
return POSITION_NONE;
}
https://hedgehogjim.wordpress.com/2013/10/03/android-updatable-swipe-navigation-with-fragmentstatepageradapter/
return POSITION_NONE "Causes adapter to reload all Fragments when notifyDataSetChanged is called"
Add a OnPageChangeListener to your ViewPager and load more data after comparing the given position value with your current data size
#Override
public void onPageSelected(int position) {
//Load previous data set if position == 0
//Load next data set if position == myAdapter.mResources.size() - 1
}
After the new data set has been loaded (and sorted), call myAdapter.notifyDataSetChanged() then calculate and set the new index to match the old offset so the user don't see any shift
myViewPager.setCurrentItem(myNewIndex, false)
Note: Sorting and new index calculation is only necessary when loading a previous data set.

How to implement shared element transition to onClickListener in RecyclerView?

I'm currently following this tutorial to implement a shared element transition between view (cards) in RecyclerView and activity but I'm not sure how can I do it since I'm using an onClickListener on MyRecyclerAdapter class to start a new activity.
Just new in development, hope you can help me about it.
MyRecyclerAdapter.java
public class MyRecyclerAdapter extends RecyclerView.Adapter<PaletteViewHolder> {
private Context context;
private List<Palette> palettes;
public MyRecyclerAdapter(Context context, List<Palette> palettes) {
this.palettes = new ArrayList<Palette>();
this.palettes.addAll(palettes);
this.context = context;
}
#Override
public PaletteViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View itemView = LayoutInflater.
from(viewGroup.getContext()).
inflate(R.layout.card_view, viewGroup, false);
itemView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Intent intent = new Intent(context, ScrollingActivity.class);
intent.putExtra("P25", "Longanissa");
context.startActivity(intent);
}
});
return new PaletteViewHolder(itemView);
}
#Override
public void onBindViewHolder(PaletteViewHolder paletteViewHolder, int i) {
Palette palette = palettes.get(i);
paletteViewHolder.titleText.setText(palette.getName());
paletteViewHolder.contentText.setText(palette.getHexValue());
paletteViewHolder.card.setCardBackgroundColor(palette.getIntValue());
}
#Override
public int getItemCount() {
return palettes.size();
}
animateIntent method:
public void animateIntent(View view) {
// Ordinary Intent for launching a new activity
Intent intent = new Intent(this, YourSecondActivity.class);
// Get the transition name from the string
String transitionName = getString(R.string.transition_string);
// Define the view that the animation will start from
View viewStart = findViewById(R.id.card_view);
ActivityOptionsCompat options =
ActivityOptionsCompat.makeSceneTransitionAnimation(this,
viewStart, // Starting view
transitionName // The String
);
//Start the Intent
ActivityCompat.startActivity(this, intent, options.toBundle());
You need to set the transition name of the view inside the onBindViewHolder. It needs to be different of the others viewHolder so get a way to make it different and unique (using palette.getName() if palette names are unique for example or use i).
Then you need to start the activity inside on click using the provided view:
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(context, v, v.getTransitionName());
context.startActivity(intent, options.toBundle());
I don't understand why you need animateIntent method. Hope it still helps

Android 6 Marshmallow ListView. Start activity with runtime permissions

I am developing an application for Android Marshmallow (API LEvel 23).
I have an Activity which contains a ListView.
I populate that ListView by using a BaseView Adapter.
I want to start an Activity whenever I push a button inside my ListView.
However, this Activity needs to access the camera which in the latest Android version means that I should ask for the camera permission during runtime.
The camera using Activity will be an ZXing Activity.
Here is the Activity code:
public class ProviderListActivity extends AppCompatActivity {
public boolean can_access_camera = false;
public final int got_camera = PackageManager.PERMISSION_DENIED;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_provider_list);
ListView listView = (ListView) findViewById(R.id.ProviderListView);
CardsDBHelper dbHelper = new CardsDBHelper(this);
SQLiteDatabase db = dbHelper.getWritableDatabase();
listView.setAdapter(new ProviderListAdapter(this, db));
}
public void start_scan(){
if (have_camera_permission()){
start_scan_activity();
} else {
Log.d("ProviderListActivity", "I CANNOT ACCESS THE CAMERA");
}
}
public void start_scan_activity(){
IntentIntegrator integrator = new IntentIntegrator(this);
integrator.initiateScan();
}
public boolean have_camera_permission(){
//Check for permissions
int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
if (permissionCheck == PackageManager.PERMISSION_DENIED){
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, got_camera);
} else {
can_access_camera = true;
}
return can_access_camera;
}
#Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case got_camera: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
can_access_camera = true;
} else {
can_access_camera = false;
}
return;
}
// other 'case' lines to check for other
// permissions this app might request
}
}
}
The getView() method in my Adapter is:
public View getView(int position, View convertView, final ViewGroup parent) {
Button grid_button;
if (convertView == null) {
// if it's not recycled, initialize some attributes
grid_button = new Button(mContext);
grid_button.setText("Hello Text");
Drawable d = mContext.getDrawable(R.drawable.circle);
grid_button.setCompoundDrawablesRelativeWithIntrinsicBounds(null,d,null,null);
grid_button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
//What do I need to put inside here?
}
});
} else {
grid_button = (Button) convertView;
}
return grid_button;
}
You may have noticed that the Adapter gets an extra db argument.
This is a SQLite database object which is which is used inside the Adapter, but this is not relevant.
I am not sure how to call my scan Activity by using the onClick override.
I tried passing the parent Activity as an argument in the Adapter and call the scan method, but I get the following exception:
java.lang.IllegalArgumentException: Can only use lower 8 bits for requestCode`
Does anyone have a solution for my problem?
You are getting java.lang.IllegalArgumentException: Can only use lower 8 bits for requestCode Error because you are using -1(PackageManager.PERMISSION_DENIED) as requestCode for requestPermissions method.
To make it work use any requestCode between 1 to 255.
public final int got_camera = 24;

Android RecyclerView addition & removal of items

I have a RecyclerView with an TextView text box and a cross button ImageView. I have a button outside of the recyclerview that makes the cross button ImageView visible / gone.
I'm looking to remove an item from the recylerview, when that items cross button ImageView is pressed.
My adapter:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> implements View.OnClickListener, View.OnLongClickListener {
private ArrayList<String> mDataset;
private static Context sContext;
public MyAdapter(Context context, ArrayList<String> myDataset) {
mDataset = myDataset;
sContext = context;
}
#Override
public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.my_text_view, parent, false);
ViewHolder holder = new ViewHolder(v);
holder.mNameTextView.setOnClickListener(MyAdapter.this);
holder.mNameTextView.setOnLongClickListener(MyAdapter.this);
holder.mNameTextView.setTag(holder);
return holder;
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.mNameTextView.setText(mDataset.get(position));
}
#Override
public int getItemCount() {
return mDataset.size();
}
#Override
public void onClick(View view) {
ViewHolder holder = (ViewHolder) view.getTag();
if (view.getId() == holder.mNameTextView.getId()) {
Toast.makeText(sContext, holder.mNameTextView.getText(), Toast.LENGTH_SHORT).show();
}
}
#Override
public boolean onLongClick(View view) {
ViewHolder holder = (ViewHolder) view.getTag();
if (view.getId() == holder.mNameTextView.getId()) {
mDataset.remove(holder.getPosition());
notifyDataSetChanged();
Toast.makeText(sContext, "Item " + holder.mNameTextView.getText() + " has been removed from list",
Toast.LENGTH_SHORT).show();
}
return false;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public TextView mNumberRowTextView;
public TextView mNameTextView;
public ViewHolder(View v) {
super(v);
mNameTextView = (TextView) v.findViewById(R.id.nameTextView);
}
}
}
My layout is:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:id="#+id/layout">
<TextView
android:id="#+id/nameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:padding="5dp"
android:background="#drawable/greyline"/>
<ImageView
android:id="#+id/crossButton"
android:layout_width="16dp"
android:layout_height="16dp"
android:visibility="gone"
android:layout_marginLeft="50dp"
android:src="#drawable/cross" />
</LinearLayout>
How can I get something like an onClick working for my crossButton ImageView? Is there a better way? Maybe changing the whole item onclick into a remove the item? The recyclerview shows a list of locations that need to be edited. Any technical advice or comments / suggestions on best implementation would be hugely appreciated.
I have done something similar.
In your MyAdapter:
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
public CardView mCardView;
public TextView mTextViewTitle;
public TextView mTextViewContent;
public ImageView mImageViewContentPic;
public ImageView imgViewRemoveIcon;
public ViewHolder(View v) {
super(v);
mCardView = (CardView) v.findViewById(R.id.card_view);
mTextViewTitle = (TextView) v.findViewById(R.id.item_title);
mTextViewContent = (TextView) v.findViewById(R.id.item_content);
mImageViewContentPic = (ImageView) v.findViewById(R.id.item_content_pic);
//......
imgViewRemoveIcon = (ImageView) v.findViewById(R.id.remove_icon);
mTextViewContent.setOnClickListener(this);
imgViewRemoveIcon.setOnClickListener(this);
v.setOnClickListener(this);
mTextViewContent.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View view) {
if (mItemClickListener != null) {
mItemClickListener.onItemClick(view, getPosition());
}
return false;
}
});
}
#Override
public void onClick(View v) {
//Log.d("View: ", v.toString());
//Toast.makeText(v.getContext(), mTextViewTitle.getText() + " position = " + getPosition(), Toast.LENGTH_SHORT).show();
if(v.equals(imgViewRemoveIcon)){
removeAt(getPosition());
}else if (mItemClickListener != null) {
mItemClickListener.onItemClick(v, getPosition());
}
}
}
public void setOnItemClickListener(final OnItemClickListener mItemClickListener) {
this.mItemClickListener = mItemClickListener;
}
public void removeAt(int position) {
mDataset.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, mDataSet.size());
}
Edit:
getPosition() is deprecated now, use getAdapterPosition() instead.
first of all, item should be removed from the list!
mDataSet.remove(getAdapterPosition());
then:
notifyItemRemoved(getAdapterPosition());
notifyItemRangeChanged(getAdapterPosition(), mDataSet.size()-getAdapterPosition());
if still item not removed use this magic method :)
private void deleteItem(int position) {
mDataSet.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, mDataSet.size());
holder.itemView.setVisibility(View.GONE);
}
Kotlin version
private fun deleteItem(position: Int) {
mDataSet.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, mDataSet.size)
holder.itemView.visibility = View.GONE
}
The Problem
RecyclerView was built to display data in an efficient and responsive manner.
Usually you have a dataset which is passed to your adapter and is looped through to display your data.
Here your dataset is:
private ArrayList<String> mDataset;
The point is that RecyclerView is not connected to your dataset, and therefore is unaware of your dataset changes.
It just reads data once and displays it through your ViewHolder, but a change to your dataset will not propagate to your UI.
This means that whenever you make a deletion/addition on your data list, those changes won't be reflected to your RecyclerView directly. (i.e. you remove the item at index 5, but the 6th element remains in your recycler view).
A (old school) solution
RecyclerView exposes some methods for you to communicate your dataset changes, reflecting those changes directly on your list items.
The standard Android APIs allow you to bind the process of data removal (for the purpose of the question) with the process of View removal.
The methods we are talking about are:
notifyItemChanged(index: Int)
notifyItemInserted(index: Int)
notifyItemRemoved(index: Int)
notifyItemRangeChanged(startPosition: Int, itemCount: Int)
notifyItemRangeInserted(startPosition: Int, itemCount: Int)
notifyItemRangeRemoved(startPosition: Int, itemCount: Int)
A Complete (old school) Solution
If you don't properly specify what happens on each addition, change or removal of items, RecyclerView list items are animated unresponsively because of a lack of information about how to move the different views around the list.
The following code will allow RecyclerView to precisely play the animation with regards to the view that is being removed (And as a side note, it fixes any IndexOutOfBoundExceptions, marked by the stacktrace as "data inconsistency").
void remove(position: Int) {
dataset.removeAt(position)
notifyItemChanged(position)
notifyItemRangeRemoved(position, 1)
}
Under the hood, if we look into RecyclerView we can find documentation explaining that the second parameter we pass to notifyItemRangeRemoved is the number of items that are removed from the dataset, not the total number of items (As wrongly reported in some others information sources).
/**
* Notify any registered observers that the <code>itemCount</code> items previously
* located at <code>positionStart</code> have been removed from the data set. The items
* previously located at and after <code>positionStart + itemCount</code> may now be found
* at <code>oldPosition - itemCount</code>.
*
* <p>This is a structural change event. Representations of other existing items in the data
* set are still considered up to date and will not be rebound, though their positions
* may be altered.</p>
*
* #param positionStart Previous position of the first item that was removed
* #param itemCount Number of items removed from the data set
*/
public final void notifyItemRangeRemoved(int positionStart, int itemCount) {
mObservable.notifyItemRangeRemoved(positionStart, itemCount);
}
Open source solutions
You can let a library like FastAdapter, Epoxy or Groupie take care of the business, and even use an observable recycler view with data binding.
New ListAdapter
Google recently introduced a new way of writing the recycler view adapter, which works really well and supports reactive data.
It is a new approach and requires a bit of refactoring, but it is 100% worth switching to it, as it makes everything smoother.
here is the documentation, and here a medium article explaining it
Here are some visual supplemental examples. See my fuller answer for examples of adding and removing a range.
Add single item
Add "Pig" at index 2.
String item = "Pig";
int insertIndex = 2;
data.add(insertIndex, item);
adapter.notifyItemInserted(insertIndex);
Remove single item
Remove "Pig" from the list.
int removeIndex = 2;
data.remove(removeIndex);
adapter.notifyItemRemoved(removeIndex);
Possibly a duplicate answer but quite useful for me. You can implement the method given below in RecyclerView.Adapter<RecyclerView.ViewHolder>
and can use this method as per your requirements, I hope it will work for you
public void removeItem(#NonNull Object object) {
mDataSetList.remove(object);
notifyDataSetChanged();
}
I tried all the above answers, but inserting or removing items to recyclerview causes problem with the position in the dataSet. Ended up using delete(getAdapterPosition()); inside the viewHolder which worked great at finding the position of items.
The problem I had was I was removing an item from the list that was no longer associated with the adapter to make sure you are modifying the correct adapter you can implement a method like this in your adapter:
public void removeItemAtPosition(int position) {
items.remove(position);
}
And call it in your fragment or activity like this:
adapter.removeItemAtPosition(position);
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
private Context context;
private List<cardview_widgets> list;
public MyAdapter(Context context, List<cardview_widgets> list) {
this.context = context;
this.list = list;
}
#NonNull
#Override
public MyViewHolder onCreateViewHolder(#NonNull ViewGroup viewGroup, int i) {
View view = LayoutInflater.from(this.context).inflate(R.layout.fragment1_one_item,
viewGroup, false);
return new MyViewHolder(view);
}
public static class MyViewHolder extends RecyclerView.ViewHolder {
TextView txtValue;
TextView txtCategory;
ImageView imgInorEx;
ImageView imgCategory;
TextView txtDate;
public MyViewHolder(#NonNull View itemView) {
super(itemView);
txtValue= itemView.findViewById(R.id.id_values);
txtCategory= itemView.findViewById(R.id.id_category);
imgInorEx= itemView.findViewById(R.id.id_inorex);
imgCategory= itemView.findViewById(R.id.id_imgcategory);
txtDate= itemView.findViewById(R.id.id_date);
}
}
#NonNull
#Override
public void onBindViewHolder(#NonNull final MyViewHolder myViewHolder, int i) {
myViewHolder.txtValue.setText(String.valueOf(list.get(i).getValuee()));
myViewHolder.txtCategory.setText(list.get(i).getCategory());
myViewHolder.imgInorEx.setBackgroundColor(list.get(i).getImg_inorex());
myViewHolder.imgCategory.setImageResource(list.get(i).getImg_category());
myViewHolder.txtDate.setText(list.get(i).getDate());
myViewHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
list.remove(myViewHolder.getAdapterPosition());
notifyDataSetChanged();
return false;
}
});
}
#Override
public int getItemCount() {
return list.size();
}}
i hope this help you.
if you want to remove item you should do this:
first remove item:
phones.remove(position);
in next step you should notify your recycler adapter that you remove an item by this code:
notifyItemRemoved(position);
notifyItemRangeChanged(position, phones.size());
but if you change an item do this:
first change a parameter of your object like this:
Service s = services.get(position);
s.done = "Cancel service";
services.set(position,s);
or new it like this :
Service s = new Service();
services.set(position,s);
then notify your recycler adapter that you modify an item by this code:
notifyItemChanged(position);
notifyItemRangeChanged(position, services.size());
hope helps you.
String str = arrayList.get(position);
arrayList.remove(str);
MyAdapter.this.notifyDataSetChanged();
To Method onBindViewHolder Write This Code
holder.remove.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Cursor del=dbAdapter.ExecuteQ("delete from TblItem where Id="+values.get(position).getId());
values.remove(position);
notifyDataSetChanged();
}
});
Incase Anyone wants to implement something like this in Main class instead of Adapter class, you can use:
public void removeAt(int position) {
peopleListUser.remove(position);
friendsListRecycler.getAdapter().notifyItemRemoved(position);
friendsListRecycler.getAdapter().notifyItemRangeChanged(position, peopleListUser.size());
}
where friendsListRecycler is the Adapter name
you must to remove this item from arrayList of data
myDataset.remove(holder.getAdapterPosition());
notifyItemRemoved(holder.getAdapterPosition());
notifyItemRangeChanged(holder.getAdapterPosition(), getItemCount());
//////// set the position
holder.cancel.setTag(position);
///// click to remove an item from recycler view and an array list
holder.cancel.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
int positionToRemove = (int)view.getTag(); //get the position of the view to delete stored in the tag
mDataset.remove(positionToRemove);
notifyDataSetChanged();
}
});
make interface into custom adapter class and handling click event on recycler view..
onItemClickListner onItemClickListner;
public void setOnItemClickListner(CommentsAdapter.onItemClickListner onItemClickListner) {
this.onItemClickListner = onItemClickListner;
}
public interface onItemClickListner {
void onClick(Contact contact);//pass your object types.
}
#Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
// below code handle click event on recycler view item.
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
onItemClickListner.onClick(mContectList.get(position));
}
});
}
after define adapter and bind into recycler view called below code..
adapter.setOnItemClickListner(new CommentsAdapter.onItemClickListner() {
#Override
public void onClick(Contact contact) {
contectList.remove(contectList.get(contectList.indexOf(contact)));
adapter.notifyDataSetChanged();
}
});
}
In case you are wondering like I did where can we get the adapter position in the method getadapterposition(); its in viewholder object.so you have to put your code like this
mdataset.remove(holder.getadapterposition());
In the activity:
mAdapter.updateAt(pos, text, completed);
mAdapter.removeAt(pos);
In the your adapter:
void removeAt(int position) {
list.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, list.size());
}
void updateAt(int position, String text, Boolean completed) {
TodoEntity todoEntity = list.get(position);
todoEntity.setText(text);
todoEntity.setCompleted(completed);
notifyItemChanged(position);
}
in 2022, after trying everything the whole internet given below is the answer
In MyViewHolder class
private myAdapter adapter;
inside MyViewHolder function initalise adapter
adapter = myAdapter.this
inside onclick
int position = getAdapterPosition()
list.remove(position);
adapter.notifyItemRemoved(position);

Categories

Resources