com.flexible.viewholders.FlexibleViewHolder.java Source code

Java tutorial

Introduction

Here is the source code for com.flexible.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 com.flexible.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 com.flexible.flexibleadapter.FlexibleAdapter;
import com.flexible.flexibleadapter.SelectableAdapter;
import com.flexible.flexibleadapter.helpers.ItemTouchHelperCallback;
import com.flexible.flexibleadapter.items.IFlexible;

import java.util.List;

/**
 * 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}
     *
     * @see #toggleActivation()
     * @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
                toggleActivation();
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     * @see #toggleActivation()
     * @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) || !isDraggable()) {
            Log.w(TAG, "Can't start drag: Item is not enabled or draggable!");
            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 this itemView.
     *
     * @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 animation
     * on inner views.
     * <p><b>IMPORTANT NOTE!</b> the selected background is visible if you added
     * {@code android:background="?attr/selectableItemBackground"} on the item layout <u>AND</u>
     * customized the file {@code style.xml}.</p>
     * Alternatively, to set a background at runtime, you can use the new
     * <p><b>Note:</b> This method 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 dragging (to show that we enabled
     * the Drag).</p>
     * If you follow the above instructions, it's not necessary to invalidate this view with
     * {@code notifyItemChanged}: In this way {@code bindViewHolder} won't be called and inner
     * views can animate without interruptions, eventually you will see the animation running
     * on those inner views at the same time of selection activation.
     *
     * @see #getActivationElevation()
     * @since 5.0.0-b1
     */
    @CallSuper
    public void toggleActivation() {
        // Only for selectable items
        int position = getFlexibleAdapterPosition();
        if (!mAdapter.isSelectable(position))
            return;
        // [De]Activate the view
        boolean selected = mAdapter.isSelected(position);
        if (itemView.isActivated() && !selected || !itemView.isActivated() && selected) {
            itemView.setActivated(selected);
            // Apply elevation
            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>
     * <b>Note:</b> returned value must be in Pixel.
     *
     * @return {@code 0px} (never elevate) if not overridden
     * @see #toggleActivation()
     * @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; Override with {@code "return true"} to Not expand or
     * collapse this itemView onClick events.</p>
     *
     * @return always false, if not overridden
     * @see #toggleActivation()
     * @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;Override with {@code "return true"}  to add the item
     * to the ActionMode count.</p>
     *
     * @return always false, if not overridden
     * @see #toggleActivation()
     * @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>
     * Use can take one of the predefined Animator from {@link } or create your own
     * {@link Animator}(s), then add it to the list of animators.
     *
     * @param animators NonNull list of animators, which you should add new 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
     * @see
     * @since 5.0.0-b8
     */
    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 {@code ItemTouchHelper} first registers an item
     * as being moved or swiped.
     * <p>In this implementation, View activation is automatically handled if dragged: 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}.
     * @see #shouldActivateViewWhileSwiping()
     * @see #shouldAddSelectionInActionMode()
     * @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
     * @see #shouldActivateViewWhileSwiping()
     * @see #shouldAddSelectionInActionMode()
     * @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;
    }

    /**
     * @return the boolean value from the item flag, true to allow dragging
     * @since 5.0.0-b7
     */
    @Override
    public final boolean isDraggable() {
        IFlexible item = mAdapter.getItem(getFlexibleAdapterPosition());
        return item != null && item.isDraggable();
    }

    /**
     * @return the boolean value from the item flag, true to allow swiping
     * @since 5.0.0-b7
     */
    @Override
    public final 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;
    }

}