co.dift.ui.SwipeToAction.java Source code

Java tutorial

Introduction

Here is the source code for co.dift.ui.SwipeToAction.java

Source

/*
 * Copyright (C) 2015 Dift.co
 * http://dift.co
 *
 * 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 co.dift.ui;

import android.animation.Animator;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v7.widget.RecyclerView;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;

import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;

import todo.fika.fikatodo.util.Logger;

public class SwipeToAction {
    private static final int SWIPE_ANIMATION_DURATION = 300;
    private static final int RESET_ANIMATION_DURATION = 500;

    /**
     * ?   ? ??.
     */
    private static final int REVEAL_THRESHOLD = 100;

    /**
     * ? ? ? ?   ?   .
     */
    private static final int SWIPE_THRESHOLD_WIDTH_RATIO = 5;

    private final Logger logger = Logger.Factory.getLogger(getClass());

    private RecyclerView recyclerView;
    private SwipeListener swipeListener;
    private View touchedView;
    private ViewHolder touchedViewHolder;
    private View frontView;
    private View revealLeftView;
    private View revealRightView;

    private float frontViewX;
    private float frontViewW;
    private float frontViewLastX;

    private float downY;
    private float downX;
    private float upX;
    private float upY;

    private long downTime;
    private long upTime;

    private Integer revealThreshold = REVEAL_THRESHOLD;
    private Integer resetAnimationDuration = RESET_ANIMATION_DURATION;
    private Integer swipeThresholdWidthRatio = SWIPE_THRESHOLD_WIDTH_RATIO;

    /**
     *  ? ? .
     */
    private Integer maxSwipeXPosition;

    private Set<View> runningAnimationsOn = new HashSet<>();
    private Queue<Integer> swipeQueue = new LinkedList<>();

    private GestureDetectorCompat gestureDetector;

    /** Constructor **/
    public SwipeToAction(RecyclerView recyclerView, SwipeListener swipeListener) {
        this.recyclerView = recyclerView;
        this.swipeListener = swipeListener;

        init();
    }

    /** Private methods **/
    private void init() {
        gestureDetector = new GestureDetectorCompat(recyclerView.getContext(),
                new GestureDetector.SimpleOnGestureListener() {
                    @Override
                    public void onLongPress(MotionEvent e) {
                        logger.d("onLongPress called!");
                        if (touchedViewHolder != null) {
                            swipeListener.onLongClick(touchedViewHolder.getItemData());
                        }
                    }

                    @Override
                    public boolean onSingleTapUp(MotionEvent e) {
                        logger.d("onSingleTapUp called!");
                        if (touchedViewHolder != null) {
                            swipeListener.onClick(touchedViewHolder.getItemData());
                            return true;
                        }
                        return super.onSingleTapUp(e);
                    }
                });

        recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {

            @Override
            public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
                gestureDetector.onTouchEvent(ev);

                switch (ev.getAction() & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN: {
                    // starting point
                    downX = ev.getX();
                    downY = ev.getY();

                    downTime = new Date().getTime();

                    // which item are we touching
                    resolveItem(downX, downY);

                    break;
                }

                case MotionEvent.ACTION_UP: {
                    upX = ev.getX();
                    upY = ev.getY();
                    upTime = new Date().getTime();

                    resolveState();
                    break;
                }

                case MotionEvent.ACTION_MOVE: {
                    final float x = ev.getX();
                    final float dx = x - downX;

                    if (!shouldMove(dx))
                        break;

                    // current position. moving only over x-axis
                    frontViewLastX = frontViewX + dx + (dx > 0 ? -getRevealThreshold() : getRevealThreshold());
                    //                        logger.d("dx: %f, frontViewLastX: %f", dx, frontViewLastX);
                    if (maxSwipeXPosition != null) {
                        if (frontViewLastX > 0 && frontViewLastX > maxSwipeXPosition) {
                            frontViewLastX = maxSwipeXPosition;
                        } else if (frontViewLastX <= -maxSwipeXPosition) {
                            frontViewLastX = -maxSwipeXPosition;
                        }
                    }
                    frontView.setX(frontViewLastX);

                    if (frontViewLastX > 0) {
                        revealRight();
                    } else {
                        revealLeft();
                    }

                    break;
                }

                case MotionEvent.ACTION_CANCEL: {
                    resolveState();

                    break;
                }
                }

                return false;
            }

            @Override
            public void onTouchEvent(RecyclerView rv, MotionEvent e) {
                logger.d("onTouchEvent called");
            }

            @Override
            public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
                logger.d("onRequestDisallowInterceptTouchEvent called");
            }
        });
    }

    private void resolveItem(float x, float y) {
        touchedView = recyclerView.findChildViewUnder(x, y);
        if (touchedView == null) {
            //no child under
            frontView = null;
            return;
        }

        if (touchedView.getAnimation() == null && runningAnimationsOn.contains(touchedView)) {
            runningAnimationsOn.remove(touchedView);
        }

        // check if the view is being animated. in that case do not allow to move it
        if (runningAnimationsOn.contains(touchedView)) {
            frontView = null;
            return;
        }

        RecyclerView.ViewHolder childViewHolder = recyclerView.getChildViewHolder(touchedView);
        if (childViewHolder instanceof ViewHolder) {
            initViewForItem((ViewHolder) childViewHolder);
        } else {
            touchedViewHolder = null;
            frontView = null;
        }
    }

    private void resolveItem(int adapterPosition) {
        initViewForItem((ViewHolder) recyclerView.findViewHolderForAdapterPosition(adapterPosition));
    }

    private void initViewForItem(ViewHolder viewHolder) {
        touchedViewHolder = viewHolder;
        frontView = viewHolder.getFront();
        revealLeftView = viewHolder.getRevealLeft();
        revealRightView = viewHolder.getRevealRight();
        frontViewX = frontView.getX();
        frontViewW = frontView.getWidth();
    }

    private boolean shouldMove(float dx) {
        if (frontView == null) {
            return false;
        }

        if (dx > 0) {
            return revealRightView != null && Math.abs(dx) > getRevealThreshold();
        } else {
            return revealLeftView != null && Math.abs(dx) > getRevealThreshold();
        }
    }

    private void clear() {
        frontViewX = 0;
        frontViewW = 0;
        frontViewLastX = 0;
        downX = 0;
        downY = 0;
        upX = 0;
        upY = 0;
        downTime = 0;
        upTime = 0;
    }

    private void checkQueue() {
        // workaround in case a swipe call while dragging
        Integer next = swipeQueue.poll();
        if (next != null) {
            int pos = Math.abs(next) - 1;
            if (next < 0) {
                swipeLeft(pos);
            } else {
                swipeRight(pos);
            }
        }
    }

    private void resetPosition() {
        if (frontView == null) {
            return;
        }

        final View animated = touchedView;
        frontView.animate().setDuration(getResetAnimationDuration())
                .setInterpolator(new AccelerateDecelerateInterpolator())
                .setListener(new Animator.AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        runningAnimationsOn.add(animated);
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        runningAnimationsOn.remove(animated);
                        checkQueue();
                        swipeListener.resetComplete();
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {
                        runningAnimationsOn.remove(animated);
                    }

                    @Override
                    public void onAnimationRepeat(Animator animation) {
                        runningAnimationsOn.add(animated);
                    }
                }).x(frontViewX);
    }

    private void resolveState() {
        if (frontView == null) {
            return;
        }

        if (frontViewLastX > frontViewX + frontViewW / getSwipeThresholdWidthRatio()) {
            swipeRight();
        } else if (frontViewLastX < frontViewX - frontViewW / getSwipeThresholdWidthRatio()) {
            swipeLeft();
        } else {
            //            float diffX = Math.abs(downX - upX);
            //            float diffY = Math.abs(downY - upY);

            //            if (diffX <= 5 && diffY <= 5) {
            //                int pressTime = (int) (upTime - downTime);
            //                if  (pressTime > LONG_PRESS_TIME) {
            //                    swipeListener.onLongClick(touchedViewHolder.getItemData());
            //                } else {
            //                    swipeListener.onClick(touchedViewHolder.getItemData());
            //                }
            //            }

            resetPosition();
        }

        clear();
    }

    private void swipeRight() {
        if (frontView == null) {
            return;
        }

        final View animated = touchedView;
        frontView.animate().setDuration(SWIPE_ANIMATION_DURATION).setInterpolator(new AccelerateInterpolator())
                .setListener(new Animator.AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        runningAnimationsOn.add(animated);
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        runningAnimationsOn.remove(animated);
                        if (swipeListener.swipeRight(touchedViewHolder.getItemData())) {
                            resetPosition();
                        } else {
                            checkQueue();
                        }
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {
                        runningAnimationsOn.remove(animated);
                    }

                    @Override
                    public void onAnimationRepeat(Animator animation) {
                        runningAnimationsOn.add(animated);
                    }
                }).x(frontViewX + frontViewW);
    }

    private void swipeLeft() {
        if (frontView == null) {
            return;
        }

        final View animated = touchedView;
        frontView.animate().setDuration(SWIPE_ANIMATION_DURATION).setInterpolator(new DecelerateInterpolator())
                .setListener(new Animator.AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        runningAnimationsOn.add(animated);
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        runningAnimationsOn.remove(animated);
                        if (swipeListener.swipeLeft(touchedViewHolder.getItemData())) {
                            resetPosition();
                        } else {
                            checkQueue();
                        }
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {
                        runningAnimationsOn.remove(animated);
                    }

                    @Override
                    public void onAnimationRepeat(Animator animation) {
                        runningAnimationsOn.add(animated);
                    }
                }).x(frontViewX - frontViewW);

    }

    private void revealRight() {
        if (revealLeftView != null) {
            revealLeftView.setVisibility(View.GONE);
        }

        if (revealRightView != null) {
            revealRightView.setVisibility(View.VISIBLE);
        }
    }

    private void revealLeft() {
        if (revealRightView != null) {
            revealRightView.setVisibility(View.GONE);
        }

        if (revealLeftView != null) {
            revealLeftView.setVisibility(View.VISIBLE);
        }
    }

    /** Exposed methods **/

    public void swipeLeft(int position) {
        // workaround in case a swipe call while dragging
        if (downTime != 0) {
            swipeQueue.add((position + 1) * -1); //use negative to express direction
            return;
        }
        resolveItem(position);
        revealLeft();
        swipeLeft();
    }

    public void swipeRight(int position) {
        // workaround in case a swipe call while dragging
        if (downTime != 0) {
            swipeQueue.add(position + 1);
            return;
        }
        resolveItem(position);
        revealRight();
        swipeRight();
    }

    public void setRevealThreshold(Integer revealThreshold) {
        this.revealThreshold = revealThreshold;
    }

    public int getRevealThreshold() {
        return revealThreshold;
    }

    public Integer getResetAnimationDuration() {
        return resetAnimationDuration;
    }

    public void setResetAnimationDuration(Integer resetAnimationDuration) {
        this.resetAnimationDuration = resetAnimationDuration;
    }

    public Integer getSwipeThresholdWidthRatio() {
        return swipeThresholdWidthRatio;
    }

    public void setSwipeThresholdWidthRatio(Integer swipeThresholdWidthRatio) {
        this.swipeThresholdWidthRatio = swipeThresholdWidthRatio;
    }

    public Integer getMaxSwipeXPosition() {
        return maxSwipeXPosition;
    }

    public void setMaxSwipeXPosition(Integer maxSwipeXPosition) {
        this.maxSwipeXPosition = maxSwipeXPosition;
    }

    /** Public interfaces & classes */

    public interface SwipeListener<T extends Object> {
        boolean swipeLeft(T itemData);

        boolean swipeRight(T itemData);

        void onClick(T itemData);

        void onLongClick(T itemData);

        void resetComplete();
    }

    public static abstract class SimpleSwipeListener<T extends Object> implements SwipeListener<T> {
        @Override
        public boolean swipeLeft(T itemData) {
            return false;
        }

        @Override
        public boolean swipeRight(T itemData) {
            return false;
        }

        @Override
        public void onClick(T itemData) {
        }

        @Override
        public void onLongClick(T itemData) {
        }

        @Override
        public void resetComplete() {
        }
    }

    public interface IViewHolder<T extends Object> {
        View getFront();

        View getRevealLeft();

        View getRevealRight();

        <T extends Object> T getItemData();
    }

    public static abstract class ViewHolder<T extends Object> extends RecyclerView.ViewHolder
            implements IViewHolder {

        public T data;
        public View front;
        public View revealLeft;
        public View revealRight;

        public ViewHolder(View v) {
            super(v);

            ViewGroup vg = (ViewGroup) v;
            front = vg.findViewWithTag("front");
            revealLeft = vg.findViewWithTag("reveal-left");
            revealRight = vg.findViewWithTag("reveal-right");

            int childCount = vg.getChildCount();
            if (front == null) {
                if (childCount < 1) {
                    throw new RuntimeException("You must provide a view with tag='front'");
                } else {
                    front = vg.getChildAt(childCount - 1);
                }
            }

            if (revealLeft == null || revealRight == null) {
                if (childCount < 2) {
                    throw new RuntimeException("You must provide at least one reveal view.");
                } else {
                    // set next to last as revealLeft view only if no revealRight was found
                    if (revealLeft == null && revealRight == null) {
                        revealLeft = vg.getChildAt(childCount - 2);
                    }

                    // if there are enough children assume the revealRight
                    int i = childCount - 3;
                    if (revealRight == null && i > -1) {
                        revealRight = vg.getChildAt(i);
                    }
                }
            }

        }

        @Override
        public View getFront() {
            return front;
        }

        @Override
        public View getRevealLeft() {
            return revealLeft;
        }

        @Override
        public View getRevealRight() {
            return revealRight;
        }

        @Override
        public T getItemData() {
            return data;
        }
    }
}