diff options
author | Allan Wang <me@allanwang.ca> | 2017-07-04 23:34:58 -0700 |
---|---|---|
committer | Allan Wang <me@allanwang.ca> | 2017-07-04 23:34:58 -0700 |
commit | 4e254b4b983b8531193c02852d2866a876d8cfcf (patch) | |
tree | f2cc65dbcbe15db27f36a3807be6b3b3628a8a88 | |
parent | 8844116ebfa5dcab29ffacc66ffab4d20447a104 (diff) | |
download | kau-4e254b4b983b8531193c02852d2866a876d8cfcf.tar.gz kau-4e254b4b983b8531193c02852d2866a876d8cfcf.tar.bz2 kau-4e254b4b983b8531193c02852d2866a876d8cfcf.zip |
Add initial port of Judes swipe back helper
7 files changed, 702 insertions, 5 deletions
diff --git a/core/src/main/kotlin/ca/allanwang/kau/swipe/RelateSlider.java b/core/src/main/kotlin/ca/allanwang/kau/swipe/RelateSlider.java new file mode 100644 index 0000000..d438d00 --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/swipe/RelateSlider.java @@ -0,0 +1,52 @@ +package ca.allanwang.kau.swipe; + +import android.os.Build; + +/** + * Created by Mr.Jude on 2015/8/26. + */ +public class RelateSlider implements SwipeListener { + public SwipeBackPage curPage; + private static final int DEFAULT_OFFSET = 40; + private int offset = 500; + + public RelateSlider(SwipeBackPage curActivity) { + this.curPage = curActivity; + //curPage.addListener(this); + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public void setEnable(boolean enable){ + if (enable)curPage.addListener(this); + else curPage.removeListener(this); + } + + @Override + public void onScroll(float percent, int px) { + if (Build.VERSION.SDK_INT>11){ + SwipeBackPage page = SwipeBackHelper.getPrePage(curPage); + if (page!=null){ + page.getSwipeBackLayout().setX(Math.min(-offset * Math.max(1 - percent,0)+DEFAULT_OFFSET,0)); + if (percent == 0){ + page.getSwipeBackLayout().setX(0); + } + } + } + } + + @Override + public void onEdgeTouch() { + + } + + @Override + public void onScrollToClose() { + SwipeBackPage page = SwipeBackHelper.getPrePage(curPage); + if (Build.VERSION.SDK_INT>11) { + if (page != null) page.getSwipeBackLayout().setX(0); + } + } +} diff --git a/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackHelper.java b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackHelper.java new file mode 100644 index 0000000..b72112e --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackHelper.java @@ -0,0 +1,68 @@ +package ca.allanwang.kau.swipe; + +import android.app.Activity; + +import java.util.Stack; + +/** + * 滑动的全局管理类 + */ +public class SwipeBackHelper { + + private static final Stack<SwipeBackPage> mPageStack = new Stack<>(); + + private static SwipeBackPage findHelperByActivity(Activity activity){ + for (SwipeBackPage swipeBackPage : mPageStack) { + if (swipeBackPage.mActivity == activity) return swipeBackPage; + } + return null; + } + + public static SwipeBackPage getCurrentPage(Activity activity){ + SwipeBackPage page; + if ((page = findHelperByActivity(activity)) == null){ + throw new RuntimeException("You Should call SwipeBackHelper.onCreate(activity) first"); + } + return page; + } + + public static void onCreate(Activity activity) { + SwipeBackPage page; + if ((page = findHelperByActivity(activity)) == null){ + page = mPageStack.push(new SwipeBackPage(activity)); + } + page.onCreate(); + } + + public static void onPostCreate(Activity activity){ + SwipeBackPage page; + if ((page = findHelperByActivity(activity)) == null){ + throw new RuntimeException("You Should call SwipeBackHelper.onCreate(activity) first"); + } + page.onPostCreate(); + } + + public static void onDestroy(Activity activity){ + SwipeBackPage page; + if ((page = findHelperByActivity(activity)) == null){ + throw new RuntimeException("You Should call SwipeBackHelper.onCreate(activity) first"); + } + mPageStack.remove(page); + page.mActivity=null; + } + + public static void finish(Activity activity){ + SwipeBackPage page; + if ((page = findHelperByActivity(activity)) == null){ + throw new RuntimeException("You Should call SwipeBackHelper.onCreate(activity) first"); + } + page.scrollToFinishActivity(); + } + + static SwipeBackPage getPrePage(SwipeBackPage activity){ + int index = mPageStack.indexOf(activity); + if (index>0)return mPageStack.get(index-1); + else return null; + } + +} diff --git a/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackLayout.java b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackLayout.java new file mode 100644 index 0000000..57195f2 --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackLayout.java @@ -0,0 +1,438 @@ +package ca.allanwang.kau.swipe; + +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v4.view.ViewCompat; +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.FrameLayout; + +import java.util.ArrayList; +import java.util.List; + +public class SwipeBackLayout extends FrameLayout { + private static final String TAG = "ViewDragHelper"; + + /** + * Minimum velocity that will be detected as a fling + */ + private static final int MIN_FLING_VELOCITY = 400; // dips per second + + private static final int DEFAULT_SCRIM_COLOR = 0x99000000; + + private static final int FULL_ALPHA = 255; + + + /** + * A view is currently being dragged. The position is currently changing as + * a result of user input or simulated user input. + */ + public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; + + /** + * A view is currently settling into place as a result of a fling or + * predefined non-interactive motion. + */ + public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; + + /** + * Default threshold of scroll + */ + private static final float DEFAULT_SCROLL_THRESHOLD = 0.3f; + + private static final int OVERSCROLL_DISTANCE = 10; + + /** + * Threshold of scroll, we will close the activity, when scrollPercent over + * this value; + */ + private float mScrollThreshold = DEFAULT_SCROLL_THRESHOLD; + + private Activity mActivity; + + private boolean mEnable = true; + + private boolean mDisallowIntercept = false; + + private View mContentView; + + private ViewDragHelper mDragHelper; + + private float mScrollPercent; + + private int mContentLeft; + + /** + * The set of listeners to be sent events through. + */ + private List<SwipeListener> mListeners; + + Drawable mShadowLeft; + + private float mScrimOpacity; + + private int mScrimColor = DEFAULT_SCRIM_COLOR; + + private boolean mInLayout; + + private Rect mTmpRect = new Rect(); + + /** + * Edge being dragged + */ + private int mTrackingEdge; + + public SwipeBackLayout(Context context) { + this(context, null); + } + + public SwipeBackLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SwipeBackLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs); + mDragHelper = ViewDragHelper.create(this, new ViewDragCallback()); + + setShadow(R.drawable.shadow_left); + + final float density = getResources().getDisplayMetrics().density; + final float minVel = MIN_FLING_VELOCITY * density; + setEdgeSize(getResources().getDisplayMetrics().widthPixels); + mDragHelper.setMinVelocity(minVel); + mDragHelper.setMaxVelocity(minVel * 2f); + mDragHelper.setSensitivity(context, 0.3f); + mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); + } + + /** + * Sets the sensitivity of the NavigationLayout. + * + * @param context The application context. + * @param sensitivity value between 0 and 1, the final value for touchSlop = + * ViewConfiguration.getScaledTouchSlop * (1 / s); + */ + public void setSensitivity(Context context, float sensitivity) { + mDragHelper.setSensitivity(context, sensitivity); + } + + /** + * Set up contentView which will be moved by user gesture + * + * @param view + */ + private void setContentView(View view) { + mContentView = view; + } + + public void setEnableGesture(boolean enable) { + mEnable = enable; + } + + /** + * Set a color to use for the scrim that obscures primary content while a + * drawer is open. + * + * @param color Color to use in 0xAARRGGBB format. + */ + public void setScrimColor(int color) { + mScrimColor = color; + invalidate(); + } + + /** + * Set the size of an edge. This is the range in pixels along the edges of + * this view that will actively detect edge touches or drags if edge + * tracking is enabled. + * + * @param size The size of an edge in pixels + */ + public void setEdgeSize(int size) { + mTrackingEdge = size; + mDragHelper.edsetEdgeSize(mTrackingEdge); + } + + + public void setEdgeSizePercent(float size) { + mTrackingEdge = (int) (getResources().getDisplayMetrics().widthPixels * size); + mDragHelper.setEdgeSize(mTrackingEdge); + } + + /** + * Register a callback to be invoked when a swipe event is sent to this + * view. + * + * @param listener the swipe listener to attach to this view + * @deprecated use {@link #addSwipeListener} instead + */ + @Deprecated + public void setSwipeListener(SwipeListener listener) { + addSwipeListener(listener); + } + + /** + * Add a callback to be invoked when a swipe event is sent to this view. + * + * @param listener the swipe listener to attach to this view + */ + public void addSwipeListener(SwipeListener listener) { + if (mListeners == null) { + mListeners = new ArrayList<SwipeListener>(); + } + mListeners.add(listener); + } + + /** + * Removes a listener from the set of listeners + * + * @param listener + */ + public void removeSwipeListener(SwipeListener listener) { + if (mListeners == null) { + return; + } + mListeners.remove(listener); + } + + + + /** + * Set scroll threshold, we will close the activity, when scrollPercent over + * this value + * + * @param threshold + */ + public void setScrollThreshold(float threshold) { + if (threshold >= 1.0f || threshold <= 0) { + throw new IllegalArgumentException("Threshold value should be between 0 and 1.0"); + } + mScrollThreshold = threshold; + } + + + public void setShadow(Drawable shadow) { + mShadowLeft = shadow; + invalidate(); + } + + + public void setShadow(int resId) { + setShadow(getResources().getDrawable(resId)); + } + + /** + * Scroll out contentView and finish the activity + */ + public void scrollToFinishActivity() { + final int childWidth = mContentView.getWidth(); + int left = 0, top = 0; + left = childWidth + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE; + mDragHelper.smoothSlideViewTo(mContentView, left, top); + invalidate(); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (!mEnable||mDisallowIntercept) { + return false; + } + try { + return mDragHelper.shouldInterceptTouchEvent(event); + } catch (Exception e) { +// e.printStackTrace(); + return false; + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!mEnable) { + return false; + } + try { + mDragHelper.processTouchEvent(event); + } catch (Exception e) { +// e.printStackTrace(); + return false; + } + return true; + } + + public void setDisallowInterceptTouchEvent(boolean disallowIntercept) { + mDisallowIntercept = disallowIntercept; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + mInLayout = true; + if (mContentView != null) + mContentView.layout(mContentLeft, 0, + mContentLeft + mContentView.getMeasuredWidth(), + mContentView.getMeasuredHeight()); + mInLayout = false; + } + + @Override + public void requestLayout() { + if (!mInLayout) { + super.requestLayout(); + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final boolean drawContent = child == mContentView; + + boolean ret = super.drawChild(canvas, child, drawingTime); + if (mScrimOpacity > 0 && drawContent + && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) { + drawShadow(canvas, child); + drawScrim(canvas, child); + } + return ret; + } + + private void drawScrim(Canvas canvas, View child) { + final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; + final int alpha = (int) (baseAlpha * mScrimOpacity); + final int color = alpha << 24 | (mScrimColor & 0xffffff); + canvas.clipRect(0, 0, child.getLeft(), getHeight()); + canvas.drawColor(color); + } + + private void drawShadow(Canvas canvas, View child) { + final Rect childRect = mTmpRect; + child.getHitRect(childRect); + + mShadowLeft.setBounds(childRect.left - mShadowLeft.getIntrinsicWidth(), childRect.top, + childRect.left, childRect.bottom); + mShadowLeft.setAlpha((int) (mScrimOpacity * FULL_ALPHA)); + mShadowLeft.draw(canvas); + } + + public void attachToActivity(Activity activity) { + if (getParent()!=null){ + return; + } + mActivity = activity; + TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{ + android.R.attr.windowBackground + }); + int background = a.getResourceId(0, 0); + a.recycle(); + + ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView(); + View decorChild = decor.findViewById(android.R.id.content); + while (decorChild.getParent() != decor){ + decorChild = (View) decorChild.getParent(); + } + decorChild.setBackgroundResource(background); + decor.removeView(decorChild); + addView(decorChild); + setContentView(decorChild); + decor.addView(this); + } + + public void removeFromActivity(Activity activity){ + if (getParent()==null)return; + ViewGroup decorChild = (ViewGroup) getChildAt(0); + ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView(); + decor.removeView(this); + removeView(decorChild); + decor.addView(decorChild); + } + + @Override + public void computeScroll() { + mScrimOpacity = 1 - mScrollPercent; + if (mDragHelper.continueSettling(true)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private class ViewDragCallback extends ViewDragHelper.Callback { + private boolean mIsScrollOverValid; + + @Override + public boolean tryCaptureView(View view, int i) { + boolean ret = mDragHelper.isEdgeTouched(ViewDragHelper.EDGE_LEFT, i); + if (ret) { + if (mListeners != null && !mListeners.isEmpty()) { + for (SwipeListener listener : mListeners) { + listener.onEdgeTouch(); + } + } + mIsScrollOverValid = true; + } + return ret; + } + + @Override + public int getViewHorizontalDragRange(View child) { + return mTrackingEdge; + } + + @Override + public int getViewVerticalDragRange(View child) { + return 0; + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + super.onViewPositionChanged(changedView, left, top, dx, dy); + mScrollPercent = Math.abs((float) left / (mContentView.getWidth() /*+ mShadowLeft.getIntrinsicWidth()*/)); + mContentLeft = left; + invalidate(); + if (mScrollPercent < mScrollThreshold && !mIsScrollOverValid) { + mIsScrollOverValid = true; + } + + if (mListeners != null && !mListeners.isEmpty()) { + for (SwipeListener listener : mListeners) { + listener.onScroll(mScrollPercent, mContentLeft); + } + } + if (mScrollPercent >= 1) { + if (!mActivity.isFinishing()){ + if (mListeners != null && !mListeners.isEmpty() + && mScrollPercent >= mScrollThreshold && mIsScrollOverValid) { + mIsScrollOverValid = false; + for (SwipeListener listener : mListeners) { + listener.onScrollToClose(); + } + } + mActivity.finish(); + } + } + + + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + final int childWidth = releasedChild.getWidth(); + + int left = 0, top = 0; + //判断释放以后是应该滑到最右边(关闭),还是最左边(还原) + left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth + + mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0; + + + mDragHelper.settleCapturedViewAt(left, top); + invalidate(); + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + return Math.min(child.getWidth(), Math.max(left, 0)); + } + + } +} diff --git a/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackPage.java b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackPage.java new file mode 100644 index 0000000..8afb5db --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackPage.java @@ -0,0 +1,120 @@ +package ca.allanwang.kau.swipe; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.view.ViewGroup; + +/** + * Created by Mr.Jude on 2015/8/3. + * 每个滑动页面的管理 + */ +public class SwipeBackPage { + //仅为判断是否需要将mSwipeBackLayout注入进去 + private boolean mEnable = true; + private boolean mRelativeEnable = false; + + Activity mActivity; + SwipeBackLayout mSwipeBackLayout; + RelateSlider slider; + SwipeBackPage(Activity activity){ + this.mActivity = activity; + } + + //页面的回调用于配置滑动效果 + void onCreate(){ + mActivity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + mActivity.getWindow().getDecorView().setBackgroundColor(Color.TRANSPARENT); + mSwipeBackLayout = new SwipeBackLayout(mActivity); + mSwipeBackLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + slider = new RelateSlider(this); + } + + void onPostCreate(){ + handleLayout(); + } + + + @TargetApi(11) + public SwipeBackPage setSwipeRelateEnable(boolean enable){ + mRelativeEnable = enable; + slider.setEnable(enable); + return this; + } + + public SwipeBackPage setSwipeRelateOffset(int offset){ + slider.setOffset(offset); + return this; + } + + //是否可滑动关闭 + public SwipeBackPage setSwipeBackEnable(boolean enable) { + mEnable = enable; + mSwipeBackLayout.setEnableGesture(enable); + handleLayout(); + return this; + } + + private void handleLayout(){ + if (mEnable||mRelativeEnable){ + mSwipeBackLayout.attachToActivity(mActivity); + }else { + mSwipeBackLayout.removeFromActivity(mActivity); + } + } + + //可滑动的范围。百分比。200表示为左边200px的屏幕 + public SwipeBackPage setSwipeEdge(int swipeEdge){ + mSwipeBackLayout.setEdgeSize(swipeEdge); + return this; + } + + //可滑动的范围。百分比。0.2表示为左边20%的屏幕 + public SwipeBackPage setSwipeEdgePercent(float swipeEdgePercent){ + mSwipeBackLayout.setEdgeSizePercent(swipeEdgePercent); + return this; + } + + //对横向滑动手势的敏感程度。0为迟钝 1为敏感 + public SwipeBackPage setSwipeSensitivity(float sensitivity){ + mSwipeBackLayout.setSensitivity(mActivity, sensitivity); + return this; + } + + //底层阴影颜色 + public SwipeBackPage setScrimColor(int color){ + mSwipeBackLayout.setScrimColor(color); + return this; + } + + //触发关闭Activity百分比 + public SwipeBackPage setClosePercent(float percent){ + mSwipeBackLayout.setScrollThreshold(percent); + return this; + } + + public SwipeBackPage setDisallowInterceptTouchEvent(boolean disallowIntercept){ + mSwipeBackLayout.setDisallowInterceptTouchEvent(disallowIntercept); + return this; + } + + public SwipeBackPage addListener(SwipeListener listener){ + mSwipeBackLayout.addSwipeListener(listener); + return this; + } + + public SwipeBackPage removeListener(SwipeListener listener){ + mSwipeBackLayout.removeSwipeListener(listener); + return this; + } + + public SwipeBackLayout getSwipeBackLayout() { + return mSwipeBackLayout; + } + + public void scrollToFinishActivity() { + mSwipeBackLayout.scrollToFinishActivity(); + } + +} diff --git a/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeListener.java b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeListener.java new file mode 100644 index 0000000..6e67692 --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeListener.java @@ -0,0 +1,10 @@ +package ca.allanwang.kau.swipe; + +public interface SwipeListener { + void onScroll(float percent,int px); + void onEdgeTouch(); + /** + * Invoke when scroll percent over the threshold for the first time + */ + void onScrollToClose(); + }
\ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt index 84794f9..a2043db 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt @@ -26,9 +26,15 @@ import java.text.DecimalFormat @DslMarker annotation class KauUtils +@KauUtils val Float.dpToPx: Float + get() = this * Resources.getSystem().displayMetrics.density + @KauUtils val Int.dpToPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt() +@KauUtils val Float.pxToDp: Float + get() = this / Resources.getSystem().displayMetrics.density + @KauUtils val Int.pxToDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt() diff --git a/core/src/main/kotlin/ca/allanwang/kau/widgets/ElasticDragDismissFrameLayout.kt b/core/src/main/kotlin/ca/allanwang/kau/widgets/ElasticDragDismissFrameLayout.kt index 38c99c3..c39a278 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/widgets/ElasticDragDismissFrameLayout.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/widgets/ElasticDragDismissFrameLayout.kt @@ -35,11 +35,15 @@ class ElasticDragDismissFrameLayout @JvmOverloads constructor( ) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { // configurable attribs - private var dragDismissDistance = Float.MAX_VALUE - private var dragDismissFraction = -1f - private var dragDismissScale = 1f + var dragDismissDistance = context.dimen(R.dimen.kau_drag_dismiss_distance).dpToPx + var dragDismissFraction = -1f + var dragDismissScale = 1f + set(value) { + field = value + shouldScale = value != 1f + } private var shouldScale = false - private var dragElacticity = 0.8f + var dragElacticity = 0.8f // state private var totalDrag: Float = 0f @@ -54,7 +58,6 @@ class ElasticDragDismissFrameLayout @JvmOverloads constructor( dragDismissDistance = a.getDimensionPixelSize(R.styleable.ElasticDragDismissFrameLayout_dragDismissDistance, Int.MAX_VALUE).toFloat() dragDismissFraction = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragDismissFraction, dragDismissFraction) dragDismissScale = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragDismissScale, dragDismissScale) - shouldScale = dragDismissScale != 1f dragElacticity = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragElasticity, dragElacticity) a.recycle() } |