diff --git a/README.md b/README.md index 803e5e3e..2de04b88 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ - [Max Degree](#max-degree) - [Swipe Direction](#swipe-direction) - [Swipe Restriction](#swipe-restriction) + - [Swipeable Method](#swipeable-method) - [Public Interfaces](#public-interfaces) - [Callbacks](#callbacks) - [Migration Guide](#migration-guide) @@ -239,6 +240,19 @@ CardStackLayoutManager.setCanScrollHorizontal(true); CardStackLayoutManager.setCanScrollVertical(true); ``` +## Swipeable Method + +| Default | Value | Sample | +| :----: | :----: | :----: | +| ✅ | AutomaticAndManual | ![SwipeableMethod-AutomaticAndManual](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipeable-method-automatic-and-manual.gif) | +| | Automatic | ![SwipwableMethod-Automatic](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipeable-method-automatic.gif) | +| | Manual | ![SwipwableMethod-Manual](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipeable-method-manual.gif) | +| | None | ![SwipwableMethod-None](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipeable-method-none.gif) | + +```java +CardStackLayoutManager.setSwipeableMethod(SwipeableMethod.AutomaticAndManual) +``` + # Public Interfaces ## Basic usages diff --git a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/CardStackLayoutManager.java b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/CardStackLayoutManager.java index f64d21c6..8b98c662 100644 --- a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/CardStackLayoutManager.java +++ b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/CardStackLayoutManager.java @@ -57,30 +57,96 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State @Override public boolean canScrollHorizontally() { - return setting.canScrollHorizontal; + return setting.swipeableMethod.canSwipe() && setting.canScrollHorizontal; } @Override public boolean canScrollVertically() { - return setting.canScrollVertical; + return setting.swipeableMethod.canSwipe() && setting.canScrollVertical; } @Override public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State s) { - if (state.status != CardStackState.Status.SwipeAnimating) { - state.dx -= dx; - update(recycler); - return dx; + switch (state.status) { + case Idle: + if (setting.swipeableMethod.canSwipeManually()) { + state.dx -= dx; + update(recycler); + return dx; + } + break; + case Dragging: + if (setting.swipeableMethod.canSwipeManually()) { + state.dx -= dx; + update(recycler); + return dx; + } + break; + case RewindAnimating: + state.dx -= dx; + update(recycler); + return dx; + case AutomaticSwipeAnimating: + if (setting.swipeableMethod.canSwipeAutomatically()) { + state.dx -= dx; + update(recycler); + return dx; + } + break; + case AutomaticSwipeAnimated: + break; + case ManualSwipeAnimating: + if (setting.swipeableMethod.canSwipeManually()) { + state.dx -= dx; + update(recycler); + return dx; + } + break; + case ManualSwipeAnimated: + break; } return 0; } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State s) { - if (state.status != CardStackState.Status.SwipeAnimating) { - state.dy -= dy; - update(recycler); - return dy; + switch (state.status) { + case Idle: + if (setting.swipeableMethod.canSwipeManually()) { + state.dy -= dy; + update(recycler); + return dy; + } + break; + case Dragging: + if (setting.swipeableMethod.canSwipeManually()) { + state.dy -= dy; + update(recycler); + return dy; + } + break; + case RewindAnimating: + state.dy -= dy; + update(recycler); + return dy; + case AutomaticSwipeAnimating: + if (setting.swipeableMethod.canSwipeAutomatically()) { + state.dy -= dy; + update(recycler); + return dy; + } + break; + case AutomaticSwipeAnimated: + break; + case ManualSwipeAnimating: + if (setting.swipeableMethod.canSwipeManually()) { + state.dy -= dy; + update(recycler); + return dy; + } + break; + case ManualSwipeAnimated: + break; } return 0; } @@ -90,54 +156,34 @@ public void onScrollStateChanged(int s) { switch (s) { // スクロールが止まったタイミング case RecyclerView.SCROLL_STATE_IDLE: - if (state.status != CardStackState.Status.PrepareSwipeAnimation) { - // ManualSwipeが完了した場合の処理 - if (state.targetPosition == RecyclerView.NO_POSITION) { - state.next(CardStackState.Status.Idle); - state.targetPosition = RecyclerView.NO_POSITION; - } else { - // 2枚以上のカードを同時にスワイプする場合の処理 - if (state.topPosition < state.targetPosition) { - // 1枚目のカードをスワイプすると一旦SCROLL_STATE_IDLEが流れる - // そのタイミングで次のアニメーションを走らせることで連続でスワイプしているように見せる - smoothScrollToNext(state.targetPosition); - } else if (state.targetPosition < state.topPosition) { - // Nextの場合と同様に、1枚目の処理が完了したタイミングで次のアニメーションは走らせる - smoothScrollToPrevious(state.targetPosition); - } else { - // AutomaticSwipeが完了した場合の処理 - state.next(CardStackState.Status.Idle); - state.targetPosition = RecyclerView.NO_POSITION; - } - } - } else { - // スワイプが何らかの理由で途中でキャンセルされた場合を考慮して、ここで状態をリセットする - // (例)AutomaticSwipeの最中にカードをタップしてManualCancelを実行した場合 - // https://github.com/yuyakaido/CardStackView/issues/175 - // https://github.com/yuyakaido/CardStackView/issues/181 + if (state.targetPosition == RecyclerView.NO_POSITION) { + // Swipeが完了した場合の処理 + state.next(CardStackState.Status.Idle); + state.targetPosition = RecyclerView.NO_POSITION; + } else if (state.topPosition == state.targetPosition) { + // Rewindが完了した場合の処理 state.next(CardStackState.Status.Idle); state.targetPosition = RecyclerView.NO_POSITION; + } else { + // 2枚以上のカードを同時にスワイプする場合の処理 + if (state.topPosition < state.targetPosition) { + // 1枚目のカードをスワイプすると一旦SCROLL_STATE_IDLEが流れる + // そのタイミングで次のアニメーションを走らせることで連続でスワイプしているように見せる + smoothScrollToNext(state.targetPosition); + } else { + // Nextの場合と同様に、1枚目の処理が完了したタイミングで次のアニメーションを走らせる + smoothScrollToPrevious(state.targetPosition); + } } break; // カードをドラッグしている最中 case RecyclerView.SCROLL_STATE_DRAGGING: - state.next(CardStackState.Status.Dragging); + if (setting.swipeableMethod.canSwipeManually()) { + state.next(CardStackState.Status.Dragging); + } break; - // カードが指から離れて、慣性アニメーションが開始したタイミング + // カードが指から離れたタイミング case RecyclerView.SCROLL_STATE_SETTLING: - if (state.status != CardStackState.Status.PrepareSwipeAnimation) { - // TODO この分岐は本当に必要か? - if (state.targetPosition == RecyclerView.NO_POSITION) { - state.next(CardStackState.Status.Idle); - state.targetPosition = RecyclerView.NO_POSITION; - } else { - if (state.topPosition < state.targetPosition) { - state.next(CardStackState.Status.PrepareSwipeAnimation); - } else if (state.targetPosition < state.topPosition) { - state.next(CardStackState.Status.RewindAnimating); - } - } - } break; } } @@ -149,22 +195,20 @@ public PointF computeScrollVectorForPosition(int targetPosition) { @Override public void scrollToPosition(int position) { - if (position == state.topPosition || position < 0 || getItemCount() < position) { - state.next(CardStackState.Status.Idle); - state.targetPosition = RecyclerView.NO_POSITION; - } else if (state.status == CardStackState.Status.Idle) { - state.topPosition = position; - requestLayout(); + if (setting.swipeableMethod.canSwipeAutomatically()) { + if (state.canScrollToPosition(position, getItemCount())) { + state.topPosition = position; + requestLayout(); + } } } @Override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State s, int position) { - if (position == state.topPosition || position < 0 || getItemCount() < position) { - state.next(CardStackState.Status.Idle); - state.targetPosition = RecyclerView.NO_POSITION; - } else if (state.status == CardStackState.Status.Idle) { - smoothScrollToPosition(position); + if (setting.swipeableMethod.canSwipeAutomatically()) { + if (state.canScrollToPosition(position, getItemCount())) { + smoothScrollToPosition(position); + } } } @@ -197,38 +241,71 @@ private void update(RecyclerView.Recycler recycler) { state.width = getWidth(); state.height = getHeight(); - if (state.status == CardStackState.Status.PrepareSwipeAnimation && (state.targetPosition == RecyclerView.NO_POSITION || state.topPosition < state.targetPosition)) { - if (Math.abs(state.dx) > getWidth() || Math.abs(state.dy) > getHeight()) { - state.next(CardStackState.Status.SwipeAnimating); - - // ■ 概要 - // Recyclerから古いViewが返却されて、スワイプ済みのカードが表示される - // データソースは正しく更新されていて、あくまで表示だけが古い状態になる - // - // ■ 再現手順 - // 1. `removeAndRecycleView(getTopView(), recycler);` をコメントアウトする - // 2. VisibleCount=1に設定し、最後のカードがスワイプされたらページングを行うようにする - // 3. カードを1枚だけ画面に表示する(このカードをAとする) - // 4. Aをスワイプする - // 5. カードを1枚だけ画面に表示する(このカードをBとする) - // 6. ページング完了後はBが表示されるはずが、Aが画面に表示される - removeAndRecycleView(getTopView(), recycler); - - state.topPosition++; - final Direction direction = state.getDirection(); - new Handler().post(new Runnable() { - @Override - public void run() { - listener.onCardSwiped(direction); - View topView = getTopView(); - if (topView != null) { - listener.onCardAppeared(getTopView(), state.topPosition); - } - } - }); - state.dx = 0; - state.dy = 0; + if (state.isSwipeCompleted()) { + // ■ 概要 + // スワイプが完了したタイミングで、スワイプ済みのViewをキャッシュから削除する + // キャッシュの削除を行わないと、次回更新時にスワイプ済みのカードが表示されてしまう + // スワイプ済みカードが表示される場合、データソースは正しく、表示だけが古い状態になっている + // + // ■ 再現手順 + // 1. `removeAndRecycleView(getTopView(), recycler);`をコメントアウトする + // 2. VisibleCount=1に設定し、最後のカードがスワイプされたらページングを行うようにする + // 3. カードを1枚だけ画面に表示する(このカードをAとする) + // 4. Aをスワイプする + // 5. カードを1枚だけ画面に表示する(このカードをBとする) + // 6. ページング完了後はBが表示されるはずが、Aが画面に表示される + removeAndRecycleView(getTopView(), recycler); + + state.next(state.status.toAnimatedStatus()); + state.topPosition++; + state.dx = 0; + state.dy = 0; + if (state.topPosition == state.targetPosition) { + state.targetPosition = RecyclerView.NO_POSITION; } + + /* Handlerを経由してイベント通知を行っているのは、以下のエラーを回避するため + * + * 2019-03-31 18:44:29.744 8496-8496/com.yuyakaido.android.cardstackview.sample E/AndroidRuntime: FATAL EXCEPTION: main + * Process: com.yuyakaido.android.cardstackview.sample, PID: 8496 + * java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling com.yuyakaido.android.cardstackview.CardStackView{9d8ff78 VFED..... .F....ID 0,0-1080,1353 #7f080027 app:id/card_stack_view}, adapter:com.yuyakaido.android.cardstackview.sample.CardStackAdapter@e0b8651, layout:com.yuyakaido.android.cardstackview.CardStackLayoutManager@17b0eb6, context:com.yuyakaido.android.cardstackview.sample.MainActivity@fe550ca + * at android.support.v7.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:2880) + * at android.support.v7.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeInserted(RecyclerView.java:5300) + * at android.support.v7.widget.RecyclerView$AdapterDataObservable.notifyItemRangeInserted(RecyclerView.java:12022) + * at android.support.v7.widget.RecyclerView$Adapter.notifyItemRangeInserted(RecyclerView.java:7214) + * at android.support.v7.util.AdapterListUpdateCallback.onInserted(AdapterListUpdateCallback.java:42) + * at android.support.v7.util.BatchingListUpdateCallback.dispatchLastEvent(BatchingListUpdateCallback.java:61) + * at android.support.v7.util.DiffUtil$DiffResult.dispatchUpdatesTo(DiffUtil.java:852) + * at android.support.v7.util.DiffUtil$DiffResult.dispatchUpdatesTo(DiffUtil.java:802) + * at com.yuyakaido.android.cardstackview.sample.MainActivity.paginate(MainActivity.kt:164) + * at com.yuyakaido.android.cardstackview.sample.MainActivity.onCardSwiped(MainActivity.kt:50) + * at com.yuyakaido.android.cardstackview.CardStackLayoutManager.update(CardStackLayoutManager.java:277) + * at com.yuyakaido.android.cardstackview.CardStackLayoutManager.scrollHorizontallyBy(CardStackLayoutManager.java:92) + * at android.support.v7.widget.RecyclerView.scrollStep(RecyclerView.java:1829) + * at android.support.v7.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:5067) + * at android.view.Choreographer$CallbackRecord.run(Choreographer.java:911) + * at android.view.Choreographer.doCallbacks(Choreographer.java:723) + * at android.view.Choreographer.doFrame(Choreographer.java:655) + * at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:897) + * at android.os.Handler.handleCallback(Handler.java:789) + * at android.os.Handler.dispatchMessage(Handler.java:98) + * at android.os.Looper.loop(Looper.java:164) + * at android.app.ActivityThread.main(ActivityThread.java:6541) + * at java.lang.reflect.Method.invoke(Native Method) + * at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) + * at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767) + */ + final Direction direction = state.getDirection(); + new Handler().post(new Runnable() { + @Override + public void run() { + listener.onCardSwiped(direction); + View topView = getTopView(); + if (topView != null) { + listener.onCardAppeared(getTopView(), state.topPosition); + } + } + }); } detachAndScrapAttachedViews(recycler); @@ -262,7 +339,7 @@ public void run() { } } - if (state.status == CardStackState.Status.Dragging) { + if (state.status.isDragging()) { listener.onCardDragging(state.getDirection(), state.getRatio()); } } @@ -531,6 +608,10 @@ public void setCanScrollVertical(boolean canScrollVertical) { setting.canScrollVertical = canScrollVertical; } + public void setSwipeableMethod(SwipeableMethod swipeableMethod) { + setting.swipeableMethod = swipeableMethod; + } + public void setSwipeAnimationSetting(@NonNull SwipeAnimationSetting swipeAnimationSetting) { setting.swipeAnimationSetting = swipeAnimationSetting; } diff --git a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/CardStackView.java b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/CardStackView.java index e9419d82..7c55b70f 100644 --- a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/CardStackView.java +++ b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/CardStackView.java @@ -77,6 +77,7 @@ public void rewind() { private void initialize() { new CardStackSnapHelper().attachToRecyclerView(this); + setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER); } } diff --git a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/SwipeableMethod.java b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/SwipeableMethod.java new file mode 100644 index 00000000..9baf6153 --- /dev/null +++ b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/SwipeableMethod.java @@ -0,0 +1,20 @@ +package com.yuyakaido.android.cardstackview; + +public enum SwipeableMethod { + AutomaticAndManual, + Automatic, + Manual, + None; + + boolean canSwipe() { + return canSwipeAutomatically() || canSwipeManually(); + } + + boolean canSwipeAutomatically() { + return this == AutomaticAndManual || this == Automatic; + } + + boolean canSwipeManually() { + return this == AutomaticAndManual || this == Manual; + } +} diff --git a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSetting.java b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSetting.java index 19cab3fc..841e10f2 100644 --- a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSetting.java +++ b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSetting.java @@ -4,6 +4,7 @@ import com.yuyakaido.android.cardstackview.RewindAnimationSetting; import com.yuyakaido.android.cardstackview.StackFrom; import com.yuyakaido.android.cardstackview.SwipeAnimationSetting; +import com.yuyakaido.android.cardstackview.SwipeableMethod; import java.util.List; @@ -17,6 +18,7 @@ public class CardStackSetting { public List directions = Direction.HORIZONTAL; public boolean canScrollHorizontal = true; public boolean canScrollVertical = true; + public SwipeableMethod swipeableMethod = SwipeableMethod.AutomaticAndManual; public SwipeAnimationSetting swipeAnimationSetting = new SwipeAnimationSetting.Builder().build(); public RewindAnimationSetting rewindAnimationSetting = new RewindAnimationSetting.Builder().build(); } diff --git a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSmoothScroller.java b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSmoothScroller.java index f3121934..c4de4712 100644 --- a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSmoothScroller.java +++ b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSmoothScroller.java @@ -1,5 +1,6 @@ package com.yuyakaido.android.cardstackview.internal; +import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.View; @@ -28,13 +29,17 @@ public CardStackSmoothScroller( } @Override - protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) { + protected void onSeekTargetStep( + int dx, + int dy, + @NonNull RecyclerView.State state, + @NonNull Action action + ) { if (type == ScrollType.AutomaticRewind) { - manager.removeAllViews(); // ■ 概要 // ここでViewのRemoveを行わないとRewindが無限ループに陥ってしまう // ■ 再現手順 - // 1. 上記処理をコメントアウト + // 1. `manager.removeAllViews();`をコメントアウト // 2. AutomaticSwipeを1度実行する // 3. AutomaticRewindを1度実行する // 4. AutomaticSwipeを1度実行する @@ -46,6 +51,7 @@ protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action // ■ 副作用 // ViewのRemoveを行っているため、表示対象となっているViewの再生成が実行されてしまう // これによってパフォーマンス上の問題が発生する可能性がある + manager.removeAllViews(); RewindAnimationSetting setting = manager.getCardStackSetting().rewindAnimationSetting; action.update( -getDx(setting), @@ -57,7 +63,11 @@ protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action } @Override - protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { + protected void onTargetFound( + @NonNull View targetView, + @NonNull RecyclerView.State state, + @NonNull Action action + ) { int x = (int) targetView.getTranslationX(); int y = (int) targetView.getTranslationY(); AnimationSetting setting; @@ -109,14 +119,14 @@ protected void onStart() { CardStackState state = manager.getCardStackState(); switch (type) { case AutomaticSwipe: - state.next(CardStackState.Status.PrepareSwipeAnimation); + state.next(CardStackState.Status.AutomaticSwipeAnimating); listener.onCardDisappeared(manager.getTopView(), manager.getTopPosition()); break; case AutomaticRewind: state.next(CardStackState.Status.RewindAnimating); break; case ManualSwipe: - state.next(CardStackState.Status.PrepareSwipeAnimation); + state.next(CardStackState.Status.ManualSwipeAnimating); listener.onCardDisappeared(manager.getTopView(), manager.getTopPosition()); break; case ManualCancel: diff --git a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSnapHelper.java b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSnapHelper.java index 15762878..cc622154 100644 --- a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSnapHelper.java +++ b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSnapHelper.java @@ -28,6 +28,7 @@ public int[] calculateDistanceToFinalSnap( if (setting.swipeThreshold < horizontal || setting.swipeThreshold < vertical) { CardStackState state = manager.getCardStackState(); if (setting.directions.contains(state.getDirection())) { + state.targetPosition = state.topPosition + 1; CardStackSmoothScroller scroller = new CardStackSmoothScroller(CardStackSmoothScroller.ScrollType.ManualSwipe, manager); scroller.setTargetPosition(manager.getTopPosition()); manager.startSmoothScroll(scroller); diff --git a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackState.java b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackState.java index bfcd79af..049559e1 100644 --- a/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackState.java +++ b/cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackState.java @@ -15,7 +15,36 @@ public class CardStackState { public float proportion = 0.0f; public enum Status { - Idle, Dragging, RewindAnimating, PrepareSwipeAnimation, SwipeAnimating + Idle, + Dragging, + RewindAnimating, + AutomaticSwipeAnimating, + AutomaticSwipeAnimated, + ManualSwipeAnimating, + ManualSwipeAnimated; + + public boolean isBusy() { + return this != Idle; + } + + public boolean isDragging() { + return this == Dragging; + } + + public boolean isSwipeAnimating() { + return this == ManualSwipeAnimating || this == AutomaticSwipeAnimating; + } + + public Status toAnimatedStatus() { + switch (this) { + case ManualSwipeAnimating: + return ManualSwipeAnimated; + case AutomaticSwipeAnimating: + return AutomaticSwipeAnimated; + default: + return Idle; + } + } } public void next(Status state) { @@ -50,4 +79,31 @@ public float getRatio() { return Math.min(ratio, 1.0f); } + public boolean isSwipeCompleted() { + if (status.isSwipeAnimating()) { + if (topPosition < targetPosition) { + if (width < Math.abs(dx) || height < Math.abs(dy)) { + return true; + } + } + } + return false; + } + + public boolean canScrollToPosition(int position, int itemCount) { + if (position == topPosition) { + return false; + } + if (position < 0) { + return false; + } + if (itemCount < position) { + return false; + } + if (status.isBusy()) { + return false; + } + return true; + } + } diff --git a/sample/src/main/java/com/yuyakaido/android/cardstackview/sample/MainActivity.kt b/sample/src/main/java/com/yuyakaido/android/cardstackview/sample/MainActivity.kt index b5148348..55e25213 100644 --- a/sample/src/main/java/com/yuyakaido/android/cardstackview/sample/MainActivity.kt +++ b/sample/src/main/java/com/yuyakaido/android/cardstackview/sample/MainActivity.kt @@ -145,6 +145,7 @@ class MainActivity : AppCompatActivity(), CardStackListener { manager.setDirections(Direction.HORIZONTAL) manager.setCanScrollHorizontal(true) manager.setCanScrollVertical(true) + manager.setSwipeableMethod(SwipeableMethod.AutomaticAndManual) cardStackView.layoutManager = manager cardStackView.adapter = adapter cardStackView.itemAnimator.apply {