com.waz.zclient.pages.main.conversationlist.views.listview.SwipeListView.java Source code

Java tutorial

Introduction

Here is the source code for com.waz.zclient.pages.main.conversationlist.views.listview.SwipeListView.java

Source

/**
 * Wire
 * Copyright (C) 2016 Wire Swiss GmbH
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.waz.zclient.pages.main.conversationlist.views.listview;

import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.support.v4.view.MotionEventCompat;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.ListView;
import com.waz.zclient.R;
import com.waz.zclient.ui.pullforaction.OverScrollListener;
import com.waz.zclient.ui.pullforaction.OverScrollMode;
import com.waz.zclient.ui.pullforaction.PullForActionView;
import timber.log.Timber;

/**
 * ListView subclass that provides the swipe functionality
 */
public class SwipeListView extends ListView implements PullForActionView {
    public static final String TAG = SwipeListView.class.getName();

    // touch states to distinguish between list and drawer
    private final static int TOUCH_STATE_REST = 0;
    private final static int TOUCH_STATE_SCROLLING_X = 1;
    private final static int TOUCH_STATE_SCROLLING_Y = 2;
    private int touchState = TOUCH_STATE_REST;

    // used to track touch movement
    private float lastMotionX;
    private float lastMotionY;

    private int touchSlop;

    // Cached ViewConfiguration and system-wide constant values
    private int minFlingVelocity;
    private int maxFlingVelocity;

    // drawer goes to....
    private float rightOffset = 0;

    // the listener that wants to be informed by the overscroll event
    OverScrollListener overScrollListener;

    // no overscroll needed from here, instead we dispatch overscrolling to the container
    private int maxOverScrollDistance = 0;

    // initialize rect once to help performance. Used to determine of touch is on view
    private Rect targetRect = new Rect();

    // Fixed properties
    private int viewWidth = 1; // 1 and not 0 to prevent dividing by zero

    // the initial touch down position
    private float downX;

    // tracks touch move to calculate swipe speed
    private VelocityTracker velocityTracker;

    // the touched view from the list
    private SwipeListRow targetView;

    // tracking the child position to see if updates are necessary
    private int targetChildPosition;

    // Max distance needed to swipe to fully reveal the menu indicator
    private int listRowMenuIndicatorMaxSwipeOffset;

    // flags target open
    boolean isItemOpen;

    boolean allowSwipeAway;

    /**
     * Opens the drawer by calling an animation on an item.
     */
    private void openItem() {
        isItemOpen = true;
        if (targetView != null) {
            targetView.open();
        }
    }

    /**
     * Closes the drawer by calling an animation on an item.
     */
    private void closeItem() {
        isItemOpen = false;
        if (targetView != null) {
            targetView.close();
        }
    }

    /**
     * Sets the overscroll listener from the PullToRefreshContainer.
     */
    public void setOverScrollListener(OverScrollListener listener) {
        overScrollListener = listener;
    }

    /**
     * CTOR - checking animation const and wrapping context to get rod of overscroll animation.
     */
    public SwipeListView(Context context) {
        super(new ContextWrapperEdgeEffect(context));
        ((ContextWrapperEdgeEffect) getContext()).setEdgeEffectColor(Color.TRANSPARENT);

        touchSlop = 10; //ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        ViewConfiguration vc = ViewConfiguration.get(getContext());
        minFlingVelocity = vc.getScaledMinimumFlingVelocity();
        maxFlingVelocity = vc.getScaledMaximumFlingVelocity();
        listRowMenuIndicatorMaxSwipeOffset = context.getResources()
                .getDimensionPixelSize(R.dimen.list__menu_indicator__max_swipe_offset);
    }

    /**
     * Set offset on right after onMeasurement is called in PullToRefreshContainer.
     */
    public void setOffsetRight(float offsetRight) {
        rightOffset = offsetRight;
    }

    /**
     * onInterceptTouchEvent is not called on move even it returns false. (Bug?)
     * This function determines the hit element and the movement.
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (viewWidth < 2) {
            viewWidth = getWidth();
        }
        switch (MotionEventCompat.getActionMasked(ev)) {
        case MotionEvent.ACTION_DOWN:
            touchState = TOUCH_STATE_REST;
            getHitChild(ev);
            break;
        case MotionEvent.ACTION_MOVE:
            if (touchState != TOUCH_STATE_REST) {
                super.dispatchTouchEvent(ev);
            }
            final float x = ev.getX();
            final float y = ev.getY();

            // make sure we hit an item
            if (targetView != null) {
                checkMotionDirection(x, y);
            }
            break;
        }
        return super.dispatchTouchEvent(ev);
    }

    /**
     * @see android.widget.ListView#onInterceptTouchEvent(android.view.MotionEvent)
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = MotionEventCompat.getActionMasked(ev);
        final float x = ev.getX();
        final float y = ev.getY();
        switch (action) {
        case MotionEvent.ACTION_DOWN:
            touchState = TOUCH_STATE_REST;
            velocityTracker = VelocityTracker.obtain();
            velocityTracker.addMovement(ev);
            lastMotionX = x;
            lastMotionY = y;
            if (isItemOpen) {
                getParent().requestDisallowInterceptTouchEvent(true);
                //return true;
            }
            return super.onInterceptTouchEvent(ev);
        case MotionEvent.ACTION_MOVE:
            if (touchState != TOUCH_STATE_REST) {
                return true;
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            velocityTracker.recycle();
            velocityTracker = null;
            break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    @SuppressWarnings("PMD.UselessOverridingMethod")
    public int computeVerticalScrollOffset() {
        return super.computeVerticalScrollOffset();
    }

    /**
     * @see android.widget.ListView#onTouchEvent(android.view.MotionEvent)
     */
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // swiping is not enabled, let parent (ListView) eat that event
        if (targetView == null || !targetView.isSwipeable() || touchState == TOUCH_STATE_SCROLLING_Y) {
            return super.onTouchEvent(ev);
        }
        float deltaX = ev.getRawX() - downX;
        switch (MotionEventCompat.getActionMasked(ev)) {
        case MotionEvent.ACTION_MOVE:
            velocityTracker.addMovement(ev);
            velocityTracker.computeCurrentVelocity(1000);
            targetView.setOffset(deltaX);
            return true;
        case MotionEvent.ACTION_UP:
            if (velocityTracker == null) {
                break;
            }

            velocityTracker.addMovement(ev);
            velocityTracker.computeCurrentVelocity(1000);

            float velocityX = Math.abs(velocityTracker.getXVelocity());

            if (velocityTracker.getXVelocity() < 0) {
                velocityX = 0;
            }

            float velocityY = Math.abs(velocityTracker.getYVelocity());

            if (targetView != null) {

                boolean flingRight = false;

                if (minFlingVelocity <= velocityX && velocityX <= maxFlingVelocity && velocityY * 2 < velocityX
                        && deltaX > 0) {
                    flingRight = true;
                }
                if (allowSwipeAway && deltaX > viewWidth / 2) {
                    if (targetView != null) {
                        targetView.swipeAway();
                    }
                } else if ((!allowSwipeAway && (deltaX > viewWidth / 2 || flingRight))
                        || (allowSwipeAway && (deltaX > viewWidth / 4 || flingRight))) {
                    if (targetView.isOpen()) {
                        closeItem();
                    } else {
                        openItem();
                    }
                } else {
                    closeItem();
                }
            }

            velocityTracker.recycle();
            velocityTracker = null;
            downX = 0;
            return true;
        }

        return super.onTouchEvent(ev);
    }

    /**
     * Check if the user is moving the cell or the list.
     */
    private void checkMotionDirection(float x, float y) {
        final int distX = (int) (x - lastMotionX);
        final int distY = (int) (y - lastMotionY);
        final boolean yMoved = Math.abs(distY) > this.touchSlop;

        // vertical is bigger than horizontal
        if (yMoved && Math.abs(distY) > Math.abs(distX)) {
            touchState = TOUCH_STATE_SCROLLING_Y;
            closeItem();
            return;
        }

        final boolean isMovingRight = distX > 0;
        final boolean xMoved = Math.abs(distX) > this.touchSlop;
        if (xMoved) {
            if (isMovingRight) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            touchState = TOUCH_STATE_SCROLLING_X;
            lastMotionX = x;
            lastMotionY = y;
        }
    }

    private MotionDirection getMotionDirection(float x, float y) {
        MotionDirection direction = null;

        final int distX = (int) (x - lastMotionX);
        final int distY = (int) (y - lastMotionY);
        boolean xMoved = Math.abs(distX) > this.touchSlop;
        boolean yMoved = Math.abs(distY) > this.touchSlop && Math.abs(distY) > Math.abs(distX);

        if (yMoved) {
            if (distY < 0) {
                direction = MotionDirection.UP;
            } else {
                direction = MotionDirection.DOWN;
            }
        } else if (xMoved) {
            if (distX < 0) {
                direction = MotionDirection.LEFT;
            } else {
                direction = MotionDirection.RIGHT;
            }
        }

        return direction;
    }

    /**
     * Retrieves the child of the list view that is hit by the touch event. If it
     * is the first item or a disabled one, targetView is set to null.
     */
    private void getHitChild(MotionEvent motionEvent) {
        int[] listViewCoords = new int[2];
        getLocationOnScreen(listViewCoords);
        int x = (int) motionEvent.getRawX() - listViewCoords[0];
        int y = (int) motionEvent.getRawY() - listViewCoords[1];
        int childCount = getChildCount();
        View child;
        for (int i = 0; i < childCount; i++) {
            child = getChildAt(i);
            child.getHitRect(targetRect);
            if (targetRect.contains(x, y)) {
                int position = getPositionForView(child);
                boolean allowSwipe = child instanceof SwipeListRow && ((SwipeListRow) child).isSwipeable();
                if (allowSwipe) {
                    if (position != targetChildPosition) {
                        closeItem();
                    }
                    targetChildPosition = position;

                    // TODO: Investigate causes of ClassCastExecption. Perhaps archiving related views?
                    try {
                        targetView = (SwipeListRow) child;
                        targetView
                                .setMaxOffset(allowSwipeAway ? viewWidth / 2 : listRowMenuIndicatorMaxSwipeOffset);
                    } catch (ClassCastException e) {
                        Timber.e(e, "ClassCastException when swiping");
                    }
                    downX = motionEvent.getRawX();
                } else {
                    closeItem();
                    targetView = null;
                }
                return;
            }
        }

        // no child hit
        closeItem();
        targetView = null;
    }

    /**
     * The BouncingListVieContainer is notified from this spot.
     * At this place the default MaxOverscrollY can be overwritten.
     */
    @Override
    protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX,
            int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {

        if (overScrollListener != null) {
            if (deltaY < 0) {
                overScrollListener.onOverScrolled(OverScrollMode.TOP);
            } else {
                overScrollListener.onOverScrolled(OverScrollMode.BOTTOM);
            }
        }
        return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX,
                maxOverScrollDistance, isTouchEvent);
    }

    public void onPagerOffsetChanged(float offset) {
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            if (view instanceof SwipeListRow) {
                ((SwipeListRow) view).setPagerOffset(offset);
            }
        }
    }

    public void resetListRowALpha() {
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            if (view instanceof SwipeListRow) {
                ((SwipeListRow) view).dimOnListRowMenuSwiped(1f);
            }
        }
    }

    public void setAllowSwipeAway(boolean allowSwipeAway) {
        this.allowSwipeAway = allowSwipeAway;
    }

    public void customSmoothScrollToPosition(int smoothScrollPosition) {
        setSelectionFromTop(smoothScrollPosition, 0);
    }

    public interface SwipeListRow {
        void open();

        void close();

        void setMaxOffset(float maxOffset);

        void setOffset(float offset);

        boolean isSwipeable();

        boolean isOpen();

        void swipeAway();

        void dimOnListRowMenuSwiped(float alpha);

        void setPagerOffset(float pagerOffset);
    }

}