/* * Copyright (C) 2017 Drakeet <drakeet.me@gmail.com> * * This file is part of rebase-android * * rebase-android 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. * * rebase-android 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 rebase-android. If not, see <http://www.gnu.org/licenses/>. */ package com.liuguangqiang.swipeback; import android.app.Activity; import android.content.Context; import android.support.v4.view.ScrollingView; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPager; import android.support.v4.widget.ViewDragHelper; 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.ScrollView; /** * 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/> * Created by Eric on 15/1/8. * Modified by drakeet to support swipe with {@link DragEdge#VERTICAL} */ public class SwipeBackLayout extends ViewGroup { private static final String TAG = "SwipeBackLayout"; private boolean isVerticalMode; public enum DragEdge { LEFT, TOP, RIGHT, BOTTOM, VERTICAL } private DragEdge dragEdge = DragEdge.TOP; public void setDragEdge(DragEdge dragEdge) { if (dragEdge == DragEdge.VERTICAL) { dragEdge = DragEdge.TOP; isVerticalMode = true; } else { isVerticalMode = false; } this.dragEdge = dragEdge; } private static final double AUTO_FINISHED_SPEED_LIMIT = 1000.0; private final ViewDragHelper viewDragHelper; 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 swipeBackEnabled = true; private static final float BACK_FACTOR = 0.05f; /** * the anchor of calling finish. */ private float finishAnchor = 0f; /** * Set the anchor of calling finish. */ public void setFinishAnchor(float offset) { finishAnchor = offset; } private boolean flingBackEnabled = true; /** * Whether allow to finish activity by fling the layout. */ public void setFlingBackEnabled(boolean enabled) { flingBackEnabled = enabled; } private SwipeBackListener swipeBackListener; @Deprecated public void setOnPullToBackListener(SwipeBackListener listener) { swipeBackListener = listener; } public void setOnSwipeBackListener(SwipeBackListener listener) { swipeBackListener = listener; } public SwipeBackLayout(Context context) { this(context, null); } public SwipeBackLayout(Context context, AttributeSet attrs) { super(context, attrs); viewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelperCallBack()); } public void setScrollChild(View view) { scrollChild = view; } public void setSwipeBackEnabled(boolean enabled) { swipeBackEnabled = enabled; } 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. */ 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 ScrollingView || child instanceof AbsListView || child instanceof ScrollView || child instanceof ViewPager || child instanceof WebView) { scrollChild = child; return; } } } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { 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); } @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); } /** * updateVerticalEdge when vertical mode * * @param isTop is top edge * @param top the top * @author drakeet */ private void updateVerticalEdge(boolean isTop, int top) { if (isTop && top < 0) { dragEdge = DragEdge.BOTTOM; } else if (!isTop && top > 0) { dragEdge = DragEdge.TOP; } } private void finish() { Activity act = (Activity) getContext(); act.finish(); act.overridePendingTransition(0, android.R.anim.fade_out); } private class ViewDragHelperCallBack extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View child, int pointerId) { return child == target && swipeBackEnabled; } @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 (isVerticalMode) { updateVerticalEdge(isTopEdge(), top); } if (isTopEdge() && !canChildScrollUp() && top > 0) { final int topBound = getPaddingTop(); final int bottomBound = verticalDragRange; result = Math.min(Math.max(top, topBound), bottomBound); } else if (isBottomEdge() && !canChildScrollDown() && top < 0) { final int topBound = -verticalDragRange; final int bottomBound = getPaddingTop(); result = Math.min(Math.max(top, topBound), bottomBound); } return child.getTop() + (result - child.getTop()) / 3; } boolean isTopEdge() { return dragEdge == DragEdge.TOP; } boolean isBottomEdge() { return dragEdge == DragEdge.BOTTOM; } @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) { 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 (flingBackEnabled && backBySpeed(xvel, yvel)) { isBack = true; } 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; } } } 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); } } 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); } }