3

I'm new to Android but after reading some tutorials I've managed to write simple app that's using RecyclerView with Realm data by extending RealmRecyclerViewAdapter.
I know that default animator needs stable ids so I've added an unique id to my entity. Everything seems to work fine (swipe to delete, adding, all realm operations) except drag and drop. Well, it kind of works meaning that data and ui stay consistent but animation looks weird and I can move items down only by one position.
A picture is worth a thousand words so here it is:
enter image description here

I guess I will also have to paste the code. I've tried to make it as short as possible.

public class MainActivity extends AppCompatActivity implements OnStartDragListener {
    private ItemTouchHelper touchHelper;
    private Realm realm;
    private TestRecyclerViewAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Irrelevant init stuff stripped

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        recyclerView.setHasFixedSize(true);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));

        adapter = new TestRecyclerViewAdapter(this, realm.where(Item.class)
                                                         .findAll()
                                                         .sort(Item.INDEX));
        recyclerView.setAdapter(adapter);

        ItemTouchHelper.Callback callback = new ItemTouchHelperCallback(adapter);
        touchHelper = new ItemTouchHelper(callback);
        touchHelper.attachToRecyclerView(recyclerView);
    }

    @Override
    public void onStartDrag(RecyclerView.ViewHolder viewHolder) {
        touchHelper.startDrag(viewHolder);
    }

    // not sure how this method should be implemented
    public void moveItem(Item item, final int fromPosition, final int toPosition) {
        final int index = item.index;
        realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                Item item = realm.where(Item.class).equalTo(Item.INDEX, index).findFirst();
                if (fromPosition < toPosition) {
                    RealmResults<Item> results = realm.where(Item.class)
                                                      .greaterThan(Item.INDEX, fromPosition)
                                                      .lessThanOrEqualTo(Item.INDEX, toPosition)
                                                      .findAll();
                    for (int i = 0; i < results.size(); i++) {
                        results.get(i).index -= 1;
                    }
                } else {
                    RealmResults<Item> results = realm.where(Item.class)
                                                      .greaterThanOrEqualTo(Item.INDEX, toPosition)
                                                      .lessThan(Item.INDEX, fromPosition)
                                                      .findAll();
                    for (int i = 0; i < results.size(); i++) {
                        results.get(i).index += 1;
                    }
                }
                item.index = toPosition;
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        realm.close();
    }
}

­

public class Item extends RealmObject {
    public static final String INDEX = "index";
    public static final String TEXT = "text";
    public static final String ID = "id";

    @PrimaryKey
    public long id;
    @Index
    public int index;
    public String text;
}

­

interface OnStartDragListener {
    void onStartDrag(RecyclerView.ViewHolder viewHolder);
}

­

class TestRecyclerViewAdapter extends RealmRecyclerViewAdapter<Item,
        TestRecyclerViewAdapter.EditInfoViewHolder> implements ItemTouchHelperAdapter {

    private final static String TAG = TestRecyclerViewAdapter.class.getName();
    private final MainActivity activity;

    public TestRecyclerViewAdapter(@NonNull MainActivity activity, @Nullable
            OrderedRealmCollection<Item> data) {
        super(activity, data, true);
        this.activity = activity;
        setHasStableIds(true);
    }

    @Override
    public long getItemId(int index) {
        return getData().get(index).id;
    }

    @Override
    public EditInfoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = inflater.inflate(R.layout.item, parent, false);
        return new EditInfoViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final EditInfoViewHolder holder, int position) {
        Item obj = getData().get(position);
        holder.data = obj;
        holder.text.setText(obj.text);
        holder.handle.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
                    activity.onStartDrag(holder);
                }
                return false;
            }
        });
    }

    @Override
    public boolean onItemMove(int fromPosition, int toPosition) {
        activity.moveItem(getData().get(fromPosition), fromPosition, toPosition);
        notifyItemMoved(fromPosition, toPosition);
        return true;
    }

    class EditInfoViewHolder extends RecyclerView.ViewHolder {
        final TextView text;
        final ImageView handle;

        public Item data;
        public EditInfoViewHolder(View itemView) {
            super(itemView);
            text = (TextView) itemView.findViewById(R.id.text);
            handle = (ImageView) itemView.findViewById(R.id.handle);
        }

    }
}

­

interface ItemTouchHelperAdapter {
    boolean onItemMove(int fromPosition, int toPosition);
}

­

class ItemTouchHelperCallback extends ItemTouchHelper.Callback {

    private final ItemTouchHelperAdapter adapter;

    public ItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
        this.adapter = adapter;
    }

    @Override
    public boolean isItemViewSwipeEnabled() {
        return true;
    }

    @Override
    public boolean isLongPressDragEnabled() {
        return false;
    }

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        int swipeFlags = 0;
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
        if (source.getItemViewType() != target.getItemViewType()) {
            return false;
        }
        adapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
    }
}

I feel so guilty for posting this amount of code...

TPlant
  • 463
  • 5
  • 15

1 Answers1

1

Your problems stems from the fact that RealmRecyclerViewAdapter registers a RealmChangeListener which calls adapter.notifyDataSetChanged(), which overrides all RecyclerView animations.

A possible workaround would be to use a regular RecyclerViewAdapter, and when you manipulate the Realm's elements with your UI thread transaction, you force a refresh immediately afterwards.

class TestRecyclerViewAdapter extends RecyclerView.Adapter<...> {

@Override
public boolean onItemMove(int fromPosition, int toPosition) {
    activity.moveItem(getData().get(fromPosition), fromPosition, toPosition); // <-- refresh in there
    notifyItemMoved(fromPosition, toPosition);
    return true;
}

public void moveItem(Item item, final int fromPosition, final int toPosition) {
    final int index = item.index;
    realm.executeTransaction(new Realm.Transaction() {
        @Override
        public void execute(Realm realm) {
            Item item = realm.where(Item.class).equalTo(Item.INDEX, index).findFirst();
            if (fromPosition < toPosition) {
                RealmResults<Item> results = realm.where(Item.class)
                                                  .greaterThan(Item.INDEX, fromPosition)
                                                  .lessThanOrEqualTo(Item.INDEX, toPosition)
                                                  .findAll();
                for (int i = 0; i < results.size(); i++) {
                    results.get(i).index -= 1;
                }
            } else {
                RealmResults<Item> results = realm.where(Item.class)
                                                  .greaterThanOrEqualTo(Item.INDEX, toPosition)
                                                  .lessThan(Item.INDEX, fromPosition)
                                                  .findAll();
                for (int i = 0; i < results.size(); i++) {
                    results.get(i).index += 1;
                }
            }
            item.index = toPosition;
        }
    });
    RealmRefresh.refreshRealm(realm);
}

RealmRefresh would force all async queries to execute synchronously, and also you don't get auto-refresh from transactions like this that execute elsewhere (like background threads). But I think it would work!

Community
  • 1
  • 1
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • I took your advice but I did it a bit differently. I've used regular `RecyclerViewAdapter` and moved realm operations to adapter and it's working so far. – TPlant Nov 10 '16 at 22:29