├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── tory
│ │ └── nestedceilingeffect
│ │ └── app
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── tory
│ │ │ └── nestedceiling
│ │ │ └── app
│ │ │ ├── MainActivity.kt
│ │ │ ├── adapter
│ │ │ └── BannerImageAdapter.kt
│ │ │ ├── model
│ │ │ ├── BannerData.kt
│ │ │ └── LastViewPager2.kt
│ │ │ ├── page
│ │ │ ├── EmptyFragment.kt
│ │ │ ├── LinearINormalFragment.kt
│ │ │ ├── NestedParentRecyclerViewActivity.kt
│ │ │ ├── StaggeredFragment.kt
│ │ │ └── TransparentToolbarActivity.kt
│ │ │ ├── utils
│ │ │ ├── Extensions.kt
│ │ │ ├── MaterialColor.java
│ │ │ ├── SystemBarUtils.java
│ │ │ ├── SystemProperties.java
│ │ │ └── TableInterpolatorHelper.kt
│ │ │ └── views
│ │ │ ├── LastViewPager2ItemView.kt
│ │ │ ├── LastViewPagerItemView.kt
│ │ │ ├── ModuleBannerView.kt
│ │ │ ├── ModuleNormalItemView.kt
│ │ │ └── ModuleStaggerItemView.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable-xxhdpi
│ │ └── ic_back_top.png
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── activity_nestedrecyclerview.xml
│ │ ├── activity_transparent_toolbar.xml
│ │ ├── fragment_recycler_view.xml
│ │ ├── item_banner.xml
│ │ ├── item_last_view_pager.xml
│ │ ├── item_last_view_pager2.xml
│ │ ├── item_normal.xml
│ │ └── item_stagger.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.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
│ │ └── values
│ │ ├── android_material_design_palette.xml
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── tory
│ └── nestedceilingeffect
│ └── app
│ └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── nestedceiling
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ ├── androidx
│ │ └── recyclerview
│ │ │ └── widget
│ │ │ └── NestedPublicRecyclerView.java
│ └── com
│ │ └── tory
│ │ └── nestedceiling
│ │ └── widget
│ │ ├── FindTarget.java
│ │ ├── FlingHelper.java
│ │ ├── NestedCeilingHelper.java
│ │ ├── NestedChildItemContainer.java
│ │ ├── NestedChildRecyclerView.java
│ │ ├── NestedParentRecyclerView.java
│ │ └── OnChildAttachStateListener.java
│ └── res
│ └── values
│ └── ids.xml
├── preview
├── app-debug.apk
└── live.gif
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 嵌套滑动的RecyclerView, fork from https://github.com/solartcc/NestedCeilingEffect
2 |
3 | 实现文档: https://juejin.cn/post/7021869390832336910
4 |
5 | 主要解决一下问题
6 | 1. 多指问题
7 | 2. fling过程中触摸view抖动问题
8 | 3. ViewPager中有不能嵌套滑动的View时整体无法滑动的问题
9 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-parcelize'
4 |
5 | android {
6 | compileSdkVersion 30
7 | //buildToolsVersion "29.0.3"
8 |
9 | defaultConfig {
10 | applicationId "cc.solart.nestedceilinge.app"
11 | minSdkVersion 21
12 | targetSdkVersion 30
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | consumerProguardFiles 'proguard-rules.pro'
18 | }
19 |
20 | compileOptions {
21 | sourceCompatibility JavaVersion.VERSION_1_8
22 | targetCompatibility JavaVersion.VERSION_1_8
23 | }
24 |
25 | kotlinOptions {
26 | jvmTarget = JavaVersion.VERSION_1_8.toString()
27 | }
28 |
29 | buildTypes {
30 | release {
31 | minifyEnabled false
32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
33 | }
34 | }
35 |
36 | }
37 |
38 | dependencies {
39 | implementation fileTree(dir: 'libs', include: ['*.jar'])
40 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
41 | implementation 'androidx.appcompat:appcompat:1.1.0'
42 | implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
43 | implementation 'androidx.recyclerview:recyclerview:1.1.0'
44 | implementation 'com.google.android.material:material:1.1.0'
45 | implementation 'androidx.lifecycle:lifecycle-viewmodel:2.2.0'
46 | implementation 'com.github.bumptech.glide:glide:4.11.0'
47 | implementation 'com.drakeet.multitype:multitype:4.2.0'
48 | implementation 'com.youth.banner:banner:2.1.0'
49 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
50 | implementation 'com.github.ToryCrox.ModuleAdapter:module_adapter:0.2.0'
51 | implementation 'com.github.ToryCrox.ModuleAdapter:module_adapter_vlayout:0.2.0'
52 | implementation "androidx.core:core-ktx:1.3.2"
53 | implementation 'androidx.appcompat:appcompat:1.2.0'
54 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
55 |
56 | testImplementation 'junit:junit:4.12'
57 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
58 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
59 | implementation project(path: ':nestedceiling')
60 | }
61 |
--------------------------------------------------------------------------------
/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 |
23 | -keepclassmembers class androidx.interpolator.view.animation.* {
24 | private static final float[] VALUES;
25 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/tory/nestedceilingeffect/app/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceilingeffect.app;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 |
25 | assertEquals("cc.solart.nestedceilingeffect.app", appContext.getPackageName());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.widget.Button
6 | import androidx.appcompat.app.AppCompatActivity
7 | import com.tory.module_adapter.base.NormalModuleAdapter
8 | import com.tory.nestedceiling.app.page.NestedParentRecyclerViewActivity
9 | import com.tory.nestedceiling.app.page.TransparentToolbarActivity
10 |
11 | class MainActivity : AppCompatActivity() {
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | setContentView(R.layout.activity_main)
15 | val btn1: Button = findViewById(R.id.btn1)
16 | val btn2: Button = findViewById(R.id.btn2)
17 | val btn3: Button = findViewById(R.id.btn3)
18 | btn1.setOnClickListener {
19 | val intent = Intent(this, NestedParentRecyclerViewActivity::class.java)
20 | intent.putExtra("isViewPager2", false)
21 | startActivity(intent)
22 | }
23 |
24 | btn2.setOnClickListener {
25 | val intent = Intent(this, NestedParentRecyclerViewActivity::class.java)
26 | intent.putExtra("isViewPager2", true)
27 | startActivity(intent)
28 | }
29 | btn3.setOnClickListener {
30 | startActivity(Intent(this, TransparentToolbarActivity::class.java))
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/adapter/BannerImageAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.adapter
2 |
3 | import android.util.Log
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.tory.nestedceiling.app.utils.MaterialColor
8 | import com.youth.banner.adapter.BannerAdapter
9 |
10 | class BannerImageAdapter(imageUrls: List) : BannerAdapter(imageUrls) {
11 |
12 |
13 | override fun onCreateHolder(parent: ViewGroup, viewType: Int): ImageHolder {
14 | val imageView = View(parent.context)
15 | val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
16 | imageView.layoutParams = params
17 | return ImageHolder(imageView)
18 | }
19 |
20 | override fun onBindView(holder: ImageHolder, data: MaterialColor, position: Int, size: Int) {
21 | Log.d("onBindView", data.name)
22 | holder.itemView.setBackgroundColor(data.color)
23 | }
24 |
25 |
26 | class ImageHolder(view: View) : RecyclerView.ViewHolder(view)
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/model/BannerData.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.model
2 |
3 | data class BannerData(val imageUrls: List = listOf(
4 | "https://hellorfimg.zcool.cn/provider_image/large/2238406784.jpg",
5 | "https://hellorfimg.zcool.cn/provider_image/large/2238407550.jpg",
6 | "https://hellorfimg.zcool.cn/provider_image/large/2238400920.jpg",
7 | "https://hellorfimg.zcool.cn/provider_image/large/2238389071.jpg"
8 | ))
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/model/LastViewPager2.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.model
2 |
3 | data class LastViewPager2(val type: Int = 1)
4 |
5 | data class LastViewPager(val type: Int = 1)
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/page/EmptyFragment.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.page
2 |
3 | import android.os.Bundle
4 | import android.view.*
5 | import androidx.appcompat.widget.AppCompatTextView
6 | import androidx.fragment.app.Fragment
7 |
8 | /**
9 | * - Author: xutao
10 | * - Date: 2021/9/16
11 | * - Email:
12 | * - Description:
13 | */
14 | class EmptyFragment: Fragment() {
15 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
16 | val textView = AppCompatTextView(requireContext())
17 | textView.gravity = Gravity.CENTER
18 | textView.text = "empty"
19 | textView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
20 | ViewGroup.LayoutParams.MATCH_PARENT)
21 | return textView
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/page/LinearINormalFragment.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.page
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.Fragment
8 | import com.tory.module_adapter.base.ItemSpace
9 | import com.tory.module_adapter.base.NormalModuleAdapter
10 | import com.tory.nestedceiling.app.utils.MaterialColor
11 | import com.tory.nestedceiling.app.utils.dp
12 | import com.tory.nestedceiling.app.views.ModuleNormalItemModel
13 | import com.tory.nestedceiling.app.views.ModuleNormalItemView
14 | import com.tory.nestedceiling.widget.NestedChildRecyclerView
15 |
16 | class LinearINormalFragment : Fragment() {
17 |
18 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
19 | val recyclerView = NestedChildRecyclerView(requireContext())
20 |
21 | val listAdapter = NormalModuleAdapter()
22 | val itemSpace = ItemSpace(spaceV = 12.dp(), edgeH = 16.dp())
23 | listAdapter.register(itemSpace = itemSpace) {
24 | ModuleNormalItemView(it.context)
25 | }
26 | recyclerView.adapter = listAdapter
27 | recyclerView.layoutManager = listAdapter.getGridLayoutManager(requireContext())
28 |
29 | listAdapter.setItems(MutableList(200) {
30 | ModuleNormalItemModel(MaterialColor.values()[it % MaterialColor.size])
31 | })
32 |
33 |
34 | return recyclerView
35 | }
36 |
37 | fun resetToTop() {
38 |
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/page/NestedParentRecyclerViewActivity.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.page
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import android.view.MenuItem
6 | import android.view.View
7 | import android.widget.ImageView
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.recyclerview.widget.RecyclerView
10 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
11 | import com.tory.nestedceiling.app.R
12 | import com.tory.module_adapter.base.NormalModuleAdapter
13 | import com.tory.module_adapter.views.ModuleEmptyModel
14 | import com.tory.nestedceiling.app.utils.MaterialColor
15 | import com.tory.nestedceiling.app.views.*
16 | import com.tory.nestedceiling.app.utils.dp
17 | import com.tory.nestedceiling.widget.NestedParentRecyclerView
18 | import com.tory.nestedceiling.widget.OnChildAttachStateListener
19 | import java.util.*
20 |
21 | class NestedParentRecyclerViewActivity : AppCompatActivity() {
22 |
23 |
24 | private val isViewPager2: Boolean
25 | get() = intent?.getBooleanExtra("isViewPager2", false) == true
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 | setContentView(R.layout.activity_nestedrecyclerview)
30 |
31 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
32 |
33 |
34 | val listAdapter = NormalModuleAdapter()
35 | listAdapter.register {
36 | ModuleBannerView(it.context)
37 | }
38 | listAdapter.register {
39 | ModuleNormalItemView(it.context)
40 | }
41 | listAdapter.register {
42 | LastViewPagerItemView(it.context)
43 | }
44 | listAdapter.register {
45 | LastViewPager2ItemView(it.context)
46 | }
47 |
48 |
49 |
50 | val swipeRefreshLayout = findViewById(R.id.swipe_refresh_layout)
51 | val recyclerView = findViewById(R.id.nested_rv)
52 | val topAnchor = findViewById(R.id.top_anchor)
53 | recyclerView.layoutManager = listAdapter.getGridLayoutManager(this)
54 | recyclerView.adapter = listAdapter
55 |
56 | swipeRefreshLayout.setOnRefreshListener {
57 | swipeRefreshLayout.postDelayed({ swipeRefreshLayout.isRefreshing = false }, 1000)
58 | }
59 | topAnchor.setOnClickListener {
60 | recyclerView.smoothScrollToPosition(0)
61 | }
62 |
63 |
64 | val data: MutableList = ArrayList()
65 | data.add(ModuleBannerModel())
66 | data.add(ModuleEmptyModel(height = 8.dp()))
67 | data.add(ModuleNormalItemModel(MaterialColor.random()))
68 | data.add(ModuleEmptyModel(height = 8.dp()))
69 | data.add(ModuleNormalItemModel(MaterialColor.random()))
70 | data.add(ModuleEmptyModel(height = 8.dp()))
71 | data.add(ModuleNormalItemModel(MaterialColor.random()))
72 | if (isViewPager2) {
73 | data.add(LastViewPager2Model())
74 | } else {
75 | data.add(LastViewPagerModel())
76 | }
77 | listAdapter.setItems(data)
78 |
79 | recyclerView.addOnChildAttachStateListener(object : OnChildAttachStateListener {
80 | override fun onChildDetachedFromTop() {
81 | topAnchor.visibility = View.GONE
82 | }
83 |
84 | override fun onChildAttachedToTop() {
85 | topAnchor.visibility = View.VISIBLE
86 | }
87 | })
88 |
89 | recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
90 | override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
91 | super.onScrollStateChanged(recyclerView, newState)
92 | Log.d("NestedCeilingEffect", "onScrollStateChanged parent newState: $newState")
93 | }
94 |
95 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
96 | super.onScrolled(recyclerView, dx, dy)
97 | //Log.d("NestedCeilingEffect", "onScrolled parent dy: $dy")
98 | }
99 |
100 | })
101 | }
102 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
103 | val id = item.itemId
104 | if (id == android.R.id.home) {
105 | onBackPressed()
106 | return true
107 | }
108 | return super.onOptionsItemSelected(item)
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/page/StaggeredFragment.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.page
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.Fragment
9 | import androidx.recyclerview.widget.RecyclerView
10 | import androidx.recyclerview.widget.StaggeredGridLayoutManager
11 | import com.tory.nestedceiling.app.views.ModuleStaggerItemModel
12 | import com.tory.nestedceiling.app.views.ModuleStaggerItemView
13 | import com.tory.module_adapter.base.ItemSpace
14 | import com.tory.module_adapter.base.NormalModuleAdapter
15 | import com.tory.nestedceiling.app.utils.dp
16 | import com.tory.nestedceiling.widget.NestedChildRecyclerView
17 |
18 | class StaggeredFragment : Fragment() {
19 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
20 | val recyclerView = NestedChildRecyclerView(requireContext())
21 |
22 | val listAdapter = NormalModuleAdapter()
23 | listAdapter.register (gridSize = 2, itemSpace = ItemSpace(spaceH = 10.dp(), spaceV = 8.dp(), edgeH = 8.dp())) {
24 | ModuleStaggerItemView(it.context)
25 | }
26 | recyclerView.adapter = listAdapter
27 | recyclerView.layoutManager = StaggeredGridLayoutManager(2, RecyclerView.VERTICAL)
28 |
29 | listAdapter.setItems(MutableList(200) {
30 | ModuleStaggerItemModel()
31 | })
32 |
33 | recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
34 | override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
35 | super.onScrollStateChanged(recyclerView, newState)
36 | Log.d("NestedCeilingEffect", "onScrollStateChanged child newState: $newState")
37 | }
38 |
39 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
40 | super.onScrolled(recyclerView, dx, dy)
41 | //Log.d("NestedCeilingEffect", "onScrolled child dy: $dy")
42 | }
43 |
44 | })
45 |
46 | return recyclerView
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/page/TransparentToolbarActivity.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.page
2 |
3 | import android.content.res.ColorStateList
4 | import android.graphics.Color
5 | import android.graphics.drawable.ColorDrawable
6 | import android.graphics.drawable.Drawable
7 | import android.os.Bundle
8 | import android.util.Log
9 | import android.view.View
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.appcompat.widget.Toolbar
12 | import androidx.core.graphics.drawable.DrawableCompat
13 | import androidx.core.view.*
14 | import androidx.recyclerview.widget.NestedPublicRecyclerView
15 | import androidx.recyclerview.widget.RecyclerView
16 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
17 | import com.google.android.material.animation.ArgbEvaluatorCompat
18 | import com.tory.nestedceiling.app.R
19 | import com.tory.nestedceiling.app.utils.*
20 | import com.tory.nestedceiling.app.views.*
21 | import com.tory.module_adapter.base.NormalModuleAdapter
22 | import com.tory.module_adapter.views.ModuleEmptyModel
23 | import com.tory.nestedceiling.widget.NestedParentRecyclerView
24 | import java.util.ArrayList
25 |
26 | /**
27 | * - Author: xutao
28 | * - Date: 2021/9/28
29 | * - Description:
30 | */
31 | class TransparentToolbarActivity: AppCompatActivity() {
32 |
33 | val toolbarBackground = ColorDrawable(Color.WHITE)
34 | var navigationIcon: Drawable? = null
35 |
36 | var scrollProgress: Float = -1f
37 |
38 | val toolbar: Toolbar by lazy { findViewById(R.id.toolbar)}
39 |
40 | var isLightBar: Boolean? = null
41 | set(value) {
42 | field = value
43 | SystemBarUtils.setStatusBarDarkMode(TransparentToolbarActivity@this, value == true)
44 | }
45 |
46 | override fun onCreate(savedInstanceState: Bundle?) {
47 | super.onCreate(savedInstanceState)
48 | setContentView(R.layout.activity_transparent_toolbar)
49 |
50 | setSupportActionBar(toolbar)
51 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
52 | toolbar.setNavigationOnClickListener {
53 | onBackPressed()
54 | }
55 | toolbar.background = toolbarBackground
56 | toolbar.navigationIcon?.mutate()?.let {
57 | navigationIcon = DrawableCompat.wrap(it)
58 | }
59 | toolbar.navigationIcon = navigationIcon
60 |
61 |
62 | toolbar.updatePadding(top = SystemBarUtils.getStatusBarHeight(this))
63 | SystemBarUtils.translucentStatusBar(this)
64 | val recyclerView = findViewById(R.id.recyclerView)
65 | toolbar.doOnPreDraw {
66 | Log.d("TransparentToolbar", "topOffset " + toolbar.measuredHeight)
67 | recyclerView.topOffset = toolbar.measuredHeight
68 | }
69 |
70 | val listAdapter = NormalModuleAdapter()
71 | listAdapter.register {
72 | ModuleBannerView(it.context)
73 | }
74 | listAdapter.register {
75 | ModuleNormalItemView(it.context)
76 | }
77 | listAdapter.register {
78 | LastViewPagerItemView(it.context)
79 | }
80 | listAdapter.register {
81 | LastViewPager2ItemView(it.context)
82 | }
83 |
84 | recyclerView.layoutManager = listAdapter.getGridLayoutManager(this)
85 | recyclerView.adapter = listAdapter
86 |
87 | val swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout)
88 | swipeRefreshLayout.setOnRefreshListener {
89 | swipeRefreshLayout.postDelayed({ swipeRefreshLayout.isRefreshing = false }, 1000)
90 | }
91 |
92 | val data: MutableList = ArrayList()
93 | data.add(ModuleBannerModel(height = 300.dp()))
94 | data.add(ModuleEmptyModel(height = 8.dp()))
95 | data.add(ModuleNormalItemModel(MaterialColor.random()))
96 | data.add(ModuleEmptyModel(height = 8.dp()))
97 | data.add(ModuleNormalItemModel(MaterialColor.random()))
98 | data.add(ModuleEmptyModel(height = 8.dp()))
99 | data.add(ModuleNormalItemModel(MaterialColor.random()))
100 | data.add(LastViewPager2Model())
101 |
102 | listAdapter.setItems(data)
103 |
104 | SystemBarUtils.setStatusBarDarkMode(TransparentToolbarActivity@this, false)
105 | onScrollProgressChanged(0f)
106 | initScroll(recyclerView)
107 | }
108 |
109 | private fun initScroll(recyclerView: RecyclerView) {
110 | recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
111 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
112 | super.onScrolled(recyclerView, dx, dy)
113 | Log.d("TransparentToolbar", "onScrolled dy: $dy")
114 | val firstChild: View? = recyclerView.getChildAt(0)
115 | val scrollFraction = if (firstChild != null && recyclerView.getChildLayoutPosition(firstChild) == 0) {
116 | val scrollRange = firstChild.height - toolbar.height
117 | val fraction = if (scrollRange > 0) (firstChild.bottom - toolbar.height).toFloat() / scrollRange else 0f
118 | Math.abs(1 - fraction).coerceAtLeast(0f).coerceAtMost(1f)
119 | } else 1f
120 | onScrollProgressChanged(scrollFraction)
121 | }
122 |
123 | override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
124 | super.onScrollStateChanged(recyclerView, newState)
125 | Log.d("NestedCeilingEffect", "onScrollStateChanged parent newState: $newState")
126 | }
127 | })
128 | }
129 |
130 | private fun onScrollProgressChanged(progress: Float) {
131 | if (scrollProgress == progress) return
132 | scrollProgress = progress
133 | val alpha = (0xFF * progress.coerceIn(0f, 1f)).toInt()
134 |
135 | toolbarBackground.alpha = alpha
136 | val resultColor = ArgbEvaluatorCompat.getInstance()
137 | .evaluate(progress, Color.WHITE, Color.BLACK)
138 | navigationIcon?.let { DrawableCompat.setTint(it, resultColor) }
139 | toolbar.setTitleTextColor(resultColor)
140 |
141 |
142 | val light = progress > 0.6
143 | if (light != isLightBar) { // 修改状态栏深浅
144 | isLightBar = light
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/utils/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.utils
2 |
3 | import android.content.Context
4 | import android.content.res.Resources
5 | import android.util.TypedValue
6 | import android.view.*
7 | import androidx.annotation.LayoutRes
8 |
9 | inline fun ViewGroup.inflate(@LayoutRes res: Int, attachRoot: Boolean = false): View {
10 | return LayoutInflater.from(context).inflate(res, this, attachRoot)
11 | }
12 |
13 | inline fun Int.dp(context: Context? = null): Int {
14 | val res = context?.resources ?: Resources.getSystem()
15 | return dp2px(res, this.toFloat())
16 | }
17 |
18 | inline fun Float.dp(context: Context? = null): Int {
19 | val res = context?.resources ?: Resources.getSystem()
20 | return dp2px(res, this)
21 | }
22 |
23 |
24 | fun dp2px(resources: Resources, dpVal: Float): Int {
25 | return TypedValue.applyDimension(
26 | TypedValue.COMPLEX_UNIT_DIP,
27 | dpVal, resources.displayMetrics
28 | ).toInt()
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/utils/MaterialColor.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.utils;
2 |
3 | import com.tory.nestedceiling.app.R;
4 |
5 |
6 | public enum MaterialColor {
7 | red(R.color.md_red_500,0xFFF44336),
8 | pink(R.color.md_pink_500, 0xFFE91E63),
9 | purple(R.color.md_purple_500, 0xFF9C27B0),
10 | deepPurple(R.color.md_deep_purple_500, 0xFF673AB7),
11 | indigo(R.color.md_indigo_500, 0xFF3F51B5),
12 | blue(R.color.md_blue_500, 0xFF2196F3),
13 | lightBlue(R.color.md_light_blue_500, 0xFF03A9F4),
14 | cyan(R.color.md_cyan_500, 0xFF00BCD4),
15 | teal(R.color.md_teal_500, 0xFF009688),
16 | green(R.color.md_green_500, 0xFF4CAF50),
17 | lightGreen(R.color.md_light_green_500, 0xFF8BC34A),
18 | lime(R.color.md_lime_500, 0xFFCDDC39),
19 | yellow(R.color.md_yellow_500, 0xFFFFEB3B),
20 | amber(R.color.md_amber_500, 0xFFFFC107),
21 | orange(R.color.md_orange_500, 0xFFFF9800),
22 | deepOrange(R.color.md_deep_orange_500, 0xFFFF5722),
23 | brown(R.color.md_brown_500, 0xFF795548),
24 | grey(R.color.md_grey_500, 0xFF9E9E9E),
25 | blueGrey(R.color.md_blue_grey_500, 0xFF607D8B);
26 |
27 | public static final int size = MaterialColor.values().length;
28 |
29 | public final int color;
30 | public final int resId;
31 |
32 | MaterialColor(int resId, int color) {
33 | this.color = color;
34 | this.resId = resId;
35 | }
36 |
37 |
38 | public static MaterialColor random() {
39 | return values()[(int)(Math.random() * size)];
40 | }
41 |
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/utils/SystemBarUtils.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.utils;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.Activity;
5 | import android.content.Context;
6 | import android.content.res.Configuration;
7 | import android.content.res.Resources;
8 | import android.graphics.Color;
9 | import android.os.Build;
10 | import android.util.TypedValue;
11 | import android.view.View;
12 | import android.view.ViewConfiguration;
13 | import android.view.Window;
14 | import android.view.WindowManager;
15 |
16 | import androidx.annotation.ColorInt;
17 | import androidx.annotation.NonNull;
18 | import androidx.annotation.Nullable;
19 |
20 | import com.tory.nestedceiling.app.R;import com.tory.nestedceiling.app.utils.SystemProperties;
21 |
22 | import java.lang.reflect.Field;
23 | import java.lang.reflect.Method;
24 |
25 | /**
26 | * Created by tao.xu2 on 2017/5/22.
27 | */
28 |
29 | public class SystemBarUtils {
30 |
31 | public static final String STATUS_BAR_HEIGHT_RES_NAME = "status_bar_height";
32 | public static final String NAV_BAR_HEIGHT_RES_NAME = "navigation_bar_height";
33 | public static final String NAV_BAR_HEIGHT_LANDSCAPE_RES_NAME = "navigation_bar_height_landscape";
34 | public static final String NAV_BAR_WIDTH_RES_NAME = "navigation_bar_width";
35 | public static final String SHOW_NAV_BAR_RES_NAME = "config_showNavigationBar";
36 |
37 | public static String sNavBarOverride;
38 | public static Boolean sHasNavbar;
39 | public static int sDarkModeFlag;
40 | public static Boolean sSupportNavbarDarkModeReflection;
41 |
42 | private static Boolean sMiuiDarkModSupport;
43 | private static Boolean sFlymeDarkModeSupport;
44 |
45 | static{
46 | String mv = SystemProperties.get("ro.miui.ui.version.name","");
47 | sMiuiDarkModSupport = "V6".equals(mv) || "V7".equals(mv) || "V8".equals(mv);
48 | //|| "V9".equals(mv);
49 |
50 | sNavBarOverride = SystemProperties.get("qemu.hw.mainkeys","");
51 | }
52 |
53 | /**
54 | * 透明状态栏
55 | * @param activity
56 | */
57 | public static void translucentStatusBar(@NonNull Activity activity){
58 | Window window = activity.getWindow();
59 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
60 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
61 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
62 | window.setStatusBarColor(Color.TRANSPARENT);
63 | int systemUiVisibility = window.getDecorView().getSystemUiVisibility() |
64 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
65 | window.getDecorView().setSystemUiVisibility(systemUiVisibility);
66 | } else {
67 | window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
68 | }
69 | }
70 |
71 | /**
72 | * 透明导航栏
73 | * @param window
74 | */
75 | public static void translucentNavBar(@NonNull Window window){
76 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
77 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
78 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
79 | int systemUiVisibility = window.getDecorView().getSystemUiVisibility() |
80 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
81 | window.getDecorView().setSystemUiVisibility(systemUiVisibility);
82 | window.setNavigationBarColor(Color.TRANSPARENT);
83 | } else {
84 | window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
85 | }
86 | }
87 |
88 |
89 | public static void translucentNavBar(@NonNull Activity activity){
90 | translucentNavBar(activity.getWindow());
91 | }
92 |
93 | public static boolean isSupportStatusBarDarkMode() {
94 | return sMiuiDarkModSupport || supportFlymeStatusBarDrakMode()
95 | || Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
96 | }
97 |
98 | public static void setStatusBarColor(@NonNull Activity activity, @ColorInt int color) {
99 | setStatusBarColor(activity.getWindow(), color);
100 | }
101 |
102 |
103 | public static void setStatusBarColor(@NonNull Window window, @ColorInt int color) {
104 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
105 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
106 | window.setStatusBarColor(color);
107 | }
108 |
109 | public static boolean setStatusBarDarkMode(@NonNull Activity activity, boolean darkMode){
110 | return setStatusBarDarkMode(activity.getWindow(), darkMode);
111 | }
112 |
113 | public static boolean setStatusBarDarkMode(@NonNull Window window, boolean darkMode) {
114 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
115 | if (sMiuiDarkModSupport) {
116 | return setMiuiStatusBarLightMode(window, darkMode);
117 | } else if (supportFlymeStatusBarDrakMode()) {
118 | return setFlymeStatusBarLightMode(window, darkMode);
119 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
120 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
121 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
122 | View decorView = window.getDecorView();
123 | int uiVisibility = decorView.getSystemUiVisibility();
124 | if(darkMode){
125 | uiVisibility |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
126 | }else{
127 | uiVisibility &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
128 | }
129 | decorView.setSystemUiVisibility(uiVisibility);
130 | return true;
131 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
132 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
133 | }
134 | }
135 | return false;
136 | }
137 |
138 | private static Boolean supportFlymeStatusBarDrakMode(){
139 | //flyme 6.0以上也是通过系统api切换statusbar的颜色
140 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
141 | return false;
142 | }
143 | if(sFlymeDarkModeSupport == null){
144 | try {
145 | Field darkFlag = WindowManager.LayoutParams.class
146 | .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
147 | darkFlag.setAccessible(true);
148 | int bit = darkFlag.getInt(null);
149 | sFlymeDarkModeSupport = bit != 0;
150 | } catch (Exception e) {
151 | sFlymeDarkModeSupport = false;
152 | }
153 | }
154 | return sFlymeDarkModeSupport;
155 | }
156 |
157 | /**
158 | * 设置状态栏字体图标为深色,需要MIUIV6以上
159 | *
160 | * @param window 需要设置的窗口
161 | * @param dark 是否把状态栏字体及图标颜色设置为深色
162 | * @return boolean 成功执行返回true
163 | */
164 | private static boolean setMiuiStatusBarLightMode(Window window, boolean dark) {
165 | if (window != null) {
166 | Class clazz = window.getClass();
167 | try {
168 | int darkModeFlag = 0;
169 | Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
170 | Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
171 | darkModeFlag = field.getInt(layoutParams);
172 | Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
173 | if (dark) {
174 | extraFlagField.invoke(window, darkModeFlag, darkModeFlag);//状态栏透明且黑色字体
175 | } else {
176 | extraFlagField.invoke(window, 0, darkModeFlag);//清除黑色字体
177 | }
178 | return true;
179 | } catch (Exception e) {
180 |
181 | }
182 | }
183 | return false;
184 | }
185 |
186 | /**
187 | * 设置状态栏图标为深色和魅族特定的文字风格
188 | * 可以用来判断是否为Flyme用户
189 | *
190 | * @param window 需要设置的窗口
191 | * @param dark 是否把状态栏字体及图标颜色设置为深色
192 | * @return boolean 成功执行返回true
193 | */
194 | private static boolean setFlymeStatusBarLightMode(Window window, boolean dark) {
195 | boolean result = false;
196 | if (window != null) {
197 | try {
198 | WindowManager.LayoutParams lp = window.getAttributes();
199 | Field darkFlag = WindowManager.LayoutParams.class
200 | .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
201 | Field meizuFlags = WindowManager.LayoutParams.class
202 | .getDeclaredField("meizuFlags");
203 | darkFlag.setAccessible(true);
204 | meizuFlags.setAccessible(true);
205 | int bit = darkFlag.getInt(null);
206 | int value = meizuFlags.getInt(lp);
207 | if (dark) {
208 | value |= bit;
209 | } else {
210 | value &= ~bit;
211 | }
212 | meizuFlags.setInt(lp, value);
213 | window.setAttributes(lp);
214 | result = true;
215 | } catch (Exception e) {
216 |
217 | }
218 | }
219 | return result;
220 | }
221 |
222 | public static void setNavigationBarDarkMode(@NonNull Activity activity, boolean darkMode){
223 | setNavigationBarDarkMode(activity.getWindow(), darkMode);
224 | }
225 |
226 | /**
227 | * 设置导致栏反色,支持
228 | * @param window
229 | * @param darkMode
230 | */
231 | public static void setNavigationBarDarkMode(@Nullable Window window, boolean darkMode){
232 | if(window == null || !hasNavBar(window.getContext())){
233 | return;
234 | }
235 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
236 | setNavigationBarDarkMode(window, darkMode, View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
237 | }else if(supportNavbarDarkModeReflection()){
238 | setNavigationBarDarkMode(window, darkMode, sDarkModeFlag);
239 | }
240 | }
241 |
242 | /**
243 | * 是否支持白色导航栏
244 | * @return
245 | */
246 | public static boolean supportNavbarDarkMode(){
247 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O || supportNavbarDarkModeReflection();
248 | }
249 |
250 | private static Boolean supportNavbarDarkModeReflection(){
251 | if(sSupportNavbarDarkModeReflection == null){
252 | try {
253 | Field filed = View.class.getField("SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR");
254 | sDarkModeFlag = filed.getInt(View.class);
255 | } catch (Exception e) {
256 | e.printStackTrace();
257 | sDarkModeFlag = 0;
258 | }
259 | sSupportNavbarDarkModeReflection = sDarkModeFlag != 0;
260 | }
261 | return sSupportNavbarDarkModeReflection;
262 | }
263 |
264 | private static void setNavigationBarDarkMode(@NonNull Window window, boolean darkMode, int darkModeFlag){
265 | View decorView = window.getDecorView();
266 | int uiVisibility = decorView.getSystemUiVisibility();
267 | if(darkMode){
268 | uiVisibility |= darkModeFlag;
269 | }else{
270 | uiVisibility &= ~darkModeFlag;
271 | }
272 | decorView.setSystemUiVisibility(uiVisibility);
273 | }
274 |
275 | public static Boolean hasNavBar(@NonNull Context context) {
276 | if (sHasNavbar != null) {
277 | return sHasNavbar;
278 | }
279 | Resources res = context.getResources();
280 | int resourceId = res.getIdentifier(SHOW_NAV_BAR_RES_NAME, "bool", "android");
281 | if (resourceId != 0) {
282 | boolean hasNav = res.getBoolean(resourceId);
283 | // check override flag (see static block)
284 | if ("1".equals(sNavBarOverride)) {
285 | hasNav = false;
286 | } else if ("0".equals(sNavBarOverride)) {
287 | hasNav = true;
288 | }
289 | sHasNavbar = hasNav;
290 | return sHasNavbar;
291 | } else { // fallback
292 | sHasNavbar = !ViewConfiguration.get(context).hasPermanentMenuKey();
293 | return sHasNavbar;
294 | }
295 | }
296 |
297 |
298 | /**
299 | * 获取状态栏的高度
300 | * @param context
301 | * @return
302 | */
303 | public static int getStatusBarHeight(Context context){
304 | return getInternalDimensionSize(context.getResources(), STATUS_BAR_HEIGHT_RES_NAME);
305 | }
306 |
307 | /**
308 | * 获取虚拟导航栏的高度
309 | * @param context
310 | * @return
311 | */
312 | @TargetApi(14)
313 | public static int getNavigationBarHeight(Context context) {
314 | Resources res = context.getResources();
315 | int result = 0;
316 | if (hasNavBar(context)) {
317 | String key;
318 | boolean mInPortrait = (res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT);
319 | if (mInPortrait) {
320 | key = NAV_BAR_HEIGHT_RES_NAME;
321 | } else {
322 | key = NAV_BAR_HEIGHT_LANDSCAPE_RES_NAME;
323 | }
324 | return getInternalDimensionSize(res, key);
325 | }
326 | return result;
327 | }
328 |
329 | /**
330 | * 获取虚拟导航栏的宽度
331 | * @param context
332 | * @return
333 | */
334 | @TargetApi(14)
335 | public static int getNavigationBarWidth(Context context) {
336 | Resources res = context.getResources();
337 | int result = 0;
338 | if (hasNavBar(context)) {
339 | return getInternalDimensionSize(res, NAV_BAR_WIDTH_RES_NAME);
340 | }
341 | return result;
342 | }
343 |
344 | /**
345 | * 获取一些系统隐藏的试题值
346 | * @param res
347 | * @param key
348 | * @return
349 | */
350 | public static int getInternalDimensionSize(Resources res, String key) {
351 | int resId = res.getIdentifier(key, "dimen", "android");
352 | int result = resId > 0 ? res.getDimensionPixelSize(resId) : 0;
353 | return result;
354 | }
355 |
356 | /**
357 | * actionbar的高度
358 | * @param context
359 | * @return
360 | */
361 | public static int getActionBarHeight(Context context) {
362 | TypedValue tv = new TypedValue();
363 | context.getTheme().resolveAttribute(R.attr.actionBarSize, tv, true);
364 | return TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());
365 | }
366 |
367 | public static void setRealFullUi(@NonNull Window window) {
368 | window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE
369 | // Set the content to appear under the system bars so that the
370 | // content doesn't resize when the system bars hide and show.
371 | | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
372 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
373 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
374 | // Hide the nav bar and status bar
375 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
376 | | View.SYSTEM_UI_FLAG_FULLSCREEN);
377 | }
378 |
379 | }
380 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/utils/SystemProperties.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.utils;
2 |
3 | import android.annotation.SuppressLint;
4 |
5 | import java.lang.reflect.Method;
6 |
7 | /**
8 | * Created by zero on 2018/3/13.
9 | *
10 | * @author zero
11 | */
12 |
13 | public class SystemProperties {
14 |
15 | private static Class> sClazz = null;
16 | private static Method sGetMethod = null;
17 |
18 |
19 | @SuppressLint("PrivateApi")
20 | private static Class> getClazz() throws ClassNotFoundException {
21 | return sClazz != null ? sClazz : Class.forName("android.os.SystemProperties");
22 | }
23 |
24 | @SuppressLint("PrivateApi")
25 | public static String get(String key, String defaultValue) {
26 | String value = defaultValue;
27 | try {
28 | Class> c = getClazz();
29 | if (sGetMethod == null){
30 | sGetMethod = c.getMethod("get", String.class, String.class);
31 | }
32 | value = (String) (sGetMethod.invoke(c, key, defaultValue));
33 | } catch (Exception e) {
34 | e.printStackTrace();
35 | }
36 | return value;
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/utils/TableInterpolatorHelper.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.utils
2 |
3 | import android.util.Log
4 | import android.view.animation.Interpolator
5 | import androidx.interpolator.view.animation.FastOutSlowInInterpolator
6 |
7 | /**
8 | * - Author: xutao
9 | * - Date: 2021/10/20
10 | * - Email: xutao@shizhuang-inc.com
11 | * - Description:
12 | */
13 | class TableInterpolatorHelper {
14 |
15 | val interpolator = FastOutSlowInInterpolator()
16 |
17 | private val tabValues: FloatArray
18 | val isEnable: Boolean
19 |
20 | init {
21 | tabValues = reflectValueArray(interpolator)
22 | isEnable = tabValues.isNotEmpty()
23 | }
24 |
25 |
26 | fun getInputByResult(value: Float) : Float {
27 | return if (isEnable) calInput(tabValues, value) else value
28 | }
29 |
30 | private fun reflectValueArray(interpolator: Interpolator): FloatArray {
31 | return try {
32 | val clazz = interpolator.javaClass
33 | val field = clazz.getDeclaredField("VALUES")
34 | field.isAccessible = true
35 | field.get(clazz) as FloatArray
36 | } catch (e: Exception) {
37 | Log.e("TableInterpolatorHelper", "reflectValueArray error $e")
38 | floatArrayOf()
39 | }
40 | }
41 |
42 |
43 | private fun binarySearch(array: FloatArray, value: Float): Int {
44 | var lo = 0
45 | var hi: Int = array.size - 1
46 |
47 | while (lo <= hi) {
48 | val mid = (lo + hi) ushr 1
49 | val midVal: Float = array[mid]
50 | if (midVal < value) {
51 | lo = mid + 1
52 | } else if (midVal > value) {
53 | hi = mid - 1
54 | } else {
55 | return mid // value found
56 | }
57 | }
58 | return lo
59 | }
60 |
61 |
62 | private fun calInput(values: FloatArray, value: Float): Float {
63 | if (value >= 1) {
64 | return 1f
65 | }
66 | if (value <= 0) {
67 | return 0f
68 | }
69 | val position = binarySearch(values, value)
70 | val d = values[position + 1] - values[position]
71 | if (d == 0f) {
72 | return values[position]
73 | }
74 | val weight = (value - values[position]) / d
75 | val stepSize = 1f / (values.size - 1)
76 | val diff = weight * stepSize
77 | val quantized: Float = position * stepSize
78 | return quantized + diff
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/views/LastViewPager2ItemView.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.views
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import androidx.fragment.app.*
6 | import androidx.viewpager2.adapter.FragmentStateAdapter
7 | import androidx.viewpager2.widget.ViewPager2
8 | import com.google.android.material.tabs.TabLayout
9 | import com.google.android.material.tabs.TabLayoutMediator
10 | import com.tory.module_adapter.views.AbsModuleView
11 | import com.tory.nestedceiling.app.R
12 | import com.tory.nestedceiling.app.page.*
13 | import com.tory.nestedceiling.widget.NestedCeilingHelper
14 | import java.util.ArrayList
15 |
16 | class LastViewPager2Model
17 |
18 | class LastViewPager2ItemView @JvmOverloads constructor(
19 | context: Context, attrs: AttributeSet? = null
20 | ) : AbsModuleView(context, attrs) {
21 |
22 | val tabLayout = findViewById(R.id.tab_layout)
23 | val viewPager = findViewById(R.id.view_pager)
24 |
25 | init {
26 | layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
27 | NestedCeilingHelper.setNestedChildContainerTag(this)
28 |
29 | val pagerAdapter = ViewPagerAdapter(context as FragmentActivity, getPageFragments())
30 | viewPager.adapter = pagerAdapter
31 |
32 | val labels = arrayOf("精选", "直播", "新品推荐", "Empty", "Empty", "Empty", "Empty", "Empty", "Empty", "Empty", "Empty" )
33 | TabLayoutMediator(tabLayout, viewPager) { tab, position ->
34 | tab.text = labels[position]
35 | }.attach()
36 | }
37 |
38 | private fun getPageFragments(): List {
39 | val data: MutableList = ArrayList()
40 | data.add(StaggeredFragment())
41 | data.add(StaggeredFragment())
42 | data.add(LinearINormalFragment())
43 | data.add(EmptyFragment())
44 | data.add(EmptyFragment())
45 | data.add(EmptyFragment())
46 | data.add(EmptyFragment())
47 | data.add(EmptyFragment())
48 | data.add(EmptyFragment())
49 | data.add(EmptyFragment())
50 | data.add(EmptyFragment())
51 | return data
52 | }
53 |
54 |
55 | override fun getLayoutId(): Int = R.layout.item_last_view_pager2
56 |
57 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
58 | super.onMeasure(widthMeasureSpec, NestedCeilingHelper.wrapContainerMeasureHeight(this, heightMeasureSpec))
59 | }
60 |
61 |
62 | private class ViewPagerAdapter(fragmentActivity: FragmentActivity,
63 | private val data: List)
64 | : FragmentStateAdapter(fragmentActivity) {
65 |
66 | override fun createFragment(position: Int): Fragment {
67 | return data[position]
68 | }
69 |
70 | override fun getItemCount(): Int {
71 | return data.size
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/views/LastViewPagerItemView.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.views
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import androidx.fragment.app.*
6 | import androidx.viewpager.widget.ViewPager
7 | import com.google.android.material.tabs.TabLayout
8 | import com.tory.module_adapter.views.AbsModuleView
9 | import com.tory.nestedceiling.app.R
10 | import com.tory.nestedceiling.app.page.*
11 | import com.tory.nestedceiling.widget.NestedCeilingHelper
12 | import java.util.ArrayList
13 |
14 | class LastViewPagerModel
15 |
16 | class LastViewPagerItemView @JvmOverloads constructor(
17 | context: Context, attrs: AttributeSet? = null
18 | ) : AbsModuleView(context, attrs) {
19 |
20 | val tabLayout = findViewById(R.id.tab_layout)
21 | val viewPager = findViewById(R.id.view_pager)
22 |
23 | init {
24 | layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
25 | NestedCeilingHelper.setNestedChildContainerTag(this)
26 |
27 | val labels = arrayOf("精选", "直播", "新品推荐", "empty")
28 | val pagerAdapter = ViewPagerAdapter(context as FragmentActivity, getPageFragments(), labels)
29 | viewPager.adapter = pagerAdapter
30 |
31 | tabLayout.setupWithViewPager(viewPager)
32 | }
33 |
34 | private fun getPageFragments(): List {
35 | val data: MutableList = ArrayList()
36 | data.add(StaggeredFragment())
37 | data.add(StaggeredFragment())
38 | data.add(LinearINormalFragment())
39 | data.add(EmptyFragment())
40 | return data
41 | }
42 |
43 |
44 | override fun getLayoutId(): Int = R.layout.item_last_view_pager
45 |
46 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
47 | super.onMeasure(widthMeasureSpec, NestedCeilingHelper.wrapContainerMeasureHeight(this, heightMeasureSpec))
48 | }
49 |
50 |
51 | private class ViewPagerAdapter(fragmentActivity: FragmentActivity,
52 | private val data: List,
53 | private val labels: Array)
54 | : FragmentStatePagerAdapter(fragmentActivity.supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
55 |
56 | override fun getPageTitle(position: Int): CharSequence {
57 | return labels[position]
58 | }
59 |
60 | override fun getCount(): Int = data.size
61 |
62 | override fun getItem(position: Int): Fragment = data[position]
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/views/ModuleBannerView.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.views
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import androidx.core.view.updateLayoutParams
6 | import com.tory.module_adapter.views.AbsModuleView
7 | import com.tory.nestedceiling.app.adapter.BannerImageAdapter
8 | import com.tory.nestedceiling.app.utils.MaterialColor
9 | import com.tory.nestedceiling.app.utils.dp
10 | import com.youth.banner.Banner
11 | import com.youth.banner.indicator.RectangleIndicator
12 |
13 | data class ModuleBannerModel(
14 | val list: List = MaterialColor.values().take(5),
15 | val height: Int = 150.dp()
16 | )
17 |
18 |
19 | class ModuleBannerView @JvmOverloads constructor(
20 | context: Context, attrs: AttributeSet? = null
21 | ) : AbsModuleView(context, attrs) {
22 |
23 |
24 | val banner = Banner(context)
25 | val bannerAdapter = BannerImageAdapter(emptyList())
26 |
27 | init {
28 | addView(banner, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
29 | banner.adapter = bannerAdapter
30 | banner.let {
31 | it.indicator = RectangleIndicator(banner.context)
32 | it.setBannerRound(20f)
33 | }
34 | }
35 |
36 | override fun onChanged(model: ModuleBannerModel) {
37 | super.onChanged(model)
38 | bannerAdapter.setDatas(model.list)
39 | updateLayoutParams {
40 | height = model.height
41 | }
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/views/ModuleNormalItemView.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.views
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import com.tory.module_adapter.views.AbsModuleView
6 | import com.tory.nestedceiling.app.utils.MaterialColor
7 | import com.tory.nestedceiling.app.utils.dp
8 |
9 | data class ModuleNormalItemModel(
10 | val color: MaterialColor
11 | )
12 |
13 |
14 | class ModuleNormalItemView @JvmOverloads constructor(
15 | context: Context, attrs: AttributeSet? = null
16 | ) : AbsModuleView(context, attrs) {
17 |
18 | init {
19 | layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, 100.dp())
20 | }
21 |
22 | override fun onChanged(model: ModuleNormalItemModel) {
23 | super.onChanged(model)
24 | setBackgroundColor(model.color.color)
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/tory/nestedceiling/app/views/ModuleStaggerItemView.kt:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.app.views
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import com.tory.module_adapter.base.groupPosition
6 | import com.tory.module_adapter.views.AbsModuleView
7 | import com.tory.nestedceiling.app.utils.MaterialColor
8 | import com.tory.nestedceiling.app.utils.dp
9 | import kotlin.random.Random
10 |
11 | class ModuleStaggerItemModel
12 |
13 | class ModuleStaggerItemView @JvmOverloads constructor(
14 | context: Context, attrs: AttributeSet? = null
15 | ) : AbsModuleView(context, attrs) {
16 |
17 | private val items = mutableListOf()
18 |
19 |
20 | init {
21 | val colorList = MaterialColor.values().toList().shuffled()
22 | for (color in colorList) {
23 | val height = Random.nextInt(200.dp(), 350.dp())
24 | items.add(StaggerItem(color, height))
25 | }
26 | }
27 |
28 | override fun onChanged(model: ModuleStaggerItemModel) {
29 | super.onChanged(model)
30 | val item = items[groupPosition % items.size]
31 | setBackgroundColor(item.color.color)
32 | val lp = layoutParams as MarginLayoutParams
33 | lp.height = item.height
34 | layoutParams = lp
35 | }
36 |
37 | class StaggerItem(
38 | val color: MaterialColor,
39 | val height: Int
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_back_top.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/app/src/main/res/drawable-xxhdpi/ic_back_top.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
25 |
26 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_nestedrecyclerview.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_transparent_toolbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_recycler_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_banner.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_last_view_pager.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
22 |
23 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_last_view_pager2.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
23 |
24 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_normal.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_stagger.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
--------------------------------------------------------------------------------
/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/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/android_material_design_palette.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #FFEBEE
5 | #FFCDD2
6 | #EF9A9A
7 | #E57373
8 | #EF5350
9 | #F44336
10 | #E53935
11 | #D32F2F
12 | #C62828
13 | #B71C1C
14 | #FF8A80
15 | #FF5252
16 | #FF1744
17 | #D50000
18 |
19 |
20 | #FCE4EC
21 | #F8BBD0
22 | #F48FB1
23 | #F06292
24 | #EC407A
25 | #E91E63
26 | #D81B60
27 | #C2185B
28 | #AD1457
29 | #880E4F
30 | #FF80AB
31 | #FF4081
32 | #F50057
33 | #C51162
34 |
35 |
36 | #F3E5F5
37 | #E1BEE7
38 | #CE93D8
39 | #BA68C8
40 | #AB47BC
41 | #9C27B0
42 | #8E24AA
43 | #7B1FA2
44 | #6A1B9A
45 | #4A148C
46 | #EA80FC
47 | #E040FB
48 | #D500F9
49 | #AA00FF
50 |
51 |
52 | #EDE7F6
53 | #D1C4E9
54 | #B39DDB
55 | #9575CD
56 | #7E57C2
57 | #673AB7
58 | #5E35B1
59 | #512DA8
60 | #4527A0
61 | #311B92
62 | #B388FF
63 | #7C4DFF
64 | #651FFF
65 | #6200EA
66 |
67 |
68 | #E8EAF6
69 | #C5CAE9
70 | #9FA8DA
71 | #7986CB
72 | #5C6BC0
73 | #3F51B5
74 | #3949AB
75 | #303F9F
76 | #283593
77 | #1A237E
78 | #8C9EFF
79 | #536DFE
80 | #3D5AFE
81 | #304FFE
82 |
83 |
84 | #E3F2FD
85 | #BBDEFB
86 | #90CAF9
87 | #64B5F6
88 | #42A5F5
89 | #2196F3
90 | #1E88E5
91 | #1976D2
92 | #1565C0
93 | #0D47A1
94 | #82B1FF
95 | #448AFF
96 | #2979FF
97 | #2962FF
98 |
99 |
100 | #E1F5FE
101 | #B3E5FC
102 | #81D4FA
103 | #4FC3F7
104 | #29B6F6
105 | #03A9F4
106 | #039BE5
107 | #0288D1
108 | #0277BD
109 | #01579B
110 | #80D8FF
111 | #40C4FF
112 | #00B0FF
113 | #0091EA
114 |
115 |
116 | #E0F7FA
117 | #B2EBF2
118 | #80DEEA
119 | #4DD0E1
120 | #26C6DA
121 | #00BCD4
122 | #00ACC1
123 | #0097A7
124 | #00838F
125 | #006064
126 | #84FFFF
127 | #18FFFF
128 | #00E5FF
129 | #00B8D4
130 |
131 |
132 | #E0F2F1
133 | #B2DFDB
134 | #80CBC4
135 | #4DB6AC
136 | #26A69A
137 | #009688
138 | #00897B
139 | #00796B
140 | #00695C
141 | #004D40
142 | #A7FFEB
143 | #64FFDA
144 | #1DE9B6
145 | #00BFA5
146 |
147 |
148 | #E8F5E9
149 | #C8E6C9
150 | #A5D6A7
151 | #81C784
152 | #66BB6A
153 | #4CAF50
154 | #43A047
155 | #388E3C
156 | #2E7D32
157 | #1B5E20
158 | #B9F6CA
159 | #69F0AE
160 | #00E676
161 | #00C853
162 |
163 |
164 | #F1F8E9
165 | #DCEDC8
166 | #C5E1A5
167 | #AED581
168 | #9CCC65
169 | #8BC34A
170 | #7CB342
171 | #689F38
172 | #558B2F
173 | #33691E
174 | #CCFF90
175 | #B2FF59
176 | #76FF03
177 | #64DD17
178 |
179 |
180 | #F9FBE7
181 | #F0F4C3
182 | #E6EE9C
183 | #DCE775
184 | #D4E157
185 | #CDDC39
186 | #C0CA33
187 | #AFB42B
188 | #9E9D24
189 | #827717
190 | #F4FF81
191 | #EEFF41
192 | #C6FF00
193 | #AEEA00
194 |
195 |
196 | #FFFDE7
197 | #FFF9C4
198 | #FFF59D
199 | #FFF176
200 | #FFEE58
201 | #FFEB3B
202 | #FDD835
203 | #FBC02D
204 | #F9A825
205 | #F57F17
206 | #FFFF8D
207 | #FFFF00
208 | #FFEA00
209 | #FFD600
210 |
211 |
212 | #FFF8E1
213 | #FFECB3
214 | #FFE082
215 | #FFD54F
216 | #FFCA28
217 | #FFC107
218 | #FFB300
219 | #FFA000
220 | #FF8F00
221 | #FF6F00
222 | #FFE57F
223 | #FFD740
224 | #FFC400
225 | #FFAB00
226 |
227 |
228 | #FFF3E0
229 | #FFE0B2
230 | #FFCC80
231 | #FFB74D
232 | #FFA726
233 | #FF9800
234 | #FB8C00
235 | #F57C00
236 | #EF6C00
237 | #E65100
238 | #FFD180
239 | #FFAB40
240 | #FF9100
241 | #FF6D00
242 |
243 |
244 | #FBE9E7
245 | #FFCCBC
246 | #FFAB91
247 | #FF8A65
248 | #FF7043
249 | #FF5722
250 | #F4511E
251 | #E64A19
252 | #D84315
253 | #BF360C
254 | #FF9E80
255 | #FF6E40
256 | #FF3D00
257 | #DD2C00
258 |
259 |
260 | #EFEBE9
261 | #D7CCC8
262 | #BCAAA4
263 | #A1887F
264 | #8D6E63
265 | #795548
266 | #6D4C41
267 | #5D4037
268 | #4E342E
269 | #3E2723
270 |
271 |
272 | #FAFAFA
273 | #F5F5F5
274 | #EEEEEE
275 | #E0E0E0
276 | #BDBDBD
277 | #9E9E9E
278 | #757575
279 | #616161
280 | #424242
281 | #212121
282 |
283 |
284 | #ECEFF1
285 | #CFD8DC
286 | #B0BEC5
287 | #90A4AE
288 | #78909C
289 | #607D8B
290 | #546E7A
291 | #455A64
292 | #37474F
293 | #263238
294 |
295 |
296 | #000000
297 |
298 |
299 | #FFFFFF
300 |
301 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | NestedCeilingEffect
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/test/java/com/tory/nestedceilingeffect/app/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceilingeffect.app;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 |
5 | ext.kotlin_version = '1.5.20'
6 | repositories {
7 | google()
8 | mavenCentral()
9 |
10 | }
11 | dependencies {
12 | classpath 'com.android.tools.build:gradle:4.2.0'
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 |
15 |
16 | // NOTE: Do not place your application dependencies here; they belong
17 | // in the individual module build.gradle files
18 | }
19 | }
20 |
21 | allprojects {
22 | repositories {
23 | google()
24 | mavenCentral()
25 | jcenter()
26 | mavenLocal()
27 | maven { url "https://jitpack.io" }
28 | }
29 | }
30 |
31 | task clean(type: Delete) {
32 | delete rootProject.buildDir
33 | }
34 |
--------------------------------------------------------------------------------
/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 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 |
21 | # http
22 | systemProp.http.proxyHost=127.0.0.1
23 | systemProp.http.proxyPort=4780
24 | # https
25 | systemProp.https.proxyHost=127.0.0.1
26 | systemProp.https.proxyPort=4780
27 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Oct 21 13:54:04 CST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/nestedceiling/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/nestedceiling/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 30
5 | buildToolsVersion "29.0.3"
6 |
7 | defaultConfig {
8 | minSdkVersion 18
9 | targetSdkVersion 30
10 | versionCode 1
11 | versionName "1.0"
12 |
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles 'consumer-rules.pro'
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 |
24 | }
25 |
26 | dependencies {
27 | implementation fileTree(dir: 'libs', include: ['*.jar'])
28 | implementation 'androidx.recyclerview:recyclerview:1.1.0'
29 | }
30 |
--------------------------------------------------------------------------------
/nestedceiling/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/nestedceiling/consumer-rules.pro
--------------------------------------------------------------------------------
/nestedceiling/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 |
--------------------------------------------------------------------------------
/nestedceiling/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/nestedceiling/src/main/java/androidx/recyclerview/widget/NestedPublicRecyclerView.java:
--------------------------------------------------------------------------------
1 | package androidx.recyclerview.widget;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.util.Log;
6 | import android.widget.OverScroller;
7 |
8 | import androidx.annotation.NonNull;
9 | import androidx.annotation.Nullable;
10 |
11 | import com.tory.nestedceiling.widget.NestedCeilingHelper;
12 |
13 | import java.lang.reflect.Field;
14 |
15 | /**
16 | * - Author: xutao
17 | * - Date: 2021/9/16
18 | * - Description:
19 | */
20 | public class NestedPublicRecyclerView extends RecyclerView {
21 |
22 | public static final String TAG = "NestedPublicRecycler";
23 |
24 | private String mTag = TAG;
25 |
26 | /**
27 | * Vivo部分机型偶现报错
28 | * java.lang.ArrayIndexOutOfBoundsException: length=101; index=-2147483648
29 | * at android.widget.OverScroller$SplineOverScroller.update(OverScroller.java:1690)
30 | * at android.widget.OverScroller.computeScrollOffset(OverScroller.java:776)
31 | * at androidx.recyclerview.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:5273)
32 | * at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1137)
33 | */
34 | class MViewFlinger extends ViewFlinger {
35 |
36 | @Override
37 | public void run() {
38 | try {
39 | super.run();
40 | } catch (ArrayIndexOutOfBoundsException e) {
41 | Log.w(mTag, "", e);
42 | }
43 | }
44 | }
45 |
46 | public NestedPublicRecyclerView(@NonNull Context context) {
47 | super(context);
48 | init();
49 | }
50 |
51 | public NestedPublicRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
52 | super(context, attrs);
53 | init();
54 | }
55 |
56 | public NestedPublicRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
57 | super(context, attrs, defStyleAttr);
58 | init();
59 | }
60 |
61 | private void init() {
62 | mTag = TAG + "@" + Integer.toHexString(hashCode());
63 | try {
64 | Field field = RecyclerView.class.getDeclaredField("mViewFlinger");
65 | field.setAccessible(true);
66 | field.set(this, new MViewFlinger());
67 | Log.i(mTag, "MViewFlinger set success");
68 | } catch (Exception e) {
69 | Log.w(mTag, "MViewFlinger set fail", e);
70 | }
71 | }
72 |
73 | @Override
74 | void scrollStep(int dx, int dy, @Nullable int[] consumed) {
75 | super.scrollStep(dx, dy, consumed);
76 | }
77 |
78 | public void doScrollConsumed(int dx, int dy, @NonNull int[] consumed) {
79 | consumed[0] = 0;
80 | consumed[1] = 1;
81 | scrollStep(dx, dy, consumed);
82 | int consumedX = consumed[0];
83 | int consumedY = consumed[1];
84 | if (consumedX != 0 || consumedY != 0) {
85 | // 分发滚动状态
86 | dispatchOnScrolled(consumedX, consumedY);
87 | }
88 | }
89 |
90 | @Nullable
91 | public OverScroller getFlingOverScroll() {
92 | return mViewFlinger.mOverScroller;
93 | }
94 |
95 | /**
96 | * 是否正在Fling
97 | *
98 | * @return
99 | */
100 | public boolean isFling() {
101 | OverScroller overScroller = getFlingOverScroll();
102 | return overScroller != null && !overScroller.isFinished();
103 | }
104 |
105 | /**
106 | * 是否使用自带的Fling
107 | *
108 | * @return
109 | */
110 | public boolean enableOverScrollFling() {
111 | return getFlingOverScroll() != null;
112 | }
113 |
114 | /**
115 | * 更新滚动状态
116 | *
117 | * @param scrollState
118 | */
119 | public void updateScrollState(int scrollState) {
120 | setScrollState(scrollState);
121 | }
122 |
123 | /**
124 | * 停止滚动,但不更新状态
125 | */
126 | public void stopScrollWithoutState() {
127 | mViewFlinger.stop();
128 | LayoutManager layout = getLayoutManager();
129 | if (layout != null) {
130 | layout.stopSmoothScroller();
131 | }
132 | }
133 |
134 | /**
135 | * Fling到边缘时回调
136 | *
137 | * @param velocityX
138 | * @param velocityY
139 | */
140 | @Override
141 | void absorbGlows(int velocityX, int velocityY) {
142 | //super.absorbGlows(velocityX, velocityY);
143 | if (NestedCeilingHelper.DEBUG) {
144 | Log.d(mTag, "absorbGlows velocityY:" + velocityY);
145 | }
146 | onFlingEnd(velocityX, velocityY);
147 | }
148 |
149 | /**
150 | * Fling到边缘时回调
151 | *
152 | * @param velocityX
153 | * @param velocityY
154 | */
155 | protected void onFlingEnd(int velocityX, int velocityY) {
156 |
157 | }
158 |
159 | @Override
160 | public boolean canScrollVertically(int direction) {
161 | final int offset = computeVerticalScrollOffset();
162 | final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
163 | if (range == 0) return false;
164 | if (direction < 0) {
165 | return offset > 0;
166 | } else {
167 | // tory fix: 这一个像素最导致慢滑动时判断是否滑动到顶部判断出错
168 | //return offset < range - 1;
169 | return offset < range;
170 | }
171 | }
172 | }
--------------------------------------------------------------------------------
/nestedceiling/src/main/java/com/tory/nestedceiling/widget/FindTarget.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.widget;
2 |
3 | import android.view.MotionEvent;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 |
7 | import androidx.annotation.Nullable;
8 |
9 | import com.tory.nestedceiling.widget.NestedChildRecyclerView;
10 |
11 | public final class FindTarget {
12 |
13 | private static final int[] sTempLocation = new int[2];
14 |
15 | /**
16 | * 查找嵌套的子RecyclerView
17 | * @param sourceView
18 | * @return
19 | */
20 | @Nullable
21 | public static NestedChildRecyclerView findChildScrollTarget(@Nullable View sourceView) {
22 | if(sourceView == null || sourceView.getVisibility() != View.VISIBLE) {
23 | return null;
24 | }
25 | if (sourceView instanceof NestedChildRecyclerView) {
26 | return (NestedChildRecyclerView) sourceView;
27 | }
28 | if (!(sourceView instanceof ViewGroup)) {
29 | return null;
30 | }
31 | ViewGroup contentView = (ViewGroup) sourceView;
32 | int childCount = contentView.getChildCount();
33 | for (int i = childCount - 1; i >= 0; i--) {
34 | View view = contentView.getChildAt(i);
35 | int centerX = (view.getLeft() + view.getRight()) / 2;
36 | int contentLeft = contentView.getScrollX();
37 | if (centerX <= contentLeft || centerX >= contentLeft + contentView.getWidth()) {
38 | continue;
39 | }
40 | NestedChildRecyclerView target = findChildScrollTarget(view);
41 | if(target != null){
42 | return target;
43 | }
44 | }
45 | return null;
46 | }
47 |
48 |
49 | public static boolean isTouchPointInView(@Nullable View view, @Nullable MotionEvent event) {
50 | if (view == null || event == null) {
51 | return false;
52 | }
53 | view.getLocationOnScreen(sTempLocation);
54 | int left = sTempLocation[0];
55 | int top = sTempLocation[1];
56 | int right = left + view.getWidth();
57 | int bottom = top + view.getHeight();
58 | int x = (int) event.getRawX();
59 | int y = (int) event.getRawY();
60 | return x > left && x < right && y > top && y < bottom;
61 | }
62 | }
--------------------------------------------------------------------------------
/nestedceiling/src/main/java/com/tory/nestedceiling/widget/FlingHelper.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.widget;
2 |
3 | import android.content.Context;
4 | import android.view.ViewConfiguration;
5 |
6 | public final class FlingHelper {
7 | private static final float DECELERATION_RATE = ((float) (Math.log(0.78d) / Math.log(0.9d)));
8 | private static final float mFlingFriction = ViewConfiguration.getScrollFriction();
9 | private static float mPhysicalCoeff;
10 |
11 | public FlingHelper(Context context) {
12 | this(context, 0.6f);
13 | }
14 |
15 | public FlingHelper(Context context, float factor) {
16 | mPhysicalCoeff = context.getResources().getDisplayMetrics().density * 160.0f * 386.0878f * 0.84f;
17 | }
18 |
19 | private double getSplineDeceleration(int i) {
20 | return Math.log((0.35f * ((float) Math.abs(i))) / (mFlingFriction * mPhysicalCoeff));
21 | }
22 |
23 | private double getSplineDecelerationByDistance(double d) {
24 | return ((((double) DECELERATION_RATE) - 1.0d) * Math.log(d / ((double) (mFlingFriction * mPhysicalCoeff)))) / ((double) DECELERATION_RATE);
25 | }
26 |
27 | public double getSplineFlingDistance(int i) {
28 | return Math.exp(getSplineDeceleration(i) * (((double) DECELERATION_RATE) / (((double) DECELERATION_RATE) - 1.0d))) * ((double) (mFlingFriction * mPhysicalCoeff));
29 | }
30 |
31 | public int getVelocityByDistance(double d) {
32 | return Math.abs((int) (((Math.exp(getSplineDecelerationByDistance(d)) * ((double) mFlingFriction)) * ((double) mPhysicalCoeff)) / 0.3499999940395355d));
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/nestedceiling/src/main/java/com/tory/nestedceiling/widget/NestedCeilingHelper.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.widget;
2 |
3 | import android.view.View;
4 | import android.view.ViewGroup;
5 | import android.view.ViewParent;
6 |
7 | import androidx.annotation.NonNull;
8 |
9 | import com.tory.nestedceiling.R;
10 |
11 | /**
12 | * - Author: xutao
13 | * - Date: 2021/9/18
14 | * - Description:
15 | */
16 | public class NestedCeilingHelper {
17 | /**
18 | * Log开关
19 | */
20 | public static boolean DEBUG = false;
21 |
22 | // 使用自带的overScroll来传递fling事件
23 | public static final boolean USE_OVER_SCROLL = true;
24 |
25 | /**
26 | * 将标记为
27 | * @param view
28 | */
29 | public static void setNestedChildContainerTag(@NonNull View view) {
30 | view.setTag(R.id.nested_child_item_container, Boolean.TRUE);
31 | }
32 |
33 | /**
34 | * 判断是否是包裹类
35 | * @param view
36 | * @return
37 | */
38 | public static boolean isNestedChildContainerTag(@NonNull View view) {
39 | return view.getTag(R.id.nested_child_item_container) == Boolean.TRUE
40 | || view instanceof NestedChildItemContainer;
41 | }
42 |
43 | /**
44 | * 重写高度
45 | * @param view
46 | * @param heightSpec
47 | * @return
48 | */
49 | public static int wrapContainerMeasureHeight(@NonNull View view, int heightSpec) {
50 | ViewParent parent = view.getParent();
51 | if (!(parent instanceof NestedParentRecyclerView)) {
52 | return heightSpec;
53 | }
54 | if (View.MeasureSpec.getMode(heightSpec) == View.MeasureSpec.UNSPECIFIED) {
55 | throw new IllegalStateException(view + " must be exactly height, layoutParam must be MATCH_PARENT");
56 | }
57 |
58 | NestedParentRecyclerView parentView = (NestedParentRecyclerView) parent;
59 | int parentHeight = View.MeasureSpec.getSize(heightSpec);
60 | int parentTopOffset = parentView.getTopOffset();
61 | final int newHeightSpec;
62 | if (parentHeight > parentTopOffset) {
63 | newHeightSpec = View.MeasureSpec.makeMeasureSpec(parentHeight - parentTopOffset, View.MeasureSpec.EXACTLY);
64 | } else {
65 | newHeightSpec = View.MeasureSpec.makeMeasureSpec(parentHeight, View.MeasureSpec.EXACTLY);
66 | }
67 | return newHeightSpec;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/nestedceiling/src/main/java/com/tory/nestedceiling/widget/NestedChildItemContainer.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.widget;
2 |
3 | /**
4 | * - Author: xutao
5 | * - Date: 2021/9/16
6 | * - Email:
7 | * - Description:
8 | */
9 | public interface NestedChildItemContainer {
10 | }
11 |
--------------------------------------------------------------------------------
/nestedceiling/src/main/java/com/tory/nestedceiling/widget/NestedChildRecyclerView.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.widget;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 |
6 | import androidx.annotation.NonNull;
7 | import androidx.annotation.Nullable;
8 | import androidx.recyclerview.widget.NestedPublicRecyclerView;
9 |
10 | /**
11 | * 嵌套滑动子View,一定要继承该View
12 | */
13 | public class NestedChildRecyclerView extends NestedPublicRecyclerView implements NestedChildItemContainer {
14 |
15 | public NestedChildRecyclerView(@NonNull Context context) {
16 | this(context, null);
17 | }
18 |
19 | public NestedChildRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
20 | this(context, attrs, 0);
21 | }
22 |
23 | public NestedChildRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
24 | super(context, attrs, defStyleAttr);
25 | setNestedScrollingEnabled(true);
26 | setOverScrollMode(OVER_SCROLL_ALWAYS);
27 | }
28 |
29 | @Override
30 | public void setNestedScrollingEnabled(boolean enabled) {
31 | if (!enabled) {
32 | // throw new IllegalArgumentException("NestedChildRecyclerView must be enable nested scrolling!!");
33 | }
34 | super.setNestedScrollingEnabled(enabled);
35 | }
36 |
37 | @Override
38 | public void setOverScrollMode(int overScrollMode) {
39 | if (overScrollMode == OVER_SCROLL_NEVER) {
40 | // throw new IllegalArgumentException("NestedChildRecyclerView must be OVER_SCROLL_ALWAYS!!");
41 | }
42 | super.setOverScrollMode(overScrollMode);
43 | }
44 |
45 | @Override
46 | protected void onMeasure(int widthSpec, int heightSpec) {
47 | super.onMeasure(widthSpec, heightSpec);
48 | }
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/nestedceiling/src/main/java/com/tory/nestedceiling/widget/NestedParentRecyclerView.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.widget;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.util.AttributeSet;
6 | import android.util.Log;
7 | import android.view.MotionEvent;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 | import android.widget.OverScroller;
11 |
12 | import androidx.annotation.NonNull;
13 | import androidx.annotation.Nullable;
14 | import androidx.core.view.NestedScrollingParent2;
15 | import androidx.core.view.NestedScrollingParent3;
16 | import androidx.core.view.NestedScrollingParentHelper;
17 | import androidx.core.view.ViewCompat;
18 | import androidx.recyclerview.widget.NestedPublicRecyclerView;
19 | import androidx.recyclerview.widget.RecyclerView;
20 |
21 | import java.util.ArrayList;
22 |
23 | import com.tory.nestedceiling.R;
24 |
25 | /**
26 | * 嵌套滑动的父View
27 | */
28 | public class NestedParentRecyclerView extends NestedPublicRecyclerView implements NestedScrollingParent3, NestedScrollingParent2 {
29 | public final static String TAG = "NestedParentRecycler";
30 |
31 | private NestedScrollingParentHelper mParentHelper;
32 | private FlingHelper mFlingHelper;
33 | private ViewGroup mContentView;
34 | private int mTotalDy = 0;
35 | // 记录y轴加速度
36 | private int mVelocityY = 0;
37 | private int mActivePointerId;
38 | private Float mLastY = 0f;
39 |
40 | private boolean mIsStartChildFling = false;
41 | private boolean mIsChildAttachedToTop = false;
42 | private boolean mIsChildDetachedFromTop = true;
43 | private final ArrayList mOnChildAttachStateListeners = new ArrayList<>();
44 |
45 | private final int[] mTempConsumed = new int[2];
46 | private final int[] mNestedScrollingV2ConsumedCompat = new int[2];
47 |
48 | private int mTopOffset = 0;
49 |
50 | private boolean mIsChildNestedScrolling = false;
51 |
52 | private int[] mTempLocation = new int[2];
53 |
54 | public NestedParentRecyclerView(@NonNull Context context) {
55 | this(context, null);
56 | }
57 |
58 | public NestedParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
59 | this(context, attrs, 0);
60 | }
61 |
62 | public NestedParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
63 | super(context, attrs, defStyleAttr);
64 | setup();
65 | }
66 |
67 | private void setup() {
68 | mParentHelper = new NestedScrollingParentHelper(this);
69 | mFlingHelper = new FlingHelper(getContext());
70 | setOverScrollMode(OVER_SCROLL_ALWAYS);
71 | }
72 |
73 | @Override
74 | public void setOverScrollMode(int overScrollMode) {
75 | if (overScrollMode == OVER_SCROLL_NEVER) {
76 | // throw new IllegalArgumentException("NestedChildRecyclerView must be OVER_SCROLL_ALWAYS!!");
77 | }
78 | super.setOverScrollMode(overScrollMode);
79 | }
80 |
81 | public int getTopOffset() {
82 | return mTopOffset;
83 | }
84 |
85 | /**
86 | * 距离嵌套滑动距离顶部应该流出的空间
87 | *
88 | * @param topOffset
89 | */
90 | public void setTopOffset(int topOffset) {
91 | this.mTopOffset = topOffset;
92 | if (mContentView != null) {
93 | mContentView.requestLayout();
94 | }
95 | }
96 |
97 | public void addOnChildAttachStateListener(OnChildAttachStateListener listener) {
98 | mOnChildAttachStateListeners.add(listener);
99 | }
100 |
101 | protected boolean isTargetContainer(View child) {
102 | return NestedCeilingHelper.isNestedChildContainerTag(child);
103 | }
104 |
105 | @Override
106 | public void onChildAttachedToWindow(@NonNull View child) {
107 | if (isTargetContainer(child)) {
108 | mContentView = (ViewGroup) child;
109 | ViewGroup.LayoutParams lp = child.getLayoutParams();
110 | if (lp == null) {
111 | lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
112 | ViewGroup.LayoutParams.MATCH_PARENT);
113 | }
114 | if (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
115 | lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
116 | }
117 | if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
118 | lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
119 | }
120 | child.setLayoutParams(lp);
121 | if (NestedCeilingHelper.DEBUG) {
122 | log("onChildAttachedToWindow....");
123 | }
124 | }
125 | }
126 |
127 | @Override
128 | public void onChildDetachedFromWindow(@NonNull View child) {
129 | if (child == mContentView) {
130 | mContentView = null;
131 | log("onChildDetachedFromWindow....");
132 | }
133 | }
134 |
135 | @Override
136 | public boolean onInterceptTouchEvent(MotionEvent e) {
137 | boolean isTouchInContent = (mContentView != null) && (e.getY() > mContentView.getTop())
138 | && (e.getY() < mContentView.getBottom());
139 | View targetView = isTouchInContent ? FindTarget.findChildScrollTarget(mContentView) : null;
140 | boolean isTouchInChildArea = false;
141 | if (targetView != null) {
142 | targetView.getLocationOnScreen(mTempLocation);
143 | int left = mTempLocation[0];
144 | int top = mTempLocation[1];
145 | int right = left + targetView.getWidth();
146 | int bottom = top + targetView.getHeight();
147 | float x = e.getRawX();
148 | float y = e.getRawY();
149 | isTouchInChildArea = x >= left && x <= right && y >= top && y <= bottom;
150 | }
151 | // 此控件滑动到底部或者触摸区域在子嵌套布局不拦截事件
152 | if (isTouchInChildArea) {
153 | if (getScrollState() == SCROLL_STATE_SETTLING) {
154 | // 上划fling过程中,停止,否则会抖动
155 | stopScroll();
156 | }
157 | return false;
158 | }
159 | return super.onInterceptTouchEvent(e);
160 | }
161 |
162 | @SuppressLint("ClickableViewAccessibility")
163 | @Override
164 | public boolean onTouchEvent(MotionEvent e) {
165 | final int action = e.getActionMasked();
166 | switch (action) {
167 | case MotionEvent.ACTION_DOWN:
168 | mLastY = e.getY();
169 | mActivePointerId = e.getPointerId(0);
170 | mVelocityY = 0;
171 | stopScroll();
172 | break;
173 | case MotionEvent.ACTION_POINTER_DOWN:
174 | final int index = e.getActionIndex();
175 | mLastY = e.getY(index);
176 | mActivePointerId = e.getPointerId(index);
177 | break;
178 | case MotionEvent.ACTION_POINTER_UP:
179 | onSecondaryPointerUp(e);
180 | break;
181 | case MotionEvent.ACTION_MOVE:
182 | final int activePointerIndex = e.findPointerIndex(mActivePointerId);
183 | if (activePointerIndex == -1) {
184 | log("Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
185 | break;
186 | }
187 | final float y = e.getY(activePointerIndex);
188 | if (isScrollEnd()) {
189 | // 如果此控件已经滑动到底部,需要让子嵌套布局滑动剩余的距离
190 | // 或者子嵌套布局向下还未到顶部,也需要让子嵌套布局先滑动一段距离
191 | NestedChildRecyclerView child = FindTarget.findChildScrollTarget(mContentView);
192 | if (child != null) {
193 | int deltaY = (int) (mLastY - y);
194 | mTempConsumed[1] = 0;
195 | child.doScrollConsumed(0, deltaY, mTempConsumed);
196 | int consumedY = mTempConsumed[1];
197 | if (consumedY != 0 && NestedCeilingHelper.DEBUG) {
198 | log("onTouch scroll consumed: " + consumedY);
199 | }
200 | }
201 | }
202 | mLastY = y;
203 | break;
204 | }
205 |
206 | return super.onTouchEvent(e);
207 | }
208 |
209 | private void onSecondaryPointerUp(MotionEvent ev) {
210 | final int pointerIndex = ev.getActionIndex();
211 | final int pointerId = ev.getPointerId(pointerIndex);
212 | if (pointerId == mActivePointerId) {
213 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
214 | mLastY = ev.getY(newPointerIndex);
215 | mActivePointerId = ev.getPointerId(newPointerIndex);
216 | }
217 | }
218 |
219 | private boolean isScrollEnd() {
220 | return !canScrollVertically(1);
221 | }
222 |
223 | private boolean isChildScrollTop(RecyclerView child) {
224 | return !child.canScrollVertically(-1);
225 | }
226 |
227 | @Override
228 | public void onScrolled(int dx, int dy) {
229 | if (mIsStartChildFling) {
230 | mTotalDy = 0;
231 | mIsStartChildFling = false;
232 | }
233 | mTotalDy += dy;
234 | boolean attached = dy > 0 && isScrollEnd();
235 | if (attached && mIsChildDetachedFromTop) {
236 | mIsChildAttachedToTop = true;
237 | mIsChildDetachedFromTop = false;
238 | final int listenerCount = mOnChildAttachStateListeners.size();
239 | for (int i = 0; i < listenerCount; i++) {
240 | OnChildAttachStateListener listener = mOnChildAttachStateListeners.get(i);
241 | listener.onChildAttachedToTop();
242 | }
243 | }
244 | boolean detached = dy < 0 && !isScrollEnd();
245 | if (detached && mIsChildAttachedToTop) {
246 | RecyclerView child = FindTarget.findChildScrollTarget(mContentView);
247 | if (child == null || isChildScrollTop(child)) {
248 | mIsChildDetachedFromTop = true;
249 | mIsChildAttachedToTop = false;
250 | final int listenerCount = mOnChildAttachStateListeners.size();
251 | for (int i = 0; i < listenerCount; i++) {
252 | OnChildAttachStateListener listener = mOnChildAttachStateListeners.get(i);
253 | listener.onChildDetachedFromTop();
254 | }
255 | }
256 | }
257 | }
258 |
259 | @Override
260 | public void onScrollStateChanged(int state) {
261 | if (state == SCROLL_STATE_IDLE) {
262 | if (NestedCeilingHelper.USE_OVER_SCROLL) {
263 | dispatchChildState(SCROLL_STATE_IDLE);
264 | } else {
265 | dispatchChildFling();
266 | }
267 | } else {
268 | dispatchChildState(state);
269 | }
270 | }
271 |
272 | @Override
273 | protected void onFlingEnd(int velocityX, int velocityY) {
274 | super.onFlingEnd(velocityX, velocityY);
275 | if (velocityY > 0 && NestedCeilingHelper.USE_OVER_SCROLL) {
276 | // 通过OverScroll传递滚动状态
277 | RecyclerView child = FindTarget.findChildScrollTarget(mContentView);
278 | if (child != null) {
279 | if (NestedCeilingHelper.DEBUG) {
280 | log("onFlingEnd fling child velocityY: " + velocityY);
281 | }
282 | child.fling(0, velocityY);
283 | }
284 | }
285 | }
286 |
287 | private void dispatchChildState(int state) {
288 | if (mIsChildNestedScrolling) {
289 | return;
290 | }
291 | NestedChildRecyclerView child = FindTarget.findChildScrollTarget(mContentView);
292 | if (child != null && !child.isFling()) {
293 | child.updateScrollState(state);
294 | }
295 | }
296 |
297 | private void dispatchChildFling() {
298 | boolean isChildFling = false;
299 | if (mVelocityY != 0 && isScrollEnd()) {
300 | double splineFlingDistance = mFlingHelper.getSplineFlingDistance(mVelocityY);
301 | if (splineFlingDistance > mTotalDy) {
302 | childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - mTotalDy));
303 | isChildFling = true;
304 | }
305 | }
306 | mTotalDy = 0;
307 | mVelocityY = 0;
308 | if (!isChildFling) {
309 | dispatchChildState(SCROLL_STATE_IDLE);
310 | }
311 | }
312 |
313 | private void childFling(int velocityY) {
314 | RecyclerView child = FindTarget.findChildScrollTarget(mContentView);
315 | if (child != null) {
316 | child.fling(0, velocityY);
317 | }
318 | }
319 |
320 | @Override
321 | public boolean fling(int velocityX, int velocityY) {
322 | boolean fling = super.fling(velocityX, velocityY);
323 | if (!fling || velocityY <= 0) {
324 | mVelocityY = 0;
325 | } else {
326 | mIsStartChildFling = true;
327 | mVelocityY = velocityY;
328 | }
329 | return fling;
330 | }
331 |
332 | @Override
333 | public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
334 | boolean isStart = (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
335 | if (NestedCeilingHelper.DEBUG) {
336 | log("onStartNestedScroll type: " + type + ", scrollState: " + getScrollState());
337 | }
338 | if (isStart && type == ViewCompat.TYPE_TOUCH && getScrollState() == SCROLL_STATE_SETTLING) {
339 | // 子view引起嵌套滑动是可能在fling,stop it
340 | stopScroll();
341 | }
342 | if (isStart) {
343 | mIsChildNestedScrolling = true;
344 | }
345 | return isStart;
346 | }
347 |
348 | @Override
349 | public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
350 | mParentHelper.onNestedScrollAccepted(child, target, axes, type);
351 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
352 | }
353 |
354 | @Override
355 | public boolean onStartNestedScroll(
356 | @NonNull View child, @NonNull View target, int nestedScrollAxes) {
357 | return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
358 | }
359 |
360 | @Override
361 | public void onNestedScrollAccepted(
362 | @NonNull View child, @NonNull View target, int nestedScrollAxes) {
363 | onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
364 | }
365 |
366 | @Override
367 | public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
368 | onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
369 | }
370 |
371 | @Override
372 | public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
373 | boolean isParentScroll = dispatchNestedPreScroll(dx, dy, consumed, null, type);
374 | // 在父嵌套布局没有滑动时,处理此控件是否需要滑动
375 | if (isParentScroll) {
376 | return;
377 | }
378 | // 向上滑动且此控件没有滑动到底部时,需要让此控件继续滑动以保证滑动连贯一致性
379 | boolean needKeepScroll = dy > 0; //&& !isScrollEnd();
380 | if (needKeepScroll) {
381 | mTempConsumed[1] = 0;
382 | doScrollConsumed(0, dy, mTempConsumed);
383 | consumed[1] = mTempConsumed[1];
384 |
385 | updateScrollState(type == ViewCompat.TYPE_TOUCH ? SCROLL_STATE_DRAGGING : SCROLL_STATE_SETTLING);
386 |
387 | if (NestedCeilingHelper.DEBUG) {
388 | final int offset = computeVerticalScrollOffset();
389 | final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
390 | log("onNestedPreScroll dy:" + dy + ", consumedY: " + consumed[1]
391 | + ", type:" + type + ", isScrollEnd: " + isScrollEnd() + ", offset:" + offset + ", range:" + range);
392 | }
393 | } else {
394 | if (NestedCeilingHelper.DEBUG) {
395 | final int offset = computeVerticalScrollOffset();
396 | final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
397 | log("onNestedPreScroll not dy:" + dy + ", type:" + type
398 | + ", isScrollEnd: " + isScrollEnd() + ", offset:" + offset + ", range:" + range);
399 | }
400 | }
401 | }
402 |
403 | @Override
404 | public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
405 | int dxUnconsumed, int dyUnconsumed) {
406 | onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, ViewCompat.TYPE_TOUCH);
407 | }
408 |
409 | @Override
410 | public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
411 | int dxUnconsumed, int dyUnconsumed, int type) {
412 | onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, mNestedScrollingV2ConsumedCompat);
413 | }
414 |
415 | @Override
416 | public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
417 | int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
418 | onNestedScrollInternal(target, dyUnconsumed, type, consumed);
419 | }
420 |
421 | /**
422 | * dyUnconsumed != 0时,嵌套的子view,表示嵌套的子view不能滑动,也就是到顶了,大于0表示下滑,小于0表示上划
423 | *
424 | * @param target
425 | * @param dyUnconsumed
426 | * @param type
427 | * @param consumed
428 | */
429 | private void onNestedScrollInternal(@NonNull View target, int dyUnconsumed, int type, @NonNull int[] consumed) {
430 | if (dyUnconsumed == 0) {
431 | return;
432 | }
433 | mTempConsumed[1] = 0;
434 | doScrollConsumed(0, dyUnconsumed, mTempConsumed);
435 | int consumedY = mTempConsumed[1];
436 | consumed[1] += consumedY;
437 | final int myUnconsumedY = dyUnconsumed - consumedY;
438 |
439 | dispatchNestedScroll(0, consumedY, 0, myUnconsumedY, null, type, consumed);
440 |
441 | if (NestedCeilingHelper.DEBUG) {
442 | log("onNestedScrollInternal dyUnconsumed:" + dyUnconsumed
443 | + ", consumedY:" + consumedY + ", myUnconsumedY:" + myUnconsumedY
444 | + ", type:" + type);
445 | }
446 |
447 | if (dyUnconsumed < 0 && type == ViewCompat.TYPE_TOUCH) {
448 | updateScrollState(SCROLL_STATE_DRAGGING);
449 | }
450 |
451 | // dyUnconsumed 大于0是下滑,小于0是上划
452 | if (dyUnconsumed < 0 && type == ViewCompat.TYPE_NON_TOUCH && target instanceof NestedChildRecyclerView) {
453 | NestedChildRecyclerView nestedView = (NestedChildRecyclerView) target;
454 | if (nestedView != FindTarget.findChildScrollTarget(mContentView)) {
455 | log("onNestedScrollInternal nestedView is changed, return");
456 | return;
457 | }
458 | OverScroller overScroller = nestedView.getFlingOverScroll();
459 | if (overScroller == null) {
460 | return;
461 | }
462 | float absVelocity = overScroller.getCurrVelocity();
463 | // nestedView.stopScroll();
464 | // 停止,但不更新状态,因为fling时在onStateChanged中要更新子view的状态
465 | nestedView.stopScrollWithoutState();
466 | float myVelocity = absVelocity * -1;
467 | fling(0, Math.round(myVelocity));
468 |
469 | if (NestedCeilingHelper.DEBUG) {
470 | log("onNestedScrollInternal start fling from child, absVelocity:" + absVelocity + ", myVelocity:" + myVelocity);
471 | }
472 | }
473 | }
474 |
475 | @Override
476 | public void onStopNestedScroll(@NonNull View target, int type) {
477 | if (NestedCeilingHelper.DEBUG) {
478 | log("onStopNestedScroll type: " + type + ", scrollState: " + getScrollState());
479 | }
480 | if (type == ViewCompat.TYPE_TOUCH && getScrollState() == SCROLL_STATE_SETTLING) {
481 | // 停止是可能正在fling,需要手动停止
482 | log("onStopNestedScroll stop it");
483 | stopScroll();
484 | } else if (getFlingOverScroll() != null && getFlingOverScroll().isFinished()
485 | && target instanceof NestedChildRecyclerView
486 | && ((NestedChildRecyclerView) target).getScrollState() == SCROLL_STATE_IDLE) {
487 | // 嵌套滑动停止时要将状态至为idle状态
488 | //((NestedChildRecyclerView) target).getScrollState() ==
489 | updateScrollState(SCROLL_STATE_IDLE);
490 | }
491 | mParentHelper.onStopNestedScroll(target, type);
492 | stopNestedScroll(type);
493 | mIsChildNestedScrolling = false;
494 | }
495 | // NestedScrollingParent
496 |
497 | @Override
498 | public void onStopNestedScroll(@NonNull View target) {
499 | onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
500 | }
501 |
502 | @Override
503 | public boolean onNestedFling(
504 | @NonNull View target, float velocityX, float velocityY, boolean consumed) {
505 | log("onNestedFling velocityY: " + velocityY + ", consumed: " + consumed);
506 | if (!consumed) {
507 | dispatchNestedFling(0, velocityY, true);
508 | fling(0, (int) velocityY);
509 | return true;
510 | }
511 | return false;
512 | }
513 |
514 | @Override
515 | public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
516 | return dispatchNestedPreFling(velocityX, velocityY);
517 | }
518 |
519 | @Override
520 | public int getNestedScrollAxes() {
521 | return mParentHelper.getNestedScrollAxes();
522 | }
523 |
524 | private void log(String msg) {
525 | Log.d(TAG, msg);
526 | }
527 | }
--------------------------------------------------------------------------------
/nestedceiling/src/main/java/com/tory/nestedceiling/widget/OnChildAttachStateListener.java:
--------------------------------------------------------------------------------
1 | package com.tory.nestedceiling.widget;
2 |
3 | public interface OnChildAttachStateListener {
4 |
5 | /**
6 | * 子布局吸附到顶部时回调
7 | */
8 | void onChildAttachedToTop();
9 |
10 | /**
11 | * 子布局从顶部脱离时回调
12 | */
13 | void onChildDetachedFromTop();
14 | }
15 |
--------------------------------------------------------------------------------
/nestedceiling/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/preview/app-debug.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/preview/app-debug.apk
--------------------------------------------------------------------------------
/preview/live.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ToryCrox/NestedCeilingEffect/bfb04e4424cf8b8fbe5b1d8a4df1f38c3aacc50d/preview/live.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name='NestedCeilingEffect'
2 | include ':app'
3 | include ':nestedceiling'
4 |
--------------------------------------------------------------------------------