├── app ├── .gitignore ├── .settings │ └── org.eclipse.buildship.core.prefs ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── ids.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ └── attrs.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── drawable │ │ │ │ ├── dynamic_tab_desc_selected_bg.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── activity_main_update.xml │ │ │ │ ├── layout_item_text.xml │ │ │ │ ├── activity_main_java.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── layout_item_category_default.xml │ │ │ │ ├── laz_recommend_tab_filter_revamp_new_rec.xml │ │ │ │ ├── layout_item_category.xml │ │ │ │ └── matrix_store_tab_view.xml │ │ │ ├── menu │ │ │ │ └── menu.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── gaohui │ │ │ │ └── nestedrecyclerview │ │ │ │ ├── kotlin │ │ │ │ ├── bean │ │ │ │ │ └── CategoryBean.kt │ │ │ │ ├── OnUserVisibleChange.kt │ │ │ │ ├── tab │ │ │ │ │ ├── DynamicTabBean.kt │ │ │ │ │ └── DynamicTabView.kt │ │ │ │ ├── holder │ │ │ │ │ ├── SimpleTextHolder.kt │ │ │ │ │ └── SimpleCategoryHolder.kt │ │ │ │ ├── FixedSwipeRefreshLayout.kt │ │ │ │ ├── helper │ │ │ │ │ └── FlingHelper.kt │ │ │ │ ├── adapter │ │ │ │ │ ├── CategoryPagerAdapter.kt │ │ │ │ │ └── MultiTypeAdapter.kt │ │ │ │ ├── ui │ │ │ │ │ └── MainActivity.kt │ │ │ │ ├── CategoryView.kt │ │ │ │ ├── ChildRecyclerView.kt │ │ │ │ └── ParentRecyclerView.kt │ │ │ │ ├── update │ │ │ │ ├── listener │ │ │ │ │ ├── NestedRVOnScrollListener.java │ │ │ │ │ ├── AbsNestedRVOnScrollListener.java │ │ │ │ │ ├── IHPScrollListener.java │ │ │ │ │ ├── TabNestedRVOnScrollListener.java │ │ │ │ │ └── SimpleNestedRVOnScrollListener.java │ │ │ │ ├── utils │ │ │ │ │ └── ScreenUtils.java │ │ │ │ ├── RecommendStaggeredGridLayoutManager.java │ │ │ │ ├── viewpager │ │ │ │ │ ├── NestedViewPager.java │ │ │ │ │ ├── AbsViewPagerAdapter.java │ │ │ │ │ └── ViewPagerAdapter.java │ │ │ │ ├── viewholder │ │ │ │ │ ├── UpdatedTextViewHolder.java │ │ │ │ │ └── UpdatedCategoryViewHolder.java │ │ │ │ ├── IRecommendTabLayout.java │ │ │ │ ├── SimpleStaggeredGridLayoutManager.java │ │ │ │ ├── MainUpdateActivity.java │ │ │ │ ├── NestedStaggeredGridLayoutManager.java │ │ │ │ ├── UpdatedNestedRecyclerView.java │ │ │ │ ├── adpter │ │ │ │ │ └── UpdatedMultiTypeAdapter.java │ │ │ │ └── SlidingTabLayoutRevamp.java │ │ │ │ ├── java │ │ │ │ ├── view │ │ │ │ │ ├── OnUserVisibleChange.java │ │ │ │ │ ├── StoreSwipeRefreshLayout.java │ │ │ │ │ ├── CategoryView.java │ │ │ │ │ ├── ChildRecyclerView.java │ │ │ │ │ └── ParentRecyclerView.java │ │ │ │ ├── viewholder │ │ │ │ │ ├── SimpleTextViewHolder.java │ │ │ │ │ └── SimpleCategoryViewHolder.java │ │ │ │ ├── bean │ │ │ │ │ └── CategoryBean.java │ │ │ │ ├── utils │ │ │ │ │ └── FlingHelper.java │ │ │ │ ├── adapter │ │ │ │ │ ├── CategoryPagerAdapter.java │ │ │ │ │ └── MultiTypeAdapter.java │ │ │ │ └── MainJavaActivity.java │ │ │ │ ├── UIUtils.java │ │ │ │ └── BaseMenuActivity.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── gaohui │ │ │ └── nestedrecyclerview │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── gaohui │ │ └── nestedrecyclerview │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── .settings └── org.eclipse.buildship.core.prefs ├── gif ├── nested_recyclerview_1.gif ├── nested_recyclerview_2.gif └── nested_recyclerview_animaion.gif ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir= 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /gif/nested_recyclerview_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/gif/nested_recyclerview_1.gif -------------------------------------------------------------------------------- /gif/nested_recyclerview_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/gif/nested_recyclerview_2.gif -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | NestedRecyclerView 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gif/nested_recyclerview_animaion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/gif/nested_recyclerview_animaion.gif -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/bean/CategoryBean.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin.bean 2 | 3 | class CategoryBean { 4 | var tabTitleList = ArrayList() 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/listener/NestedRVOnScrollListener.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.listener; 2 | 3 | public class NestedRVOnScrollListener extends TabNestedRVOnScrollListener { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/view/OnUserVisibleChange.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java.view; 2 | 3 | public interface OnUserVisibleChange { 4 | void onUserVisibleChange(boolean isVisibleToUser); 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dynamic_tab_desc_selected_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Aug 15 17:36:56 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/OnUserVisibleChange.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin 2 | 3 | /** 4 | * VisibleChang回调 5 | */ 6 | interface OnUserVisibleChange { 7 | /** 8 | * 是否对用户可见 9 | * 10 | * @param isVisibleToUser 11 | */ 12 | fun onUserVisibleChange(isVisibleToUser:Boolean) 13 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main_update.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/tab/DynamicTabBean.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin.tab 2 | 3 | data class TabIcon(var normal:String = "", 4 | var clicked:String = "") 5 | 6 | 7 | data class DynamicTabBean(val title: String, 8 | val desc: String? = "", 9 | val icon: TabIcon? = null) -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/holder/SimpleTextHolder.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin.holder 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.View 5 | import android.widget.TextView 6 | import com.gaohui.nestedrecyclerview.R 7 | 8 | class SimpleTextViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 9 | val mTv: TextView = itemView.findViewById(R.id.textView) as TextView 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/utils/ScreenUtils.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.utils; 2 | 3 | import android.content.Context; 4 | import android.util.TypedValue; 5 | 6 | public class ScreenUtils { 7 | public static int dp2px(Context context, float dp) { 8 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 9 | dp, context.getResources().getDisplayMetrics()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/gaohui/nestedrecyclerview/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/listener/AbsNestedRVOnScrollListener.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.listener; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | 5 | import com.gaohui.nestedrecyclerview.update.UpdatedNestedRecyclerView; 6 | 7 | public abstract class AbsNestedRVOnScrollListener extends RecyclerView.OnScrollListener { 8 | abstract public void setVelocityY(int velocityY); 9 | 10 | abstract public void updatePullState(UpdatedNestedRecyclerView parentRV); 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | .DS_Store 5 | build 6 | gen 7 | log.txt 8 | 9 | main/build 10 | main/gen 11 | 12 | .metadata 13 | out/ 14 | 15 | # Intellij project files 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/* 20 | !.idea/inspectionProfiles 21 | !.idea/inspectionProfiles/* 22 | 23 | # files for the dex VM 24 | *.dex 25 | 26 | # Java class files 27 | *.class 28 | 29 | # Eclipse project files 30 | .classpath 31 | .project 32 | 33 | captures/ 34 | BuglyUploadLog.txt 35 | 36 | freeline.py 37 | freeline_project_description.json 38 | freeline/ -------------------------------------------------------------------------------- /app/src/main/res/menu/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | 11 | 15 | 16 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/viewholder/SimpleTextViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java.viewholder; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.View; 6 | import android.widget.TextView; 7 | 8 | import com.gaohui.nestedrecyclerview.R; 9 | 10 | public class SimpleTextViewHolder extends RecyclerView.ViewHolder { 11 | public TextView mTv; 12 | public SimpleTextViewHolder(@NonNull View itemView) { 13 | super(itemView); 14 | mTv = (TextView) itemView.findViewById(R.id.textView); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/listener/IHPScrollListener.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.listener; 2 | 3 | 4 | /** 5 | * @author tangmingjian 6 | * @version v1.0 7 | * @date 2023/5/4 8 | * @description 首页滚动状态监听 9 | */ 10 | public interface IHPScrollListener { 11 | int SCROLL_FROM_HP = 0; 12 | int SCROLL_FROM_JFY = 1; 13 | 14 | /** 15 | * 首页滚动状态监听 16 | * 17 | * @param from 首页或者jfy 18 | * {@link #SCROLL_FROM_HP} 19 | * {@link #SCROLL_FROM_JFY} 20 | * @param state 滚动状态 21 | */ 22 | void onScrollStateChanged(int from, int state); 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/RecommendStaggeredGridLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | 6 | public class RecommendStaggeredGridLayoutManager extends SimpleStaggeredGridLayoutManager { 7 | public RecommendStaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 8 | super(context, attrs, defStyleAttr, defStyleRes); 9 | } 10 | 11 | public RecommendStaggeredGridLayoutManager(int spanCount, int orientation) { 12 | super(spanCount, orientation); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_item_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/bean/CategoryBean.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java.bean; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import java.util.ArrayList; 6 | 7 | public class CategoryBean { 8 | ArrayList tabTitleList = new ArrayList(); 9 | 10 | public ArrayList getTabTitleList() { 11 | return tabTitleList; 12 | } 13 | 14 | public void setTabTitleList(ArrayList tabTitleList) { 15 | this.tabTitleList = tabTitleList; 16 | } 17 | 18 | @NonNull 19 | @Override 20 | public String toString() { 21 | return tabTitleList.toString(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main_java.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/gaohui/nestedrecyclerview/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.gaohui.nestedrecyclerview", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/viewpager/NestedViewPager.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.viewpager; 2 | 3 | import android.content.Context; 4 | import android.support.v4.view.ViewPager; 5 | import android.view.MotionEvent; 6 | 7 | public class NestedViewPager extends ViewPager { 8 | private boolean canScroll = true; 9 | 10 | public NestedViewPager(Context context) { 11 | super(context); 12 | } 13 | 14 | 15 | public void setCanScroll(boolean canScroll) { 16 | this.canScroll = canScroll; 17 | } 18 | 19 | @Override 20 | public boolean onTouchEvent(MotionEvent ev) { 21 | return canScroll && super.onTouchEvent(ev); 22 | } 23 | 24 | @Override 25 | public boolean onInterceptTouchEvent(MotionEvent ev) { 26 | return canScroll && super.onInterceptTouchEvent(ev); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # Kotlin code style for this project: "official" or "obsolete": 15 | kotlin.code.style=official 16 | 17 | #android.useAndroidX=true 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_item_category_default.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/viewholder/UpdatedTextViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.viewholder; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.support.v7.widget.StaggeredGridLayoutManager; 6 | import android.view.View; 7 | import android.widget.TextView; 8 | 9 | import com.gaohui.nestedrecyclerview.R; 10 | 11 | 12 | public class UpdatedTextViewHolder extends RecyclerView.ViewHolder { 13 | public TextView mTv; 14 | public UpdatedTextViewHolder(@NonNull View itemView) { 15 | super(itemView); 16 | mTv = (TextView) itemView.findViewById(R.id.textView); 17 | StaggeredGridLayoutManager.LayoutParams slp; 18 | if ((itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams)) { 19 | slp = (StaggeredGridLayoutManager.LayoutParams)itemView.getLayoutParams(); 20 | slp.setFullSpan(true); 21 | } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/FixedSwipeRefreshLayout.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin 2 | 3 | import android.content.Context 4 | import android.support.v4.widget.SwipeRefreshLayout 5 | import android.util.AttributeSet 6 | 7 | class StoreSwipeRefreshLayout : SwipeRefreshLayout { 8 | 9 | private var mParentRecyclerView: ParentRecyclerView? = null 10 | 11 | constructor(context: Context) : super(context) 12 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 13 | 14 | override fun onAttachedToWindow() { 15 | super.onAttachedToWindow() 16 | if (mParentRecyclerView == null) { 17 | for (i in 0 until childCount) { 18 | val child = getChildAt(i) 19 | if (child is ParentRecyclerView) { 20 | mParentRecyclerView = child 21 | break 22 | } 23 | } 24 | } 25 | 26 | } 27 | 28 | override fun canChildScrollUp(): Boolean { 29 | return super.canChildScrollUp() || mParentRecyclerView?.isChildRecyclerViewCanScrollUp()?:false 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/laz_recommend_tab_filter_revamp_new_rec.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/UIUtils.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview; 2 | 3 | import android.content.res.Resources; 4 | import android.util.DisplayMetrics; 5 | 6 | public class UIUtils { 7 | private static final DisplayMetrics sMetrics = Resources.getSystem().getDisplayMetrics(); 8 | 9 | public static int getScreenWidth() { 10 | return sMetrics != null ? sMetrics.widthPixels : 0; 11 | } 12 | 13 | public static int getScreenHeight() { 14 | return sMetrics != null ? sMetrics.heightPixels : 0; 15 | } 16 | 17 | 18 | public static int px2dp(float pxValue) { 19 | final float scale = sMetrics != null ? sMetrics.density : 1; 20 | return (int) (pxValue / scale + 0.5f); 21 | } 22 | 23 | public static int dp2px(float dipValue) { 24 | final float scale = sMetrics != null ? sMetrics.density : 1; 25 | return (int) (dipValue * scale + 0.5f); 26 | } 27 | 28 | 29 | public static int px2sp(float pxValue) { 30 | final float fontScale = sMetrics != null ? sMetrics.scaledDensity : 1; 31 | return (int) (pxValue / fontScale + 0.5f); 32 | } 33 | 34 | public static int sp2px(float spValue) { 35 | final float fontScale = sMetrics != null ? sMetrics.scaledDensity : 1; 36 | return (int) (spValue * fontScale + 0.5f); 37 | } 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/viewpager/AbsViewPagerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.viewpager; 2 | 3 | 4 | import android.support.v4.view.PagerAdapter; 5 | 6 | import com.gaohui.nestedrecyclerview.update.UpdatedNestedRecyclerView; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Copyright (C) 2004 - 2023 Lazada Inc. All Rights Reserved. 12 | * Description : 下沉首页主RecyclerView的一些方法 13 | *

14 | * Created by zukai.kzk@alibaba-inc.com on 2023/11/14 15 | */ 16 | public abstract class AbsViewPagerAdapter extends PagerAdapter { 17 | /** 18 | * NestedRVOnScrollListener使用到 19 | */ 20 | public static int currentPageTab = 0; 21 | 22 | /** 23 | * NestedRecyclerView内部使用到 24 | */ 25 | abstract public UpdatedNestedRecyclerView getCurrentView(); 26 | 27 | abstract public int getCurrentPageTab(); 28 | 29 | /** 30 | * NestedRVOnScrollListener使用到 31 | */ 32 | abstract public void clearAllOffset(); 33 | 34 | /** 35 | * NestedRVOnScrollListener使用到 36 | */ 37 | abstract public List getTabItems(); 38 | 39 | /** 40 | * NestedRVOnScrollListener使用到 41 | */ 42 | abstract public String getTabItem(int tabIndex); 43 | 44 | /** 45 | * NestedRVOnScrollListener使用到 46 | */ 47 | abstract public void saveRecyclerViewState(UpdatedNestedRecyclerView recyclerView, String tabId); 48 | } 49 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 28 9 | defaultConfig { 10 | applicationId "com.gaohui.nestedrecyclerview" 11 | minSdkVersion 16 12 | targetSdkVersion 28 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 28 | implementation 'com.android.support:appcompat-v7:28.0.0' 29 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 30 | testImplementation 'junit:junit:4.12' 31 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 32 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 33 | 34 | implementation 'com.android.support:recyclerview-v7:28.0.0' 35 | implementation 'com.android.support:design:28.0.0' 36 | 37 | // implementation "com.bytedance.tools.codelocator:codelocator-core-support:2.0.0" 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/view/StoreSwipeRefreshLayout.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java.view; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.widget.SwipeRefreshLayout; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | 10 | public class StoreSwipeRefreshLayout extends SwipeRefreshLayout { 11 | private ParentRecyclerView mParentRecyclerView = null; 12 | 13 | public StoreSwipeRefreshLayout(@NonNull Context context) { 14 | super(context); 15 | } 16 | 17 | public StoreSwipeRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | @Override 22 | protected void onAttachedToWindow() { 23 | super.onAttachedToWindow(); 24 | if(mParentRecyclerView == null) { 25 | for(int i =0;i < getChildCount();i++) { 26 | View child = getChildAt(i); 27 | if(child instanceof ParentRecyclerView) { 28 | mParentRecyclerView = (ParentRecyclerView) child; 29 | break; 30 | } 31 | } 32 | } 33 | } 34 | 35 | @Override 36 | public boolean canChildScrollUp() { 37 | return super.canChildScrollUp() || (mParentRecyclerView !=null 38 | && mParentRecyclerView.isChildRecyclerViewCanScrollUp()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_item_category.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 28 | 29 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/utils/FlingHelper.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java.utils; 2 | 3 | import android.content.Context; 4 | import android.view.ViewConfiguration; 5 | 6 | public class FlingHelper { 7 | private static float DECELERATION_RATE = ((float) (Math.log(0.78d) / Math.log(0.9d))); 8 | private static float mFlingFriction = ViewConfiguration.getScrollFriction(); 9 | private static float mPhysicalCoeff; 10 | 11 | public FlingHelper(Context context) { 12 | mPhysicalCoeff = context.getResources().getDisplayMetrics().density * 160.0f * 386.0878f * 0.84f; 13 | } 14 | 15 | private double getSplineDeceleration(int i) { 16 | return Math.log((double) ((0.35f * ((float) Math.abs(i))) / (mFlingFriction * mPhysicalCoeff))); 17 | } 18 | 19 | private double getSplineDecelerationByDistance(double d) { 20 | return ((((double) DECELERATION_RATE) - 1.0d) * Math.log(d / ((double) (mFlingFriction * mPhysicalCoeff)))) / ((double) DECELERATION_RATE); 21 | } 22 | 23 | public double getSplineFlingDistance(int i) { 24 | return Math.exp(getSplineDeceleration(i) * (((double) DECELERATION_RATE) / (((double) DECELERATION_RATE) - 1.0d))) * ((double) (mFlingFriction * mPhysicalCoeff)); 25 | } 26 | 27 | public int getVelocityByDistance(double d) { 28 | return Math.abs((int) (((Math.exp(getSplineDecelerationByDistance(d)) * ((double) mFlingFriction)) * ((double) mPhysicalCoeff)) / 0.3499999940395355d)); 29 | } 30 | 31 | public int getSplineFlingDuration(int i) { 32 | return (int) (Math.exp(getSplineDeceleration(i) / (((double) DECELERATION_RATE) - 1.0d)) * 1000.0d); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/helper/FlingHelper.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin.helper 2 | 3 | import android.content.Context 4 | import android.view.ViewConfiguration 5 | import kotlin.math.abs 6 | import kotlin.math.exp 7 | import kotlin.math.ln 8 | 9 | /** 10 | * Fling 帮助类 11 | */ 12 | class FlingHelper(context: Context) { 13 | 14 | private var mPhysicalCoeff: Float = context.resources.displayMetrics.density * 160.0f * 386.0878f * 0.84f 15 | 16 | private fun getSplineDeceleration(i: Int): Double { 17 | return ln((0.35f * abs(i).toFloat() / (mFlingFriction * mPhysicalCoeff)).toDouble()) 18 | } 19 | 20 | private fun getSplineDecelerationByDistance(d: Double): Double { 21 | return (DECELERATION_RATE.toDouble() - 1.0) * ln(d / (mFlingFriction * mPhysicalCoeff).toDouble()) / DECELERATION_RATE.toDouble() 22 | } 23 | 24 | /** 25 | * 根据加速度来获取需要fling的距离 26 | * @param i 加速度 27 | * @return fling的距离 28 | */ 29 | fun getSplineFlingDistance(i: Int): Double { 30 | return exp(getSplineDeceleration(i) * (DECELERATION_RATE.toDouble() / (DECELERATION_RATE.toDouble() - 1.0))) * (mFlingFriction * mPhysicalCoeff).toDouble() 31 | } 32 | 33 | /** 34 | * 根据距离来获取加速度 35 | * @param d 距离 36 | * @return 返回加速度 37 | */ 38 | fun getVelocityByDistance(d: Double): Int { 39 | return abs((exp(getSplineDecelerationByDistance(d)) * mFlingFriction.toDouble() * mPhysicalCoeff.toDouble() / 0.3499999940395355).toInt()) 40 | } 41 | 42 | companion object { 43 | private val DECELERATION_RATE = (ln(0.78) / ln(0.9)).toFloat() 44 | private val mFlingFriction = ViewConfiguration.getScrollFriction() 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/adapter/CategoryPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin.adapter 2 | 3 | import android.support.v4.view.PagerAdapter 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import com.gaohui.nestedrecyclerview.kotlin.CategoryView 7 | import com.gaohui.nestedrecyclerview.kotlin.tab.DynamicTabBean 8 | import com.gaohui.nestedrecyclerview.kotlin.tab.DynamicTabLayout 9 | 10 | class CategoryPagerAdapter( 11 | private val viewList: ArrayList, 12 | private val tabTitleList: ArrayList 13 | ) : PagerAdapter(), DynamicTabLayout.DynamicTabProvider { 14 | 15 | private var mCurrentPrimaryItem: CategoryView? = null 16 | 17 | override fun getCount(): Int { 18 | return viewList.size 19 | } 20 | 21 | override fun isViewFromObject(view: View, obj: Any): Boolean { 22 | return view === obj 23 | } 24 | 25 | override fun instantiateItem(container: ViewGroup, position: Int): Any { 26 | val view = viewList[position] 27 | if (container == view.parent) { 28 | container.removeView(view) 29 | } 30 | container.addView(view) 31 | return view 32 | } 33 | 34 | override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { 35 | //container.removeView((View) object); 36 | } 37 | 38 | override fun setPrimaryItem(container: ViewGroup, position: Int, obj: Any) { 39 | val item = obj as CategoryView 40 | if(item != mCurrentPrimaryItem) { 41 | mCurrentPrimaryItem?.onUserVisibleChange(false) 42 | } 43 | item.onUserVisibleChange(true) 44 | mCurrentPrimaryItem = item 45 | } 46 | 47 | override fun getPageTitle(position: Int): CharSequence? { 48 | return tabTitleList[position] 49 | } 50 | 51 | override fun getPageTitleItem(position: Int): DynamicTabBean? { 52 | return DynamicTabBean("推荐", "精品推荐") 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/IRecommendTabLayout.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.ColorInt; 5 | import android.support.v4.view.ViewPager; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * @author tangmingjian 14 | * @version v1.0 15 | * @date 2022/10/25 16 | * @description 20221121 jfy tab抽象 17 | */ 18 | public interface IRecommendTabLayout { 19 | //----------------------------------------------------------------- 20 | // View相关方法 start 21 | //----------------------------------------------------------------- 22 | View getView(); 23 | 24 | Context getContext(); 25 | 26 | int getWidth(); 27 | 28 | void setBackgroundColor(@ColorInt int color); 29 | 30 | ViewGroup.LayoutParams getLayoutParams(); 31 | 32 | int getMeasuredHeight(); 33 | 34 | View getChildAt(int index); 35 | 36 | void setLayoutParams(ViewGroup.LayoutParams params); 37 | 38 | void requestLayout(); 39 | //----------------------------------------------------------------- 40 | // View相关方法 end 41 | //----------------------------------------------------------------- 42 | 43 | void setViewPager(ViewPager viewPager); 44 | 45 | void setIndicatorColor(int indicatorColor); 46 | 47 | void setFixTabFlag(boolean fixTabFlag); 48 | 49 | void updateTabStyles(List list); 50 | 51 | void setLineDrawableEnabled(boolean enabled); 52 | 53 | void setJfyAtTop(boolean jfyAtTop); 54 | 55 | View getTabAt(int position); 56 | 57 | int getTabCount(); 58 | 59 | void setOnLayoutListener(OnLayoutListener onLayoutListener); 60 | 61 | interface OnLayoutListener { 62 | void onScrollChanged(int scrollX); 63 | 64 | void onReachStart(); 65 | 66 | void onReachEnd(); 67 | 68 | void onWidthChanged(int width); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/matrix_store_tab_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 25 | 26 | 32 | 33 | 34 | 35 | 48 | 49 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin.ui 2 | 3 | import android.graphics.Color 4 | import android.support.v7.app.AppCompatActivity 5 | import android.os.Bundle 6 | import android.widget.Toast 7 | import com.gaohui.nestedrecyclerview.BaseMenuActivity 8 | import com.gaohui.nestedrecyclerview.R 9 | import com.gaohui.nestedrecyclerview.kotlin.adapter.MultiTypeAdapter 10 | import com.gaohui.nestedrecyclerview.kotlin.bean.CategoryBean 11 | import kotlinx.android.synthetic.main.activity_main.* 12 | import java.util.* 13 | 14 | 15 | class MainActivity : BaseMenuActivity() { 16 | 17 | private val mDataList = ArrayList() 18 | 19 | private val strArray = arrayOf("推荐", "视频", "直播", "图片", "精华", "热门") 20 | 21 | var lastBackPressedTime = 0L 22 | 23 | private val multiTypeAdapter = 24 | MultiTypeAdapter(mDataList) 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | setContentView(R.layout.activity_main) 29 | 30 | kttRecyclerView.initLayoutManager() 31 | kttRecyclerView.adapter = multiTypeAdapter 32 | 33 | refresh() 34 | 35 | swipeRefreshLayout.setColorSchemeColors(Color.RED) 36 | swipeRefreshLayout.setOnRefreshListener { 37 | refresh() 38 | } 39 | 40 | } 41 | 42 | private fun refresh() { 43 | mDataList.clear() 44 | for (i in 0..8) { 45 | mDataList.add("parent item text $i") 46 | } 47 | val categoryBean = CategoryBean() 48 | categoryBean.tabTitleList.clear() 49 | categoryBean.tabTitleList.addAll(strArray.asList()) 50 | mDataList.add(categoryBean) 51 | multiTypeAdapter.notifyDataSetChanged() 52 | swipeRefreshLayout.isRefreshing = false 53 | } 54 | 55 | override fun onBackPressed() { 56 | if (System.currentTimeMillis() - lastBackPressedTime < 2000) { 57 | super.onBackPressed() 58 | } else { 59 | kttRecyclerView.scrollToPosition(0) 60 | Toast.makeText(this,"再按一次退出程序",Toast.LENGTH_SHORT).show() 61 | lastBackPressedTime = System.currentTimeMillis() 62 | } 63 | } 64 | 65 | override fun onDestroy() { 66 | super.onDestroy() 67 | multiTypeAdapter.destroy() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/adapter/CategoryPagerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java.adapter; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.view.PagerAdapter; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | 10 | import com.gaohui.nestedrecyclerview.java.view.CategoryView; 11 | 12 | import java.util.ArrayList; 13 | 14 | public class CategoryPagerAdapter extends PagerAdapter { 15 | 16 | private ArrayList mViewList; 17 | private ArrayList mTabList; 18 | 19 | private CategoryView mCurrentPrimaryItem = null; 20 | 21 | 22 | public CategoryPagerAdapter(ArrayList viewList,ArrayList tabList) { 23 | mViewList = viewList; 24 | mTabList = tabList; 25 | } 26 | 27 | @Override 28 | public int getCount() { 29 | return mViewList.size(); 30 | } 31 | 32 | @NonNull 33 | @Override 34 | public Object instantiateItem(@NonNull ViewGroup container, int position) { 35 | CategoryView categoryView = mViewList.get(position); 36 | if(container == categoryView.getParent()) { 37 | container.removeView(categoryView); 38 | } 39 | container.addView(categoryView); 40 | return categoryView; 41 | } 42 | 43 | @Override 44 | public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { 45 | // super.destroyItem(container, position, object); 46 | } 47 | 48 | @Override 49 | public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { 50 | CategoryView categoryView = (CategoryView) object; 51 | if(categoryView != mCurrentPrimaryItem) { 52 | if(mCurrentPrimaryItem != null) { 53 | mCurrentPrimaryItem.onUserVisibleChange(false); 54 | } 55 | } 56 | categoryView.onUserVisibleChange(true); 57 | mCurrentPrimaryItem = categoryView; 58 | } 59 | 60 | @Override 61 | public boolean isViewFromObject(@NonNull View view, @NonNull Object o) { 62 | return view == o; 63 | } 64 | 65 | @Nullable 66 | @Override 67 | public CharSequence getPageTitle(int position) { 68 | return mTabList.get(position); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/adapter/MultiTypeAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin.adapter 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import com.gaohui.nestedrecyclerview.kotlin.ChildRecyclerView 7 | import com.gaohui.nestedrecyclerview.R 8 | import com.gaohui.nestedrecyclerview.kotlin.bean.CategoryBean 9 | import com.gaohui.nestedrecyclerview.kotlin.holder.SimpleCategoryViewHolder 10 | import com.gaohui.nestedrecyclerview.kotlin.holder.SimpleTextViewHolder 11 | 12 | class MultiTypeAdapter(private val dataSet:ArrayList) : RecyclerView.Adapter() { 13 | 14 | companion object { 15 | private const val TYPE_TEXT = 0 16 | private const val TYPE_CATEGORY = 1 17 | } 18 | 19 | private var mCategoryViewHolder: SimpleCategoryViewHolder? = null 20 | 21 | override fun getItemViewType(position: Int): Int { 22 | return if(dataSet[position] is String) { 23 | TYPE_TEXT 24 | } else { 25 | TYPE_CATEGORY 26 | } 27 | } 28 | 29 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 30 | return if(viewType == TYPE_TEXT) { 31 | SimpleTextViewHolder( 32 | LayoutInflater.from( 33 | viewGroup.context 34 | ).inflate(R.layout.layout_item_text, viewGroup, false) 35 | ) 36 | } else { 37 | val categoryViewHolder = 38 | SimpleCategoryViewHolder( 39 | LayoutInflater.from(viewGroup.context).inflate( 40 | R.layout.layout_item_category, 41 | viewGroup, 42 | false 43 | ) 44 | ) 45 | mCategoryViewHolder = categoryViewHolder 46 | return categoryViewHolder 47 | } 48 | } 49 | 50 | override fun getItemCount(): Int = dataSet.size 51 | 52 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, pos: Int) { 53 | if(holder is SimpleTextViewHolder) { 54 | holder.mTv.text = dataSet[pos] as String 55 | } else if(holder is SimpleCategoryViewHolder){ 56 | holder.bindData(dataSet[pos] as CategoryBean) 57 | } 58 | } 59 | 60 | fun getCurrentChildRecyclerView(): ChildRecyclerView? { 61 | mCategoryViewHolder?.apply { 62 | return this.getCurrentChildRecyclerView() 63 | } 64 | return null 65 | } 66 | 67 | fun destroy() { 68 | mCategoryViewHolder?.destroy() 69 | 70 | } 71 | 72 | 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/CategoryView.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin 2 | 3 | import android.content.Context 4 | import android.support.v7.widget.RecyclerView 5 | import android.support.v7.widget.StaggeredGridLayoutManager 6 | import android.util.AttributeSet 7 | import com.gaohui.nestedrecyclerview.kotlin.adapter.MultiTypeAdapter 8 | 9 | class CategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ChildRecyclerView(context, attrs, defStyleAttr), 10 | OnUserVisibleChange { 11 | 12 | private val mDataList = ArrayList() 13 | 14 | private var hasLoadData = false 15 | 16 | init { 17 | initRecyclerView() 18 | initLoadMore() 19 | } 20 | 21 | private fun initRecyclerView() { 22 | val staggeredGridLayoutManager = StaggeredGridLayoutManager(2,StaggeredGridLayoutManager.VERTICAL) 23 | layoutManager = staggeredGridLayoutManager 24 | adapter = MultiTypeAdapter(mDataList) 25 | } 26 | 27 | private fun initLoadMore() { 28 | addOnScrollListener(object :OnScrollListener(){ 29 | override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { 30 | super.onScrollStateChanged(recyclerView, newState) 31 | tryLoadMoreIfNeed() 32 | } 33 | }) 34 | 35 | } 36 | 37 | private fun tryLoadMoreIfNeed() { 38 | if(adapter == null) return 39 | val layoutManager = layoutManager 40 | val last: IntArray 41 | if (layoutManager is StaggeredGridLayoutManager) { 42 | last = IntArray(layoutManager.spanCount) 43 | layoutManager.findLastVisibleItemPositions(last) 44 | for (i in last.indices) { 45 | if ((last[i] >= adapter!!.itemCount - 4)) { 46 | if (loadMore()) return 47 | break 48 | } 49 | } 50 | } 51 | } 52 | 53 | private fun initData() { 54 | hasLoadData = true 55 | for (i in 0..10) { 56 | mDataList.add("default child item $i") 57 | } 58 | adapter?.notifyDataSetChanged() 59 | } 60 | 61 | private fun loadMore():Boolean { 62 | val loadMoreSize = 5 63 | for (i in 0..loadMoreSize) { 64 | mDataList.add("load more child item $i") 65 | } 66 | adapter?.notifyItemRangeChanged(mDataList.size-loadMoreSize,mDataList.size) 67 | return true 68 | } 69 | 70 | override fun onUserVisibleChange(isVisibleToUser: Boolean) { 71 | if(hasLoadData.not() && isVisibleToUser) { 72 | initData() 73 | } 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/viewholder/UpdatedCategoryViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.viewholder; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.support.v7.widget.StaggeredGridLayoutManager; 6 | import android.util.Log; 7 | import android.view.View; 8 | 9 | 10 | public class UpdatedCategoryViewHolder extends RecyclerView.ViewHolder { 11 | 12 | // private TabLayout mTabLayout; 13 | // private ViewPager mViewPager; 14 | 15 | // private ChildRecyclerView mCurrentRecyclerView; 16 | // 17 | // HashMap cacheVies = new HashMap(); 18 | // 19 | // 20 | // ArrayList viewList = new ArrayList(); 21 | 22 | 23 | public UpdatedCategoryViewHolder(@NonNull View itemView) { 24 | super(itemView); 25 | Log.d("gaohui","11111 category" + itemView.getLayoutParams()); 26 | StaggeredGridLayoutManager.LayoutParams slp; 27 | if ((itemView.getLayoutParams() instanceof StaggeredGridLayoutManager.LayoutParams)) { 28 | slp = (StaggeredGridLayoutManager.LayoutParams)itemView.getLayoutParams(); 29 | slp.setFullSpan(true); 30 | Log.d("gaohui","22222 category"); 31 | } 32 | } 33 | 34 | public void bindData(Object obj) { 35 | // if(obj instanceof CategoryBean) { 36 | // CategoryBean categoryBean = (CategoryBean)obj; 37 | // viewList.clear(); 38 | // if(cacheVies.size() > categoryBean.getTabTitleList().size()) { 39 | // cacheVies.clear(); 40 | // } 41 | // for(String str :categoryBean.getTabTitleList()) { 42 | // CategoryView categoryView = cacheVies.get(str); 43 | // if(categoryView == null || categoryView.getParent() != mViewPager) { 44 | // categoryView = new CategoryView(mViewPager.getContext()); 45 | // cacheVies.put(str, categoryView); 46 | // } 47 | // viewList.add(categoryView); 48 | // } 49 | // mCurrentRecyclerView = viewList.get(mViewPager.getCurrentItem()); 50 | // int lastItem = mViewPager.getCurrentItem(); 51 | // mViewPager.setAdapter(new CategoryPagerAdapter(viewList,categoryBean.getTabTitleList())); 52 | // mTabLayout.setupWithViewPager(mViewPager); 53 | // mViewPager.setCurrentItem(lastItem); 54 | // 55 | // } 56 | } 57 | 58 | public void destroy() { 59 | // cacheVies.clear(); 60 | } 61 | 62 | // public ChildRecyclerView getCurrentChildRecyclerView() { 63 | // return mCurrentRecyclerView; 64 | // } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/BaseMenuActivity.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.support.v7.app.AppCompatActivity 7 | import android.support.v7.widget.AppCompatTextView 8 | import android.util.TypedValue 9 | import android.view.Menu 10 | import android.view.MenuItem 11 | import android.view.View 12 | import android.view.View.* 13 | import android.widget.LinearLayout 14 | import android.widget.TextView 15 | import android.widget.Toast 16 | import com.gaohui.nestedrecyclerview.java.MainJavaActivity 17 | import com.gaohui.nestedrecyclerview.update.MainUpdateActivity 18 | import com.gaohui.nestedrecyclerview.kotlin.ui.MainActivity 19 | 20 | open class BaseMenuActivity : AppCompatActivity() { 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | // fullscreen() 24 | } 25 | 26 | override fun onResume() { 27 | super.onResume() 28 | // fullscreen() 29 | } 30 | 31 | private fun fullscreen() { 32 | window.decorView.systemUiVisibility = ( 33 | window.decorView.systemUiVisibility 34 | or SYSTEM_UI_FLAG_FULLSCREEN 35 | or SYSTEM_UI_FLAG_HIDE_NAVIGATION 36 | or SYSTEM_UI_FLAG_IMMERSIVE_STICKY 37 | ) 38 | } 39 | 40 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 41 | menuInflater.inflate(R.menu.menu, menu) 42 | return true 43 | } 44 | 45 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 46 | when (item.itemId) { 47 | R.id.java -> startActivity(Intent(this, MainJavaActivity::class.java)) 48 | R.id.kotlin -> startActivity(Intent(this, MainActivity::class.java)) 49 | R.id.update -> startActivity(Intent(this, 50 | MainUpdateActivity::class.java)) 51 | } 52 | finish() 53 | return true 54 | } 55 | } 56 | 57 | fun Context.newActionTextView(text: String, onClick: () -> Unit): TextView { 58 | return AppCompatTextView(this) 59 | .apply { 60 | layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) 61 | setRippleBackground() 62 | setPadding(6.dp, 6.dp, 6.dp, 6.dp) 63 | setOnClickListener { 64 | try { 65 | onClick() 66 | } catch (e: Exception) { 67 | Toast.makeText(context, "${e.message}", Toast.LENGTH_SHORT).show() 68 | } 69 | } 70 | setHorizontallyScrolling(true) 71 | setText(text) 72 | } 73 | } 74 | 75 | fun View.setRippleBackground() = with(TypedValue()) { 76 | context.theme.resolveAttribute(android.R.attr.selectableItemBackground, this, true) 77 | setBackgroundResource(resourceId) 78 | } 79 | 80 | val Number.dp get() = this.toInt() * 4 -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/MainJavaActivity.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java; 2 | 3 | import android.graphics.Color; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.widget.SwipeRefreshLayout; 7 | import android.widget.Toast; 8 | 9 | import com.gaohui.nestedrecyclerview.BaseMenuActivity; 10 | import com.gaohui.nestedrecyclerview.R; 11 | import com.gaohui.nestedrecyclerview.java.adapter.MultiTypeAdapter; 12 | import com.gaohui.nestedrecyclerview.java.bean.CategoryBean; 13 | import com.gaohui.nestedrecyclerview.java.view.ParentRecyclerView; 14 | import com.gaohui.nestedrecyclerview.java.view.StoreSwipeRefreshLayout; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | 19 | public class MainJavaActivity extends BaseMenuActivity { 20 | 21 | ArrayList mDataList = new ArrayList(); 22 | 23 | MultiTypeAdapter adapter = new MultiTypeAdapter(mDataList); 24 | 25 | StoreSwipeRefreshLayout storeSwipeRefreshLayout; 26 | ParentRecyclerView javaRecyclerView; 27 | 28 | Long lastBackPressedTime = 0L; 29 | 30 | String[] strArray = new String[]{"推荐", "视频", "直播", "图片", "精华", "热门"}; 31 | 32 | @Override 33 | protected void onCreate(@Nullable Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_main_java); 36 | 37 | javaRecyclerView = findViewById(R.id.javaRecyclerView); 38 | 39 | storeSwipeRefreshLayout = findViewById(R.id.swipeRefreshLayout); 40 | javaRecyclerView.setAdapter(adapter); 41 | 42 | javaRecyclerView.initLayoutManager(this); 43 | 44 | refresh(); 45 | 46 | storeSwipeRefreshLayout.setColorSchemeColors(Color.RED); 47 | storeSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 48 | @Override 49 | public void onRefresh() { 50 | refresh(); 51 | } 52 | }); 53 | 54 | } 55 | 56 | @Override 57 | public void onBackPressed() { 58 | if(System.currentTimeMillis() - lastBackPressedTime < 2000) { 59 | super.onBackPressed(); 60 | } else { 61 | javaRecyclerView.scrollToPosition(0); 62 | Toast.makeText(this,"再按一次退出程序",Toast.LENGTH_SHORT).show(); 63 | lastBackPressedTime = System.currentTimeMillis(); 64 | } 65 | } 66 | 67 | private void refresh() { 68 | mDataList.clear(); 69 | for(int i = 0;i<8;i++) { 70 | mDataList.add("parent item text " + i ); 71 | } 72 | CategoryBean categoryBean = new CategoryBean(); 73 | categoryBean.getTabTitleList().clear(); 74 | categoryBean.getTabTitleList().addAll(Arrays.asList(strArray)); 75 | mDataList.add(categoryBean); 76 | adapter.notifyDataSetChanged(); 77 | storeSwipeRefreshLayout.setRefreshing(false); 78 | } 79 | 80 | @Override 81 | protected void onDestroy() { 82 | super.onDestroy(); 83 | adapter.destroy(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/SimpleStaggeredGridLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.support.v7.widget.StaggeredGridLayoutManager; 6 | import android.util.AttributeSet; 7 | 8 | public class SimpleStaggeredGridLayoutManager extends StaggeredGridLayoutManager { 9 | private UpdatedNestedRecyclerView recyclerView; 10 | 11 | public void setRecyclerView(UpdatedNestedRecyclerView recyclerView) { 12 | this.recyclerView = recyclerView; 13 | } 14 | 15 | public SimpleStaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 16 | super(context, attrs, defStyleAttr, defStyleRes); 17 | } 18 | 19 | public SimpleStaggeredGridLayoutManager(int spanCount, int orientation) { 20 | super(spanCount, orientation); 21 | } 22 | 23 | @Override 24 | public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { 25 | int scroll = 0; 26 | try { 27 | // TODO: there is np exception from emas, but didn't find the root cause 28 | scroll = super.scrollVerticallyBy(dy, recycler, state); 29 | if (scroll == 0) { 30 | if (dy > 0) { 31 | setReachEdge(null, true); 32 | } else if (dy < 0) { 33 | setReachEdge(true, null); 34 | } 35 | } else if (dy != 0) { 36 | setReachEdge(false, false); 37 | } 38 | } catch (Throwable throwable) { 39 | throwable.printStackTrace(); 40 | // Log.e(TAG, "scrollVerticallyBy " + throwable); 41 | } 42 | 43 | return scroll; 44 | } 45 | 46 | @Override 47 | public void scrollToPosition(int position) { 48 | super.scrollToPosition(position); 49 | setScrollToState(position); 50 | } 51 | 52 | @Override 53 | public void scrollToPositionWithOffset(int position, int offset) { 54 | super.scrollToPositionWithOffset(position, offset); 55 | setScrollToState(position); 56 | } 57 | 58 | private void setScrollToState(int targetPos) { 59 | if (targetPos == 0) { 60 | setReachEdge(true, false); 61 | } else if (recyclerView.getAdapter() != null 62 | && targetPos == recyclerView.getAdapter().getItemCount() - 1) { 63 | setReachEdge(false, true); 64 | } else { 65 | setReachEdge(false, false); 66 | } 67 | } 68 | 69 | private void setReachEdge(Boolean isReachTopEdge, Boolean isReachBottomEdge) { 70 | if (recyclerView == null || recyclerView.scrollListener == null) { 71 | return; 72 | } 73 | if (isReachTopEdge != null) { 74 | recyclerView.isReachTopEdge = isReachTopEdge; 75 | } 76 | if (isReachBottomEdge != null) { 77 | recyclerView.isReachBottomEdge = isReachBottomEdge; 78 | } 79 | } 80 | 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/adapter/MultiTypeAdapter.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java.adapter; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.util.Log; 6 | import android.view.LayoutInflater; 7 | import android.view.ViewGroup; 8 | 9 | import com.gaohui.nestedrecyclerview.R; 10 | import com.gaohui.nestedrecyclerview.java.view.ChildRecyclerView; 11 | import com.gaohui.nestedrecyclerview.java.viewholder.SimpleCategoryViewHolder; 12 | import com.gaohui.nestedrecyclerview.java.viewholder.SimpleTextViewHolder; 13 | 14 | import java.util.ArrayList; 15 | 16 | public class MultiTypeAdapter extends RecyclerView.Adapter { 17 | private ArrayList mDataList; 18 | 19 | public static final int TYPE_TEXT = 0; 20 | public static final int TYPE_CATEGORY = 1; 21 | 22 | SimpleCategoryViewHolder mCategoryViewHolder; 23 | public MultiTypeAdapter(ArrayList dataList) { 24 | mDataList = dataList; 25 | } 26 | 27 | @Override 28 | public int getItemViewType(int position) { 29 | if(mDataList.get(position) instanceof String) { 30 | return TYPE_TEXT; 31 | } else { 32 | return TYPE_CATEGORY; 33 | } 34 | } 35 | 36 | @NonNull 37 | @Override 38 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { 39 | if(viewType == TYPE_TEXT) { 40 | return new SimpleTextViewHolder(LayoutInflater.from( 41 | viewGroup.getContext() 42 | ).inflate(R.layout.layout_item_text, viewGroup, false)); 43 | } else { 44 | SimpleCategoryViewHolder simpleCategoryViewHolder = 45 | new SimpleCategoryViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate( 46 | R.layout.layout_item_category_default, 47 | viewGroup, 48 | false 49 | )); 50 | mCategoryViewHolder = simpleCategoryViewHolder; 51 | return simpleCategoryViewHolder; 52 | } 53 | } 54 | 55 | @Override 56 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int pos) { 57 | if(viewHolder instanceof SimpleTextViewHolder) { 58 | // Log.d("gaohui","pos " + pos + mDataList.get(pos) + mDataList.get(pos)); 59 | ( (SimpleTextViewHolder)viewHolder).mTv.setText((String)mDataList.get(pos)); 60 | } else if(viewHolder instanceof SimpleCategoryViewHolder) { 61 | ((SimpleCategoryViewHolder)viewHolder).bindData(mDataList.get(pos)); 62 | } 63 | } 64 | 65 | @Override 66 | public int getItemCount() { 67 | return mDataList.size(); 68 | } 69 | 70 | public ChildRecyclerView getCurrentChildRecyclerView() { 71 | if(mCategoryViewHolder != null) { 72 | return mCategoryViewHolder.getCurrentChildRecyclerView(); 73 | } 74 | return null; 75 | } 76 | 77 | public void destroy() { 78 | if(mCategoryViewHolder != null) { 79 | mCategoryViewHolder.destroy(); 80 | } 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/viewholder/SimpleCategoryViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java.viewholder; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.design.widget.TabLayout; 5 | import android.support.v4.view.ViewPager; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.View; 8 | 9 | import com.gaohui.nestedrecyclerview.R; 10 | import com.gaohui.nestedrecyclerview.java.adapter.CategoryPagerAdapter; 11 | import com.gaohui.nestedrecyclerview.java.bean.CategoryBean; 12 | import com.gaohui.nestedrecyclerview.java.view.CategoryView; 13 | import com.gaohui.nestedrecyclerview.java.view.ChildRecyclerView; 14 | 15 | import java.util.ArrayList; 16 | import java.util.HashMap; 17 | 18 | public class SimpleCategoryViewHolder extends RecyclerView.ViewHolder { 19 | 20 | private TabLayout mTabLayout; 21 | private ViewPager mViewPager; 22 | 23 | private ChildRecyclerView mCurrentRecyclerView; 24 | 25 | HashMap cacheVies = new HashMap(); 26 | 27 | 28 | ArrayList viewList = new ArrayList(); 29 | 30 | 31 | public SimpleCategoryViewHolder(@NonNull View itemView) { 32 | super(itemView); 33 | mTabLayout = itemView.findViewById(R.id.tabs); 34 | mViewPager = itemView.findViewById(R.id.viewPager); 35 | 36 | mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { 37 | @Override 38 | public void onPageScrolled(int i, float v, int i1) { 39 | 40 | } 41 | 42 | @Override 43 | public void onPageSelected(int i) { 44 | if(!viewList.isEmpty()) { 45 | mCurrentRecyclerView = viewList.get(i); 46 | } 47 | 48 | } 49 | 50 | @Override 51 | public void onPageScrollStateChanged(int i) { 52 | 53 | } 54 | }); 55 | } 56 | 57 | public void bindData(Object obj) { 58 | if(obj instanceof CategoryBean) { 59 | CategoryBean categoryBean = (CategoryBean)obj; 60 | viewList.clear(); 61 | if(cacheVies.size() > categoryBean.getTabTitleList().size()) { 62 | cacheVies.clear(); 63 | } 64 | for(String str :categoryBean.getTabTitleList()) { 65 | CategoryView categoryView = cacheVies.get(str); 66 | if(categoryView == null || categoryView.getParent() != mViewPager) { 67 | categoryView = new CategoryView(mViewPager.getContext()); 68 | cacheVies.put(str, categoryView); 69 | } 70 | viewList.add(categoryView); 71 | } 72 | mCurrentRecyclerView = viewList.get(mViewPager.getCurrentItem()); 73 | int lastItem = mViewPager.getCurrentItem(); 74 | mViewPager.setAdapter(new CategoryPagerAdapter(viewList,categoryBean.getTabTitleList())); 75 | mTabLayout.setupWithViewPager(mViewPager); 76 | mViewPager.setCurrentItem(lastItem); 77 | 78 | } 79 | } 80 | 81 | public void destroy() { 82 | cacheVies.clear(); 83 | } 84 | 85 | public ChildRecyclerView getCurrentChildRecyclerView() { 86 | return mCurrentRecyclerView; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/MainUpdateActivity.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update; 2 | 3 | 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.widget.StaggeredGridLayoutManager; 7 | import android.widget.Toast; 8 | 9 | import com.gaohui.nestedrecyclerview.BaseMenuActivity; 10 | import com.gaohui.nestedrecyclerview.R; 11 | //import com.gaohui.nestedrecyclerview.java.adapter.MultiTypeAdapter; 12 | import com.gaohui.nestedrecyclerview.java.bean.CategoryBean; 13 | import com.gaohui.nestedrecyclerview.update.NestedStaggeredGridLayoutManager; 14 | import com.gaohui.nestedrecyclerview.update.UpdatedNestedRecyclerView; 15 | import com.gaohui.nestedrecyclerview.update.adpter.UpdatedMultiTypeAdapter; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | 20 | public class MainUpdateActivity extends BaseMenuActivity { 21 | 22 | ArrayList mDataList = new ArrayList(); 23 | 24 | // MultiTypeAdapter adapter = new MultiTypeAdapter(mDataList); 25 | 26 | UpdatedMultiTypeAdapter adapter = new UpdatedMultiTypeAdapter(mDataList); 27 | 28 | // StoreSwipeRefreshLayout storeSwipeRefreshLayout; 29 | UpdatedNestedRecyclerView updatedNestedRecyclerView; 30 | 31 | Long lastBackPressedTime = 0L; 32 | 33 | String[] strArray = new String[]{"推荐", "视频", "直播", "图片", "精华", "热门"}; 34 | 35 | @Override 36 | protected void onCreate(@Nullable Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | setContentView(R.layout.activity_main_update); 39 | 40 | updatedNestedRecyclerView = findViewById(R.id.recyclerView); 41 | 42 | // storeSwipeRefreshLayout = findViewById(R.id.refresh_layout); 43 | updatedNestedRecyclerView.setAdapter(adapter); 44 | 45 | NestedStaggeredGridLayoutManager mLayoutManager = new NestedStaggeredGridLayoutManager(2, 46 | StaggeredGridLayoutManager.VERTICAL, updatedNestedRecyclerView); 47 | mLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE); 48 | updatedNestedRecyclerView.setLayoutManager(mLayoutManager); 49 | 50 | refresh(); 51 | 52 | // storeSwipeRefreshLayout.setColorSchemeColors(Color.RED); 53 | // storeSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 54 | // @Override 55 | // public void onRefresh() { 56 | // refresh(); 57 | // } 58 | // }); 59 | 60 | } 61 | 62 | @Override 63 | public void onBackPressed() { 64 | if(System.currentTimeMillis() - lastBackPressedTime < 2000) { 65 | super.onBackPressed(); 66 | } else { 67 | updatedNestedRecyclerView.scrollToPosition(0); 68 | Toast.makeText(this,"再按一次退出程序",Toast.LENGTH_SHORT).show(); 69 | lastBackPressedTime = System.currentTimeMillis(); 70 | } 71 | } 72 | 73 | private void refresh() { 74 | mDataList.clear(); 75 | for(int i = 0;i<8;i++) { 76 | mDataList.add("parent item text " + i ); 77 | } 78 | CategoryBean categoryBean = new CategoryBean(); 79 | categoryBean.getTabTitleList().clear(); 80 | categoryBean.getTabTitleList().addAll(Arrays.asList(strArray)); 81 | mDataList.add(categoryBean); 82 | adapter.notifyDataSetChanged(); 83 | // storeSwipeRefreshLayout.setRefreshing(false); 84 | } 85 | 86 | @Override 87 | protected void onDestroy() { 88 | super.onDestroy(); 89 | // adapter.destroy(); 90 | } 91 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## NestedRecyclerView 2 | 3 | [仿淘宝、京东首页,通过两层嵌套的RecyclerView实现tab的吸顶效果](https://juejin.im/post/5d5f4cfcf265da03e61b18b8) 4 | 5 | ### 现已提供Java和Kotlin两种版本,具体可查看项目代码。 6 | 7 | ### 项目效果展示: 8 | 9 | ![](./gif/nested_recyclerview_1.gif) 10 | 11 | ![](./gif/nested_recyclerview_2.gif) 12 | 13 | ### 背景 14 | 我们自己的商城的首页和淘宝、京东首页效果类似,上面为配置数据,中间是各种分类频道,下面是商品流数据,商品流部分支持左右横滑,分类频道是支持吸顶的。 15 | 16 | 最早是用CoordinatorLayout实现,在AppBarLayout下放一个RecyclerView,下面部分则放一个ViewPager,ViewPager里则是商品流的RecyclerView,CoordinatorLayout支持设置他的某个子View吸顶,这样基本能满足需求。 17 | 18 | 在[CoordinatorLayoutFix](https://github.com/JasonGaoH/CoordinatorLayoutFix) 中修复了一些体验问题,基本可以让这种实现满足商业性的项目需求。 19 | 20 | ### 调研 21 | 22 | 在调用了淘宝和京东的实现发现可以通过两层RecyclerView来实现。 23 | 24 | ![image](https://raw.githubusercontent.com/JasonGaoH/Images/master/jingdong_layout.png) 25 | 26 | 可以看到京东首页的实现是上面这样的。 27 | 28 | ### 大致实现方式 29 | 30 | - 外部RecyclerView为ParentRecyclerView,将需要吸顶的悬浮效果的TabLayout和ViewPager作为ParentRecyclerView的一个item。 31 | - 内部RecyclerView为ChildRecyclerView,ParentRecyclerView和ChildRecyclerView相互协调: 32 | - 当ParentRecyclerView滚动到底部的时候,让ChildRecyclerView去滚动。 33 | - 当ChildRecyclerView滚动到顶部的时候,让ParentRecyclerView去滚动。 34 | 35 | #### 利用canScrollVertically这个方法来判断当前View是否滚动到底或者是否滚动到顶。 36 | 37 | ```java 38 | //ParentRecyclerView 39 | private boolean isScrollEnd() { 40 | //RecyclerView.canScrollVertically(1)的值表示是否能向上滚动,false表示已经滚动到底部 41 | return !canScrollVertically(1); 42 | } 43 | 44 | ``` 45 | 46 | ```java 47 | //ChildRecyclerView 48 | boolean isScrollTop() { 49 | //RecyclerView.canScrollVertically(-1)的值表示是否能向下滚动,false表示已经滚动到顶部 50 | return !canScrollVertically(-1); 51 | } 52 | ``` 53 | #### ParentRecyclerView和ChildRecyclerView需要拿到对方的引用,以便能够协调滚动。 54 | 55 | 目前ChildRecyclerView是直接通过往上查找的方式来进行的。而ParentRecyclerView需要通过ViewPager来找到当前显示的是哪一个ChidRecyclerView。 56 | 57 | ```java 58 | //ChildRecyclerView 59 | private ParentRecyclerView findParentRecyclerView() { 60 | ViewParent parentView = getParent(); 61 | while (!(parentView instanceof ParentRecyclerView)) { 62 | parentView = parentView.getParent(); 63 | } 64 | return (ParentRecyclerView)parentView; 65 | } 66 | ``` 67 | 68 | ```java 69 | //ParentRecyclerView 70 | private ChildRecyclerView findNestedScrollingChildRecyclerView() { 71 | if(getAdapter()!= null && (getAdapter() instanceof MultiTypeAdapter)) { 72 | return ((MultiTypeAdapter)getAdapter()).getCurrentChildRecyclerView(); 73 | } 74 | return null; 75 | } 76 | ``` 77 | 78 | #### 处理fling效果 79 | 在RecyclerView的onScrolled记录总的偏移,当ParentRecyclerView滑动到底部的时候,将多余的需要消费的总偏移转换成加速度,从而交给子View去Fling,反之,类型。 80 | 81 | #### 处理嵌套滚动 82 | 如果不处理嵌套滚动,在某些边界场景下,当我们滑动不松手时会出现无法滑动的问题。 83 | 84 | ### 另外 85 | > 新增Tab折叠动画 86 | 87 | ![](https://raw.githubusercontent.com/JasonGaoH/NestedRecyclerView/master/gif/nested_recyclerview_animaion.gif) 88 | 89 | 关于 90 | -- 91 | 92 | 博客:[https://jasongaoh.github.io/](https://jasongaoh.github.io/) 93 | 94 | 邮箱:jasongaohui@gmail.com 95 | 96 | License 97 | -- 98 | Copyright 2018 JasonGaoH 99 | 100 | 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 101 | 102 | http://www.apache.org/licenses/LICENSE-2.0 103 | 104 | 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 105 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/tab/DynamicTabView.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin.tab 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.util.AttributeSet 6 | import android.view.Gravity 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.widget.RelativeLayout 10 | import com.gaohui.nestedrecyclerview.R 11 | import com.gaohui.nestedrecyclerview.UIUtils 12 | import kotlinx.android.synthetic.main.matrix_store_tab_view.view.* 13 | 14 | /** 15 | * DynamicTabView 16 | * 17 | * @property title 18 | * @property descText 19 | * 20 | * @constructor 21 | * @param context 22 | */ 23 | class DynamicTabView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, 24 | defStyleAttr: Int = 0, private val title: String = "", 25 | private val descText: String? = "", private val tabIcon: TabIcon? = null 26 | ) : RelativeLayout(context, attrs, defStyleAttr) { 27 | 28 | val MAX_CHANGE_HEIGHT = UIUtils.dp2px(20f) 29 | 30 | init { 31 | LayoutInflater.from(context).inflate(R.layout.matrix_store_tab_view, this, true) 32 | layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) 33 | setBackgroundColor(Color.TRANSPARENT) 34 | setPadding(0,0,0, UIUtils.dp2px(2f)) 35 | gravity = Gravity.CENTER_HORIZONTAL 36 | } 37 | 38 | /** 39 | * init 40 | */ 41 | fun setup() { 42 | if (isTabIconNullOrEmpty(tabIcon)) { 43 | tabTitle.visibility = View.VISIBLE 44 | tabImage.visibility = View.GONE 45 | tabTitle.text = title 46 | } else { 47 | tabTitle.visibility = View.GONE 48 | tabImage.visibility = View.VISIBLE 49 | //支持Image的自定义 50 | // tabImage.loadImage(tabIcon?.normal?:"") 51 | } 52 | if(descText.isNullOrEmpty()) { 53 | tabDesc.visibility = View.GONE 54 | } else { 55 | tabDesc.visibility = View.VISIBLE 56 | } 57 | 58 | tabDesc.text = descText 59 | divider.setBackgroundColor(Color.parseColor("#999999")) 60 | } 61 | 62 | private fun isTabIconNullOrEmpty(tabIcon: TabIcon?) = 63 | tabIcon == null || tabIcon.normal.isEmpty() 64 | 65 | /** 66 | * set is selected 67 | * 68 | * @param isSelect 69 | */ 70 | fun setIsSelected(isSelect: Boolean) { 71 | if (isSelect) { 72 | if (isTabIconNullOrEmpty(tabIcon)) { 73 | tabTitle.setTextColor(Color.RED) 74 | } 75 | tabDesc.setTextColor(Color.parseColor("#666666")) 76 | tabDesc.setBackgroundResource(R.drawable.dynamic_tab_desc_selected_bg) 77 | } else { 78 | if (isTabIconNullOrEmpty(tabIcon)) { 79 | tabTitle.setTextColor(Color.parseColor("#333333")) 80 | } 81 | tabDesc.setTextColor(Color.parseColor("#666666")) 82 | tabDesc.setBackgroundResource(0) 83 | } 84 | } 85 | 86 | /** 87 | * 改变子标题的的高度(支持动画) 88 | * 89 | * @param progress 90 | */ 91 | fun changeProgress(progress: Float) { 92 | tabDesc.alpha = progress 93 | tabDesc.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { 94 | addRule(BELOW,titleContainer.id) 95 | addRule(CENTER_HORIZONTAL) 96 | topMargin = UIUtils.dp2px(2f) 97 | height = (MAX_CHANGE_HEIGHT * progress).toInt() 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/view/CategoryView.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java.view; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.support.v7.widget.StaggeredGridLayoutManager; 8 | import android.util.AttributeSet; 9 | 10 | import com.gaohui.nestedrecyclerview.java.adapter.MultiTypeAdapter; 11 | 12 | import java.util.ArrayList; 13 | 14 | public class CategoryView extends ChildRecyclerView implements OnUserVisibleChange { 15 | private ArrayList mDataList = new ArrayList<>(); 16 | 17 | boolean hasLoadData = false; 18 | 19 | public CategoryView(@NonNull Context context) { 20 | super(context); 21 | init(); 22 | } 23 | 24 | private void init() { 25 | initRecyclerView(); 26 | initLoadMore(); 27 | } 28 | 29 | private void initLoadMore() { 30 | addOnScrollListener(new OnScrollListener() { 31 | @Override 32 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { 33 | super.onScrollStateChanged(recyclerView, newState); 34 | tryLoadMoreIfNeed(); 35 | } 36 | 37 | 38 | @Override 39 | public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 40 | super.onScrolled(recyclerView, dx, dy); 41 | } 42 | }); 43 | } 44 | 45 | private void tryLoadMoreIfNeed() { 46 | if(getAdapter() == null) return; 47 | LayoutManager layoutManager = getLayoutManager(); 48 | int[] intArray; 49 | if(layoutManager instanceof StaggeredGridLayoutManager) { 50 | intArray = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()]; 51 | ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(intArray); 52 | for (int value : intArray) { 53 | if (value >= getAdapter().getItemCount() - 4) { 54 | if (loadMore()) return; 55 | break; 56 | } 57 | } 58 | } 59 | } 60 | 61 | private boolean loadMore() { 62 | int loadMoreSize = 5; 63 | for(int i =0;i (Math.abs(this@ChildRecyclerView.totalDy))) { 58 | fling(0,-mFlingHelper.getVelocityByDistance(flingDistance + this@ChildRecyclerView.totalDy)) 59 | } 60 | //fix 在run方法里面,注意 this@ChildRecyclerView的使用,否则使用的是ParentRecyclerView的变量 61 | this@ChildRecyclerView.totalDy = 0 62 | mVelocityY = 0 63 | } 64 | } 65 | } 66 | 67 | override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { 68 | if(ev != null && ev.action == MotionEvent.ACTION_DOWN) { 69 | mVelocityY = 0 70 | } 71 | return super.dispatchTouchEvent(ev) 72 | } 73 | 74 | override fun fling(velocityX: Int, velocityY: Int): Boolean { 75 | if(isAttachedToWindow.not()) return false 76 | val fling = super.fling(velocityX, velocityY) 77 | if(!fling || velocityY >= 0) { 78 | //fling为false表示加速度达不到fling的要求,将mVelocityY重置 79 | mVelocityY = 0 80 | } else { 81 | //正在进行fling 82 | isStartFling = true 83 | mVelocityY = velocityY 84 | } 85 | return fling 86 | } 87 | 88 | 89 | fun isScrollTop(): Boolean { 90 | //RecyclerView.canScrollVertically(-1)的值表示是否能向下滚动,false表示已经滚动到顶部 91 | return !canScrollVertically(-1) 92 | } 93 | 94 | private fun findParentRecyclerView(): ParentRecyclerView? { 95 | var parentView = parent 96 | while ((parentView is ParentRecyclerView).not()) { 97 | parentView = parentView.parent 98 | } 99 | return parentView as? ParentRecyclerView 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/view/ChildRecyclerView.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java.view; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.util.AttributeSet; 8 | import android.view.MotionEvent; 9 | import android.view.ViewParent; 10 | 11 | import com.gaohui.nestedrecyclerview.UIUtils; 12 | import com.gaohui.nestedrecyclerview.java.utils.FlingHelper; 13 | 14 | public class ChildRecyclerView extends RecyclerView { 15 | private FlingHelper mFlingHelper; 16 | private int mMaxDistance = 0; 17 | private int mVelocity = 0; 18 | 19 | private boolean isStartFling = false; 20 | private int totalDy = 0; 21 | 22 | private ParentRecyclerView mParentRecyclerView = null; 23 | 24 | public ChildRecyclerView(@NonNull Context context) { 25 | super(context); 26 | init(context); 27 | } 28 | 29 | public ChildRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { 30 | super(context, attrs); 31 | init(context); 32 | } 33 | 34 | public ChildRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { 35 | super(context, attrs, defStyle); 36 | init(context); 37 | } 38 | 39 | private void init(Context context) { 40 | mFlingHelper = new FlingHelper(context); 41 | mMaxDistance = mFlingHelper.getVelocityByDistance((double)(UIUtils.getScreenHeight() * 4)); 42 | setOverScrollMode(OVER_SCROLL_NEVER); 43 | initScrollListener(); 44 | } 45 | 46 | private void initScrollListener() { 47 | addOnScrollListener(new OnScrollListener() { 48 | @Override 49 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { 50 | if(newState == SCROLL_STATE_IDLE) { 51 | dispatchParentFling(); 52 | } 53 | super.onScrollStateChanged(recyclerView, newState); 54 | 55 | } 56 | 57 | @Override 58 | public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 59 | super.onScrolled(recyclerView, dx, dy); 60 | if(isStartFling) { 61 | totalDy = 0; 62 | isStartFling = false; 63 | } 64 | totalDy += dy; 65 | } 66 | }); 67 | } 68 | 69 | private void dispatchParentFling() { 70 | mParentRecyclerView = findParentRecyclerView(); 71 | if(isScrollTop() && mVelocity != 0) { 72 | //当前ChildRecyclerView已经滑动到顶部,且竖直方向加速度不为0,如果有多余的需要交由父RecyclerView继续fling 73 | double flingDistance = mFlingHelper.getSplineFlingDistance(mVelocity); 74 | if(flingDistance > (Math.abs(totalDy))) { 75 | mParentRecyclerView.fling(0,-mFlingHelper.getVelocityByDistance(flingDistance + totalDy)); 76 | } 77 | totalDy = 0; 78 | mVelocity = 0; 79 | } 80 | } 81 | 82 | @Override 83 | public boolean dispatchTouchEvent(MotionEvent ev) { 84 | if(ev != null && ev.getAction() == MotionEvent.ACTION_DOWN) { 85 | mVelocity = 0; 86 | } 87 | return super.dispatchTouchEvent(ev); 88 | } 89 | 90 | @Override 91 | public boolean fling(int velocityX, int velocityY) { 92 | if(!isAttachedToWindow()) return false; 93 | boolean fling = super.fling(velocityX, velocityY); 94 | if(!fling || velocityY >=0) { 95 | mVelocity =0; 96 | } else { 97 | isStartFling = true; 98 | mVelocity = velocityY; 99 | } 100 | return fling; 101 | } 102 | 103 | boolean isScrollTop() { 104 | //RecyclerView.canScrollVertically(-1)的值表示是否能向下滚动,false表示已经滚动到顶部 105 | return !canScrollVertically(-1); 106 | 107 | } 108 | 109 | private ParentRecyclerView parentRecyclerView; 110 | private ParentRecyclerView findParentRecyclerView() { 111 | if(parentRecyclerView == null) { 112 | ViewParent parentView = getParent(); 113 | while (!(parentView instanceof ParentRecyclerView)) { 114 | parentView = parentView.getParent(); 115 | } 116 | parentRecyclerView = (ParentRecyclerView) parentView; 117 | } 118 | return parentRecyclerView; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/holder/SimpleCategoryHolder.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin.holder 2 | 3 | import android.graphics.Color 4 | import android.support.v4.view.ViewPager 5 | import android.support.v7.widget.RecyclerView 6 | import android.view.View 7 | import com.gaohui.nestedrecyclerview.kotlin.CategoryView 8 | import com.gaohui.nestedrecyclerview.kotlin.ChildRecyclerView 9 | import com.gaohui.nestedrecyclerview.R 10 | import com.gaohui.nestedrecyclerview.kotlin.adapter.CategoryPagerAdapter 11 | import com.gaohui.nestedrecyclerview.kotlin.bean.CategoryBean 12 | import com.gaohui.nestedrecyclerview.kotlin.tab.DynamicTabLayout 13 | 14 | class SimpleCategoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 15 | 16 | private val mTabLayout: DynamicTabLayout = itemView.findViewById(R.id.newTabLayout) as DynamicTabLayout 17 | private val mViewPager: ViewPager = itemView.findViewById(R.id.viewPager) as ViewPager 18 | 19 | val viewList = ArrayList() 20 | 21 | var cacheVies = HashMap() 22 | 23 | private var mCurrentRecyclerView : ChildRecyclerView? = null 24 | 25 | private var isTabExpanded = true 26 | 27 | init { 28 | mTabLayout.setSelectedTabIndicatorColor(Color.TRANSPARENT) 29 | mViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener{ 30 | override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { 31 | 32 | } 33 | override fun onPageSelected(position: Int) { 34 | if(viewList.isEmpty().not()) { 35 | mCurrentRecyclerView = viewList[position] 36 | mCurrentRecyclerView?.apply { 37 | addOnScrollListener(object :RecyclerView.OnScrollListener(){ 38 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { 39 | super.onScrolled(recyclerView, dx, dy) 40 | if(dy != 0) { 41 | dealWithChildScrollEvents(this@apply.isScrollTop()) 42 | } 43 | } 44 | }) 45 | } 46 | } 47 | } 48 | override fun onPageScrollStateChanged(state: Int) { 49 | 50 | } 51 | 52 | }) 53 | } 54 | 55 | private fun dealWithChildScrollEvents(scrollTop: Boolean) { 56 | if(isTabExpanded.not() && scrollTop) { 57 | mTabLayout.changeDescHeightWithAnimation(false) 58 | mTabLayout.setSelectedTabIndicatorColor(Color.TRANSPARENT) 59 | isTabExpanded = true 60 | } else if(isTabExpanded && scrollTop.not()) { 61 | mTabLayout.changeDescHeightWithAnimation(true) 62 | mTabLayout.setSelectedTabIndicatorColor(Color.RED) 63 | isTabExpanded = false 64 | } 65 | 66 | } 67 | 68 | fun bindData(categoryBean: CategoryBean) { 69 | categoryBean.apply { 70 | viewList.clear() 71 | if(cacheVies.size > tabTitleList.size) { 72 | cacheVies.clear() 73 | } 74 | tabTitleList.forEach{ 75 | var categoryView = cacheVies[it] 76 | if(categoryView == null || categoryView.parent != mViewPager) { 77 | categoryView = 78 | CategoryView(itemView.context) 79 | cacheVies[it] = categoryView 80 | } 81 | viewList.add(categoryView) 82 | } 83 | mCurrentRecyclerView = viewList[mViewPager.currentItem] 84 | val lastItem = mViewPager.currentItem 85 | 86 | mViewPager.adapter = CategoryPagerAdapter( 87 | viewList, 88 | tabTitleList 89 | ) 90 | mTabLayout.setupWithViewPager(mViewPager) 91 | mViewPager.currentItem = lastItem 92 | //默认bind第一个子RecyclerView的滑动,不然第一个tab不会执行动画 93 | bindDefaultChildRecyclerViewScrolling(viewList[0]) 94 | } 95 | } 96 | 97 | private fun bindDefaultChildRecyclerViewScrolling(categoryView: CategoryView) { 98 | categoryView.apply { 99 | addOnScrollListener(object : RecyclerView.OnScrollListener(){ 100 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { 101 | super.onScrolled(recyclerView, dx, dy) 102 | if(dy != 0) { 103 | dealWithChildScrollEvents(this@apply.isScrollTop()) 104 | } 105 | } 106 | }) 107 | } 108 | } 109 | 110 | fun getCurrentChildRecyclerView(): ChildRecyclerView? { 111 | return mCurrentRecyclerView 112 | } 113 | 114 | fun destroy() { 115 | cacheVies.clear() 116 | } 117 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/listener/TabNestedRVOnScrollListener.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.listener; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v4.view.ViewPager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.LinearLayout; 9 | 10 | import com.gaohui.nestedrecyclerview.update.UpdatedNestedRecyclerView; 11 | import com.gaohui.nestedrecyclerview.update.viewpager.AbsViewPagerAdapter; 12 | import com.gaohui.nestedrecyclerview.update.viewpager.NestedViewPager; 13 | 14 | public class TabNestedRVOnScrollListener extends SimpleNestedRVOnScrollListener { 15 | 16 | private boolean needReset = false; 17 | 18 | @Override 19 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { 20 | super.onScrollStateChanged(recyclerView, newState); 21 | if (recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) { 22 | UpdatedNestedRecyclerView parentRV = null; 23 | if (recyclerView instanceof UpdatedNestedRecyclerView) { 24 | parentRV = ((UpdatedNestedRecyclerView) recyclerView).getParentRecyclerView(); 25 | 26 | // parent是ViewPager才处理! 27 | if (parentRV != null && recyclerView != parentRV && recyclerView.getParent() instanceof ViewPager) { 28 | ViewPager viewPager = (ViewPager) recyclerView.getParent(); 29 | 30 | // 滚动停止时,保存一次位置 31 | if (viewPager.getAdapter() instanceof AbsViewPagerAdapter) { 32 | AbsViewPagerAdapter tabsAdapter = (AbsViewPagerAdapter) viewPager.getAdapter(); 33 | if (tabsAdapter.getTabItems() != null) { 34 | tabsAdapter.saveRecyclerViewState((UpdatedNestedRecyclerView) recyclerView 35 | , tabsAdapter.getTabItems().get(viewPager.getCurrentItem())); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | // @Override 44 | // protected String getBehaviorScrollName(@NonNull RecyclerView recyclerView) { 45 | // if (recyclerView.getParent() instanceof ViewPager) { 46 | // ViewPager viewPager = (ViewPager) recyclerView.getParent(); 47 | // 48 | // if (viewPager.getAdapter() instanceof AbsViewPagerAdapter) { 49 | // AbsViewPagerAdapter tabsAdapter = (AbsViewPagerAdapter) viewPager.getAdapter(); 50 | // int tabPosition = tabsAdapter.getCurrentPageTab(); 51 | // String tabName = null; 52 | // if (tabsAdapter != null) { 53 | // String item = tabsAdapter.getTabItem(tabPosition); 54 | // if (item != null) { 55 | // tabName = item; 56 | // } 57 | // } 58 | // // 为了区分不同的Tab, 需要加上Tab名字, 和iOS对齐实现. 59 | // return SCROLL_EVENT_NAME + "-" + tabName; 60 | // } 61 | // } 62 | // return super.getBehaviorScrollName(recyclerView); 63 | // } 64 | 65 | @Override 66 | protected void onFlingChild(UpdatedNestedRecyclerView parentRV, RecyclerView recyclerView) { 67 | if (parentRV.getAdapter() == null || parentRV.getLayoutManager() == null) { 68 | return; 69 | } 70 | int position = parentRV.getAdapter().getItemCount() - 1; 71 | ViewGroup itemView = null; 72 | if (parentRV.getLayoutManager().findViewByPosition(position) instanceof ViewGroup) { 73 | itemView = (ViewGroup) parentRV.getLayoutManager().findViewByPosition(position); 74 | } 75 | if (itemView == null || itemView.getChildCount() <= 0) { 76 | return; 77 | } 78 | 79 | View view = itemView.getChildAt(1); 80 | if (view instanceof ViewPager && ((ViewPager) view).getAdapter() instanceof AbsViewPagerAdapter) { 81 | UpdatedNestedRecyclerView child = ((AbsViewPagerAdapter) ((ViewPager) view).getAdapter()).getCurrentView(); 82 | if (child != null) { 83 | child.fling(0, (int) (velocityY * 0.5)); 84 | // 用过就清空速度 85 | velocityY = 0; 86 | } 87 | } 88 | } 89 | 90 | @Override 91 | public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 92 | super.onScrolled(recyclerView, dx, dy); 93 | 94 | if (!(recyclerView instanceof UpdatedNestedRecyclerView)) { 95 | return; 96 | } 97 | UpdatedNestedRecyclerView parentRV = ((UpdatedNestedRecyclerView) recyclerView).getParentRecyclerView(); 98 | if (parentRV == null) { 99 | return; 100 | } 101 | boolean isClear = false; 102 | // 父容器向上滚动,所有tab回顶,清空offset 103 | if (recyclerView == parentRV && (dy <= 0 || needReset)) { 104 | if (parentRV.getAdapter() == null) { 105 | return; 106 | } 107 | 108 | RecyclerView.ViewHolder viewHolder = parentRV.findViewHolderForAdapterPosition(parentRV.getAdapter().getItemCount() - 1); 109 | 110 | if (viewHolder != null && viewHolder.itemView instanceof LinearLayout 111 | && ((LinearLayout) viewHolder.itemView).getChildCount() > 1 112 | && ((LinearLayout) viewHolder.itemView).getChildAt(1) instanceof NestedViewPager) { 113 | NestedViewPager viewPager = (NestedViewPager) ((LinearLayout) viewHolder.itemView).getChildAt(1); 114 | if (viewPager != null && viewPager.getAdapter() != null) { 115 | isClear = true; 116 | needReset = false; 117 | ((AbsViewPagerAdapter) viewPager.getAdapter()).clearAllOffset(); 118 | } 119 | } else { 120 | // 这里viewholder已经被释放,所以需要记录状态,下次滚动到底部时重置 121 | needReset = true; 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/NestedStaggeredGridLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.support.v7.widget.StaggeredGridLayoutManager; 7 | import android.util.AttributeSet; 8 | 9 | public class NestedStaggeredGridLayoutManager extends StaggeredGridLayoutManager { 10 | 11 | @NonNull 12 | protected final UpdatedNestedRecyclerView mBindRecyclerView; // 当前layoutManger绑定的recyclerview 13 | 14 | public NestedStaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,@NonNull UpdatedNestedRecyclerView bindRecyclerView) { 15 | super(context, attrs, defStyleAttr, defStyleRes); 16 | mBindRecyclerView = bindRecyclerView; 17 | } 18 | 19 | public NestedStaggeredGridLayoutManager(int spanCount, int orientation,@NonNull UpdatedNestedRecyclerView bindRecyclerView) { 20 | super(spanCount, orientation); 21 | mBindRecyclerView = bindRecyclerView; 22 | } 23 | 24 | @Override 25 | public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { 26 | // if (!isScrollEnabled) { 27 | // return 0; 28 | // } 29 | // 30 | int scroll = 0; 31 | try { 32 | scroll = super.scrollVerticallyBy(dy, recycler, state); 33 | // if (isDisAbleCustomFunc()) { 34 | // return scroll; 35 | // } 36 | } catch (Exception e) { 37 | // sendCustomTrack("page_home", "/lz_home.home.nsgm_scroll_exception", null); 38 | } 39 | 40 | if (scroll == 0) { 41 | if (dy > 0) { 42 | mBindRecyclerView.isReachBottomEdge = true; 43 | mBindRecyclerView.isReachTopEdge = false; 44 | } else if (dy < 0) { 45 | mBindRecyclerView.isReachBottomEdge = false; 46 | mBindRecyclerView.isReachTopEdge = true; 47 | } 48 | if (mBindRecyclerView != null && mBindRecyclerView.scrollListener != null) { 49 | mBindRecyclerView.scrollListener.updatePullState(mBindRecyclerView); 50 | } 51 | } else if (dy != 0) { 52 | mBindRecyclerView.isReachBottomEdge = false; 53 | mBindRecyclerView.isReachTopEdge = false; 54 | } 55 | return scroll; 56 | } 57 | 58 | 59 | protected void childScrollToTop() { 60 | 61 | } 62 | @Override 63 | public void scrollToPosition(int position) { 64 | childScrollToTop(); 65 | setScrollToState(position); 66 | super.scrollToPosition(position); 67 | } 68 | 69 | @Override 70 | public void scrollToPositionWithOffset(int position, int offset) { 71 | childScrollToTop(); 72 | setScrollToState(position); 73 | super.scrollToPositionWithOffset(position, offset); 74 | } 75 | 76 | 77 | @Override 78 | public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { 79 | childScrollToTop(); 80 | setScrollToState(position); 81 | super.smoothScrollToPosition(recyclerView, state, position); 82 | } 83 | 84 | private void setScrollToState(int targetPos) { 85 | if (mBindRecyclerView == null || mBindRecyclerView.scrollListener == null) { 86 | return; 87 | } 88 | 89 | if (targetPos == 0) { 90 | mBindRecyclerView.isReachTopEdge = true; 91 | mBindRecyclerView.isReachBottomEdge = false; 92 | } else if (targetPos == mBindRecyclerView.getAdapter().getItemCount() - 1) { 93 | mBindRecyclerView.isReachBottomEdge = true; 94 | mBindRecyclerView.isReachTopEdge = false; 95 | } else { 96 | mBindRecyclerView.isReachBottomEdge = false; 97 | mBindRecyclerView.isReachTopEdge = false; 98 | } 99 | // if (sDebug) { 100 | // Log.d(TAG, "setScrollToState " + " ,isReachBottomEdge: " + mBindRecyclerView.isReachBottomEdge + " ,isReachTopEdge: " + mBindRecyclerView.isReachTopEdge); 101 | // } 102 | mBindRecyclerView.scrollListener.updatePullState(mBindRecyclerView.getParentRecyclerView()); 103 | } 104 | 105 | @Override 106 | public boolean canScrollVertically() { 107 | // TODO: add this, 禁止滑动 108 | // if (forbidScrollListener != null && forbidScrollListener.forbidScroll()) { 109 | // Log.i(TAG, "do not scroll vertical"); 110 | // return false; 111 | // } 112 | 113 | // super always return true 114 | boolean superBoolean = super.canScrollVertically(); 115 | if (mBindRecyclerView == null/* || isDisAbleCustomFunc()*/) { 116 | return superBoolean; 117 | } 118 | 119 | UpdatedNestedRecyclerView childView = mBindRecyclerView.getLastRecyclerView(); 120 | if (childView == null) { 121 | return superBoolean; 122 | } 123 | 124 | // 手指下移时 最后一个item即猜你喜欢到达顶部且首页未到达顶部 则可以滚动 125 | boolean canScrollUp = (!mBindRecyclerView.isScrollDown && !mBindRecyclerView.isReachTopEdge && childView.isReachTopEdge); 126 | // 手指上移时 首页未到达底部或【首页已到达底部且猜你喜欢也到达底部】 则可以滚动 127 | boolean canScrollDown = (mBindRecyclerView.isScrollDown && !mBindRecyclerView.isReachBottomEdge); 128 | boolean canScroll = superBoolean && (canScrollUp || canScrollDown); 129 | 130 | // if (sDebug) { 131 | // Log.d(TAG, "canScrollVertically canScroll: " + canScroll + " ,isReachBottomEdge: " + mBindRecyclerView.isReachBottomEdge + " ,isReachTopEdge: " 132 | // + mBindRecyclerView.isReachTopEdge + " ,childView.isReachTopEdge: " + childView.isReachTopEdge + " ,childView.isReachBottom: " + childView.isReachBottom()); 133 | // Log.d(TAG, "canScrollVertically superBoolean: " + superBoolean + " ,canScrollUp: " + canScrollUp + " ,canScrollDown: " + canScrollDown); 134 | // } 135 | 136 | return canScroll/* && isScrollEnabled*/; 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/listener/SimpleNestedRVOnScrollListener.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.listener; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.util.Log; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import com.gaohui.nestedrecyclerview.update.UpdatedNestedRecyclerView; 10 | 11 | public class SimpleNestedRVOnScrollListener extends AbsNestedRVOnScrollListener { 12 | public static final String TAG = "Simple.NestedRVOnScroll"; 13 | 14 | public static final String SCROLL_EVENT_NAME = "justForYouContainer"; 15 | 16 | public float velocityY = 0; 17 | 18 | int oldState = RecyclerView.SCROLL_STATE_IDLE; 19 | 20 | private boolean isChecked = false; 21 | 22 | protected int viewPagerHeight = -1; 23 | 24 | 25 | 26 | protected String getBehaviorScrollName(@NonNull RecyclerView recyclerView) { 27 | return SCROLL_EVENT_NAME; 28 | 29 | 30 | } 31 | 32 | protected void onScrollStateChanged(int from, int state) { 33 | // nop 34 | } 35 | 36 | @Override 37 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { 38 | UpdatedNestedRecyclerView parentRV = null; 39 | UpdatedNestedRecyclerView currentRV = null; 40 | if (recyclerView instanceof UpdatedNestedRecyclerView) { 41 | currentRV = (UpdatedNestedRecyclerView) recyclerView; 42 | parentRV = ((UpdatedNestedRecyclerView) recyclerView).getParentRecyclerView(); 43 | } 44 | if (recyclerView == parentRV) { 45 | onScrollStateChanged(IHPScrollListener.SCROLL_FROM_HP, newState); 46 | } else { 47 | onScrollStateChanged(IHPScrollListener.SCROLL_FROM_JFY, newState); 48 | } 49 | 50 | if (recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) { 51 | if (recyclerView.getParent() == null) { 52 | return; 53 | } 54 | 55 | if (parentRV == null) { 56 | return; 57 | } 58 | 59 | // 这里补偿fling传递 60 | if (oldState == RecyclerView.SCROLL_STATE_SETTLING && newState == RecyclerView.SCROLL_STATE_IDLE) { 61 | // 触发一次边界检查 62 | recyclerView.scrollBy(0, ((UpdatedNestedRecyclerView) recyclerView).isScrollDown() ? 1 : -1); 63 | flingWhere(parentRV, currentRV); 64 | } 65 | reInitChildRecyclerViewHeight(recyclerView, parentRV); 66 | } 67 | oldState = newState; 68 | super.onScrollStateChanged(recyclerView, newState); 69 | } 70 | 71 | protected void reInitChildRecyclerViewHeight(RecyclerView recyclerView, RecyclerView parentRV) { 72 | if (recyclerView != parentRV) { 73 | // 避免初始化高度不对,这里要再检查一次。只重复检查一次 74 | if (!isChecked) { 75 | isChecked = true; 76 | ViewGroup parentView = (ViewGroup) recyclerView.getParent().getParent(); 77 | ViewGroup parent = (ViewGroup) recyclerView.getParent(); 78 | int otherHeight = 0; 79 | for (int i = 0; i < parentView.getChildCount(); i++) { 80 | View child = parentView.getChildAt(i); 81 | if (child != parent) { 82 | otherHeight += child == null ? 0 : child.getHeight(); 83 | } 84 | } 85 | if (otherHeight + parent.getHeight() != parentView.getHeight()) { 86 | ViewGroup.LayoutParams layoutParams = parent.getLayoutParams(); 87 | layoutParams.height = parentView.getHeight() - otherHeight; 88 | viewPagerHeight = layoutParams.height; 89 | parent.setLayoutParams(layoutParams); 90 | } 91 | } 92 | } 93 | } 94 | 95 | 96 | private void flingWhere(UpdatedNestedRecyclerView parentRV, UpdatedNestedRecyclerView currentRV) { 97 | updatePullState(parentRV); 98 | // if (SDebug) { 99 | Log.d(TAG, "parentRV : " + parentRV); 100 | Log.d(TAG, "currentRV : " + currentRV); 101 | 102 | Log.d(TAG, "flingWhere parentRV.isReachBottom(): " + parentRV.isReachBottom() + " ,parentRV.isReachTop: " + parentRV.isReachTop() 103 | + " current.isReachTop: " + (currentRV).isReachTop() + " ,velocityY: " + velocityY); 104 | // } 105 | if (parentRV.isReachBottom()) { 106 | if (parentRV == currentRV) { 107 | // 父容器滚到底部了,需要传递fling事件 108 | if (parentRV.getAdapter() == null || parentRV.getLayoutManager() == null) { 109 | return; 110 | } 111 | onFlingChild(parentRV, currentRV); 112 | } else { 113 | onFlingParent(parentRV, currentRV); 114 | } 115 | } else { 116 | onFlingParentWhenChildAppear(parentRV, currentRV); 117 | } 118 | } 119 | 120 | protected void onFlingParent(UpdatedNestedRecyclerView parentRV, UpdatedNestedRecyclerView currentRV) { 121 | // 子recyclerView滚到顶部了,需要传递fling事件到parent 122 | if ((currentRV).isReachTop() && velocityY != 0) { 123 | if (isSmoothScrollToParent()) { 124 | parentRV.isScrollDown = false; // 确保父容器的canScrollVertically能返回true 125 | } 126 | parentRV.fling(0, (int) (velocityY * 0.4)); 127 | 128 | // 用过就清空速度 129 | velocityY = 0; 130 | } 131 | } 132 | 133 | protected void onFlingParentWhenChildAppear(UpdatedNestedRecyclerView parentRV, UpdatedNestedRecyclerView currentRV) { 134 | } 135 | 136 | /** 137 | * 传递fling给child 138 | */ 139 | protected void onFlingChild(UpdatedNestedRecyclerView parentRV, RecyclerView recyclerView) { 140 | // 找到底部的recyclerView 传递一部分滚动事件 141 | UpdatedNestedRecyclerView child = parentRV.getLastRecyclerView(); 142 | Log.d(TAG, "parent scroll to child : " + child + " ,velocityY: " + velocityY); 143 | if (child != null) { 144 | child.fling(0, (int) (velocityY * 0.5)); 145 | // 用过就清空速度 146 | velocityY = 0; 147 | } 148 | } 149 | 150 | /** 151 | * 从JFY往上滑到父容器时,是否滑动到父容器: 152 | * true表示直接滑动到父容器 153 | * false表示先停留在JFY顶部,下次滑动才能滑到父容器 154 | */ 155 | protected boolean isSmoothScrollToParent() { 156 | return true; 157 | } 158 | 159 | @Override 160 | public void setVelocityY(int velocityY) { 161 | this.velocityY = velocityY; 162 | } 163 | 164 | @Override 165 | public void updatePullState(UpdatedNestedRecyclerView parentRV) { 166 | 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/kotlin/ParentRecyclerView.kt: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.kotlin 2 | 3 | import android.content.Context 4 | import android.support.v7.widget.LinearLayoutManager 5 | import android.support.v7.widget.RecyclerView 6 | import android.util.AttributeSet 7 | import android.view.MotionEvent 8 | import android.view.View 9 | import com.gaohui.nestedrecyclerview.UIUtils 10 | import com.gaohui.nestedrecyclerview.kotlin.adapter.MultiTypeAdapter 11 | import com.gaohui.nestedrecyclerview.kotlin.helper.FlingHelper 12 | import java.util.concurrent.atomic.AtomicBoolean 13 | 14 | 15 | class ParentRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : 16 | RecyclerView(context, attrs, defStyleAttr) { 17 | 18 | private var mMaxDistance:Int = 0 19 | 20 | private val mFlingHelper = FlingHelper(context) 21 | /** 22 | * 记录上次Event事件的y坐标 23 | */ 24 | private var lastY:Float = 0f 25 | 26 | var totalDy = 0 27 | /** 28 | * 用于判断RecyclerView是否在fling 29 | */ 30 | var isStartFling = false 31 | /** 32 | * 记录当前滑动的y轴加速度 33 | */ 34 | private var velocityY: Int = 0 35 | 36 | var canScrollVertically: AtomicBoolean 37 | 38 | init { 39 | mMaxDistance = mFlingHelper.getVelocityByDistance((UIUtils.getScreenHeight() * 4).toDouble()) 40 | 41 | canScrollVertically = AtomicBoolean(true) 42 | addOnScrollListener(object :OnScrollListener(){ 43 | override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { 44 | super.onScrollStateChanged(recyclerView, newState) 45 | //如果父RecyclerView fling过程中已经到底部,需要让子RecyclerView滑动神域的fling 46 | if(newState == RecyclerView.SCROLL_STATE_IDLE) { 47 | dispatchChildFling() 48 | } 49 | } 50 | 51 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { 52 | super.onScrolled(recyclerView, dx, dy) 53 | if(isStartFling) { 54 | totalDy = 0 55 | isStartFling = false 56 | } 57 | //在RecyclerView fling情况下,记录当前RecyclerView在y轴的偏移 58 | totalDy += dy 59 | } 60 | }) 61 | } 62 | 63 | private fun dispatchChildFling() { 64 | if (isScrollEnd() && velocityY != 0) { 65 | val splineFlingDistance = mFlingHelper.getSplineFlingDistance(velocityY) 66 | if (splineFlingDistance > totalDy) { 67 | childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - totalDy.toDouble())) 68 | } 69 | } 70 | totalDy = 0 71 | velocityY = 0 72 | } 73 | 74 | private fun childFling(velY: Int) { 75 | findNestedScrollingChildRecyclerView()?.fling(0,velY) 76 | } 77 | 78 | fun initLayoutManager() { 79 | val linearLayoutManager = object :LinearLayoutManager(context) { 80 | override fun scrollVerticallyBy(dy: Int, recycler: Recycler?, state: State?): Int { 81 | return try { 82 | super.scrollVerticallyBy(dy, recycler, state) 83 | } catch (e: Exception) { 84 | e.printStackTrace() 85 | 0 86 | } 87 | } 88 | 89 | override fun onLayoutChildren(recycler: Recycler?, state: State?) { 90 | try { 91 | super.onLayoutChildren(recycler, state) 92 | } catch (e: Exception) { 93 | e.printStackTrace() 94 | } 95 | } 96 | 97 | override fun canScrollVertically(): Boolean { 98 | val childRecyclerView = findNestedScrollingChildRecyclerView() 99 | return canScrollVertically.get() || childRecyclerView == null || childRecyclerView.isScrollTop() 100 | 101 | } 102 | 103 | override fun addDisappearingView(child: View?) { 104 | try { 105 | super.addDisappearingView(child) 106 | } catch (e: Exception) { 107 | e.printStackTrace() 108 | } 109 | } 110 | 111 | override fun supportsPredictiveItemAnimations(): Boolean { 112 | return false 113 | } 114 | } 115 | linearLayoutManager.orientation = LinearLayoutManager.VERTICAL 116 | layoutManager = linearLayoutManager 117 | } 118 | 119 | 120 | override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { 121 | if(ev != null && ev.action == MotionEvent.ACTION_DOWN) { 122 | //ACTION_DOWN的时候重置加速度 123 | velocityY = 0 124 | stopScroll() 125 | } 126 | if((ev == null || ev.action == MotionEvent.ACTION_MOVE).not()) { 127 | //在非ACTION_MOVE的情况下,将lastY置为0 128 | lastY = 0f 129 | } 130 | return try { 131 | super.dispatchTouchEvent(ev) 132 | } catch (e: Exception) { 133 | e.printStackTrace() 134 | false 135 | } 136 | } 137 | 138 | override fun onTouchEvent(e: MotionEvent): Boolean { 139 | if(lastY == 0f) { 140 | lastY = e.y 141 | } 142 | if(isScrollEnd()) { 143 | //如果父RecyclerView已经滑动到底部,需要让子RecyclerView滑动剩余的距离 144 | 145 | val childRecyclerView = findNestedScrollingChildRecyclerView() 146 | childRecyclerView?.run { 147 | val deltaY = (lastY - e.y).toInt() 148 | 149 | canScrollVertically.set(false) 150 | scrollBy(0,deltaY) 151 | } 152 | } 153 | if(e.action == MotionEvent.ACTION_UP) { 154 | canScrollVertically.set(true) 155 | } 156 | lastY = e.y 157 | return try { 158 | super.onTouchEvent(e) 159 | } catch (e: Exception) { 160 | e.printStackTrace() 161 | false 162 | } 163 | } 164 | 165 | override fun fling(velX: Int, velY: Int): Boolean { 166 | val fling = super.fling(velX, velY) 167 | if (!fling || velY <= 0) { 168 | velocityY = 0 169 | } else { 170 | isStartFling = true 171 | velocityY = velY 172 | } 173 | return fling 174 | } 175 | 176 | private fun isScrollEnd(): Boolean { 177 | //RecyclerView.canScrollVertically(1)的值表示是否能向上滚动,false表示已经滚动到底部 178 | return !canScrollVertically(1) 179 | } 180 | 181 | private fun findNestedScrollingChildRecyclerView(): ChildRecyclerView? { 182 | (adapter as? MultiTypeAdapter)?.apply { 183 | return getCurrentChildRecyclerView() 184 | } 185 | return null 186 | } 187 | 188 | 189 | override fun scrollToPosition(position: Int) { 190 | //处理一键置顶会出现卡顿的问题 191 | findNestedScrollingChildRecyclerView()?.scrollToPosition(position) 192 | postDelayed({ 193 | super.scrollToPosition(position) 194 | },50) 195 | } 196 | 197 | //---------------------------------------------------------------------------------------------- 198 | // NestedScroll. fix:当ChildRecyclerView下滑时(手指未放开),ChildRecyclerView滑动到顶部(非fling),此时ParentRecyclerView不会继续下滑。 199 | //---------------------------------------------------------------------------------------------- 200 | 201 | override fun onStartNestedScroll(child: View?, target: View?, nestedScrollAxes: Int): Boolean { 202 | return (target != null) && target is ChildRecyclerView 203 | } 204 | 205 | override fun onNestedPreScroll(target: View?, dx: Int, dy: Int, consumed: IntArray) { 206 | val childRecyclerView = findNestedScrollingChildRecyclerView() 207 | //1.当前Parent RecyclerView没有滑动底,且dy> 0 是下滑 208 | val isParentCanScroll = dy > 0 && isScrollEnd().not() 209 | //2.当前Child RecyclerView滑到顶部了,且dy < 0,即上滑 210 | val isChildCanNotScroll = !(dy >= 0 211 | || childRecyclerView == null 212 | || childRecyclerView.isScrollTop().not()) 213 | //以上两种情况都需要让Parent RecyclerView去scroll,和下面onNestedPreFling机制类似 214 | if(isParentCanScroll || isChildCanNotScroll) { 215 | scrollBy(0,dy) 216 | consumed[1] = dy 217 | } 218 | } 219 | 220 | override fun onNestedFling(target: View?, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean { 221 | return true 222 | } 223 | 224 | override fun onNestedPreFling(target: View?, velocityX: Float, velocityY: Float): Boolean { 225 | val childRecyclerView = findNestedScrollingChildRecyclerView() 226 | val isParentCanFling = velocityY > 0f && isScrollEnd().not() 227 | val isChildCanNotFling = !(velocityY >= 0 228 | || childRecyclerView == null 229 | || childRecyclerView.isScrollTop().not()) 230 | if(isParentCanFling.not() && isChildCanNotFling.not()) { 231 | return false 232 | } 233 | fling(0,velocityY.toInt()) 234 | return true 235 | } 236 | 237 | fun isChildRecyclerViewCanScrollUp(): Boolean { 238 | return findNestedScrollingChildRecyclerView()?.isScrollTop()?.not() ?: false 239 | } 240 | 241 | //---------------------------------------------------------------------------------------------- 242 | // NestedScroll. fix:当ChildRecyclerView下滑时(手指未放开),ChildRecyclerView滑动到顶部(非fling),此时ParentRecyclerView不会继续下滑。 243 | //---------------------------------------------------------------------------------------------- 244 | 245 | } -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/UpdatedNestedRecyclerView.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.view.ViewPager; 7 | import android.support.v7.widget.LinearLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.support.v7.widget.StaggeredGridLayoutManager; 10 | import android.util.AttributeSet; 11 | import android.util.Log; 12 | import android.view.MotionEvent; 13 | import android.view.View; 14 | import android.widget.LinearLayout; 15 | 16 | import com.gaohui.nestedrecyclerview.update.listener.AbsNestedRVOnScrollListener; 17 | import com.gaohui.nestedrecyclerview.update.listener.SimpleNestedRVOnScrollListener; 18 | import com.gaohui.nestedrecyclerview.update.viewpager.AbsViewPagerAdapter; 19 | 20 | import java.lang.ref.WeakReference; 21 | 22 | public class UpdatedNestedRecyclerView extends RecyclerView { 23 | 24 | private static final String TAG = "NestedRecyclerView"; 25 | 26 | 27 | public AbsNestedRVOnScrollListener scrollListener = new SimpleNestedRVOnScrollListener(); 28 | 29 | private WeakReference parentReference = new WeakReference<>(this); // 注意要判断不等于自己,否则容易进入死循环 30 | 31 | private WeakReference childReference = null; // 注意判空,默认没有子类 32 | 33 | private float lastDownY = -1; 34 | 35 | public boolean isScrollDown = true; 36 | 37 | public boolean oldScrollOrientation = true; 38 | 39 | private static boolean sIsInterceptNextClick = false; 40 | 41 | public boolean isReachBottomEdge = false; 42 | public boolean isReachTopEdge = true; 43 | 44 | 45 | public UpdatedNestedRecyclerView(@NonNull Context context) { 46 | this(context,null); 47 | } 48 | 49 | public UpdatedNestedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { 50 | super(context, attrs); 51 | init(); 52 | } 53 | 54 | protected void init() { 55 | // listener放到设置时绑定 56 | setOnScrollListener(scrollListener); 57 | } 58 | 59 | public void initEnv(AbsNestedRVOnScrollListener listener) { 60 | setOnScrollListener(listener); 61 | } 62 | 63 | 64 | private void setOnScrollListener(AbsNestedRVOnScrollListener listener) { 65 | if (scrollListener != null) { 66 | removeOnScrollListener(scrollListener); 67 | } 68 | scrollListener = listener; 69 | addOnScrollListener(scrollListener); 70 | } 71 | 72 | public void updateParentRecyclerView(UpdatedNestedRecyclerView recyclerView) { 73 | parentReference = new WeakReference<>(recyclerView); 74 | } 75 | 76 | public UpdatedNestedRecyclerView getParentRecyclerView() { 77 | return parentReference == null ? null : parentReference.get(); 78 | } 79 | 80 | public UpdatedNestedRecyclerView getChildRecyclerView() { 81 | return childReference == null ? null : childReference.get(); 82 | } 83 | 84 | public boolean isScrollDown() { 85 | if (lastDownY >= 0) { 86 | return isScrollDown; 87 | } else { 88 | return oldScrollOrientation; 89 | } 90 | } 91 | 92 | @Override 93 | public boolean onInterceptTouchEvent(MotionEvent e) { 94 | int event = e.getActionMasked(); 95 | if (event == MotionEvent.ACTION_DOWN) { 96 | lastDownY = e.getY(); 97 | } else if (event == MotionEvent.ACTION_MOVE) { 98 | if (lastDownY < 0) { 99 | lastDownY = e.getY(); 100 | } else { 101 | isScrollDown = (e.getY() - lastDownY) <= 0; 102 | } 103 | } else if (event == MotionEvent.ACTION_UP || event == MotionEvent.ACTION_CANCEL) { 104 | if (lastDownY >= 0) { 105 | oldScrollOrientation = isScrollDown; 106 | } 107 | lastDownY = -1; 108 | isScrollDown = true; 109 | } 110 | 111 | boolean result = super.onInterceptTouchEvent(e); 112 | Log.d(TAG, "result=" + result + ", onInterceptTouchEvent, ev=" + e); 113 | return result; 114 | } 115 | 116 | private static boolean checkNeedInterceptClick(int event) { 117 | return sIsInterceptNextClick && event == MotionEvent.ACTION_UP; 118 | } 119 | 120 | @Override 121 | public boolean onTouchEvent(MotionEvent e) { 122 | int event = e.getActionMasked(); 123 | if (event == MotionEvent.ACTION_DOWN) { 124 | lastDownY = e.getY(); 125 | sIsInterceptNextClick = false; 126 | } else if (event == MotionEvent.ACTION_MOVE) { 127 | if (lastDownY < 0) { 128 | lastDownY = e.getY(); 129 | } else { 130 | isScrollDown = (e.getY() - lastDownY) <= 0; 131 | } 132 | } else if (event == MotionEvent.ACTION_UP || event == MotionEvent.ACTION_CANCEL) { 133 | if (lastDownY >= 0) { 134 | oldScrollOrientation = isScrollDown; 135 | } 136 | lastDownY = -1; 137 | isScrollDown = true; 138 | 139 | if (checkNeedInterceptClick(event)) { 140 | sIsInterceptNextClick = false; 141 | if (isParent()) { //只做一边效果,防止动画导致奇怪的Fling 142 | e.setAction(MotionEvent.ACTION_CANCEL); 143 | } 144 | } 145 | } 146 | 147 | boolean oldReachBottomState = this.isReachBottom(); 148 | boolean oldReachTopState = this.isReachTop(); 149 | 150 | boolean result = super.onTouchEvent(e); 151 | 152 | if (event == MotionEvent.ACTION_MOVE) { 153 | boolean parentReachBottom = this.isParent() && this.isScrollDown && !oldReachBottomState && this.isReachBottom(); 154 | boolean childReachTop = !this.isParent() && !this.isScrollDown && !oldReachTopState && this.isReachTop(); 155 | if (childReachTop || parentReachBottom) { 156 | reDispatchDownEvent(e); 157 | } 158 | } 159 | 160 | // if (IHPPullRefresh.ENABLE_TOUCH_DEBUG) { 161 | // LLog.d(IHPPullRefresh.TOUCH_TAG, "result=" + result + ", onTouchEvent, ev=" + e); 162 | // } 163 | return result; 164 | } 165 | 166 | private boolean isParent() { 167 | return this.getParentRecyclerView() == this; 168 | } 169 | 170 | 171 | public boolean isReachTop() { 172 | return isReachTopEdge; 173 | } 174 | 175 | public boolean isReachBottom() { 176 | return isReachBottomEdge; 177 | } 178 | 179 | protected void reDispatchDownEvent(MotionEvent motionEvent) { 180 | View parent = getParentRecyclerView(); 181 | if (parent == null) { 182 | return; 183 | } 184 | int oldAction = motionEvent.getActionMasked(); 185 | motionEvent.setAction(MotionEvent.ACTION_CANCEL); 186 | parent.dispatchTouchEvent(motionEvent); 187 | motionEvent.setAction(MotionEvent.ACTION_DOWN); 188 | parent.dispatchTouchEvent(motionEvent); 189 | sIsInterceptNextClick = true; 190 | motionEvent.setAction(oldAction); 191 | } 192 | 193 | @Override 194 | public boolean fling(final int velocityX, final int velocityY) { 195 | if (scrollListener == null) { 196 | throw new IllegalArgumentException("## Fetal Error: need initEnv for the NestedRVOnScrollListener..."); 197 | } 198 | if (lastDownY < 0) { 199 | if (velocityY > 0) { 200 | isScrollDown = true; 201 | oldScrollOrientation = true; 202 | } else if (velocityY < 0) { 203 | isScrollDown = false; 204 | oldScrollOrientation = false; 205 | } 206 | } 207 | 208 | scrollListener.setVelocityY(velocityY); 209 | return super.fling(velocityX, velocityY); 210 | } 211 | /** 212 | * 获取最后一个item是recyclerView的 如果是且完全可见则说明父RecyclerView不能再滚动了,需要留给子RecyclerView滚动 213 | * 214 | * @return 215 | */ 216 | @Nullable 217 | public UpdatedNestedRecyclerView getLastRecyclerView() { 218 | View view = getLastItem(); 219 | 220 | if (view != null && view instanceof LinearLayout && ((LinearLayout) view).getChildAt(1) instanceof ViewPager) { 221 | ViewPager pager = (ViewPager) ((LinearLayout) view).getChildAt(1); 222 | if (pager != null && pager.getAdapter() instanceof AbsViewPagerAdapter) { 223 | return ((AbsViewPagerAdapter) pager.getAdapter()).getCurrentView(); 224 | } 225 | } 226 | return null; 227 | } 228 | 229 | protected View getLastItem() { 230 | LayoutManager layoutManager = getLayoutManager(); 231 | int position = -1; 232 | if (layoutManager instanceof StaggeredGridLayoutManager) { 233 | StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager; 234 | int[] lastPosition = staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(null); 235 | position = lastPosition[0] > 0 ? lastPosition[0] : lastPosition[1]; 236 | if (position == -1) { 237 | // 超过一屏高度的item,不识别 238 | lastPosition = staggeredGridLayoutManager.findLastVisibleItemPositions(null); 239 | position = lastPosition[0] > 0 ? lastPosition[0] : lastPosition[1]; 240 | } 241 | } else if (layoutManager instanceof LinearLayoutManager) { 242 | LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; 243 | position = linearLayoutManager.findLastCompletelyVisibleItemPosition(); 244 | if (position == -1) { 245 | // 超过一屏高度的item,不识别 246 | position = linearLayoutManager.findLastVisibleItemPosition(); 247 | } 248 | } 249 | if (position < 0) { 250 | return null; 251 | } 252 | 253 | ViewHolder holder = findViewHolderForLayoutPosition(position); 254 | return holder == null ? null : holder.itemView; 255 | } 256 | 257 | 258 | 259 | } 260 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/adpter/UpdatedMultiTypeAdapter.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.adpter; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.support.annotation.NonNull; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.support.v7.widget.StaggeredGridLayoutManager; 8 | import android.util.Log; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.FrameLayout; 13 | import android.widget.LinearLayout; 14 | import android.widget.Toast; 15 | 16 | import com.gaohui.nestedrecyclerview.R; 17 | import com.gaohui.nestedrecyclerview.update.IRecommendTabLayout; 18 | import com.gaohui.nestedrecyclerview.update.SlidingTabLayoutRevamp; 19 | import com.gaohui.nestedrecyclerview.update.UpdatedNestedRecyclerView; 20 | import com.gaohui.nestedrecyclerview.update.viewholder.UpdatedCategoryViewHolder; 21 | import com.gaohui.nestedrecyclerview.update.viewholder.UpdatedTextViewHolder; 22 | import com.gaohui.nestedrecyclerview.update.viewpager.NestedViewPager; 23 | import com.gaohui.nestedrecyclerview.update.viewpager.ViewPagerAdapter; 24 | import com.gaohui.nestedrecyclerview.update.utils.ScreenUtils; 25 | 26 | import java.lang.ref.WeakReference; 27 | import java.util.ArrayList; 28 | import java.util.Arrays; 29 | 30 | public class UpdatedMultiTypeAdapter extends RecyclerView.Adapter { 31 | private ArrayList mDataList; 32 | 33 | public static final int TYPE_TEXT = 0; 34 | public static final int TYPE_CATEGORY = 1; 35 | 36 | public static final int RECOMMEND_TAB_HEIGHT = 68; 37 | 38 | private static WeakReference sViewPagerRef; 39 | 40 | 41 | 42 | // UpdatedCategoryViewHolder mCategoryViewHolder; 43 | public UpdatedMultiTypeAdapter(ArrayList dataList) { 44 | mDataList = dataList; 45 | } 46 | 47 | @Override 48 | public int getItemViewType(int position) { 49 | if(mDataList.get(position) instanceof String) { 50 | return TYPE_TEXT; 51 | } else { 52 | return TYPE_CATEGORY; 53 | } 54 | } 55 | 56 | @NonNull 57 | @Override 58 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { 59 | if(viewType == TYPE_TEXT) { 60 | return new UpdatedTextViewHolder(LayoutInflater.from( 61 | viewGroup.getContext() 62 | ).inflate(R.layout.layout_item_text, viewGroup, false)); 63 | } else { 64 | UpdatedCategoryViewHolder simpleCategoryViewHolder = 65 | new UpdatedCategoryViewHolder(createView(viewGroup,viewGroup.getContext())); 66 | // mCategoryViewHolder = simpleCategoryViewHolder; 67 | return simpleCategoryViewHolder; 68 | } 69 | } 70 | 71 | String[] strArray = new String[]{"推荐", "视频", "直播", "图片", "精华", "热门"}; 72 | 73 | private View createView(View parent, @NonNull Context context) { 74 | final int targetHeight = parent == null ? context.getResources().getDisplayMetrics().heightPixels : parent.getHeight(); 75 | final LinearLayout layout = new LinearLayout(context); 76 | layout.setOrientation(LinearLayout.VERTICAL); 77 | layout.setLayoutParams(new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 78 | 79 | // final RecommendRepo recommendRepo = getRepo(); 80 | final IRecommendTabLayout slidingTabLayoutRevamp; 81 | final int tabHeight; 82 | // if (LazDataPools.getInstance().isHomeVersionV7()) { 83 | // tabHeight = ScreenUtils.dp2px(context, RECOMMEND_TAB_HEIGHT_NEW); 84 | // slidingTabLayoutRevamp = new RecommendTabLayout(context); 85 | // } else { 86 | tabHeight = ScreenUtils.dp2px(context, RECOMMEND_TAB_HEIGHT); 87 | slidingTabLayoutRevamp = new SlidingTabLayoutRevamp(context); 88 | // } 89 | 90 | FrameLayout frameLayout = new FrameLayout(context); 91 | layout.addView(frameLayout, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, tabHeight)); 92 | 93 | slidingTabLayoutRevamp.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 94 | ViewGroup.LayoutParams.MATCH_PARENT)); 95 | frameLayout.addView((slidingTabLayoutRevamp.getView())); 96 | 97 | // add ViewPager 98 | final NestedViewPager viewPager = new NestedViewPager(context); 99 | viewPager.setId(R.id.id_guess_view_pager); 100 | viewPager.setOverScrollMode(View.OVER_SCROLL_NEVER); 101 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 102 | viewPager.setNestedScrollingEnabled(false); 103 | } 104 | // if (LazDataPools.getInstance().isHomeVersionV7() 105 | // && !LazDataPools.getInstance().isHideJfyTab()) { 106 | // viewPager.setBackground(new RecommendContainerDrawable(context)); 107 | // } else { 108 | // viewPager.setBackgroundColor(Color.parseColor(GREY_BG_F0F1F6)); 109 | // } 110 | viewPager.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, targetHeight - tabHeight)); 111 | 112 | final ViewPagerAdapter adapter = new ViewPagerAdapter(context, viewPager); 113 | adapter.setTabItems(Arrays.asList(strArray)); 114 | // adapter.setRecommendRepo(recommendRepo); 115 | 116 | if (parent instanceof UpdatedNestedRecyclerView) { 117 | adapter.setParentRecyclerView((UpdatedNestedRecyclerView) parent); 118 | } 119 | viewPager.setAdapter(adapter); 120 | 121 | sViewPagerRef = new WeakReference<>(viewPager); 122 | slidingTabLayoutRevamp.setViewPager(viewPager); 123 | 124 | // get tab resource firstly 125 | // final IRecommendTabResource tabResource = recommendRepo.getRecommendTabs(); 126 | // List list = tabResource.getTabItems(); 127 | // if (list != null) { 128 | // LLog.i(TAG, "create container after get tab resource"); 129 | //// List oldList = new ArrayList<>(); 130 | //// oldList.addAll(adapter.getTabItems()); 131 | // List oldList = new ArrayList<>(adapter.getTabItems()); 132 | // if (isDifferent(oldList, list)) { 133 | // adapter.setTabItems(list); 134 | // slidingTabLayoutRevamp.setIndicatorColor(tabResource.getIndicatorColor()); 135 | // slidingTabLayoutRevamp.setFixTabFlag(tabResource.isFixedTab()); 136 | // slidingTabLayoutRevamp.setViewPager(viewPager); 137 | // 138 | // if(!oldList.isEmpty()) { 139 | // adapter.resetState(); 140 | // } else { 141 | // viewPager.setCurrentItem(adapter.defaultPage); 142 | // } 143 | // } else { 144 | // slidingTabLayoutRevamp.setIndicatorColor(tabResource.getIndicatorColor()); 145 | // slidingTabLayoutRevamp.setFixTabFlag(tabResource.isFixedTab()); 146 | // slidingTabLayoutRevamp.updateTabStyles(list); 147 | // } 148 | // 149 | // // update the relationship of tabs 150 | // adapter.updateTabInfos(list); 151 | // } 152 | 153 | // tab data refresh listener 154 | // if (BaseUtils.isMemoryOpt(context) ) { 155 | // Log.e(TAG, "createRecommendContainer: opt!!!!"); 156 | // tabResource.setTabResourceListener(new TabResourceListenerImpl(adapter, slidingTabLayoutRevamp, viewPager)); 157 | // } else { 158 | // Log.e(TAG, "createRecommendContainer: not opt!!!!"); 159 | // tabResource.setTabResourceListener(new IRecommendTabResource.TabResourceListener() { 160 | // @Override 161 | // public void notifyTabResource(List list) { 162 | // //LLog.i(TAG, "response ---> tab resource success list is: " + list); 163 | // 164 | //// List oldList = new ArrayList<>(); 165 | //// oldList.addAll(adapter.getTabItems()); 166 | // List oldList = new ArrayList<>(adapter.getTabItems()); 167 | // if ( isDifferent(oldList, list) ) { 168 | // adapter.setTabItems(list); 169 | // slidingTabLayoutRevamp.setIndicatorColor(tabResource.getIndicatorColor()); 170 | // slidingTabLayoutRevamp.setFixTabFlag(tabResource.isFixedTab()); 171 | // slidingTabLayoutRevamp.setViewPager(viewPager); 172 | // if ( !oldList.isEmpty() ) { 173 | // adapter.resetState(); 174 | // } else { 175 | // viewPager.setCurrentItem(adapter.defaultPage); 176 | // } 177 | // } else { 178 | // slidingTabLayoutRevamp.setIndicatorColor(tabResource.getIndicatorColor()); 179 | // slidingTabLayoutRevamp.setFixTabFlag(tabResource.isFixedTab()); 180 | // slidingTabLayoutRevamp.updateTabStyles(list); 181 | // } 182 | // 183 | // if ( rmdCacheListener != null ) { 184 | // rmdCacheListener.getTabData(); 185 | // } 186 | // 187 | // // update the relationship of tabs 188 | // adapter.updateTabInfos(list); 189 | // } 190 | // }); 191 | // } 192 | 193 | layout.addView(viewPager); 194 | return layout; 195 | } 196 | 197 | @Override 198 | public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, int pos) { 199 | if(viewHolder instanceof UpdatedTextViewHolder) { 200 | Log.d("gaohui","pos " + pos + mDataList.get(pos) + mDataList.get(pos)); 201 | ( (UpdatedTextViewHolder)viewHolder).mTv.setText((String)mDataList.get(pos)); 202 | final String text = (String) mDataList.get(pos); 203 | viewHolder.itemView.setOnClickListener(new View.OnClickListener() { 204 | @Override 205 | public void onClick(View v) { 206 | Toast.makeText(viewHolder.itemView.getContext(),text + "Clicked",Toast.LENGTH_SHORT).show(); 207 | } 208 | }); 209 | } else if(viewHolder instanceof UpdatedCategoryViewHolder) { 210 | ((UpdatedCategoryViewHolder)viewHolder).bindData(mDataList.get(pos)); 211 | } 212 | } 213 | 214 | @Override 215 | public int getItemCount() { 216 | return mDataList.size(); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/viewpager/ViewPagerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update.viewpager; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.v4.view.ViewPager; 6 | import android.support.v7.widget.StaggeredGridLayoutManager; 7 | import android.util.SparseArray; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import com.gaohui.nestedrecyclerview.update.RecommendStaggeredGridLayoutManager; 12 | import com.gaohui.nestedrecyclerview.update.UpdatedNestedRecyclerView; 13 | import com.gaohui.nestedrecyclerview.update.adpter.UpdatedMultiTypeAdapter; 14 | import com.gaohui.nestedrecyclerview.update.listener.NestedRVOnScrollListener; 15 | 16 | import java.lang.ref.WeakReference; 17 | import java.util.ArrayList; 18 | import java.util.HashMap; 19 | import java.util.LinkedList; 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | public class ViewPagerAdapter extends AbsViewPagerAdapter { 24 | 25 | private SparseArray pages = new SparseArray<>(3); 26 | private LinkedList positionList = new LinkedList<>(); 27 | private Map realPosition = new HashMap<>(3); 28 | public int defaultPage = 0; 29 | 30 | private Map scrollToPosition = new HashMap<>(32); 31 | private Map offsets = new HashMap<>(32); 32 | 33 | private Context context; 34 | 35 | // private RecommendRepo recommendRepo; 36 | 37 | private ViewPager viewPager; 38 | 39 | private WeakReference parentReference; 40 | 41 | public List tabItems = new ArrayList<>(); 42 | 43 | private int startPosition = -1; 44 | private int currentPosition = -1; 45 | 46 | public ViewPagerAdapter(Context context, ViewPager pager) { 47 | this.context = context; 48 | viewPager = pager; 49 | viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { 50 | @Override 51 | public void onPageScrolled(int i, float v, int i1) { 52 | 53 | } 54 | 55 | @Override 56 | public void onPageSelected(int i) { 57 | currentPageTab = i; 58 | } 59 | 60 | @Override 61 | public void onPageScrollStateChanged(int i) { 62 | 63 | } 64 | }); 65 | } 66 | 67 | public synchronized void setTabItems(List tabList) { 68 | if (tabList != null) { 69 | tabItems = tabList; 70 | // select default tab and the default one index is 0 71 | defaultPage = 0; 72 | notifyDataSetChanged(); 73 | 74 | // add hp load track 75 | // Map argsMap = new HashMap<>(); 76 | // argsMap.put(SPMConstants.HOME_UT_PARAMS_JFY_DATA_FROM, LazDataPools.getInstance().getJfyRenderSourceType()); 77 | // HpLoadTrackManager.getInstance().sendTrackEventWithDuration(SPMConstants.HOME_UT_EVENT_JFY_RENDER_FINISH, 78 | // SPMConstants.HOME_UT_EVENT_JFY_CONTAINER_FIRST_DISPLAY, argsMap, true); 79 | } 80 | } 81 | 82 | public void setParentRecyclerView(UpdatedNestedRecyclerView parent) { 83 | parentReference = new WeakReference<>(parent); 84 | } 85 | 86 | @Override 87 | public UpdatedNestedRecyclerView getCurrentView() { 88 | return (viewPager == null) ? null : pages.get(getCommonIndex(viewPager.getCurrentItem())); 89 | 90 | } 91 | 92 | @Override 93 | public int getCurrentPageTab() { 94 | return currentPageTab; 95 | } 96 | 97 | @Override 98 | public void clearAllOffset() { 99 | scrollToPosition.clear(); 100 | offsets.clear(); 101 | for (UpdatedNestedRecyclerView recyclerView : realPosition.keySet()) { 102 | if (recyclerView != null) { 103 | recyclerView.scrollToPosition(0); 104 | } 105 | } 106 | } 107 | 108 | @Override 109 | public List getTabItems() { 110 | return tabItems; 111 | } 112 | 113 | @Override 114 | public String getTabItem(int tabIndex) { 115 | if (tabItems == null || tabIndex < 0 || tabIndex >= tabItems.size()) { 116 | return null; 117 | } 118 | return tabItems.get(tabIndex); 119 | } 120 | 121 | @Override 122 | public void saveRecyclerViewState(UpdatedNestedRecyclerView recyclerView, String tabId) { 123 | 124 | } 125 | 126 | @Override 127 | public int getCount() { 128 | return tabItems.size(); 129 | } 130 | 131 | private int getCommonIndex(int position) { 132 | return position % 5; 133 | } 134 | @NonNull 135 | @Override 136 | public Object instantiateItem(@NonNull ViewGroup container, int position) { 137 | final String tabId = tabItems.get(position); 138 | int commonIndex = getCommonIndex(position); 139 | UpdatedNestedRecyclerView recyclerView = pages.get(getCommonIndex(position)); 140 | final Integer posObj = Integer.valueOf(position); 141 | if (recyclerView == null) { 142 | recyclerView = createRecyclerView(position); 143 | adjustItemViewLayout(recyclerView); 144 | 145 | recyclerView.setClipToPadding(false); 146 | recyclerView.updateParentRecyclerView(parentReference == null ? null : parentReference.get()); 147 | pages.put(commonIndex, recyclerView); 148 | realPosition.put(recyclerView, posObj); 149 | } 150 | 151 | // current cache view has contains the target and no need to rebind 152 | if (positionList.contains(posObj)) { 153 | return posObj; 154 | } 155 | 156 | Integer oldPosition = realPosition.get(recyclerView); 157 | // refresh real position of the recycler view 158 | realPosition.put(recyclerView, posObj); 159 | 160 | /** 需要对子view重排 */ 161 | // remove reused view 162 | if (oldPosition != null) { 163 | positionList.remove(oldPosition); 164 | } 165 | 166 | // 剩余的view重新排列 167 | int insertIndex; 168 | int firstViewPos = positionList.size() > 0 ? positionList.get(0) : -1; 169 | int lastViewPos = positionList.size() > 1 ? positionList.get(1) : -1; 170 | 171 | if (position < firstViewPos) { 172 | // 比最小的位置小 173 | insertIndex = 0; 174 | positionList.add(0, posObj); 175 | } else if (position > firstViewPos && position < lastViewPos) { 176 | // 比最小的位置大,比最大的位置小 177 | insertIndex = 1; 178 | positionList.add(1, posObj); 179 | } else { 180 | // 比最大的位置大 181 | insertIndex = -1; 182 | positionList.add(posObj); 183 | } 184 | 185 | // 如果还没有destroy,需要手动destroy 186 | if (recyclerView.getParent() != null) { 187 | // 记录状态 188 | //越界问题保护 189 | if (oldPosition != null && oldPosition < tabItems.size()) { 190 | saveRecyclerViewState(recyclerView, tabItems.get(oldPosition)); 191 | } 192 | ((ViewGroup) recyclerView.getParent()).removeView(recyclerView); 193 | } 194 | 195 | container.addView(recyclerView, insertIndex); 196 | // awesomeRequest(position, recyclerView); 197 | 198 | // setLoadMore(recyclerView, position); 199 | 200 | // 滚动到上次数据的位置 201 | final UpdatedNestedRecyclerView finalRecyclerView = recyclerView; 202 | ((StaggeredGridLayoutManager) finalRecyclerView.getLayoutManager()).scrollToPositionWithOffset(scrollToPosition.get(tabId) == null ? 0 : scrollToPosition.get(tabId), offsets.get(tabId) == null ? 0 : offsets.get(tabId)); 203 | return posObj; 204 | } 205 | 206 | @Override 207 | public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { 208 | // do nothing 209 | // Log.d(TAG, "destroyItem position is: " + position); 210 | } 211 | 212 | private void adjustItemViewLayout(UpdatedNestedRecyclerView recyclerView) { 213 | 214 | } 215 | 216 | private UpdatedNestedRecyclerView createRecyclerView(int position) { 217 | final UpdatedNestedRecyclerView recyclerView = new UpdatedNestedRecyclerView(context); 218 | recyclerView.initEnv(new NestedRVOnScrollListener()); 219 | RecommendStaggeredGridLayoutManager layoutManager = new RecommendStaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); 220 | layoutManager.setRecyclerView(recyclerView); 221 | // layoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE); 222 | recyclerView.setLayoutManager(layoutManager); 223 | recyclerView.setNestedScrollingEnabled(false); 224 | recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER); 225 | 226 | if (position == 0) { 227 | //JFY首Tab才定制Item动画 228 | // recyclerView.setItemAnimator(new RecommendItemAnimator()); 229 | } 230 | 231 | ViewGroup.LayoutParams layoutParams = recyclerView.getLayoutParams(); 232 | if (layoutParams == null) { 233 | layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 234 | recyclerView.setLayoutParams(layoutParams); 235 | } else { 236 | layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; 237 | } 238 | // ui 和 data 不需要sdk内的实现,所以用空实现覆盖 239 | // RecommendServer recommendServer = new RecommendServer.Builder((Activity) context, RecommendConst.SCENE_HOMEPAGE) 240 | // .registerDataSourceServer(new HomePageRecommendDataSourceServer()) 241 | // .registerUiServer(new HomePageRecommendUiServer((Activity) context)) 242 | // .registerInsertCardServer(new HomePageInsertCardServer()) 243 | // .registerSmartClientServer(new HomePageSmartClientServer()) 244 | // .build(); 245 | // // create Just For You RecyclerView Adapter by lazada 246 | // JustForYouComponentMappingV4 jFYMapping = new JustForYouComponentMappingV4(); 247 | // NestedRVAdapter adapter = new NestedRVAdapter(HomePageChameleonInfo.SCENE, recommendServer, jFYMapping, recyclerView, context); 248 | // // use load more adapter 249 | // RecyclerView.Adapter loadMoreAdapter = new LazLoadMoreAdapterV4(adapter); 250 | ArrayList mDataList = new ArrayList<>(); 251 | 252 | for(int i = 0;i<=10;i++) { 253 | mDataList.add("default child item " + i); 254 | } 255 | UpdatedMultiTypeAdapter loadMoreAdapter = new UpdatedMultiTypeAdapter(mDataList); 256 | recyclerView.setAdapter(loadMoreAdapter); 257 | loadMoreAdapter.notifyDataSetChanged(); 258 | 259 | recyclerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 260 | return recyclerView; 261 | } 262 | 263 | 264 | @Override 265 | public boolean isViewFromObject(@NonNull View view, @NonNull Object o) { 266 | return (realPosition.get(view) == o); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/java/view/ParentRecyclerView.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.java.view; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.util.AttributeSet; 9 | import android.view.MotionEvent; 10 | import android.view.View; 11 | import android.view.ViewConfiguration; 12 | 13 | import com.gaohui.nestedrecyclerview.UIUtils; 14 | import com.gaohui.nestedrecyclerview.java.adapter.MultiTypeAdapter; 15 | import com.gaohui.nestedrecyclerview.java.utils.FlingHelper; 16 | 17 | import java.util.concurrent.atomic.AtomicBoolean; 18 | 19 | public class ParentRecyclerView extends RecyclerView { 20 | FlingHelper mFlingHelper; 21 | 22 | int mMaxDistance = 0; 23 | /** 24 | * 记录当前滑动的y轴加速度 25 | */ 26 | int velocityY = 0 ; 27 | /** 28 | * 记录上次Event事件的y坐标 29 | */ 30 | Float lastY = 0f; 31 | int mTotalDy = 0; 32 | /** 33 | * 用于判断RecyclerView是否在fling 34 | */ 35 | boolean isStartFling = false; 36 | 37 | AtomicBoolean canScrollVertically; 38 | private int mLastXInterceptX; 39 | private int mLastYInterceptY; 40 | 41 | private int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 42 | 43 | private void init(Context context) { 44 | mFlingHelper = new FlingHelper(context); 45 | mMaxDistance = mFlingHelper.getVelocityByDistance((double)(UIUtils.getScreenHeight() * 4)); 46 | canScrollVertically = new AtomicBoolean(true); 47 | addOnScrollListener(new OnScrollListener() { 48 | @Override 49 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { 50 | super.onScrollStateChanged(recyclerView, newState); 51 | //如果父RecyclerView fling过程中已经到底部,需要让子RecyclerView滑动神域的fling 52 | if(newState == SCROLL_STATE_IDLE) { 53 | dispatchChildFling(); 54 | } 55 | } 56 | 57 | @Override 58 | public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 59 | super.onScrolled(recyclerView, dx, dy); 60 | if(isStartFling) { 61 | mTotalDy = 0; 62 | isStartFling = false; 63 | } 64 | //记录当前RecyclerView在y轴的偏移 65 | mTotalDy += dy; 66 | 67 | } 68 | }); 69 | 70 | } 71 | 72 | public void initLayoutManager(Context context) { 73 | LinearLayoutManager layoutManager = new LinearLayoutManager(context) { 74 | @Override 75 | public int scrollVerticallyBy(int dy, Recycler recycler, State state) { 76 | try { 77 | return super.scrollVerticallyBy(dy, recycler, state); 78 | } catch (Exception e) { 79 | e.printStackTrace(); 80 | return 0; 81 | } 82 | } 83 | 84 | @Override 85 | public void onLayoutChildren(Recycler recycler, State state) { 86 | try { 87 | super.onLayoutChildren(recycler, state); 88 | } catch (Exception e) { 89 | e.printStackTrace(); 90 | } 91 | } 92 | 93 | @Override 94 | public boolean canScrollVertically() { 95 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView(); 96 | return (canScrollVertically.get() || childRecyclerView == null || childRecyclerView.isScrollTop()); 97 | } 98 | 99 | @Override 100 | public void addDisappearingView(View child) { 101 | try { 102 | super.addDisappearingView(child); 103 | } catch ( Exception e) { 104 | e.printStackTrace(); 105 | } 106 | } 107 | 108 | @Override 109 | public boolean supportsPredictiveItemAnimations() { 110 | return false; 111 | } 112 | }; 113 | 114 | layoutManager.setOrientation(VERTICAL); 115 | setLayoutManager(layoutManager); 116 | } 117 | 118 | @Override 119 | public boolean onInterceptTouchEvent(MotionEvent event) { 120 | boolean res = false; 121 | if(!isChildConsumeTouch(event)) { 122 | res = super.onInterceptHoverEvent(event); 123 | } 124 | return res; 125 | } 126 | 127 | 128 | 129 | private boolean isChildConsumeTouch(MotionEvent event) { 130 | int x = (int) event.getX(); 131 | int y = (int) event.getY(); 132 | if(event.getAction() != MotionEvent.ACTION_MOVE) { 133 | mLastXInterceptX = x; 134 | mLastYInterceptY = y; 135 | return false; 136 | } 137 | int deltaX = x - mLastXInterceptX; 138 | int deltaY = y - mLastYInterceptY; 139 | if(Math.abs(deltaX) <= Math.abs(deltaY) || Math.abs(deltaY) <= mTouchSlop) { 140 | return false; 141 | } 142 | return shouldChildScroll(deltaY); 143 | } 144 | 145 | private boolean shouldChildScroll(int deltaY) { 146 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView(); 147 | if(childRecyclerView == null) { 148 | return false; 149 | } 150 | if(isScrollToBottom()) { 151 | if(deltaY > 0) { 152 | return false; 153 | } else if(deltaY < 0 && !childRecyclerView.isScrollTop()) { 154 | return true; 155 | } 156 | } else { 157 | if(deltaY > 0 && !childRecyclerView.isScrollTop()) { 158 | return true; 159 | } else if(deltaY < 0) { 160 | return false; 161 | } 162 | } 163 | return false; 164 | } 165 | 166 | @Override 167 | public boolean dispatchTouchEvent(MotionEvent ev) { 168 | if(ev != null && ev.getAction() == MotionEvent.ACTION_DOWN) { 169 | //ACTION_DOWN的时候重置加速度 170 | velocityY = 0; 171 | stopScroll(); 172 | } 173 | if(!(ev == null || ev.getAction() == MotionEvent.ACTION_MOVE)) { 174 | //在非ACTION_MOVE的情况下,将lastY置为0 175 | lastY = 0f; 176 | } 177 | try { 178 | return super.dispatchTouchEvent(ev); 179 | } catch (Exception e) { 180 | e.printStackTrace(); 181 | return false; 182 | } 183 | } 184 | 185 | @Override 186 | public boolean onTouchEvent(MotionEvent e) { 187 | if(lastY == 0f) { 188 | lastY = e.getY(); 189 | } 190 | if(isScrollToBottom()) { 191 | //如果父RecyclerView已经滑动到底部,需要让子RecyclerView滑动剩余的距离 192 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView(); 193 | if(childRecyclerView != null) { 194 | int deltaY = (int) (lastY - e.getY()); 195 | canScrollVertically.set(false); 196 | childRecyclerView.scrollBy(0,deltaY); 197 | } 198 | } 199 | if(e.getAction() == MotionEvent.ACTION_UP) { 200 | canScrollVertically.set(true); 201 | } 202 | lastY = e.getY(); 203 | try { 204 | return super.onTouchEvent(e); 205 | } catch (Exception ex) { 206 | ex.printStackTrace(); 207 | return false; 208 | } 209 | } 210 | 211 | @Override 212 | public boolean fling(int velx, int velY) { 213 | boolean fling = super.fling(velx,velY); 214 | if(!fling || velY <= 0) { 215 | velocityY = 0; 216 | 217 | } else { 218 | isStartFling = true; 219 | velocityY = velY; 220 | } 221 | return fling; 222 | } 223 | 224 | private void dispatchChildFling() { 225 | if(isScrollToBottom() && velocityY != 0) { 226 | double splineFlingDistance = mFlingHelper.getSplineFlingDistance(velocityY); 227 | if(splineFlingDistance > mTotalDy) { 228 | childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - mTotalDy)); 229 | } 230 | } 231 | mTotalDy = 0; 232 | velocityY = 0; 233 | } 234 | 235 | private void childFling(int velocityByDistance) { 236 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView(); 237 | if(childRecyclerView != null) { 238 | childRecyclerView.fling(0,velocityByDistance); 239 | } 240 | } 241 | 242 | private ChildRecyclerView findNestedScrollingChildRecyclerView() { 243 | if(getAdapter()!= null && (getAdapter() instanceof MultiTypeAdapter)) { 244 | return ((MultiTypeAdapter)getAdapter()).getCurrentChildRecyclerView(); 245 | } 246 | return null; 247 | } 248 | 249 | private boolean isScrollToBottom() { 250 | //RecyclerView.canScrollVertically(1)的值表示是否能向上滚动,false表示已经滚动到底部 251 | return !canScrollVertically(1); 252 | } 253 | 254 | @Override 255 | public void scrollToPosition(final int position) { 256 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView(); 257 | if(childRecyclerView != null) { 258 | childRecyclerView.scrollToPosition(position); 259 | } 260 | postDelayed(new Runnable() { 261 | @Override 262 | public void run() { 263 | ParentRecyclerView.super.scrollToPosition(position); 264 | } 265 | },50); 266 | } 267 | 268 | @Override 269 | public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 270 | return (target != null) && (target instanceof ChildRecyclerView); 271 | } 272 | 273 | @Override 274 | public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { 275 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView(); 276 | //1.当前Parent RecyclerView没有滑动底,且dy> 0 是下滑 277 | boolean isParentCanScroll = dy > 0 && !isScrollToBottom(); 278 | //2.当前Child RecyclerView滑到顶部了,且dy < 0,即上滑 279 | boolean isChildCanNotScroll = !(dy >= 0 280 | || childRecyclerView == null 281 | || !childRecyclerView.isScrollTop()); 282 | //以上两种情况都需要让Parent RecyclerView去scroll,和下面onNestedPreFling机制类似 283 | if(isParentCanScroll || isChildCanNotScroll) { 284 | scrollBy(0,dy); 285 | consumed[1] = dy; 286 | } 287 | } 288 | 289 | @Override 290 | public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 291 | return true; 292 | } 293 | 294 | @Override 295 | public boolean onNestedPreFling(View target, float velocityX, float velocityY) { 296 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView(); 297 | boolean isParentCanFling = velocityY > 0f && !isScrollToBottom(); 298 | boolean isChildCanNotFling = !(velocityY >= 0 299 | || childRecyclerView == null 300 | || !childRecyclerView.isScrollTop()); 301 | 302 | if(!isParentCanFling && !isChildCanNotFling) { 303 | return false; 304 | } 305 | fling(0,(int) velocityY); 306 | return true; 307 | } 308 | 309 | public boolean isChildRecyclerViewCanScrollUp() { 310 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView(); 311 | if(childRecyclerView != null) { 312 | return !childRecyclerView.isScrollTop(); 313 | } 314 | return false; 315 | } 316 | 317 | public ParentRecyclerView(@NonNull Context context) { 318 | super(context); 319 | init(context); 320 | } 321 | 322 | public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { 323 | super(context, attrs); 324 | init(context); 325 | } 326 | 327 | public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { 328 | super(context, attrs, defStyle); 329 | init(context); 330 | } 331 | 332 | } 333 | -------------------------------------------------------------------------------- /app/src/main/java/com/gaohui/nestedrecyclerview/update/SlidingTabLayoutRevamp.java: -------------------------------------------------------------------------------- 1 | package com.gaohui.nestedrecyclerview.update; 2 | 3 | 4 | import android.content.Context; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Rect; 8 | import android.graphics.drawable.GradientDrawable; 9 | import android.support.v4.view.ViewPager; 10 | import android.util.AttributeSet; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.widget.FrameLayout; 14 | import android.widget.HorizontalScrollView; 15 | import android.widget.LinearLayout; 16 | import android.widget.TextView; 17 | 18 | 19 | import com.gaohui.nestedrecyclerview.R; 20 | import com.gaohui.nestedrecyclerview.update.viewpager.ViewPagerAdapter; 21 | import com.gaohui.nestedrecyclerview.update.utils.ScreenUtils; 22 | 23 | import java.util.List; 24 | 25 | /** 26 | * @author: zhuhuihui 27 | * @date: 2019/6/13 28 | * @description: tabs for just for you 29 | * /** 30 | * sliding tab layout and have strong relationshipi with view pager 31 | */ 32 | public class SlidingTabLayoutRevamp extends HorizontalScrollView implements ViewPager.OnPageChangeListener, IRecommendTabLayout { 33 | public static final String TAG = "SlidingTabLayoutRevamp"; 34 | 35 | public static final String TAB_ID = "tabId"; 36 | 37 | 38 | private static final String UNSELECT_TITLE_COLOR = "#595F6D"; 39 | private final Context mContext; 40 | private ViewPager mViewPager; 41 | private final LinearLayout mTabsContainer; 42 | private int mCurrentTab; 43 | private float mCurrentPositionOffset; 44 | private int mTabCount; 45 | /** 46 | * draw indicator 47 | */ 48 | private final Rect mIndicatorRect = new Rect(); 49 | /** 50 | * scroll and position center 51 | */ 52 | private final Rect mTabRect = new Rect(); 53 | private final GradientDrawable mIndicatorDrawable = new GradientDrawable(); 54 | 55 | /** 56 | * the bottom separator line 57 | */ 58 | private boolean lineDrawable = false; 59 | private final GradientDrawable mLineDrawable = new GradientDrawable(); 60 | 61 | /** 62 | * indicator 63 | */ 64 | private int mIndicatorColor; 65 | private float mIndicatorHeight; 66 | private int mIndicatorPadding; 67 | private float mIndicatorCornerRadius; 68 | private int mLastScrollX; 69 | private final int mUnSelectTitleColor = Color.parseColor(UNSELECT_TITLE_COLOR); 70 | 71 | /** 72 | * tabs data 73 | * JSONObject must contains TAB_ID 74 | */ 75 | private List tabItems; 76 | 77 | private String selectedTabId = ""; 78 | 79 | /** 80 | * true means fixed 81 | */ 82 | private boolean mIsFixedTab; 83 | 84 | /** 85 | * 标记JFY TAB是否吸顶 86 | */ 87 | private boolean mJfyAtTop; 88 | 89 | public SlidingTabLayoutRevamp(Context context) { 90 | this(context, null, 0); 91 | } 92 | 93 | public SlidingTabLayoutRevamp(Context context, AttributeSet attrs) { 94 | this(context, attrs, 0); 95 | } 96 | 97 | public SlidingTabLayoutRevamp(Context context, AttributeSet attrs, int defStyleAttr) { 98 | super(context, attrs, defStyleAttr); 99 | 100 | /** 101 | * true 设置滚动视图是否可以伸缩其内容以填充视口 102 | */ 103 | //setFillViewport(true); 104 | setFillViewport(false); 105 | /** 106 | * 重写onDraw方法,需要调用这个方法来清除flag 107 | */ 108 | setWillNotDraw(false); 109 | setClipChildren(false); 110 | setClipToPadding(false); 111 | 112 | this.mContext = context; 113 | int adaptSize = 50; 114 | setPadding(adaptSize, 0, adaptSize, 0); 115 | mTabsContainer = new LinearLayout(context); 116 | addView(mTabsContainer); 117 | obtainAttributes(context, attrs); 118 | } 119 | 120 | private void obtainAttributes(Context context, AttributeSet attrs) { 121 | // TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingTabLayoutRevamp); 122 | // 123 | // mIndicatorColor = ta.getColor(R.styleable.SlidingTabLayoutRevamp_tl_indicator_color_revamp, Color.parseColor("#ff330c")); 124 | // mIndicatorHeight = ta.getDimension(R.styleable.SlidingTabLayoutRevamp_tl_indicator_height_revamp, 125 | // dp2px(3)); 126 | // mIndicatorWidth = ta.getDimension(R.styleable.SlidingTabLayoutRevamp_tl_indicator_width_revamp, dp2px(-1)); 127 | // mIndicatorCornerRadius = ta.getDimension(R.styleable.SlidingTabLayoutRevamp_tl_indicator_corner_radius_revamp, dp2px(2)); 128 | // mIndicatorMarginLeft = ta.getDimension(R.styleable.SlidingTabLayoutRevamp_tl_indicator_margin_left_revamp, dp2px(0)); 129 | // mIndicatorMarginRight = ta.getDimension(R.styleable.SlidingTabLayoutRevamp_tl_indicator_margin_right_revamp, dp2px(0)); 130 | // mIndicatorMarginBottom = ta.getDimension(R.styleable.SlidingTabLayoutRevamp_tl_indicator_margin_bottom_revamp, dp2px(0)); 131 | // 132 | // ta.recycle(); 133 | 134 | mIndicatorColor = Color.RED; 135 | mIndicatorHeight = dp2px(3); 136 | mIndicatorPadding = dp2px(3); 137 | mIndicatorCornerRadius = dp2px(12); 138 | } 139 | 140 | // private OnLayoutListener mOnLayoutListener; 141 | // 142 | // @Override 143 | // public void setOnLayoutListener(OnLayoutListener onLayoutListener) { 144 | // mOnLayoutListener = onLayoutListener; 145 | // } 146 | 147 | @Override 148 | protected void onScrollChanged(int l, int t, int oldl, int oldt) { 149 | super.onScrollChanged(l, t, oldl, oldt); 150 | try { 151 | if (getChildCount() > 0) { 152 | int maxScroll = getChildAt(0).getWidth() 153 | - (getWidth() - getPaddingLeft() - getPaddingRight()); 154 | // if (mOnLayoutListener != null) { 155 | // mOnLayoutListener.onScrollChanged(getScrollX()); 156 | // if (getScrollX() == 0) { 157 | // mOnLayoutListener.onReachStart(); 158 | // } else if (getScrollX() == maxScroll) { 159 | // mOnLayoutListener.onReachEnd(); 160 | // } 161 | // } 162 | } 163 | } catch (Exception e) { 164 | e.printStackTrace(); 165 | } 166 | } 167 | 168 | @Override 169 | public View getView() { 170 | return this; 171 | } 172 | 173 | /** 174 | * related ViewPager 175 | */ 176 | @Override 177 | public void setViewPager(ViewPager vp) { 178 | if (vp == null || vp.getAdapter() == null) { 179 | throw new IllegalStateException("ViewPager or ViewPager adapter can not be NULL !"); 180 | } 181 | 182 | this.mViewPager = vp; 183 | 184 | this.mViewPager.removeOnPageChangeListener(this); 185 | this.mViewPager.addOnPageChangeListener(this); 186 | if (mViewPager.getAdapter() instanceof ViewPagerAdapter) { 187 | tabItems = ((ViewPagerAdapter) mViewPager.getAdapter()).tabItems; 188 | } 189 | if (tabItems != null && tabItems.size() > 0 && mViewPager.getCurrentItem() < tabItems.size()) { 190 | // selectedTabId = tabItems.get(mViewPager.getCurrentItem()).getString(TAB_ID); 191 | // LazDataPools.getInstance().setSelectedJFYTabId(selectedTabId); 192 | } 193 | notifyDataSetChanged(); 194 | } 195 | 196 | /** 197 | * true means fixed 198 | * 199 | * @param fixTabFlag 200 | */ 201 | @Override 202 | public void setFixTabFlag(boolean fixTabFlag) { 203 | mIsFixedTab = fixTabFlag; 204 | } 205 | 206 | /** 207 | * 更新数据 208 | * 因为tab数量有可能增减而且tab状态有收起与展开两种形态,所以此处更新时要注意状态 209 | * 不同状态的差距只有副标题是否隐藏、分割线是否展示及topMargin 210 | */ 211 | public void notifyDataSetChanged() { 212 | addAllTabs(); 213 | updateTabStyles(); 214 | 215 | // update the bg of sliding tab bar 216 | updateSlidingTabBackground(); 217 | } 218 | 219 | private void updateSlidingTabBackground() { 220 | if (tabItems == null || tabItems.isEmpty()) { 221 | return; 222 | } 223 | 224 | if (getParent() == null) { 225 | return; 226 | } 227 | 228 | 229 | if (getParent() instanceof FrameLayout) { 230 | // ((FrameLayout) getParent()).setBackgroundColor(Color.parseColor(GREY_BG_F0F1F6)); 231 | } 232 | } 233 | 234 | private boolean isFixedTabModel() { 235 | return mIsFixedTab; 236 | } 237 | 238 | private void addAllTabs() { 239 | // 如果mTabsContainer含有子view,则是非首次更新,记录当前状态 240 | mTabsContainer.removeAllViews(); 241 | this.mTabCount = tabItems == null ? 0 : tabItems.size(); 242 | View tabView; 243 | for (int i = 0; i < mTabCount; i++) { 244 | try { 245 | tabView = LayoutInflater.from(mContext).inflate(R.layout.laz_recommend_tab_filter_revamp_new_rec, mTabsContainer, false); 246 | // HpImageUtils.attachHomePageTag(tabView); 247 | addTab(i, tabView); 248 | } catch (Throwable t) { 249 | // pass 250 | } 251 | } 252 | } 253 | 254 | /** 255 | * add and create tab item 256 | */ 257 | private void addTab(final int position, View tabView) { 258 | tabView.setOnClickListener(new OnClickListener() { 259 | @Override 260 | public void onClick(View v) { 261 | int position = mTabsContainer.indexOfChild(v); 262 | if (position != -1) { 263 | if (mViewPager.getCurrentItem() != position) { 264 | if (position < tabItems.size()) { 265 | mViewPager.setCurrentItem(position, false); 266 | } 267 | } 268 | } 269 | } 270 | }); 271 | mTabsContainer.addView(tabView, position); 272 | 273 | // add touch feedback 274 | // TouchFeedbackUtils.addTouchFeedback(tabView, true, true); 275 | } 276 | 277 | private void initTabView(final View tabView, String tabItem, boolean isSelected) { 278 | if (tabView == null || tabItem == null) { 279 | return; 280 | } 281 | 282 | // Step2: 设置tab顶部icon 283 | // String festival = isSelected ? tabItem.getString(TAB_SELECTED_TOP_IMG) : tabItem.getString(TAB_UNSELECTED_TOP_IMG); 284 | // 285 | // if (isSelected && TextUtils.isEmpty(festival)) { 286 | // festival = tabItem.getString(TAB_UNSELECTED_TOP_IMG); 287 | // } 288 | // 289 | // // Step3: 设置icon 290 | // TUrlImageView festivalView = tabView.findViewById(R.id.tab_icon); 291 | // // add placeholder 292 | // festivalView.setPlaceHoldImageResId(R.drawable.hp_banner_h100_placeholder); 293 | // festivalView.setErrorImageResId(R.drawable.hp_banner_h100_placeholder); 294 | // HpImageUtils.dealWithGifImage(festival, festivalView); 295 | // HpImageUtils.adapterBitmapToWidget(festivalView, festival, LazHPDimenUtils.adaptTwentyOneDpToPx(mContext), 0); 296 | // 297 | // // Step4: 设置title 298 | TextView titleView = tabView.findViewById(R.id.tab_title); 299 | titleView.setText(tabItem); 300 | // if (I18NMgt.getInstance(LazGlobal.sApplication).getENVCountry() == Country.TH 301 | // && I18NMgt.getInstance(LazGlobal.sApplication).getENVLanguage() == Language.TH_TH) { 302 | // //泰文减小行间距,否则会被裁剪 303 | // titleView.setLineSpacing(0, 0.8f); 304 | // } else { 305 | // titleView.setLineSpacing(0, 1.0f); 306 | // } 307 | 308 | refreshTabTitle(tabView, tabItem, isSelected); 309 | } 310 | 311 | @Override 312 | public View getTabAt(int position) { 313 | if (mTabsContainer != null && position < mTabsContainer.getChildCount()) { 314 | return mTabsContainer.getChildAt(position); 315 | } 316 | return null; 317 | } 318 | 319 | @Override 320 | public void setLineDrawableEnabled(boolean enabled) { 321 | lineDrawable = enabled; 322 | mLineDrawable.setColor(Color.parseColor("#eeeeee")); 323 | mLineDrawable.setBounds(0, getHeight() - dp2px(0.5f), mTabsContainer.getWidth(), getHeight()); 324 | invalidate(); 325 | } 326 | 327 | @Override 328 | public void setJfyAtTop(boolean jfyAtTop) { 329 | mJfyAtTop = jfyAtTop; 330 | invalidate(); 331 | } 332 | 333 | public void updateTabStyles() { 334 | for (int i = 0; i < mTabCount; i++) { 335 | View v = mTabsContainer.getChildAt(i); 336 | initTabView(v, tabItems.get(i), selectedTabId.equals(tabItems.get(i))); 337 | } 338 | 339 | invalidate(); 340 | } 341 | 342 | @Override 343 | public void updateTabStyles(List list) { 344 | this.tabItems.clear(); 345 | this.tabItems.addAll(list); 346 | mTabCount = tabItems.size(); 347 | updateSlidingTabBackground(); 348 | for (int i = 0; i < mTabCount; i++) { 349 | View v = mTabsContainer.getChildAt(i); 350 | initTabView(v, tabItems.get(i), selectedTabId.equals(tabItems.get(i))); 351 | } 352 | invalidate(); 353 | } 354 | 355 | @Override 356 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 357 | /** 358 | * position:当前View的位置 359 | * mCurrentPositionOffset:当前View的偏移量比例.[0,1) 360 | */ 361 | this.mCurrentTab = position; 362 | this.mCurrentPositionOffset = positionOffset; 363 | scrollToCurrentTab(); 364 | invalidate(); 365 | } 366 | 367 | @Override 368 | public void onPageSelected(int position) { 369 | if (position < tabItems.size()) { 370 | updateTabSelection(position); 371 | // 对于Lazada,点击和滚动到某个tab,都需要一个点击事件;对于滚动,只需要统计滚动行为(当前是在ViewPagerAdapter中) 372 | // final String spm = HpSPMUtil.buildHomeSPM(SPMConstants.HOME_22_JFY_TABS, position); 373 | // String tabType = tabItems.get(position).getString(TAB_NAME_KEY); 374 | // final Map args = new HashMap<>(); 375 | // if (!TextUtils.isEmpty(tabType)) { 376 | // args.put("tabType", tabType); 377 | // } 378 | // HpSPMUtil.trackClickEventV2(SPMConstants.HOME_PAGE, SPMConstants.UT_TRACK_JFY_TAB_CLICK, 379 | // spm, 380 | // args); 381 | // 382 | // // TODO: current is all tab 383 | // if (position == 0) { 384 | // LazDataPools.getInstance().setJfyLastTimeMs(System.currentTimeMillis()); 385 | // } 386 | // 387 | // // send event to notify recommend tab change 388 | // EventCenter.getInstance().post(new RecommendTabChangeEvent(selectedTabId)); 389 | } 390 | } 391 | 392 | @Override 393 | public void onPageScrollStateChanged(int state) { 394 | } 395 | 396 | /** 397 | * HorizontalScrollView scroll to current tab and at the center of whole sliding tab layout 398 | */ 399 | private void scrollToCurrentTab() { 400 | if (mTabCount <= 0) { 401 | return; 402 | } 403 | 404 | if (mTabsContainer == null || mTabsContainer.getChildAt(mCurrentTab) == null) { 405 | return; 406 | } 407 | 408 | int offset = (int) (mCurrentPositionOffset * mTabsContainer.getChildAt(mCurrentTab).getWidth()); 409 | /**当前Tab的left+当前Tab的Width乘以positionOffset*/ 410 | int newScrollX = mTabsContainer.getChildAt(mCurrentTab).getLeft() + offset; 411 | 412 | if (mCurrentTab > 0 || offset > 0) { 413 | /**HorizontalScrollView移动到当前tab,并居中*/ 414 | newScrollX -= getWidth() / 2 - getPaddingLeft(); 415 | calcIndicatorRect(); 416 | newScrollX += ((mTabRect.right - mTabRect.left) / 2); 417 | } 418 | 419 | if (newScrollX != mLastScrollX) { 420 | mLastScrollX = newScrollX; 421 | /** scrollTo(int x,int y):x,y代表的不是坐标点,而是偏移量 422 | * x:表示离起始位置的x水平方向的偏移量 423 | * y:表示离起始位置的y垂直方向的偏移量 424 | */ 425 | smoothScrollTo(newScrollX, 0); 426 | } 427 | } 428 | 429 | private void updateTabSelection(int position) { 430 | if (position >= tabItems.size() || position < 0) { 431 | return; 432 | } 433 | 434 | // selectedTabId = tabItems.get(position).getString(TAB_ID); 435 | // LazDataPools.getInstance().setSelectedJFYTabId(selectedTabId); 436 | 437 | for (int i = 0; i < mTabCount; ++i) { 438 | final View tabView = mTabsContainer.getChildAt(i); 439 | final boolean isSelect = i == position; 440 | if (tabItems.size() <= i) { 441 | // 并发异常了 442 | break; 443 | } 444 | String item = tabItems.get(i); 445 | refreshTabTitle(tabView, item, isSelect); 446 | } 447 | } 448 | 449 | private void calcIndicatorRect() { 450 | if (mCurrentTab < 0 || mCurrentTab >= mTabsContainer.getChildCount()) { 451 | return; 452 | } 453 | 454 | View currentTabView = mTabsContainer.getChildAt(this.mCurrentTab); 455 | if (currentTabView == null) { 456 | return; 457 | } 458 | 459 | float left = currentTabView.getLeft(); 460 | float right = currentTabView.getRight(); 461 | 462 | if (this.mCurrentTab < mTabCount - 1) { 463 | View nextTabView = mTabsContainer.getChildAt(this.mCurrentTab + 1); 464 | float nextTabLeft = nextTabView.getLeft(); 465 | float nextTabRight = nextTabView.getRight(); 466 | 467 | left = left + mCurrentPositionOffset * (nextTabLeft - left); 468 | right = right + mCurrentPositionOffset * (nextTabRight - right); 469 | } 470 | 471 | mIndicatorRect.left = (int) (left); 472 | mIndicatorRect.right = (int) (right); 473 | 474 | mTabRect.left = (int) left; 475 | mTabRect.right = (int) right; 476 | } 477 | 478 | @Override 479 | protected void onDraw(Canvas canvas) { 480 | super.onDraw(canvas); 481 | 482 | if (isInEditMode() || mTabCount <= 0) { 483 | return; 484 | } 485 | 486 | int height = getHeight(); 487 | int paddingLeft = getPaddingLeft(); 488 | 489 | if (lineDrawable) { 490 | mLineDrawable.draw(canvas); 491 | } 492 | 493 | //计算indicator位置 494 | calcIndicatorRect(); 495 | 496 | if (mIndicatorHeight > 0) { 497 | if (mJfyAtTop) { 498 | mIndicatorDrawable.setColor(mIndicatorColor); 499 | mIndicatorDrawable.setBounds(paddingLeft + mIndicatorRect.left + mIndicatorPadding, 500 | height - (int) mIndicatorHeight, 501 | paddingLeft + mIndicatorRect.right - mIndicatorPadding, 502 | height); 503 | } else { 504 | mIndicatorDrawable.setColor(Color.WHITE); 505 | 506 | mIndicatorDrawable.setBounds(paddingLeft + mIndicatorRect.left, 507 | 0, paddingLeft + mIndicatorRect.right, height); 508 | 509 | mIndicatorDrawable.setStroke(dp2px(1.0f), mIndicatorColor); 510 | } 511 | mIndicatorDrawable.setCornerRadius(mIndicatorCornerRadius); 512 | mIndicatorDrawable.draw(canvas); 513 | } 514 | } 515 | 516 | @Override 517 | public void setIndicatorColor(int indicatorColor) { 518 | if (indicatorColor != 0) { 519 | this.mIndicatorColor = indicatorColor; 520 | invalidate(); 521 | } 522 | } 523 | 524 | public void setIndicatorHeight(float indicatorHeight) { 525 | this.mIndicatorHeight = dp2px(indicatorHeight); 526 | invalidate(); 527 | } 528 | 529 | public void setIndicatorCornerRadius(float indicatorCornerRadius) { 530 | this.mIndicatorCornerRadius = dp2px(indicatorCornerRadius); 531 | invalidate(); 532 | } 533 | 534 | @Override 535 | public int getTabCount() { 536 | return mTabCount; 537 | } 538 | 539 | @Override 540 | public void setOnLayoutListener(OnLayoutListener onLayoutListener) { 541 | 542 | } 543 | 544 | private int dp2px(float dp) { 545 | return ScreenUtils.dp2px(getContext(), dp); 546 | } 547 | 548 | private void refreshTabTitle(View view, String tabItem, boolean isSelected) { 549 | // refresh top image 550 | // String festival = isSelected ? tabItem.getString(TAB_SELECTED_TOP_IMG) : tabItem.getString(TAB_UNSELECTED_TOP_IMG); 551 | // 552 | // if (isSelected && TextUtils.isEmpty(festival)) { 553 | // festival = tabItem.getString(TAB_UNSELECTED_TOP_IMG); 554 | // } 555 | // HpImageUtils.dealWithGifImage(festival, ((TUrlImageView) view.findViewById(R.id.tab_icon))); 556 | // HpImageUtils.adapterBitmapToWidget(((TUrlImageView) view.findViewById(R.id.tab_icon)), festival, LazHPDimenUtils.adaptTwentyOneDpToPx(mContext), 0); 557 | 558 | // refresh text 559 | // int titleSelectedColor = (mIndicatorColor == 0) ? 560 | // ContextCompat.getColor(getContext(), R.color.laz_common_FE4960) : mIndicatorColor; 561 | // int titleColor = isSelected ? titleSelectedColor : mUnSelectTitleColor; 562 | // TextView titleTV = (TextView) view.findViewById(R.id.tab_title); 563 | // titleTV.setTextColor(titleColor); 564 | // if (isSelected) { 565 | // titleTV.setTypeface(FontStyle.getCurrentTypeface(getContext(), FontStyle.STYLE_BOLD)); 566 | // } else { 567 | // titleTV.setTypeface(FontStyle.getCurrentTypeface(getContext(), FontStyle.STYLE_SEMI_BOLD)); 568 | // } 569 | } 570 | } 571 | --------------------------------------------------------------------------------