diff --git a/README.md b/README.md index ebe4586..f09237d 100644 --- a/README.md +++ b/README.md @@ -11,21 +11,21 @@ ConsecutiveScrollerLayout是Android下支持多个滑动布局(RecyclerView、We ```groovy allprojects { - repositories { - ... - maven { url 'https://jitpack.io' } - } - } + repositories { + ... + maven { url 'https://jitpack.io' } + } + } ``` 在Module的build.gradle在添加以下代码 ```groovy // 使用了Androidx -implementation 'com.github.donkingliang:ConsecutiveScroller:2.3.0' +implementation 'com.github.donkingliang:ConsecutiveScroller:2.4.0' // 或者 // 使用Android support包 -implementation 'com.github.donkingliang:ConsecutiveScroller:1.3.0' +implementation 'com.github.donkingliang:ConsecutiveScroller:1.4.0' ``` 由于Androidx和Android support包不兼容,所以ConsecutiveScroller使用两个版本分别支持使用Androidx和使用Android support包的项目。 大版本号1使用Android support包,大版本号2使用Androidx。 @@ -242,43 +242,139 @@ ConsecutiveScrollerLayout将所有的子View视作一个整体,由它统一处 ConsecutiveScrollerLayout支持NestedScrolling机制,如果你的局部滑动的view实现了NestedScrollingChild接口(如:RecyclerView、NestedScrollView等),它滑动完成后会把滑动事件交给父布局。如果你不想你的子view或它的下级view与父布局嵌套滑动,可以给子view设置app:layout_isNestedScroll="false"。它可以禁止子view与ConsecutiveScrollerLayout的嵌套滑动 -### 使用腾讯x5的WebView -由于腾讯x5的VebView是一个FrameLayout嵌套WebView的布局,而不是一个WebView的子类,所以要在ConsecutiveScrollerLayout里使用它,需要把它的滑动交给它里面的WebView。自定义MyWebView继承腾讯的WebView,重写它的scrollBy()方法即可。 +### 滑动子view的下级view + +ConsecutiveScrollerLayout默认情况下只会处理它的直接子view的滑动,但有时候需要滑动的布局可能不是ConsecutiveScrollerLayout的直接子view,而是子view所嵌套的下级view。比如ConsecutiveScrollerLayout嵌套FrameLayout,FrameLayout嵌套ScrollView,我们希望ConsecutiveScrollerLayout也能正常处理ScrollView的滑动。为了支持这种需求,ConsecutiveScroller提供了一个接口:IConsecutiveScroller。子view实现IConsecutiveScroller接口,并通过实现接口方法告诉ConsecutiveScrollerLayout需要滑动的下级view,ConsecutiveScrollerLayout就能正确地处理它的滑动事件。IConsecutiveScroller需要实现两个方法: ```java -public class MyWebView extends com.tencent.smtt.sdk.WebView { + /** + * 返回当前需要滑动的下级view。在一个时间点里只能有一个view可以滑动。 + */ + View getCurrentScrollerView(); + + /** + * 返回所有可以滑动的子view。由于ConsecutiveScrollerLayout允许它的子view包含多个可滑动的子view,所以返回一个view列表。 + */ + List getScrolledViews(); +``` +在前面提到的例子中,我们可以这样实现: +```java +public class MyFrameLayout extends FrameLayout implements IConsecutiveScroller { - public MyWebView(Context context, boolean b) { - super(context, b); + @Override + public View getCurrentScrollerView() { + // 返回需要滑动的ScrollView + return getChildAt(0); } - public MyWebView(Context context) { - super(context); + @Override + public List getScrolledViews() { + // 返回需要滑动的ScrollView + List views = new ArrayList<>(); + views.add(getChildAt(0)); + return views; } +} +``` +```xml + + - public MyWebView(Context context, AttributeSet attributeSet) { - super(context, attributeSet); - } + + + + + + + + +``` +这样ConsecutiveScrollerLayout就能正确地处理ScrollView的滑动。这是一个简单的例子,在实际的需求中,我们一般不需要这样做。 - public MyWebView(Context context, AttributeSet attributeSet, int i) { - super(context, attributeSet, i); - } +**注意:** getCurrentScrollerView()和getScrolledViews()必须正确地返回需要滑动的view,这些view可以是经过多层嵌套的,不一定是直接子view。所以使用者应该按照自己的实际场景去实现者两个方法。 - public MyWebView(Context context, AttributeSet attributeSet, int i, boolean b) { - super(context, attributeSet, i, b); +#### 对ViewPager的支持 +IConsecutiveScroller的一个常用的场景是对ViewPager的支持。ViewPager是左右滑动的控件,但是我们一般会在ViewPager下嵌套RecyclerView等列表布局。为了能让ConsecutiveScrollerLayout正确地滑动ViewPager下的RecyclerView,使RecyclerView与ConsecutiveScrollerLayout形成一个滑动整体。需要让ViewPager实现IConsecutiveScroller接口,并返回需要滑动的RecyclerView。 +```java + public class MyViewPager extends ViewPager implements IConsecutiveScroller { + + /** + * 返回当前需要滑动的view。 + */ + @Override + public View getCurrentScrollerView() { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View view = getChildAt(i); + if (view.getX() == getScrollX()) { + return view; + } + } + return this; } - public MyWebView(Context context, AttributeSet attributeSet, int i, Map map, boolean b) { - super(context, attributeSet, i, map, b); + /** + * 返回全部需要滑动的下级view + */ + @Override + public List getScrolledViews() { + List views = new ArrayList<>(); + int count = getChildCount(); + if (count > 0) { + for (int i = 0; i < count; i++) { + views.add(getChildAt(i)); + } + } else { + views.add(this); + } + return views; } +} +``` +我在demo中提供了ViewPager实现IConsecutiveScroller的例子和效果,有兴趣的朋友可以去体验一下。 + +**重要:** 再提醒一次,我在这里提供的例子并不是通用的,使用者应该按照自己的实际场景去实现者两个方法。 + +### 使用腾讯x5的WebView +由于腾讯x5的VebView是一个FrameLayout嵌套WebView的布局,而不是一个WebView的子类,所以要在ConsecutiveScrollerLayout里使用它,需要把它的滑动交给它里面的WebView。自定义MyWebView继承腾讯的WebView,重写它的scrollBy()方法即可。 +```java +public class MyWebView extends com.tencent.smtt.sdk.WebView { - @Override public void scrollBy(int x, int y) { - // 把滑动交给它的子view + // 把滑动交给它的子view getView().scrollBy(x, y); } } ``` +通过实现IConsecutiveScroller接口同样可以实现对x5的WebView支持。 +```java +public class MyWebView extends com.tencent.smtt.sdk.WebView { + + @Override + public View getCurrentScrollerView() { + return getView(); + } + + @Override + public List getScrolledViews() { + List views = new ArrayList<>(); + views.add(getView()); + return views; + } + +} +``` 另外需要隐藏它的子view的滚动条 ```java View view = webView.getView(); @@ -308,4 +404,3 @@ webView.setWebChromeClient(new WebChromeClient() { 4、使用ConsecutiveScrollerLayout提供的setOnVerticalScrollChangeListener()方法监听布局的滑动事件。View所提供的setOnScrollChangeListener()方法已无效。 5、通过getOwnScrollY()方法获取ConsecutiveScrollerLayout的垂直滑动距离,View的getScrollY()方法获取的不是ConsecutiveScrollerLayout的整体滑动距离。 - diff --git a/app/src/main/java/com/donkingliang/consecutivescrollerdemo/ViewPagerActivity.java b/app/src/main/java/com/donkingliang/consecutivescrollerdemo/ViewPagerActivity.java index ecb2026..3f4544c 100644 --- a/app/src/main/java/com/donkingliang/consecutivescrollerdemo/ViewPagerActivity.java +++ b/app/src/main/java/com/donkingliang/consecutivescrollerdemo/ViewPagerActivity.java @@ -21,8 +21,9 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_viewpager); TextView text = findViewById(R.id.text); - text.setText("子view实现IConsecutiveScroller接口,并通过实现接口方法告诉ConsecutiveScrollerLayout需要滑动的下级view,\n" + - " * ConsecutiveScrollerLayout就能正确地处理它的滑动事件。"); + text.setText("子view通过实现IConsecutiveScroller接口,可以使ConsecutiveScrollerLayout能正确地处理子view的下级view的滑动事件。\n" + + "下面的例子中,通过自定义ViewPager,实现IConsecutiveScroller接口,ConsecutiveScrollerLayout能正确的处理ViewPager里" + + "的RecyclerView滑动,使RecyclerView与ConsecutiveScrollerLayout形成整体的滑动效果"); ViewPager viewPager = findViewById(R.id.viewPager); TabLayout tabLayout = findViewById(R.id.tabLayout); viewPager.setAdapter(new TabPagerAdapter(getSupportFragmentManager(), getTabs(), getFragments())); diff --git a/app/src/main/java/com/donkingliang/consecutivescrollerdemo/widget/MyViewPager.java b/app/src/main/java/com/donkingliang/consecutivescrollerdemo/widget/MyViewPager.java index 689e333..90323a6 100644 --- a/app/src/main/java/com/donkingliang/consecutivescrollerdemo/widget/MyViewPager.java +++ b/app/src/main/java/com/donkingliang/consecutivescrollerdemo/widget/MyViewPager.java @@ -27,9 +27,13 @@ public MyViewPager(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } + /** + * 返回当前需要滑动的view。 + * 注意:这个view不一定是ViewPager的直接子view,使用者应该根据自己的业务情况返回需要滑动的下级view。 + * @return + */ @Override public View getCurrentScrollerView() { - int count = getChildCount(); for (int i = 0; i < count; i++) { View view = getChildAt(i); @@ -37,10 +41,13 @@ public View getCurrentScrollerView() { return view; } } - return this; } + /** + * 返回全部需要滑动的下级view + * @return + */ @Override public List getScrolledViews() { List views = new ArrayList<>(); diff --git a/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/ConsecutiveScrollerLayout.java b/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/ConsecutiveScrollerLayout.java index 477cab0..3b2b057 100644 --- a/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/ConsecutiveScrollerLayout.java +++ b/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/ConsecutiveScrollerLayout.java @@ -5,7 +5,6 @@ import android.content.res.TypedArray; import android.os.Build; import android.util.AttributeSet; -import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; @@ -75,7 +74,6 @@ public class ConsecutiveScrollerLayout extends ViewGroup implements NestedScroll private int mScrollOffset = 0; private boolean isConsecutiveScrollerChild = false; - private boolean isAdjust = true; /** * 是否处于状态 @@ -99,6 +97,16 @@ public class ConsecutiveScrollerLayout extends ViewGroup implements NestedScroll private View mScrollToTopView; private int mAdjust; + /** + * 滑动到指定view,目标view的index + */ + private int mScrollToIndex = -1; + + /** + * 滑动到指定view,平滑滑动时,每次滑动的距离 + */ + private int mSmoothScrollOffset = 0; + // 这是RecyclerView的代码,让ConsecutiveScrollerLayout的fling效果更接近于RecyclerView。 static final Interpolator sQuinticInterpolator = new Interpolator() { @Override @@ -178,7 +186,7 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) { } // 布局发生变化,检测滑动位置 - checkLayoutChange(); + checkLayoutChange(false); } private void resetScrollToTopView() { @@ -211,7 +219,7 @@ public boolean dispatchTouchEvent(MotionEvent ev) { case MotionEvent.ACTION_DOWN: // 停止滑动 stopScroll(); - checkTargetsScroll(false); + checkTargetsScroll(false, false); mFixedEventY = ev.getY(); mTouching = true; SCROLL_ORIENTATION = SCROLL_NONE; @@ -398,15 +406,22 @@ private void fling(int velocityY) { @Override public void computeScroll() { - if (mScroller.computeScrollOffset()) { - int curY = mScroller.getCurrY(); - dispatchScroll(curY); + if (mScrollToIndex != -1 && mSmoothScrollOffset != 0) { + // 正在平滑滑动到某个子view + scrollBy(0, mSmoothScrollOffset); invalidate(); - } + } else { + // fling + if (mScroller.computeScrollOffset()) { + int curY = mScroller.getCurrY(); + dispatchScroll(curY); + invalidate(); + } - if (mScroller.isFinished()) { - // 滚动结束,校验子view内容的滚动位置 - checkTargetsScroll(false); + if (mScroller.isFinished()) { + // 滚动结束,校验子view内容的滚动位置 + checkTargetsScroll(false, false); + } } } @@ -416,13 +431,11 @@ public void computeScroll() { * @param y */ private void dispatchScroll(int y) { - if (SCROLL_ORIENTATION != SCROLL_HORIZONTAL) { - int offset = y - mOwnScrollY; - if (mOwnScrollY < y) { - scrollUp(offset); - } else if (mOwnScrollY > y) { - scrollDown(offset); - } + int offset = y - mOwnScrollY; + if (mOwnScrollY < y) { + scrollUp(offset); + } else if (mOwnScrollY > y) { + scrollDown(offset); } } @@ -436,6 +449,17 @@ private void scrollUp(int offset) { int remainder = offset; int oldScrollY = mOwnScrollY; do { + + // 如果是要滑动到指定的View,判断滑动到目标位置,就停止滑动 + if (mScrollToIndex != -1) { + View view = getChildAt(mScrollToIndex); + if (getScrollY() + getPaddingTop() >= view.getTop()) { + mScrollToIndex = -1; + mSmoothScrollOffset = 0; + break; + } + } + scrollOffset = 0; if (!isScrollBottom()) { // 找到当前显示的第一个View @@ -462,6 +486,7 @@ private void scrollUp(int offset) { remainder = remainder - scrollOffset; } } + } while (scrollOffset > 0 && remainder > 0); if (oldScrollY != mOwnScrollY) { @@ -475,6 +500,18 @@ private void scrollDown(int offset) { int remainder = offset; int oldScrollY = mOwnScrollY; do { + + // 如果是要滑动到指定的View,判断滑动到目标位置,就停止滑动 + if (mScrollToIndex != -1) { + View view = getChildAt(mScrollToIndex); + if (getScrollY() + getPaddingTop() >= view.getTop() + && ScrollUtils.getScrollTopOffset(view) >= 0) { + mScrollToIndex = -1; + mSmoothScrollOffset = 0; + break; + } + } + scrollOffset = 0; if (!isScrollTop()) { // 找到当前显示的最后一个View @@ -502,6 +539,7 @@ private void scrollDown(int offset) { remainder = remainder - scrollOffset; } } + } while (scrollOffset < 0 && remainder < 0); if (oldScrollY != mOwnScrollY) { @@ -554,10 +592,15 @@ private void scrollChild(View child, int y) { } } + + public void checkLayoutChange() { + checkLayoutChange(true); + } + /** * 布局发生变化,重新检查所有子View是否正确显示 */ - public void checkLayoutChange() { + public void checkLayoutChange(boolean isForce) { if (mScrollToTopView != null) { if (indexOfChild(mScrollToTopView) != -1) { scrollSelf(mScrollToTopView.getTop() + mAdjust); @@ -567,7 +610,7 @@ public void checkLayoutChange() { } mScrollToTopView = null; mAdjust = 0; - checkTargetsScroll(true); + checkTargetsScroll(true, isForce); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { resetChildren(); @@ -578,9 +621,9 @@ public void checkLayoutChange() { /** * 校验子view内容滚动位置是否正确 */ - private void checkTargetsScroll(boolean isLayoutChange) { + private void checkTargetsScroll(boolean isLayoutChange, boolean isForce) { - if (mTouching || !mScroller.isFinished()) { + if (!isForce && (mTouching || !mScroller.isFinished() || mScrollToIndex != -1)) { return; } @@ -1131,7 +1174,7 @@ public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, in @Override public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes) { mParentHelper.onNestedScrollAccepted(child, target, axes); - checkTargetsScroll(false); + checkTargetsScroll(false, false); } @Override @@ -1180,4 +1223,46 @@ public boolean onNestedFling(@NonNull View target, float velocityX, float veloci public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) { return super.onNestedPreFling(target, velocityX, velocityY); } + + /** + * 滑动到指定的view + * + * @param view + */ + public void scrollToChild(View view) { + int scrollToIndex = indexOfChild(view); + if (scrollToIndex != -1) { + mScrollToIndex = scrollToIndex; + // 停止fling + stopScroll(); + do { + if (getScrollY() + getPaddingTop() >= view.getTop()) { + scrollBy(0, -200); + } else { + scrollBy(0, 200); + } + + } while (mScrollToIndex != -1); + } + } + + /** + * 平滑滑动到指定的view + * + * @param view + */ + public void smoothScrollToChild(View view) { + int scrollToIndex = indexOfChild(view); + if (scrollToIndex != -1) { + mScrollToIndex = scrollToIndex; + // 停止fling + stopScroll(); + if (getScrollY() + getPaddingTop() >= view.getTop()) { + mSmoothScrollOffset = -200; + } else { + mSmoothScrollOffset = 200; + } + invalidate(); + } + } } diff --git a/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/IConsecutiveScroller.java b/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/IConsecutiveScroller.java index a10510e..4a661d4 100644 --- a/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/IConsecutiveScroller.java +++ b/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/IConsecutiveScroller.java @@ -5,13 +5,25 @@ import java.util.List; /** - * @Author teach-梁任彦 - * @Description + * @Author donkingliang + * @Description ConsecutiveScrollerLayout默认只会处理它的直接子view的滑动事件, + * 为了让ConsecutiveScrollerLayout能支持滑动子view的下级view,提供了IConsecutiveScroller接口。 + * + * 子view实现IConsecutiveScroller接口,并通过实现接口方法告诉ConsecutiveScrollerLayout需要滑动的下级view, + * ConsecutiveScrollerLayout就能正确地处理它的滑动事件。 * @Date 2020/4/18 */ public interface IConsecutiveScroller { + /** + * 返回当前需要滑动的下级view。在一个时间点里只能有一个view可以滑动。 + * @return + */ View getCurrentScrollerView(); + /** + * 返回所有可以滑动的子view。由于ConsecutiveScrollerLayout允许它的子view包含多个可滑动的子view,所以返回一个view列表。 + * @return + */ List getScrolledViews(); } diff --git a/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/ScrollUtils.java b/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/ScrollUtils.java index b7f67a0..7775f49 100644 --- a/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/ScrollUtils.java +++ b/consecutivescroller/src/main/java/com/donkingliang/consecutivescroller/ScrollUtils.java @@ -95,6 +95,7 @@ static boolean canScrollVertically(View view) { /** * 判断是否可以滑动 + * * @param view * @param direction * @return @@ -225,13 +226,26 @@ static boolean isConsecutiveScrollerChild(View view) { return false; } - static boolean isRecyclerLayout(View view){ + /** + * 判断是否是item复用的view(RecyclerView、AbsListView) + * @param view + * @return + */ + static boolean isRecyclerLayout(View view) { return view instanceof RecyclerView || view instanceof AbsListView; } - static View getScrolledView(View view){ - if (view instanceof IConsecutiveScroller){ - return ((IConsecutiveScroller) view).getCurrentScrollerView(); + /** + * 返回需要滑动的view,如果没有,就返回本身。 + * @param view + * @return + */ + static View getScrolledView(View view) { + if (view instanceof IConsecutiveScroller) { + View scrolledView = ((IConsecutiveScroller) view).getCurrentScrollerView(); + if (scrolledView != null) { + return scrolledView; + } } return view; }