0

I am trying to use a swipeCallback on a list with modeladapter. In order to make it work, I stripped down all my customization and modeled it close to the sample app, but the combination produces the error of not allowing undo. When I swipe, this happens:

enter image description here

The swipe works, but the undo icon does not show up. Any ideas what I am doing wrong? The underlying fragment is this:

public class EditFragment extends Fragment implements ItemTouchCallback, SimpleSwipeCallback.ItemSwipeCallback {

    private FragmentEditBinding oBinding;
    private SongViewModel oViewModel;
    //save our FastAdapter
    private FastAdapter fastAdapter;
    private ModelAdapter<ModelSongCounter, ModelItemView> itemAdapter;
    //drag & drop
    private SimpleDragCallback touchCallback;
    private ItemTouchHelper touchHelper;

    public EditFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        //init Databinding
        oBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_edit, container, false);//.setContentView(getActivity(), R.layout.fragment_main);

        //LayoutInflaterCompat.setFactory(getLayoutInflater(), new IconicsLayoutInflater(getActivity()));

        //style our ui
        new MaterializeBuilder().withActivity(getActivity()).build();

        //adapters
        //FastScrollIndicatorAdapter fastScrollIndicatorAdapter = new FastScrollIndicatorAdapter();
        itemAdapter = new ModelAdapter<>(new IInterceptor<ModelSongCounter, ModelItemView>() {
            @Override
            public ModelItemView intercept(ModelSongCounter iconModel) {
                return new ModelItemView(iconModel);
            }
        });

        //create our FastAdapter which will manage everything
        fastAdapter = FastAdapter.with(Arrays.asList(itemAdapter));
        fastAdapter.withSelectable(true);

        //get our recyclerView and do basic setup
        //RecyclerView rv = oBinding.SongRecyclerView;
        oBinding.SongRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        //oBinding.SongRecyclerView.setItemAnimator(new SlideDownAlphaAnimator());
        oBinding.SongRecyclerView.setAdapter(fastAdapter);

        //get ViewModels from Provider
        oViewModel = ViewModelProviders.of(getActivity()).get(SongViewModel.class);

        //get rid of the annoying blink
        oBinding.SongRecyclerView.setItemAnimator(null);

        //add Observer to ViewModel
        // The onChanged() method fires when the observed data changes and the activity is
        // in the foreground.
        oViewModel.getAllCatsLive().observe(this, new Observer<List<ModelSongCounter>>() {
            @Override
            public void onChanged(@Nullable List<ModelSongCounter> modelSongCounters) {
                itemAdapter.set(modelSongCounters);
            }
        });

        fastAdapter.withEventHook(new ClickEventHook<ModelItemView>() {

            @Nullable
            @Override
            public View onBind(@NonNull RecyclerView.ViewHolder viewHolder) {
                if (viewHolder instanceof ModelItemView.ViewHolder) {
                    return ((ModelItemView.ViewHolder) viewHolder).Minus;
                }
                return null;
            }

            @Override
            public void onClick(View v, int position, FastAdapter<ModelItemView> fastAdapter, ModelItemView item) {
                //react on the click event
                oViewModel.decrement(item.getModel().uid);
            }
        });

        fastAdapter.withEventHook(new ClickEventHook<ModelItemView>() {

            @Nullable
            @Override
            public View onBind(@NonNull RecyclerView.ViewHolder viewHolder) {
                if (viewHolder instanceof ModelItemView.ViewHolder) {
                    return ((ModelItemView.ViewHolder) viewHolder).Plus;
                }
                return null;
            }

            @Override
            public void onClick(View v, int position, FastAdapter<ModelItemView> fastAdapter, ModelItemView item) {
                //react on the click event
                oViewModel.increment(item.getModel().uid);
            }
        });

        //restore selections (this has to be done after the items were added
        fastAdapter.withSavedInstanceState(savedInstanceState);

        //Swipable stuff within OnCreateView

        Drawable leaveBehindDrawableLeft = new IconicsDrawable(getContext())
                .icon(MaterialDesignIconic.Icon.gmi_delete)
                .color(Color.WHITE)
                .sizeDp(24);
        Drawable leaveBehindDrawableRight = new IconicsDrawable(getContext())
                .icon(MaterialDesignIconic.Icon.gmi_archive)
                .color(Color.WHITE)
                .sizeDp(24);

        touchCallback = new SimpleSwipeDragCallback(
                this,
                this,
                leaveBehindDrawableLeft,
                ItemTouchHelper.LEFT,
                ContextCompat.getColor(getContext(), R.color.md_red_900)
        )
                .withBackgroundSwipeRight(ContextCompat.getColor(getContext(), R.color.md_blue_900))
                .withLeaveBehindSwipeRight(leaveBehindDrawableRight);

        touchHelper = new ItemTouchHelper(touchCallback); // Create ItemTouchHelper and pass with parameter the SimpleDragCallback
        touchHelper.attachToRecyclerView(oBinding.SongRecyclerView); // Attach ItemTouchHelper to RecyclerView

        //restore selections (this has to be done after the items were added
        fastAdapter.withSavedInstanceState(savedInstanceState);

        return oBinding.getRoot();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        //add the values which need to be saved from the adapter to the bundle
        outState = fastAdapter.saveInstanceState(outState);
        super.onSaveInstanceState(outState);
    }

    //Swipable...and probably relevant for expandables, since there is TouchOnMove
    @Override
    public boolean itemTouchOnMove(int oldPosition, int newPosition) {
        //DragDropUtil.onMove((ItemAdapter)itemAdapter, oldPosition, newPosition);  // change position
        return true;
    }

    @Override
    public void itemTouchDropped(int oldPosition, int newPosition) {
        //f.e. save new order in database
    }

    @Override
    public void itemSwiped(int position, int direction) {
        // -- Option 1: Direct action --
        //do something when swiped such as: select, remove, update, ...:
        //A) fastItemAdapter.select(position);
        //B) fastItemAdapter.remove(position);
        //C) update item, set "read" if an email etc

        // -- Option 2: Delayed action --
        final ModelItemView item = itemAdapter.getAdapterItem(position);
        item.setSwipedDirection(direction);

        // This can vary depending on direction but remove & archive simulated here both results in
        // removal from list
        final Runnable removeRunnable = new Runnable() {
            @Override
            public void run() {
                item.setSwipedAction(null);
                int position = itemAdapter.getAdapterPosition(item);
                if (position != RecyclerView.NO_POSITION) {
                    //this sample uses a filter. If a filter is used we should use the methods provided by the filter (to make sure filter and normal state is updated)
                    //fastItemAdapter.getItemFilter().remove(position);
                    itemAdapter.remove(position);
                }
            }
        };
        final View rv = oBinding.SongRecyclerView;
        rv.postDelayed(removeRunnable, 3000);

        item.setSwipedAction(new Runnable() {
            @Override
            public void run() {
                rv.removeCallbacks(removeRunnable);
                item.setSwipedDirection(0);
                int position = itemAdapter.getAdapterPosition(item);
                if (position != RecyclerView.NO_POSITION) {
                    fastAdapter.notifyItemChanged(position);
                }
            }
        });

        fastAdapter.notifyItemChanged(position);

        //TODO can this above be made more generic, along with the support in the item?
    }
}

This is the swipable ModelItem (the model "ModelSongCounter" is just a POJO):

public class ModelItemView
        extends ModelAbstractItem<ModelSongCounter, ModelItemView, ModelItemView.ViewHolder>
        implements ISwipeable<ModelItemView, IItem>, IDraggable<ModelItemView, IItem> {

    public StringHolder undoTextSwipeFromLeft;
    public int iSwipedDirection;
    private Runnable rSwipedAction;
    public boolean bSwipable = true;
    public boolean draggable = true;

    public ModelItemView(ModelSongCounter icon) {
        super(icon);
    }

    /**
     * defines the type defining this item. must be unique. preferably an id
     *
     * @return the type
     */
    @Override
    public int getType() {
        return R.id.iconics_tag_id;
    }

    /**
     * defines the layout which will be used for this item in the list
     *
     * @return the layout for this item
     */
    @Override
    public int getLayoutRes() {
        return R.layout.item_view;
    }

    /**
     * binds the data of this item onto the viewHolder
     *
     * @param viewHolder the viewHolder of this item
     */
    @Override
    public void bindView(ViewHolder viewHolder, List<Object> payloads) {
        super.bindView(viewHolder, payloads);

        //define our data for the view
        viewHolder.name.setText(getModel().getName());
        viewHolder.counter.setText(Integer.toString(getModel().getCounter()));

        viewHolder.swipeResultContent.setVisibility(iSwipedDirection != 0 ? View.VISIBLE : View.GONE);
        viewHolder.itemContent.setVisibility(iSwipedDirection != 0 ? View.GONE : View.VISIBLE);

        CharSequence swipedAction = null;
        CharSequence swipedText = null;
        if(iSwipedDirection != 0){
            swipedAction = viewHolder.itemView.getContext().getString(R.string.action_undo);
            swipedText = iSwipedDirection == ItemTouchHelper.LEFT ? "Removed" : "Archived - Should not be implemented!";
            viewHolder.swipeResultContent.setBackgroundColor(
                    ContextCompat.getColor(viewHolder.itemView.getContext(),
                    iSwipedDirection == ItemTouchHelper.LEFT ? R.color.md_red_900 : R.color.md_blue_900));
        }
        viewHolder.swipedAction.setText(swipedAction == null ? "" : swipedAction);
        viewHolder.swipedText.setText(swipedText == null ? "" : swipedText);
        viewHolder.rSwipedActionRunnable = this.rSwipedAction;
    }

    @Override
    public void unbindView(ViewHolder holder) {
        super.unbindView(holder);
        holder.name.setText(null);
        holder.counter.setText(null);
        holder.swipedAction.setText(null);
        holder.swipedText.setText(null);
        holder.rSwipedActionRunnable = this.rSwipedAction;
    }

    @Override
    public ViewHolder getViewHolder(View v) {
        return new ViewHolder(v);
    }

    //SWipable
    @Override
    public boolean isSwipeable() {
        return this.bSwipable;
    }

    @Override
    public ModelItemView withIsSwipeable(boolean swipeableP) {
        this.bSwipable = swipeableP;
        return this;
    }

    public void setSwipedDirection(int iSwipedDirectionP){
        this.iSwipedDirection = iSwipedDirectionP;
    }

    public void setSwipedAction(Runnable actionP){
        this.rSwipedAction = actionP;
    }

    @Override
    public boolean isDraggable() {
        return draggable;
    }

    @Override
    public ModelItemView withIsDraggable(boolean draggableP) {
        this.draggable = draggableP;
        return this;
    }

    /**
     * our ViewHolder
     */
    protected static class ViewHolder extends RecyclerView.ViewHolder {

        protected View view;

        @BindView(R.id.material_drawer_song)
        public TextView name;
        @BindView(R.id.material_drawer_counter)
        public TextView counter;
        @BindView(R.id.material_drawer_minus)
        public ImageView Minus;
        @BindView(R.id.material_drawer_plus)
        public ImageView Plus;

        @BindView(R.id.swipe_result_content)
        public View swipeResultContent;
        @BindView(R.id.item_content)
        public View itemContent;
        @BindView(R.id.swiped_text)
        public TextView swipedText;
        @BindView(R.id.swiped_action)
        public TextView swipedAction;

        public Runnable rSwipedActionRunnable;

        public ViewHolder(View view) {
            super(view);
            ButterKnife.bind(this, view);
            //this.view = view;// ?
            swipedAction.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (rSwipedActionRunnable != null){
                        rSwipedActionRunnable.run();
                    }
                }
            });
        }
    }
}

And this is the XML-view of the List-Item:

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="@dimen/material_drawer_item_primary">

    <LinearLayout
        android:id="@+id/swipe_result_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:visibility="visible"
        android:paddingEnd="@dimen/material_drawer_vertical_padding"
        android:paddingLeft="@dimen/material_drawer_vertical_padding"
        android:paddingRight="@dimen/material_drawer_vertical_padding"
        android:paddingStart="@dimen/material_drawer_vertical_padding">

        <TextView
            android:id="@+id/swiped_text"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:gravity="center_vertical|start"
            android:lines="1"
            android:singleLine="true"
            android:textDirection="anyRtl"
            android:textColor="@android:color/primary_text_dark"
            android:textSize="@dimen/material_drawer_item_primary_text"
            tools:text="Removed"/>
        <TextView
            android:id="@+id/swiped_action"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:fontFamily="sans-serif"
            android:gravity="center_vertical|start"
            android:lines="1"
            android:singleLine="true"
            android:textDirection="anyRtl"
            android:textAllCaps="true"
            android:textColor="@android:color/primary_text_dark"
            android:textStyle="bold"
            android:textSize="@dimen/material_drawer_item_primary_description"
            android:text="@string/action_undo"/>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/item_content"
        android:layout_width="match_parent"
        android:layout_height="@dimen/material_drawer_item_primary"
        android:orientation="horizontal"
        android:paddingEnd="@dimen/material_drawer_vertical_padding"
        android:paddingLeft="@dimen/material_drawer_vertical_padding"
        android:paddingRight="@dimen/material_drawer_vertical_padding"
        android:paddingStart="@dimen/material_drawer_vertical_padding">

        <TextView
            android:id="@+id/material_drawer_song"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginTop="12dp"
            android:layout_marginLeft="12dp"
            android:lines="1"
            android:singleLine="true"
            android:textSize="@dimen/material_drawer_item_primary_text"
            tools:text="Some drawer text" />

        <TextView
            android:id="@+id/material_drawer_counter"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:layout_marginLeft="12dp"
            android:fontFamily="sans-serif"
            android:lines="1"
            android:singleLine="true"
            android:textSize="@dimen/material_drawer_item_primary_description"
            tools:text="Some counter text"


            android:layout_weight="1"
            android:gravity="center_vertical|start" />

        <ImageView
            android:id="@+id/material_drawer_minus"
            android:layout_width="50dp"
            android:layout_height="match_parent"
            app:ico_color="@color/md_black_1000"
            app:ico_icon="@string/gmd_remove_circle"
            app:ico_size="50dp" />

        <ImageView
            android:id="@+id/material_drawer_plus"
            android:layout_width="50dp"
            android:layout_height="match_parent"
            app:ico_color="@color/md_black_1000"
            app:ico_icon="gmd-add_circle"
            app:ico_size="50dp" />
    </LinearLayout>

    </FrameLayout>
</layout>
mikepenz
  • 12,708
  • 14
  • 77
  • 117
Apfelsaft23
  • 316
  • 2
  • 23

2 Answers2

2

The part of the code managing the display of the undo button is inside the bindView() method of the ModelItemView.

Please ensure that after swiping the correct item is retrieved via the getItem(position) in the itemSwiped and ensure that the correct item gets notified via notifyItemChanged().

After that ensure that the bindView() is triggered again on that element, and that it has the proper swipeDirection as set via setSwipedDirection(direction).

This is important as:

viewHolder.swipeResultContent.setVisibility(iSwipedDirection != 0 ? View.VISIBLE : View.GONE);
viewHolder.itemContent.setVisibility(iSwipedDirection != 0 ? View.GONE : View.VISIBLE);

Is used to properly adjust the visibility of the views, including showing the undo button.

mikepenz
  • 12,708
  • 14
  • 77
  • 117
0

After several weeks, the answer is simple:

The undo-Button depends on the itemanimator which I always nullified to avoid the blinking. Here is a nice custom animator class that supresses whatever animation you dont want. Now all I had to do was

RecyclerView.setItemAnimator(new CustomItemAnimator());
Apfelsaft23
  • 316
  • 2
  • 23
  • Funny enough that should not be necessary. Did you debug and see if the `notify` would correctly trigger the update of the item via its `bindView()`? – mikepenz Nov 24 '18 at 11:20
  • Hi Mike! Yes, the bindview stuff ist triggered. I also tested it with your sample app - when I nullify the animator, the undo button wont show up. From what I can tell, the Undo Swipe definitely depends on an itemAnimator. – Apfelsaft23 Nov 28 '18 at 15:54