├── .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 |