cn.wyx.android.swipeback.swipe.SwipeBackLayout.java Source code

Java tutorial

Introduction

Here is the source code for cn.wyx.android.swipeback.swipe.SwipeBackLayout.java

Source

/*
 * Copyright 2015 Eric Liu
 *
 * 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 cn.wyx.android.swipeback.swipe;

import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.ViewDragHelper;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.AbsListView;
import android.widget.RelativeLayout;
import android.widget.ScrollView;

import java.lang.reflect.Field;

import cn.wyx.android.swipeback.R;

/**
 * Swipe or Pull to finish a Activity.
 * <p/>
 * This layout must be a root layout and contains only one direct child view.
 * <p/>
 * The activity must use a theme that with translucent style.
 * <style name="Theme.Swipe.Back" parent="AppTheme">
 * <item name="android:windowIsTranslucent">true</item>
 * <item name="android:windowBackground">@android:color/transparent</item>
 * </style>
 * <p/>
 */
public class SwipeBackLayout extends RelativeLayout {

    private static final String TAG = "SwipeBackLayout";
    private static final double AUTO_FINISHED_SPEED_LIMIT = 2000.0;
    private static final float BACK_FACTOR = 0.5f;
    private final ViewDragHelper viewDragHelper;
    private DragEdge dragEdge = DragEdge.LEFT;
    private View target;
    private View scrollChild;
    private int verticalDragRange = 0;
    private int horizontalDragRange = 0;
    private int draggingState = 0;
    private int draggingOffset;
    /**
     * Whether allow to pull this layout.
     */
    private boolean enablePullToBack = true;
    /**
     * the anchor of calling finish.
     */
    private float finishAnchor = 0;
    private boolean enableFlingBack = true;
    private Drawable mShadow;

    private SwipeBackListener swipeBackListener;

    /**
     * ?
     */
    private Bitmap mLastScreenshot;

    private boolean isFullscreen;

    public SwipeBackLayout(Context context) {
        this(context, null);
    }

    public SwipeBackLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setWillNotDraw(false);
        mShadow = getResources().getDrawable(R.drawable.left_shadow);
        viewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelperCallBack());
    }

    public int getStatusBarHeight() {
        Class<?> c = null;
        Object obj = null;
        Field field = null;
        int x = 0, statusBarHeight = 0;
        try {
            c = Class.forName("com.android.internal.R$dimen");
            obj = c.newInstance();
            field = c.getField("status_bar_height");
            x = Integer.parseInt(field.get(obj).toString());
            statusBarHeight = getResources().getDimensionPixelSize(x);
        } catch (Exception e1) {
            e1.printStackTrace();
        }
        return statusBarHeight;
    }

    public void setDragEdge(DragEdge dragEdge) {
        this.dragEdge = dragEdge;
    }

    /**
     * Set the anchor of calling finish.
     *
     * @param offset
     */
    public void setFinishAnchor(float offset) {
        finishAnchor = offset;
    }

    /**
     * Whether allow to finish activity by fling the layout.
     *
     * @param b
     */
    public void setEnableFlingBack(boolean b) {
        enableFlingBack = b;
    }

    @Deprecated
    public void setOnPullToBackListener(SwipeBackListener listener) {
        swipeBackListener = listener;
    }

    public void setLastScreenshot(Bitmap lastScreenshot) {
        this.mLastScreenshot = lastScreenshot;
    }

    public void setOnSwipeBackListener(SwipeBackListener listener) {
        swipeBackListener = listener;
    }

    public void setScrollChild(View view) {
        scrollChild = view;
    }

    public void setEnablePullToBack(boolean b) {
        enablePullToBack = b;
    }

    private void ensureTarget() {
        if (target == null) {
            if (getChildCount() > 1) {
                throw new IllegalStateException("SwipeBackLayout must contains only one direct child");
            }
            target = getChildAt(0);

            if (scrollChild == null && target != null) {
                if (target instanceof ViewGroup) {
                    findScrollView((ViewGroup) target);
                } else {
                    scrollChild = target;
                }

            }
        }
    }

    /**
     * Find out the scrollable child view from a ViewGroup.
     *
     * @param viewGroup
     */
    private void findScrollView(ViewGroup viewGroup) {
        scrollChild = viewGroup;
        if (viewGroup.getChildCount() > 0) {
            int count = viewGroup.getChildCount();
            View child;
            for (int i = 0; i < count; i++) {
                child = viewGroup.getChildAt(i);
                if (child instanceof AbsListView || child instanceof ScrollView || child instanceof ViewPager
                        || child instanceof WebView || child instanceof RecyclerView) {
                    scrollChild = child;
                    return;
                }
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        viewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        if (getChildCount() == 0)
            return;

        View child = getChildAt(0);

        int childWidth = width - getPaddingLeft() - getPaddingRight();
        int childHeight = height - getPaddingTop() - getPaddingBottom();
        int childLeft = getPaddingLeft();
        int childTop = getPaddingTop();
        int childRight = childLeft + childWidth;
        int childBottom = childTop + childHeight;
        child.layout(childLeft, childTop, childRight, childBottom);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 1) {
            throw new IllegalStateException("SwipeBackLayout must contains only one direct child.");
        }

        if (getChildCount() > 0) {
            int measureWidth = MeasureSpec.makeMeasureSpec(
                    getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY);
            int measureHeight = MeasureSpec.makeMeasureSpec(
                    getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY);
            getChildAt(0).measure(measureWidth, measureHeight);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        verticalDragRange = h;
        horizontalDragRange = w;

        switch (dragEdge) {
        case TOP:
        case BOTTOM:
            finishAnchor = finishAnchor > 0 ? finishAnchor : verticalDragRange * BACK_FACTOR;
            break;
        case LEFT:
        case RIGHT:
            finishAnchor = finishAnchor > 0 ? finishAnchor : horizontalDragRange * BACK_FACTOR;
            break;
        }
    }

    private int getDragRange() {
        switch (dragEdge) {
        case TOP:
        case BOTTOM:
            return verticalDragRange;
        case LEFT:
        case RIGHT:
            return horizontalDragRange;
        default:
            return verticalDragRange;
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean handled = false;
        ensureTarget();
        if (isEnabled()) {
            handled = viewDragHelper.shouldInterceptTouchEvent(ev);
        } else {
            viewDragHelper.cancel();
        }
        return !handled ? super.onInterceptTouchEvent(ev) : handled;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        viewDragHelper.processTouchEvent(event);
        return true;
    }

    @Override
    public void computeScroll() {
        if (viewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    public boolean canChildScrollUp() {
        return ViewCompat.canScrollVertically(scrollChild, -1);
    }

    public boolean canChildScrollDown() {
        return ViewCompat.canScrollVertically(scrollChild, 1);
    }

    private boolean canChildScrollRight() {
        return ViewCompat.canScrollHorizontally(scrollChild, -1);
    }

    private boolean canChildScrollLeft() {
        return ViewCompat.canScrollHorizontally(scrollChild, 1);
    }

    private void finish() {
        Activity act = (Activity) getContext();
        act.finish();
        act.overridePendingTransition(0, android.R.anim.fade_out);
    }

    private boolean backBySpeed(float xvel, float yvel) {
        switch (dragEdge) {
        case TOP:
        case BOTTOM:
            if (Math.abs(yvel) > Math.abs(xvel) && Math.abs(yvel) > AUTO_FINISHED_SPEED_LIMIT) {
                return dragEdge == DragEdge.TOP ? !canChildScrollUp() : !canChildScrollDown();
            }
            break;
        case LEFT:
        case RIGHT:
            if (Math.abs(xvel) > Math.abs(yvel) && Math.abs(xvel) > AUTO_FINISHED_SPEED_LIMIT) {
                return dragEdge == DragEdge.LEFT ? !canChildScrollLeft() : !canChildScrollRight();
            }
            break;
        }
        return false;
    }

    private void smoothScrollToX(int finalLeft) {
        if (viewDragHelper.settleCapturedViewAt(finalLeft, 0)) {
            ViewCompat.postInvalidateOnAnimation(SwipeBackLayout.this);
        }
    }

    private void smoothScrollToY(int finalTop) {
        if (viewDragHelper.settleCapturedViewAt(0, finalTop)) {
            ViewCompat.postInvalidateOnAnimation(SwipeBackLayout.this);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawLastScreenshot(canvas);
        drawShadow(canvas);
    }

    /**
     * ?
     *
     * @param canvas canvas
     */
    private void drawLastScreenshot(Canvas canvas) {
        if (mLastScreenshot != null && target != null) {
            int right = target.getLeft();
            Paint paint = new Paint();
            paint.setAntiAlias(true);
            Rect src = new Rect(mLastScreenshot.getWidth() - target.getLeft(), 0, mLastScreenshot.getWidth(),
                    target.getBottom());
            //target??
            Rect dst = new Rect(0, target.getBottom() - mLastScreenshot.getHeight(), right, target.getBottom());

            int navigationBarHeight = getNavigationBarHeight();

            isFullscreen = target.getHeight() == getResources().getDisplayMetrics().heightPixels
                    + navigationBarHeight;

            int statusBarHeight = getStatusBarHeight();

            switch (dragEdge) {
            case LEFT:
                src = new Rect(mLastScreenshot.getWidth() - target.getLeft(), 0, mLastScreenshot.getWidth(),
                        mLastScreenshot.getHeight());
                dst = new Rect(0, isFullscreen ? statusBarHeight : target.getBottom() - mLastScreenshot.getHeight(),
                        right, isFullscreen ? target.getHeight() - navigationBarHeight : target.getBottom());

                break;
            case RIGHT:
                src = new Rect(0, 0, mLastScreenshot.getWidth() - target.getRight(), mLastScreenshot.getHeight());
                dst = new Rect(target.getRight(),
                        isFullscreen ? statusBarHeight : target.getBottom() - mLastScreenshot.getHeight(),
                        mLastScreenshot.getWidth(),
                        isFullscreen ? target.getHeight() - navigationBarHeight : target.getBottom());
                break;

            case TOP:
                src = new Rect(0, mLastScreenshot.getHeight() - target.getTop(), mLastScreenshot.getWidth(),
                        target.getBottom());
                dst = new Rect(0, 0, target.getRight(), target.getTop());
                break;
            case BOTTOM:
                src = new Rect(0, 0, mLastScreenshot.getWidth(), mLastScreenshot.getHeight() - target.getBottom());
                dst = new Rect(0, target.getBottom(), mLastScreenshot.getWidth(), mLastScreenshot.getHeight());
                break;
            }

            canvas.drawBitmap(mLastScreenshot, src, dst, paint);
        }
    }

    /**
     * 
     */
    private void drawShadow(Canvas canvas) {

        if (target == null) {
            return;
        }

        switch (dragEdge) {
        case LEFT:
            mShadow.setBounds(target.getLeft() - mShadow.getIntrinsicWidth(), target.getTop(), target.getLeft(),
                    target.getBottom());
            break;
        case RIGHT:
            mShadow.setBounds(target.getRight(), target.getTop(), target.getRight() + mShadow.getIntrinsicWidth(),
                    target.getBottom());
            break;
        case TOP:
            mShadow.setBounds(target.getLeft(), target.getTop() - mShadow.getIntrinsicHeight(), target.getRight(),
                    target.getTop());
            break;
        case BOTTOM:
            mShadow.setBounds(target.getLeft(), target.getBottom(), target.getRight(),
                    target.getBottom() + mShadow.getIntrinsicHeight());
            break;
        }

        // drawable?
        // ?

        // Drawable
        mShadow.draw(canvas);

    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mLastScreenshot != null) {
            mLastScreenshot = null;
        }
    }

    private int getNavigationBarHeight() {
        Resources resources = getResources();
        int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
        //?NavigationBar
        return resources.getDimensionPixelSize(resourceId);
    }

    public enum DragEdge {
        LEFT,

        TOP,

        RIGHT,

        BOTTOM
    }

    public interface SwipeBackListener {

        /**
         * Return scrolled fraction of the layout.
         *
         * @param fractionAnchor relative to the anchor.
         * @param fractionScreen relative to the screen.
         */
        void onViewPositionChanged(float fractionAnchor, float fractionScreen);

    }

    private class ViewDragHelperCallBack extends ViewDragHelper.Callback {

        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child == target && enablePullToBack;
        }

        @Override
        public int getViewVerticalDragRange(View child) {
            return verticalDragRange;
        }

        @Override
        public int getViewHorizontalDragRange(View child) {
            return horizontalDragRange;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {

            int result = 0;

            if (dragEdge == DragEdge.TOP && !canChildScrollUp() && top > 0) {
                final int topBound = getPaddingTop();
                final int bottomBound = verticalDragRange;
                result = Math.min(Math.max(top, topBound), bottomBound);
            } else if (dragEdge == DragEdge.BOTTOM && !canChildScrollDown() && top < 0) {
                final int topBound = -verticalDragRange;
                final int bottomBound = getPaddingTop();
                result = Math.min(Math.max(top, topBound), bottomBound);
            }

            return result;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {

            int result = 0;

            if (dragEdge == DragEdge.LEFT && !canChildScrollRight() && left > 0) {
                final int leftBound = getPaddingLeft();
                final int rightBound = horizontalDragRange;
                result = Math.min(Math.max(left, leftBound), rightBound);
            } else if (dragEdge == DragEdge.RIGHT && !canChildScrollLeft() && left < 0) {
                final int leftBound = -horizontalDragRange;
                final int rightBound = getPaddingLeft();
                result = Math.min(Math.max(left, leftBound), rightBound);
            }

            return result;
        }

        @Override
        public void onViewDragStateChanged(int state) {

            if (state == draggingState)
                return;

            if ((draggingState == ViewDragHelper.STATE_DRAGGING || draggingState == ViewDragHelper.STATE_SETTLING)
                    && state == ViewDragHelper.STATE_IDLE) {
                // the view stopped from moving.
                if (draggingOffset == getDragRange()) {
                    finish();
                }
            }

            draggingState = state;
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            invalidate();
            switch (dragEdge) {
            case TOP:
            case BOTTOM:
                draggingOffset = Math.abs(top);
                break;
            case LEFT:
            case RIGHT:
                draggingOffset = Math.abs(left);
                break;
            default:
                break;
            }

            //The proportion of the sliding.
            float fractionAnchor = (float) draggingOffset / finishAnchor;
            if (fractionAnchor >= 1)
                fractionAnchor = 1;

            float fractionScreen = (float) draggingOffset / (float) getDragRange();
            if (fractionScreen >= 1)
                fractionScreen = 1;

            if (swipeBackListener != null) {
                swipeBackListener.onViewPositionChanged(fractionAnchor, fractionScreen);
            }
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            if (draggingOffset == 0)
                return;

            if (draggingOffset == getDragRange())
                return;

            boolean isBack = false;

            if (enableFlingBack && backBySpeed(xvel, yvel)) {
                isBack = !canChildScrollUp();
            } else if (draggingOffset >= finishAnchor) {
                isBack = true;
            } else if (draggingOffset < finishAnchor) {
                isBack = false;
            }
            int finalLeft;
            int finalTop;
            switch (dragEdge) {
            case LEFT:
                finalLeft = isBack ? horizontalDragRange : 0;
                smoothScrollToX(finalLeft);
                break;
            case RIGHT:
                finalLeft = isBack ? -horizontalDragRange : 0;
                smoothScrollToX(finalLeft);
                break;
            case TOP:
                finalTop = isBack ? verticalDragRange : 0;
                smoothScrollToY(finalTop);
                break;
            case BOTTOM:
                finalTop = isBack ? -verticalDragRange : 0;
                smoothScrollToY(finalTop);
                break;
            }

        }
    }
}