I have a RealmRecyclerViewAdapter that listens to a query in realm, and I've Implemented a drag and drop functionality on it using the ItemTouchHelper.
private final ItemTouchHelper.Callback _ithCallback = new ItemTouchHelper.Callback() {
private int fromPosition;
private int toPosition;
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
if (viewHolder.getItemViewType() != target.getItemViewType()) {
return false;
}
toPosition = target.getAdapterPosition();
adapter.notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
}
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (viewHolder.getItemViewType() == TaskListRecyclerViewAdapter.FOOTER) {
return 0;
} else {
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
}
#Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
fromPosition = viewHolder.getAdapterPosition();
toPosition = viewHolder.getAdapterPosition();
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE
&& fromPosition != toPosition) {
// adapter.onDetachedFromRecyclerView(mRecyclerView);
dragAndDropManager.executeDragAndDrop(liveRealm, store.getStoreUid(), fromPosition, toPosition);
...
...
// adapter.onAttachedToRecyclerView(mRecyclerView);
}
}
};
To indicate the movement, whenever I drag an item over another Item, I call the adapter's notifyItemMoved method:
adapter.notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
and whenever the user releases the item he dragged, I commit the changes to the realm DB:
dragAndDropManager.executeDragAndDrop(liveRealm, store.getStoreUid(), fromPosition, toPosition);
the problem is - whenever I release the item I've dragged, the animation appears to be working as if I haven't dropped the item in its location, but as if the item starts moving from its original location to the location in which I've dropped it.
I understand that this happens because I've committed the changes to Realm and as such the change is notified in the adapter, but the animation looks buggy.
I've tried calling
adapter.onDetachedFromRecyclerView(mRecyclerView);
and
adapter.onAttachedToRecyclerView(mRecyclerView);
to remove and add the listener to the adapter but it seems to be unreliable.
Is there a better solution for this issue?
Michael - did you ever get this working to your satisfaction?
I just ran across your post, having the same issue. I'm using the local Java DB version of Realm and after reviewing several examples that show similar techniques for implementing drag and drop with the stock recyclerView (generally they are all using an array of strings as the data model, NOT something like a live DB such as Realm) here is my experience - what I saw as issues with the "standard" techniques and how I resolved them:
Issues To Resolve
Issue 1: Custom Callback class forwards large numbers of onMove events
The various examples I found (like this one here) always utilize a
custom class that extends ItemTouchHelper.Callback. This classes typically defines
an interface that is in turn implemented by the adapter. Then this class overrides
onMove()/onSwiped from the Callback to call the onViewMoved()/onViewSwiped() method of the interface implemented by the adapter:
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
contract.onViewMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
In the onViewMoved() implementation that is in the adapter, the examples typically update the data source, then call notifyItemMoved():
public void onViewMoved(int oldPosition, int newPosition) {
User targetUser = usersList.get(oldPosition);
User user = new User(targetUser);
usersList.remove(oldPosition);
usersList.add(newPosition, user);
notifyItemMoved(oldPosition, newPosition);
}
In the example above, the data model is a simple ArrayList of "User" objects.
The problem I found with this when using Realm is that in the Callback onMove() gets called a lot as the user drags, which
given what is happening in the standard interface implementation would cause a huge amount of DB
writes.
Issue 2: The drag animation stops as soon as a single row change is detected
I noticed that as I was dragging the top row of the recyclerview down, as soon as it reached the very next row the animation stopped. Why? It is because as you found out the base class RealmRecyclerViewAdapter automatically updates itself when it gets notifications that the data model has changed, and this update apparently triggers the ItemTouchHelper to stop the animation.
To solve these two issues, I did the following:
Like the examples I found, I extended ItemTouchHelper.Callback into my own class. I utilize some variables to keep track of whether I have dragged over a new target or not:
public class RecyclerViewSwipeAndDragHelper extends ItemTouchHelper.Callback {
private SwipeAndDraggable implementor;
private static final int POSITION_UNKNOWN = -1;
private int oldPosition = POSITION_UNKNOWN;
private int newPosition = POSITION_UNKNOWN;
And similar to the example, in this class I define the interface that will be used by my adapter:
public interface SwipeAndDraggable {
void onViewMoved(int oldPosition, int newPosition);
void onViewSwiped(int position);
void onClearView(#NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder);
}
I added the onClearView() method in this interface to what is typically shown in the examples, more on how that is used in a moment.
To solve the first issue, I needed to consolidate all the onMove() callbacks into only a single call into my adapter. Fortunately, ItemTouchHelper already does a lot of this work for us. A return value of TRUE from onMove() will trigger a follow-up call to onMoved(). Returning FALSE does not. So, in my Callback implementation when I override onMove(), I don't call the implementer of the interface. Instead, I only keep track of whether or not I've dragged over the same target position, so that it only returns TRUE the first time I hit a new target (and I also noticed through testing the sometimes I would get an initial -1 value for viewHolder.getAdapterPosition(), so I test for that as well):
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
oldPosition = viewHolder.getAdapterPosition();
if ( oldPosition > POSITION_UNKNOWN) {
if ( newPosition != target.getAdapterPosition() ) {
newPosition = target.getAdapterPosition();
return true;
}
}
return false;
}
Only when onMove() returns TRUE is onMoved() in the Callback subsequently called. It is in that method that I call my implementation of onViewMoved():
#Override
public void onMoved(#NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder, int fromPos, #NonNull RecyclerView.ViewHolder target, int toPos, int x, int y) {
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y);
implementor.onViewMoved(fromPos, toPos);
}
That is how I solved the first issue of needing to consolidate all the onMove() callbacks into a single update. I only get a call a single time now as the user drags over individual viewholders.
As to the second issue - like you I have disabled the data listening in the RealmRecyclerView. I used the technique in the article to utilize an image in my viewholder to instigate the drag (in my case, it is a thumbnail image representing the individual entry in the recyclerview). This is done as the last step in the onBindViewHolder() method:
holder.getMediaImageView().setOnTouchListener((View view, MotionEvent motionEvent) -> {
if (motionEvent.getAction()== MotionEvent.ACTION_DOWN) {
// Turn off the data change listener within the RealmRecyclerViewAdapter superclass, so that
// when we make changes via the drag it doesn't automatically update - we will handle that manually
onDetachedFromRecyclerView(mMediaRecyclerView);
mItemTouchHelper.startDrag(holder);
}
return false;
});
Now when my onViewMoved() method is called, I can adjust the data model manually and notify the adapter:
public void onViewMoved(int oldPosition, int newPosition) {
Timber.d("Recognized a movement to a new position: old position, new position is: %d, %d", oldPosition, newPosition);
MediaDataMgr.get().moveMediaInPlaylist(oldPosition, newPosition, mPlaylist.getId());
// RealmRecyclerViewAdapter is no longer automatically notified when underlying data changes, so need to make notify()* calls here
notifyItemMoved(oldPosition, newPosition);
}
MediaDataManager is a class I wrote that consolidates all my Realm access. Here is that method as an example of how I do this (as well as the technique of insuring I'm calling Realm from the same thread I am currently on):
public void moveMediaInPlaylist(int from, int to, String playlistId) {
boolean success = false;
boolean mainThread = Thread.currentThread().equals(Looper.getMainLooper().getThread());
Realm realm = null;
if (mainThread) {
realm = mUIThreadRealm;
} else {
realm = Realm.getDefaultInstance();
}
try {
Playlist p = getPlaylist(playlistId);
if ( p== null) {
throw new Exception("Error retrieving playlist with ID: "+ playlistId );
}
realm.beginTransaction();
p.getMediaList().move(from, to);
success = true;
} catch (Exception e) {
Timber.d( "Exception deleting a Media in Playlist: %s", e.getMessage());
success = false;
} finally {
if ( success ) {
realm.commitTransaction();
} else {
realm.cancelTransaction();
}
if (!mainThread) {
realm.close();
}
}
}
Then when clearView() is called in the Callback, I forward this to the adapter via the interface definition. In Callback:
public void clearView(#NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
oldPosition = POSITION_UNKNOWN;
newPosition = POSITION_UNKNOWN;
implementor.onClearView(recyclerView, viewHolder);
}
And in adapter implementation of the interface I use this to re-attach the data listener:
public void onClearView(#NonNull RecyclerView recyclerView, #NonNull RecyclerView.ViewHolder viewHolder) {
// reset RealmRecyclerViewAdapter data listener
onAttachedToRecyclerView(mMediaRecyclerView);
}
Now I am able to get a smooth looking animation, updating the DB in real time only once as items are dragged down the recyclerview using Realm:
Related
i have the following code. If the name of the checkbox is in the 'deneme' array that the function takes, I want the checkbox to be checked. but those that don't enter the 'if' are marked strangely. and there are random checked when scrolling up and down
public SubjectRecyclerViewAdapter(ArrayList<Subject> subjects, ArrayList<String> deneme) {
this.subjects = subjects;
this.deneme = deneme;
}
#Override
public void onBindViewHolder(#NonNull SubjectRecyclerViewAdapter.MyViewHolder holder, int position) {
holder.checkBox.setText(subjects.get(position).getCheckBoxSubject());
if (deneme.contains(subjects.get(position).getCheckBoxSubject())) {
holder.checkBox.setChecked(true);
}}
Because the ViewHolders are recycled (meaning they are reused as you scroll) you need to explicitly set the checkbox's state either way (so that it changes whatever previous state the ViewHolder had):
if (deneme.contains(subjects.get(position).getCheckBoxSubject())) {
holder.checkBox.setChecked(true);
} else {
holder.checkBox.setChecked(false);
}
I've been learning Java and Android Development for about a month. Maybe, I don't know some features... But I can't find any advise for a week.
I need to send value from one of my fields in ViewHolder (created by onBindViewHolder) to overrided method onChildDraw (in class, that extends ItemTouchHelper). But I can't understand, how can I do this.
I want to call new activity after swipe action and send value of field "name" to that activity. Can I do this with Intent? Or maybe have other way?
Creating ViewHolder (public class ModelsDataAdapter extends RecyclerView.Adapter):
#Override
public void onBindViewHolder(ModelsViewHolder holder, int position) {
ModelsData model = models.get(position);
holder.name.setText(model.getName()); // <-- I NEED THIS VALUE
holder.tags.setText(model.getTags());
holder.keywords.setText(model.getKeywords());
holder.cost.setText(model.getCost().toString());
//-- some code --//
}
My onChildDraw (public class SwipeControl extends ItemTouchHelper.Callback):
#Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
//-- some code --//
int x = 0;
if (dX < -200){ //
viewHolder.itemView.findViewById(R.id.card_foreground).setTranslationX(dX / 3);
} else if (dX > 200){
viewHolder.itemView.findViewById(R.id.card_foreground).setTranslationX(dX / 3);
openStatCard();
} else if (dX == 0){
checkActivity = false;
}
}
Opening new activity (inside SwipeControl.class):
private void openStatCard(){
if (!checkActivity) {
checkActivity = true;
Intent intent = new Intent(context, StatCards.class);
intent.putExtra("choosingModel", model); // <-- PUT "name" IN "model"
context.startActivity(intent);
}
}
Inside onChildDraw method use viewHolder.getAdapterPositon() method to get the position. And when you need the value just use that position to find Name from the models list
EDIT
Inside onChildDraw get the TextView using viewHolder.itemView.findViewById(R.id.name); and then use getText() method to get the Name.
I'm reading Cards - Material design. I'd like to implement swipe gesture in my layout, but I'm facing some problems with this.
This is the effect I want to get.
The problem with my current implementation with ItemTouchHelper is that the item is always completely removed from the layout, I do not want that to happen, as in the video above I would like to allow the layout to be swiped to the direct in the maximum 30% of the entire width of the screen, if the user drops it before it, I'd like it to back up so that the layout goes back to its default position (without being partially moved to the side) and if the user reaches 30% of the screen, to perform an action (such as saving the item to watch later) and returning the item to its initial 0% position.
Whatsapp does something very similar to what I want to have, when swiped right a message, you can respod the message.
This is my code
public class RecyclerItemTouchHelper extends ItemTouchHelper.SimpleCallback {
private RecyclerItemTouchHelperListener listener;
RecyclerView recyclerView;
public RecyclerItemTouchHelper(int dragDirs, int swipeDirs, RecyclerItemTouchHelperListener listener, RecyclerView recyclerView) {
super(dragDirs, swipeDirs);
this.listener = listener;
this.recyclerView = recyclerView;
}
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
Log.i("waeaweaweaeawe", "onMove");
return true;
}
#Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
Log.i("waeaweaweaeawe", "onSelectedChanged");
if (viewHolder != null) {
final View foregroundView = ((clv_exibirPosts.ViewHolder) viewHolder).container_post;
getDefaultUIUtil().onSelected(foregroundView);
}
}
#Override
public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder, float dX, float dY,
int actionState, boolean isCurrentlyActive) {
}
#Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
Log.i("waeaweaweaeawe", "clearView");
final View foregroundView = ((clv_exibirPosts.ViewHolder) viewHolder).container_post;
getDefaultUIUtil().clearView(foregroundView);
}
#Override
public void onChildDraw(Canvas c, RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder, float dX, float dY,
int actionState, boolean isCurrentlyActive) {
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
Log.i("waeaweaweaeawe", "onSwiped");
}
#Override
public int convertToAbsoluteDirection(int flags, int layoutDirection) {
Log.i("waeaweaweaeawe", "convertToAbsoluteDirection");
return super.convertToAbsoluteDirection(flags, layoutDirection);
}
public interface RecyclerItemTouchHelperListener {
void onSwiped(RecyclerView.ViewHolder viewHolder, int direction, int position);
}
}
I have no idea how to set a maximum distance that swipe gesture can be dragged. I found this in stackOverflow, this makes me able to make the layout not completely exit the screen
super.onChildDraw(c, recyclerView, viewHolder, dX / 2, dY, actionState, isCurrentlyActive);
But a few problems here.
1 - Swipe speed is slow.
2 - When swiped to 50% of the entire width allowed to swiped, the object continues with its position changed, keeping it in its maximum allowable position (I want the item to always return to 0% after reaching 50% of swiped allowed.)
I've looked all over the internet and found no examples that talk about it. I'm sure this problem is not just mine. If you can give me some clues, examples of codes, posts, anything that might help I'll already be extremely grateful!
I think this library is what you need https://github.com/yanzhenjie/SwipeRecyclerView
In case you want to implement it yourself, have a look at the code of the library itself.
I have fragment which includes a viewpager which includes many fragments. I am calling methods of fragments which are in viewpager from outer fragment. I can do this without any problem but I am facing an issue, when I go to another fragment from fragments which are in viewpager and then I come back, I cannot use that method because method-calling object viewed as null.
Problem phases in order
layout structure
1- In outer fragment, a method of one of viewpager's fragment is called without anyproblem.
2- Click on anyitem in recycler view in fragments which are settled in viewpager.
3- It directs user to another fragment.
4- Go back via popbackstackimmediate().
5- First phase causes problem with null pointer exception.
This is the method implemented in outer fragment.
private void setPagination() {
nestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
#Override
public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if (v.getChildAt(v.getChildCount() - 1) != null) {
if ((scrollY >= (v.getChildAt(v.getChildCount() - 1).getMeasuredHeight() - v.getMeasuredHeight())) &&
scrollY > oldScrollY) {
if (currentPage.equalsIgnoreCase("A"))
AFragment.loadMore();
else if (currentPage.equalsIgnoreCase("B"))
BFragment.loadMore();
else if (currentPage.equalsIgnoreCase("C"))
CFragment.loadMore();
else if (currentPage.equalsIgnoreCase("D"))
DFragment.loadMore();
}
}
}
});
}
LoadMore() method
public void loadMore() {
if (list_a.size() % 4 == 0) {
ManagerAll.getInstance().fetchA(id_number, page).enqueue(new Callback<List<A>>() {
#Override
public void onResponse(Call<List<A>> call, Response<List<A>> response) {
if (response.isSuccessful()) {
int before_update = list_a.size();
list_a.addAll(response.body());
int after_update = list_a.size();
adapter.notifyItemRangeInserted(before_update, after_update);
page++;
}
}
#Override
public void onFailure(Call<List<A>> call, Throwable t) {
}
});
}
}
I cannot access list_a here because it throws null pointer exception. I guess it is not initialized yet after I come back from another fragment.
Got a list (recyclerview) that should show an image for certain types of Class X, everything is working perfectly, the thing is that after I start another activity and finish to go back to it, all of the images are removed, except for the ones that don't have a type 1 after them, so it seems there is a reason that causes if type 1 doesn't make its image only gone, but all the previous
private class XHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private ImageView mImageView;
private X mX;
private XHolder(View v) {
super(v);
v.setOnClickListener(this);
mImageView = (ImageView) v.findViewById(R.id.imageview);
}
public void bindX(X x){
mX = x;
if(mX.getType() == 1) {
mImageView.setVisibility(View.GONE);
}
}
#Override
public void onClick(View v) {
xClickEvent(mX);
}
}
Add else condition too, like:
if (mX.getType() == 1) {
mImageView.setVisibility(View.GONE);
} else {
mImageView.setVisibility(View.VISIBLE);
}
RecyclerView does not inflate your View every time, it rather uses the View going out of the screen in the onBindViewHolder() for the next item to appear. So you need to handle if and else condition both each time.