/*
* 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 com.bob.digcsdn.utils;
import android.app.Activity;
import android.content.Context;
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.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.
*/
public class SwipeBackLayout extends ViewGroup {
private static final String TAG = "SwipeBackLayout";
public enum DragEdge {
LEFT,
TOP,
RIGHT,
BOTTOM
}
private DragEdge dragEdge = DragEdge.TOP;
public void setDragEdge(DragEdge dragEdge) {
this.dragEdge = dragEdge;
}
private static final double AUTO_FINISHED_SPEED_LIMIT = 2000.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 enablePullToBack = true;
private static final float BACK_FACTOR = 0.5f;
/**
* the anchor of calling finish.
*/
private float finishAnchor = 0;
/**
* Set the anchor of calling finish.
*
* @param offset
*/
public void setFinishAnchor(float offset) {
finishAnchor = offset;
}
private boolean enableFlingBack = true;
/**
* Whether allow to finish activity by fling the layout.
*
* @param b
*/
public void setEnableFlingBack(boolean b) {
enableFlingBack = b;
}
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 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) {
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) : 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 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) {
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;
}
}
}
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.
*/
public void onViewPositionChanged(float fractionAnchor, float fractionScreen);
}
}