eu.davidea.viewholders.FlexibleViewHolder.java Source code

Java tutorial

Introduction

Here is the source code for eu.davidea.viewholders.FlexibleViewHolder.java

Source

/*
 * Copyright 2016 Davide Steduto
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package eu.davidea.viewholders;

import android.animation.Animator;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import java.util.List;

import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.SelectableAdapter;
import eu.davidea.flexibleadapter.helpers.ItemTouchHelperCallback;
import eu.davidea.flexibleadapter.items.IFlexible;

/**
 * Helper Class that implements:
 * <br/>- Single tap
 * <br/>- Long tap
 * <br/>- Touch for Drag and Swipe.
 * <p>You must extend and implement this class for the own ViewHolder.</p>
 *
 * @author Davide Steduto
 * @since 03/01/2016 Created
 * <br/>23/01/2016 ItemTouch with Drag&Drop, Swipe
 * <br/>26/01/2016 Constructor revisited
 * <br/>18/06/2016 StickyHeader flag is delegated to the super class (ContentViewHolder)
 */
public abstract class FlexibleViewHolder extends ContentViewHolder implements View.OnClickListener,
        View.OnLongClickListener, View.OnTouchListener, ItemTouchHelperCallback.ViewHolderCallback {

    private static final String TAG = FlexibleViewHolder.class.getSimpleName();

    //FlexibleAdapter is needed to retrieve listeners and item status
    protected final FlexibleAdapter mAdapter;

    //These 2 fields avoid double tactile feedback triggered by Android during the touch event
    // (Drag or Swipe), also assure the LongClick event is correctly fired for ActionMode if that
    // was the user intention.
    private boolean mLongClickSkipped = false;
    private boolean alreadySelected = false;

    //State for Dragging & Swiping actions
    protected int mActionState = ItemTouchHelper.ACTION_STATE_IDLE;

    /*--------------*/
    /* CONSTRUCTORS */
    /*--------------*/

    /**
     * Default constructor.
     *
     * @param view    The {@link View} being hosted in this ViewHolder
     * @param adapter Adapter instance of type {@link FlexibleAdapter}
     * @since 5.0.0-b1
     */
    public FlexibleViewHolder(View view, FlexibleAdapter adapter) {
        this(view, adapter, false);
    }

    /**
     * Constructor to configure the sticky behaviour of a view.
     * <p><b>Note:</b> StickyHeader works only if the item has been declared of type
     * {@link eu.davidea.flexibleadapter.items.IHeader}.</p>
     *
     * @param view         The {@link View} being hosted in this ViewHolder
     * @param adapter      Adapter instance of type {@link FlexibleAdapter}
     * @param stickyHeader true if the View can be a Sticky Header, false otherwise
     * @since 5.0.0-b7
     */
    public FlexibleViewHolder(View view, FlexibleAdapter adapter, boolean stickyHeader) {
        super(view, adapter, stickyHeader);
        this.mAdapter = adapter;

        getContentView().setOnClickListener(this);
        getContentView().setOnLongClickListener(this);
    }

    /*--------------------------------*/
    /* CLICK LISTENERS IMPLEMENTATION */
    /*--------------------------------*/

    /**
     * {@inheritDoc}
     *
     * @since 5.0.0-b1
     */
    @Override
    @CallSuper
    public void onClick(View view) {
        int position = getFlexibleAdapterPosition();
        if (!mAdapter.isEnabled(position))
            return;
        //Experimented that, if LongClick is not consumed, onClick is fired. We skip the
        //call to the listener in this case, which is allowed only in ACTION_STATE_IDLE.
        if (mAdapter.mItemClickListener != null && mActionState == ItemTouchHelper.ACTION_STATE_IDLE) {
            if (FlexibleAdapter.DEBUG)
                Log.v(TAG, "onClick on position " + position + " mode=" + mAdapter.getMode());
            //Get the permission to activate the View from user
            if (mAdapter.mItemClickListener.onItemClick(position)) {
                //Now toggle the activation
                if (!mAdapter.isSelected(position) && itemView.isActivated()
                        || mAdapter.isSelected(position) && !itemView.isActivated()) {
                    toggleActivation();
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     * @since 5.0.0-b1
     */
    @Override
    @CallSuper
    public boolean onLongClick(View view) {
        int position = getFlexibleAdapterPosition();
        if (!mAdapter.isEnabled(position))
            return false;
        if (FlexibleAdapter.DEBUG)
            Log.v(TAG, "onLongClick on position " + position + " mode=" + mAdapter.getMode());
        //If DragLongPress is enabled, then LongClick must be skipped and the listener will
        // be called in onActionStateChanged in Drag mode.
        if (mAdapter.mItemLongClickListener != null && !mAdapter.isLongPressDragEnabled()) {
            mAdapter.mItemLongClickListener.onItemLongClick(position);
            toggleActivation();
            return true;
        }
        mLongClickSkipped = true;
        return false;
    }

    /**
     * <b>Should be used only by the Handle View!</b><br/>
     * {@inheritDoc}
     *
     * @see #setDragHandleView(View)
     * @since 5.0.0-b1
     */
    @Override
    public boolean onTouch(View view, MotionEvent event) {
        int position = getFlexibleAdapterPosition();
        if (!mAdapter.isEnabled(position))
            return false;
        if (FlexibleAdapter.DEBUG)
            Log.v(TAG, "onTouch with DragHandleView on position " + position + " mode=" + mAdapter.getMode());
        if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN && mAdapter.isHandleDragEnabled()) {
            //Start Drag!
            mAdapter.getItemTouchHelper().startDrag(this);
        }
        return false;
    }

    /*--------------*/
    /* MAIN METHODS */
    /*--------------*/

    /**
     * Sets the inner view which will be used to drag the Item ViewHolder.
     *
     * @param view handle view
     * @see #onTouch(View, MotionEvent)
     * @since 5.0.0-b1
     */
    @CallSuper
    @SuppressWarnings("ConstantConditions")
    protected void setDragHandleView(@NonNull View view) {
        if (view != null)
            view.setOnTouchListener(this);
    }

    /**
     * Allows to change and see the activation status on the ItemView and to perform object
     * animation in it.
     * <p><b>IMPORTANT NOTE!</b> the change of the background is visible if you added
     * <i>android:background="?attr/selectableItemBackground"</i> on the item layout AND
     * in the style.xml.<br/>
     * Adapter must have a reference to its instance to check selection state.</p>
     * <p>This must be called every time we want the activation state visible on the ItemView,
     * for instance, after a Click (to add the item to the selection list) or after a LongClick
     * (to activate the ActionMode) or during a Drag (to show that we enabled the Drag).</p>
     * If you do this, it's not necessary to invalidate the row (with notifyItemChanged):
     * In this way <i>bindViewHolder</i> is NOT called and inner Views can animate without
     * interruption, so you can see the animation running still having the selection activated.
     *
     * @since 5.0.0-b1
     */
    @CallSuper
    protected void toggleActivation() {
        itemView.setActivated(mAdapter.isSelected(getFlexibleAdapterPosition()));
        if (itemView.isActivated() && getActivationElevation() > 0)
            ViewCompat.setElevation(itemView, getActivationElevation());
        else if (getActivationElevation() > 0)//Leave unaltered the default elevation
            ViewCompat.setElevation(itemView, 0);
    }

    /**
     * Allows to set elevation while the view is activated.
     * <p>Override to return desired value of elevation on this itemView.</p>
     *
     * @return never elevate, returns 0dp if not overridden
     * @since 5.0.0-b2
     */
    public float getActivationElevation() {
        return 0f;
    }

    /**
     * Allows to activate the itemView when Swipe event occurs.
     * <p>This method returns always false; Extend with "return true" to Not expand or collapse
     * this ItemView onClick events.</p>
     *
     * @return always false, if not overridden
     * @since 5.0.0-b2
     */
    protected boolean shouldActivateViewWhileSwiping() {
        return false;
    }

    /**
     * Allows to add and keep item selection if ActionMode is active.
     * <p>This method returns always false; Extend with "return true" to add item to the ActionMode
     * count.</p>
     *
     * @return always false, if not overridden
     * @since 5.0.0-b2
     */
    protected boolean shouldAddSelectionInActionMode() {
        return false;
    }

    /*-----------*/
    /* ANIMATION */
    /*-----------*/

    /**
     * This method is automatically called by FlexibleAdapter to animate the View while the user
     * actively scrolls the list (forward or backward).
     * <p>Implement your logic for different animators based on position, selection and/or
     * direction.</p>
     * Create your {@link Animator}(s), then add it to the list of animators.
     *
     * @param position  can be used to differentiate the Animators based on positions
     * @param isForward can be used to separate animation from top/bottom or from left/right scrolling
     * @since 5.0.0-b8
     * @see eu.davidea.flexibleadapter.helpers.AnimatorHelper
     */
    public void scrollAnimators(@NonNull List<Animator> animators, int position, boolean isForward) {
        //Free to implement
    }

    /*--------------------------------*/
    /* TOUCH LISTENERS IMPLEMENTATION */
    /*--------------------------------*/

    /**
     * Here we handle the event of when the ItemTouchHelper first registers an item as being
     * moved or swiped.
     * <p>In this implementations, View activation is automatically handled in case of Drag:
     * The Item will be added to the selection list if not selected yet and mode MULTI is activated.</p>
     *
     * @param position    the position of the item touched
     * @param actionState one of {@link ItemTouchHelper#ACTION_STATE_SWIPE} or
     *                    {@link ItemTouchHelper#ACTION_STATE_DRAG}.
     * @since 5.0.0-b1
     */
    @Override
    @CallSuper
    public void onActionStateChanged(int position, int actionState) {
        mActionState = actionState;
        alreadySelected = mAdapter.isSelected(position);
        if (FlexibleAdapter.DEBUG)
            Log.v(TAG, "onActionStateChanged position=" + position + " mode=" + mAdapter.getMode() + " actionState="
                    + (actionState == ItemTouchHelper.ACTION_STATE_SWIPE ? "Swipe(1)" : "Drag(2)"));
        if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
            if (!alreadySelected) {
                //Be sure, if MODE_MULTI is active, to add this item to the selection list (call listener!)
                //Also be sure user consumes the long click event if not done in onLongClick.
                //Drag by LongPress or Drag by handleView
                if (mLongClickSkipped || mAdapter.getMode() == SelectableAdapter.MODE_MULTI) {
                    //Next check, allows to initiate the ActionMode and to add selection if configured
                    if ((shouldAddSelectionInActionMode() || mAdapter.getMode() != SelectableAdapter.MODE_MULTI)
                            && mAdapter.mItemLongClickListener != null && mAdapter.isSelectable(position)) {
                        mAdapter.mItemLongClickListener.onItemLongClick(position);
                        alreadySelected = true; //Keep selection on release!
                    }
                }
                //If still not selected, be sure current item appears selected for the Drag transition
                if (!alreadySelected) {
                    mAdapter.toggleSelection(position);
                }
            }
            //Now toggle the activation, Activate view and make selection visible only if necessary
            if (!itemView.isActivated()) {
                toggleActivation();
            }
        } else if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && shouldActivateViewWhileSwiping()
                && !alreadySelected) {
            mAdapter.toggleSelection(position);
            toggleActivation();
        }
    }

    /**
     * Here we handle the event of when the ItemTouchHelper has completed the move or swipe.
     * <p>In this implementation, View activation is automatically handled.</p>
     * In case of Drag, the state will be cleared depends by current selection mode!
     *
     * @param position the position of the item released
     * @since 5.0.0-b1
     */
    @Override
    @CallSuper
    public void onItemReleased(int position) {
        if (FlexibleAdapter.DEBUG)
            Log.v(TAG, "onItemReleased position=" + position + " mode=" + mAdapter.getMode() + " actionState="
                    + (mActionState == ItemTouchHelper.ACTION_STATE_SWIPE ? "Swipe(1)" : "Drag(2)"));
        //Be sure to keep selection if MODE_MULTI and shouldAddSelectionInActionMode is active
        if (!alreadySelected) {
            if (shouldAddSelectionInActionMode() && mAdapter.getMode() == SelectableAdapter.MODE_MULTI) {
                mAdapter.mItemLongClickListener.onItemLongClick(position);
                if (mAdapter.isSelected(position)) {
                    toggleActivation();
                }
            } else if (shouldActivateViewWhileSwiping() && itemView.isActivated()) {
                mAdapter.toggleSelection(position);
                toggleActivation();
            } else if (mActionState == ItemTouchHelper.ACTION_STATE_DRAG) {
                mAdapter.toggleSelection(position);
                if (itemView.isActivated()) {
                    toggleActivation();
                }
            }
        }
        //Reset internal action state ready for next action
        mLongClickSkipped = false;
        mActionState = ItemTouchHelper.ACTION_STATE_IDLE;
    }

    /**
     * @since 5.0.0-b7
     */
    @Override
    public boolean isDraggable() {
        IFlexible item = mAdapter.getItem(getFlexibleAdapterPosition());
        return item != null && item.isDraggable();
    }

    /**
     * @since 5.0.0-b7
     */
    @Override
    public boolean isSwipeable() {
        IFlexible item = mAdapter.getItem(getFlexibleAdapterPosition());
        return item != null && item.isSwipeable();
    }

    /**
     * @since 5.0.0-b6
     */
    @Override
    public View getFrontView() {
        return itemView;
    }

    /**
     * @since 5.0.0-b6
     */
    @Override
    public View getRearLeftView() {
        return null;
    }

    /**
     * @since 5.0.0-b6
     */
    @Override
    public View getRearRightView() {
        return null;
    }

}