├── app ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── dimes.xml │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── drawable │ │ │ ├── draw_circle_shape.xml │ │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── layout │ │ │ ├── item_goods_grid_container.xml │ │ │ ├── item_horizontal_text.xml │ │ │ ├── activity_main.xml │ │ │ ├── item_recommend_container.xml │ │ │ ├── item_horizontal_scroll_container.xml │ │ │ ├── item_category_container.xml │ │ │ ├── item_top_banner.xml │ │ │ ├── item_category_child.xml │ │ │ ├── item_recommend_goods.xml │ │ │ └── item_goods.xml │ │ └── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ └── com │ │ │ └── multitype │ │ │ └── adapter │ │ │ └── sample │ │ │ ├── binder │ │ │ ├── TopBannerBinder.kt │ │ │ ├── HorizontalItemBinder.kt │ │ │ ├── HorizontalScrollBinder.kt │ │ │ ├── CategoryBinder.kt │ │ │ ├── GoodsGridBinder.kt │ │ │ └── RecommendBinder.kt │ │ │ ├── GridLayoutDecorationDivider.java │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle ├── multitype-adapter ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ ├── values │ │ │ └── ids.xml │ │ └── layout │ │ │ └── layout_databinding_data.xml │ │ └── kotlin │ │ └── com │ │ └── multitype │ │ └── adapter │ │ ├── callback │ │ ├── OnViewClickListener.kt │ │ └── DiffItemCallback.kt │ │ ├── holder │ │ └── MultiTypeViewHolder.kt │ │ ├── Adapter.kt │ │ ├── binder │ │ ├── ClickBinder.kt │ │ └── MultiTypeBinder.kt │ │ └── MultiTypeAdapter.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── screenshots └── multitype-adapter.jpg ├── gradle.properties ├── .gitignore ├── gradlew.bat ├── gradlew └── README.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /multitype-adapter/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='multitype-adapter-sample' 2 | include ':app' 3 | include ':multitype-adapter' 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MultiTypeAdapter 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /screenshots/multitype-adapter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/screenshots/multitype-adapter.jpg -------------------------------------------------------------------------------- /app/src/main/res/values/dimes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10dp 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /multitype-adapter/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /multitype-adapter/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjingbo/multitype-adapter-sample/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri May 08 10:47:13 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/draw_circle_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /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/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_goods_grid_container.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /multitype-adapter/src/main/res/layout/layout_databinding_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/multitype/adapter/sample/binder/TopBannerBinder.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.sample.binder 2 | 3 | import com.multitype.adapter.binder.MultiTypeBinder 4 | import com.multitype.adapter.sample.R 5 | import com.multitype.adapter.sample.databinding.ItemTopBannerBinding 6 | 7 | class TopBannerBinder: MultiTypeBinder() { 8 | 9 | override fun layoutId(): Int = R.layout.item_top_banner 10 | 11 | override fun areContentsTheSame(other: Any): Boolean = other is TopBannerBinder 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/multitype/adapter/sample/binder/HorizontalItemBinder.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.sample.binder 2 | 3 | import com.multitype.adapter.binder.MultiTypeBinder 4 | import com.multitype.adapter.sample.R 5 | import com.multitype.adapter.sample.databinding.ItemHorizontalTextBinding 6 | 7 | class HorizontalItemBinder(val index: String): MultiTypeBinder() { 8 | 9 | override fun layoutId(): Int = R.layout.item_horizontal_text 10 | 11 | override fun areContentsTheSame(other: Any): Boolean = other is HorizontalItemBinder 12 | } -------------------------------------------------------------------------------- /multitype-adapter/src/main/kotlin/com/multitype/adapter/callback/OnViewClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.callback 2 | 3 | import android.view.View 4 | 5 | /** 6 | * date : 2019/5/31 7 | * author : 秦川·小将 8 | * description : 9 | */ 10 | interface OnViewClickListener { 11 | 12 | // 不需要额外参数事件时,默认转发给带额外参数事件 13 | fun onClick(view: View) { 14 | onClick(view, null) 15 | } 16 | 17 | fun onClick(view: View, any: Any?) { 18 | 19 | } 20 | 21 | fun onLongClick(view: View) { 22 | onLongClick(view, null) 23 | } 24 | 25 | fun onLongClick(view: View, any: Any?) { 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_horizontal_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /multitype-adapter/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 | -------------------------------------------------------------------------------- /multitype-adapter/src/main/kotlin/com/multitype/adapter/callback/DiffItemCallback.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.callback 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import com.multitype.adapter.binder.MultiTypeBinder 5 | 6 | /** 7 | * date : 2019/5/31 8 | * author : 秦川·小将 9 | * description : 10 | */ 11 | class DiffItemCallback> : DiffUtil.ItemCallback() { 12 | 13 | override fun areItemsTheSame(oldItem: T, newItem: T): Boolean { 14 | return oldItem.layoutId() == newItem.layoutId() 15 | } 16 | 17 | override fun areContentsTheSame(oldItem: T, newItem: T): Boolean { 18 | return oldItem.hashCode() == newItem.hashCode() && oldItem.areContentsTheSame(newItem) 19 | } 20 | 21 | override fun getChangePayload(oldItem: T, newItem: T): Any? { 22 | return super.getChangePayload(oldItem, newItem) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /multitype-adapter/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.3" 8 | 9 | defaultConfig { 10 | minSdkVersion 21 11 | targetSdkVersion 29 12 | versionCode 1 13 | versionName "1.0" 14 | } 15 | 16 | compileOptions { 17 | sourceCompatibility JavaVersion.VERSION_1_8 18 | targetCompatibility JavaVersion.VERSION_1_8 19 | } 20 | 21 | kotlinOptions { 22 | jvmTarget = "1.8" 23 | } 24 | 25 | dataBinding { 26 | enabled = true 27 | } 28 | 29 | sourceSets { 30 | main.java.srcDirs += 'src/main/kotlin' 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation fileTree(dir: 'libs', include: ['*.jar']) 36 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 37 | api 'androidx.recyclerview:recyclerview:1.2.0-alpha03' 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_recommend_container.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 17 | 18 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_horizontal_scroll_container.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 16 | 17 | 24 | 25 | -------------------------------------------------------------------------------- /multitype-adapter/src/main/kotlin/com/multitype/adapter/holder/MultiTypeViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.holder 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.multitype.adapter.binder.MultiTypeBinder 6 | 7 | /** 8 | * date : 2019/5/31 9 | * author : 秦川·小将 10 | * description : 11 | */ 12 | class MultiTypeViewHolder(private val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root), AutoCloseable { 13 | 14 | private var mAlreadyBinding: MultiTypeBinder? = null 15 | 16 | /** 17 | * 绑定Binder 18 | */ 19 | fun onBindViewHolder(items: MultiTypeBinder) { 20 | // 如果两次绑定的 Binder 不一致,则直接销毁 21 | if (mAlreadyBinding != null && items !== mAlreadyBinding) close() 22 | // 开始绑定 23 | items.bindViewDataBinding(binding) 24 | // 保存绑定的 Binder 25 | mAlreadyBinding = items 26 | } 27 | 28 | /** 29 | * 销毁绑定的Binder 30 | */ 31 | override fun close() { 32 | mAlreadyBinding?.unbindDataBinding() 33 | mAlreadyBinding = null 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/multitype/adapter/sample/binder/HorizontalScrollBinder.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.sample.binder 2 | 3 | import androidx.recyclerview.widget.LinearLayoutManager 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.multitype.adapter.binder.MultiTypeBinder 6 | import com.multitype.adapter.createMultiTypeAdapter 7 | import com.multitype.adapter.invoke 8 | import com.multitype.adapter.sample.R 9 | import com.multitype.adapter.sample.databinding.ItemHorizontalScrollContainerBinding 10 | 11 | class HorizontalScrollBinder(val data: List): MultiTypeBinder() { 12 | 13 | override fun layoutId(): Int = R.layout.item_horizontal_scroll_container 14 | 15 | override fun areContentsTheSame(other: Any): Boolean = other is HorizontalScrollBinder && other.data == data 16 | 17 | override fun onBindViewHolder(binding: ItemHorizontalScrollContainerBinding) { 18 | (createMultiTypeAdapter(binding.multiTypeScrollRecycler, LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false))) { 19 | notifyAdapterChanged(data) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_category_container.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 16 | 17 | 25 | 26 | -------------------------------------------------------------------------------- /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 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # IntelliJ 37 | *.iml 38 | .idea 39 | 40 | # Keystore files 41 | # Uncomment the following lines if you do not want to check your keystore files in. 42 | #*.jks 43 | #*.keystore 44 | 45 | # External native build folder generated in Android Studio 2.2 and later 46 | .externalNativeBuild 47 | 48 | # Google Services (e.g. APIs or Firebase) 49 | # google-services.json 50 | 51 | # Freeline 52 | freeline.py 53 | freeline/ 54 | freeline_project_description.json 55 | 56 | # fastlane 57 | fastlane/report.xml 58 | fastlane/Preview.html 59 | fastlane/screenshots 60 | fastlane/test_output 61 | fastlane/readme.md 62 | 63 | # Version control 64 | vcs.xml 65 | 66 | # lint 67 | lint/intermediates/ 68 | lint/generated/ 69 | lint/outputs/ 70 | lint/tmp/ 71 | # lint/reports/ 72 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.3" 8 | 9 | defaultConfig { 10 | applicationId "com.multitype.adapter.sample" 11 | minSdkVersion 21 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | 29 | kotlinOptions { 30 | jvmTarget = "1.8" 31 | } 32 | 33 | dataBinding { 34 | enabled = true 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation fileTree(dir: 'libs', include: ['*.jar']) 40 | // kotlin 41 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 42 | // Ui 43 | implementation 'androidx.appcompat:appcompat:1.1.0' 44 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 45 | implementation 'com.google.android.material:material:1.2.0-alpha06' 46 | implementation project(path: ':multitype-adapter') 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/multitype/adapter/sample/binder/CategoryBinder.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.sample.binder 2 | 3 | import androidx.recyclerview.widget.GridLayoutManager 4 | import com.multitype.adapter.binder.MultiTypeBinder 5 | import com.multitype.adapter.createMultiTypeAdapter 6 | import com.multitype.adapter.invoke 7 | import com.multitype.adapter.sample.R 8 | import com.multitype.adapter.sample.databinding.ItemCategoryChildBinding 9 | import com.multitype.adapter.sample.databinding.ItemCategoryContainerBinding 10 | 11 | class CategoryContainerBinder(val category: List): MultiTypeBinder() { 12 | 13 | override fun layoutId(): Int = R.layout.item_category_container 14 | 15 | override fun areContentsTheSame(other: Any): Boolean = other is CategoryContainerBinder && other.category == category 16 | 17 | override fun onBindViewHolder(binding: ItemCategoryContainerBinding) { 18 | (createMultiTypeAdapter(binding.categoryRecycler, GridLayoutManager(binding.root.context, 5))) { 19 | notifyAdapterChanged(category) 20 | } 21 | } 22 | } 23 | 24 | class CategoryItemBinder(val title: String): MultiTypeBinder() { 25 | 26 | override fun layoutId(): Int = R.layout.item_category_child 27 | 28 | override fun areContentsTheSame(other: Any): Boolean = other is CategoryItemBinder && other.title == title 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/multitype/adapter/sample/binder/GoodsGridBinder.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.sample.binder 2 | 3 | import androidx.recyclerview.widget.GridLayoutManager 4 | import com.multitype.adapter.binder.MultiTypeBinder 5 | import com.multitype.adapter.createMultiTypeAdapter 6 | import com.multitype.adapter.invoke 7 | import com.multitype.adapter.sample.GridLayoutDecorationDivider 8 | import com.multitype.adapter.sample.R 9 | import com.multitype.adapter.sample.databinding.ItemGoodsBinding 10 | import com.multitype.adapter.sample.databinding.ItemGoodsGridContainerBinding 11 | 12 | class GoodsGridContainerBinder(val goods: List): MultiTypeBinder() { 13 | 14 | override fun layoutId(): Int = R.layout.item_goods_grid_container 15 | 16 | override fun areContentsTheSame(other: Any): Boolean = other is GoodsGridContainerBinder && other.goods == goods 17 | 18 | override fun onBindViewHolder(binding: ItemGoodsGridContainerBinding) { 19 | binding.goodsRecycler.addItemDecoration(GridLayoutDecorationDivider(binding.root.context, 2, 10)) 20 | (createMultiTypeAdapter(binding.goodsRecycler, GridLayoutManager(binding.root.context, 2))) { 21 | notifyAdapterChanged(goods) 22 | } 23 | } 24 | } 25 | 26 | class GoodsBinder(val index: Int): MultiTypeBinder() { 27 | 28 | override fun layoutId(): Int = R.layout.item_goods 29 | 30 | override fun areContentsTheSame(other: Any): Boolean = other is GoodsBinder && other.index == index 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/multitype/adapter/sample/binder/RecommendBinder.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.sample.binder 2 | 3 | import androidx.recyclerview.widget.GridLayoutManager 4 | import com.multitype.adapter.binder.MultiTypeBinder 5 | import com.multitype.adapter.createMultiTypeAdapter 6 | import com.multitype.adapter.invoke 7 | import com.multitype.adapter.sample.GridLayoutDecorationDivider 8 | import com.multitype.adapter.sample.R 9 | import com.multitype.adapter.sample.databinding.ItemRecommendContainerBinding 10 | import com.multitype.adapter.sample.databinding.ItemRecommendGoodsBinding 11 | 12 | class RecommendContainerBinder(val goods: List): MultiTypeBinder() { 13 | 14 | override fun layoutId(): Int = R.layout.item_recommend_container 15 | 16 | override fun areContentsTheSame(other: Any): Boolean = other is RecommendContainerBinder && other.goods == goods 17 | 18 | override fun onBindViewHolder(binding: ItemRecommendContainerBinding) { 19 | binding.recommendGoodsRecycler.addItemDecoration(GridLayoutDecorationDivider(binding.root.context, 4, 10)) 20 | (createMultiTypeAdapter(binding.recommendGoodsRecycler, GridLayoutManager(binding.root.context, 4))) { 21 | notifyAdapterChanged(goods) 22 | } 23 | } 24 | } 25 | 26 | class RecommendGoodsBinder: MultiTypeBinder() { 27 | 28 | override fun layoutId(): Int = R.layout.item_recommend_goods 29 | 30 | override fun areContentsTheSame(other: Any): Boolean = other is RecommendGoodsBinder 31 | } -------------------------------------------------------------------------------- /multitype-adapter/src/main/kotlin/com/multitype/adapter/Adapter.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.databinding.DataBindingUtil 7 | import androidx.databinding.ViewDataBinding 8 | import androidx.recyclerview.widget.RecyclerView 9 | 10 | /** 11 | * date : 2019/5/31 12 | * author : 秦川·小将 13 | * description : 14 | */ 15 | 16 | /** 17 | * 创建一个MultiTypeGeneralAdapter 18 | */ 19 | fun createMultiTypeAdapter(recyclerView: RecyclerView, layoutManager: RecyclerView.LayoutManager): MultiTypeAdapter { 20 | recyclerView.layoutManager = layoutManager 21 | val mMultiTypeAdapter = MultiTypeAdapter(layoutManager) 22 | recyclerView.adapter = mMultiTypeAdapter 23 | // 处理RecyclerView的触发回调 24 | recyclerView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { 25 | override fun onViewDetachedFromWindow(v: View?) { 26 | mMultiTypeAdapter.onDetachedFromRecyclerView(recyclerView) 27 | } 28 | override fun onViewAttachedToWindow(v: View?) { } 29 | }) 30 | return mMultiTypeAdapter 31 | } 32 | 33 | /** 34 | * MultiTypeGeneralAdapter扩展函数,重载MultiTypeGeneralAdapter类,使用invoke操作符调用MultiTypeGeneralAdapter内部函数。 35 | */ 36 | inline operator fun MultiTypeAdapter.invoke(block: MultiTypeAdapter.() -> Unit): MultiTypeAdapter { 37 | this.block() 38 | return this 39 | } 40 | 41 | /** 42 | * Layout converter ViewDataBinding 43 | */ 44 | fun ViewGroup.inflateDataBinding(layoutId: Int): T = DataBindingUtil.inflate(LayoutInflater.from(context), layoutId, this, false)!! 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_top_banner.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 16 | 17 | 31 | 32 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /multitype-adapter/src/main/kotlin/com/multitype/adapter/binder/ClickBinder.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.binder 2 | 3 | import android.view.View 4 | import androidx.viewbinding.BuildConfig 5 | import com.multitype.adapter.callback.OnViewClickListener 6 | 7 | /** 8 | * date : 2019/5/31 9 | * author : 秦川·小将 10 | * description : 11 | */ 12 | open class ClickBinder: OnViewClickListener { 13 | 14 | protected open var mOnClickListener: ((view: View, any: Any?) -> Unit)? = null 15 | 16 | protected open var mOnLongClickListener: ((view: View, any: Any?) -> Unit)? = null 17 | 18 | /** 19 | * 设置View点击事件 20 | */ 21 | open fun setOnClickListener(listener: (view: View, any: Any?) -> Unit): ClickBinder { 22 | this.mOnClickListener = listener 23 | return this 24 | } 25 | 26 | /** 27 | * 设置View长按点击事件 28 | */ 29 | open fun setOnLongClickListener(listener: (view: View, any: Any?) -> Unit): ClickBinder { 30 | this.mOnLongClickListener = listener 31 | return this 32 | } 33 | 34 | /** 35 | * 触发View点击事件时回调,携带参数 36 | */ 37 | override fun onClick(view: View) { 38 | onClick(view, this) 39 | } 40 | 41 | override fun onClick(view: View, any: Any?) { 42 | if (mOnClickListener != null) { 43 | mOnClickListener?.invoke(view, any) 44 | } else { 45 | if (BuildConfig.DEBUG) throw NullPointerException("OnClick事件未绑定!") 46 | } 47 | } 48 | 49 | /** 50 | * 触发View长按事件时回调,携带参数 51 | */ 52 | override fun onLongClick(view: View) { 53 | onLongClick(view, this) 54 | } 55 | 56 | override fun onLongClick(view: View, any: Any?){ 57 | if (mOnLongClickListener != null) { 58 | mOnLongClickListener?.invoke(view, any) 59 | } else { 60 | throw NullPointerException("OnLongClick事件未绑定!") 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_category_child.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 18 | 19 | 31 | 32 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/multitype/adapter/sample/GridLayoutDecorationDivider.java: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.sample; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Rect; 9 | import android.graphics.drawable.Drawable; 10 | import android.util.Log; 11 | import android.view.View; 12 | 13 | import androidx.recyclerview.widget.RecyclerView; 14 | 15 | public class GridLayoutDecorationDivider extends RecyclerView.ItemDecoration { 16 | 17 | private int spanCount; 18 | private int dividerWidth; 19 | private int dividerWidthTop; 20 | private int dividerWidthBot; 21 | 22 | private Paint dividerPaint; 23 | 24 | /** 25 | * @param spanCount gridLayoutManager 列数 26 | * @param dividerWidthDp 分割块宽高,单位:dp 27 | */ 28 | public GridLayoutDecorationDivider(Context context, int spanCount, int dividerWidthDp) { 29 | this.spanCount = spanCount; 30 | this.dividerPaint = new Paint(); 31 | this.dividerPaint.setColor(Color.BLUE); 32 | this.dividerWidth = dpToPx(context, dividerWidthDp); 33 | this.dividerWidthTop = dividerWidth / 2; 34 | this.dividerWidthBot = dividerWidth - dividerWidthTop; 35 | } 36 | 37 | @Override 38 | public void getItemOffsets(Rect outRect, View child, RecyclerView parent, RecyclerView.State state) { 39 | super.getItemOffsets(outRect, child, parent, state); 40 | int pos = parent.getChildAdapterPosition(child); 41 | int column = (pos) % spanCount;// 计算这个child 处于第几列 42 | outRect.top = dividerWidthTop; 43 | outRect.bottom = dividerWidthBot; 44 | outRect.left = (column * dividerWidth / spanCount); 45 | outRect.right = dividerWidth - (column + 1) * dividerWidth / spanCount; 46 | } 47 | 48 | private int dpToPx(Context context, float value) { 49 | if (value <= 0) return 0; 50 | return (int) (value * context.getResources().getDisplayMetrics().density + 0.5f); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_recommend_goods.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 10 | 11 | 16 | 17 | 22 | 23 | 32 | 33 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /multitype-adapter/src/main/kotlin/com/multitype/adapter/MultiTypeAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter 2 | 3 | import android.view.ViewGroup 4 | import androidx.databinding.ViewDataBinding 5 | import androidx.recyclerview.widget.AsyncListDiffer 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.multitype.adapter.binder.MultiTypeBinder 8 | import com.multitype.adapter.callback.DiffItemCallback 9 | import com.multitype.adapter.holder.MultiTypeViewHolder 10 | 11 | /** 12 | * date : 2019/5/31 13 | * author : 秦川·小将 14 | * description : 15 | * @param layoutManager 将LayoutManager向外扩展 16 | */ 17 | class MultiTypeAdapter constructor(val layoutManager: RecyclerView.LayoutManager): RecyclerView.Adapter() { 18 | 19 | // 使用后台线程通过差异性计算来更新列表 20 | private val mAsyncListChange by lazy { AsyncListDiffer(this, DiffItemCallback>()) } 21 | 22 | // 存储 MultiTypeBinder 和 MultiTypeViewHolder Type 23 | private var mHashCodeViewType = LinkedHashMap>() 24 | 25 | init { 26 | setHasStableIds(true) 27 | } 28 | 29 | fun notifyAdapterChanged(binders: List>) { 30 | mHashCodeViewType = LinkedHashMap() 31 | binders.forEach { 32 | mHashCodeViewType[it.hashCode()] = it 33 | } 34 | mAsyncListChange.submitList(mHashCodeViewType.map { it.value }) 35 | } 36 | 37 | fun notifyAdapterChanged(binder: MultiTypeBinder<*>) { 38 | mHashCodeViewType = LinkedHashMap() 39 | mHashCodeViewType[binder.hashCode()] = binder 40 | mAsyncListChange.submitList(mHashCodeViewType.map { it.value }) 41 | } 42 | 43 | override fun getItemViewType(position: Int): Int { 44 | val mItemBinder = mAsyncListChange.currentList[position] 45 | val mHasCode = mItemBinder.hashCode() 46 | // 如果Map中不存在当前Binder的hasCode,则向Map中添加当前类型的Binder 47 | if (!mHashCodeViewType.containsKey(mHasCode)) { 48 | mHashCodeViewType[mHasCode] = mItemBinder 49 | } 50 | return mHasCode 51 | } 52 | 53 | override fun getItemId(position: Int): Long = position.toLong() 54 | 55 | override fun getItemCount(): Int = mAsyncListChange.currentList.size 56 | 57 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MultiTypeViewHolder { 58 | try { 59 | return MultiTypeViewHolder(parent.inflateDataBinding(mHashCodeViewType[viewType]?.layoutId()!!)) 60 | }catch (e: Exception){ 61 | throw NullPointerException("不存在${mHashCodeViewType[viewType]}类型的ViewHolder!") 62 | } 63 | } 64 | 65 | @Suppress("UNCHECKED_CAST") 66 | override fun onBindViewHolder(holder: MultiTypeViewHolder, position: Int) { 67 | val mCurrentBinder = mAsyncListChange.currentList[position] as MultiTypeBinder 68 | holder.itemView.tag = mCurrentBinder.layoutId() 69 | holder.onBindViewHolder(mCurrentBinder) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /multitype-adapter/src/main/kotlin/com/multitype/adapter/binder/MultiTypeBinder.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.binder 2 | 3 | import androidx.annotation.LayoutRes 4 | import androidx.databinding.ViewDataBinding 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.lifecycle.LifecycleOwner 7 | import androidx.lifecycle.LifecycleRegistry 8 | import com.multitype.adapter.BR 9 | import com.multitype.adapter.R 10 | 11 | /** 12 | * date : 2019/5/31 13 | * author : 秦川·小将 14 | * description : 15 | */ 16 | abstract class MultiTypeBinder : ClickBinder() { 17 | 18 | /** 19 | * BR.data 20 | */ 21 | protected open val variableId = BR.data 22 | 23 | /** 24 | * 被绑定的ViewDataBinding 25 | */ 26 | open var binding: V? = null 27 | 28 | /** 29 | * 给绑定的View设置tag 30 | */ 31 | private var bindingViewVersion = (0L until Long.MAX_VALUE).random() 32 | 33 | /** 34 | * 返回LayoutId,供Adapter使用 35 | */ 36 | @LayoutRes 37 | abstract fun layoutId(): Int 38 | 39 | /** 40 | * 两次更新的Binder内容是否相同 41 | */ 42 | abstract fun areContentsTheSame(other: Any): Boolean 43 | 44 | /** 45 | * 绑定ViewDataBinding 46 | */ 47 | fun bindViewDataBinding(binding: V) { 48 | // 如果此次绑定与已绑定的一至,则不做绑定 49 | if (this.binding === binding && binding.root.getTag(R.id.bindingVersion) == bindingViewVersion) return 50 | binding.root.setTag(R.id.bindingVersion, ++bindingViewVersion) 51 | onUnBindViewHolder() 52 | this.binding = binding 53 | binding.setVariable(variableId, this) 54 | // 给 binding 绑定生命周期,方便观察LiveData的值,进而更新UI。如果不绑定,LiveData的值改变时,UI不会更新 55 | if (binding.root.context is LifecycleOwner) { 56 | binding.lifecycleOwner = binding.root.context as LifecycleOwner 57 | } else { 58 | binding.lifecycleOwner = AlwaysActiveLifecycleOwner() 59 | } 60 | onBindViewHolder(binding) 61 | // 及时更新绑定数据的View 62 | binding.executePendingBindings() 63 | } 64 | 65 | /** 66 | * 解绑ViewDataBinding 67 | */ 68 | fun unbindDataBinding() { 69 | if (this.binding != null) { 70 | onUnBindViewHolder() 71 | this.binding = null 72 | } 73 | } 74 | 75 | /** 76 | * 绑定后对View的一些操作,如:赋值,修改属性 77 | */ 78 | protected open fun onBindViewHolder(binding: V) { 79 | 80 | } 81 | 82 | /** 83 | * 解绑操作 84 | */ 85 | protected open fun onUnBindViewHolder() { 86 | 87 | } 88 | 89 | /** 90 | * 为 Binder 绑定生命周期,在 {@link Lifecycle.Event#ON_RESUME} 时响应 91 | */ 92 | internal class AlwaysActiveLifecycleOwner : LifecycleOwner { 93 | 94 | override fun getLifecycle(): Lifecycle = object : LifecycleRegistry(this) { 95 | init { 96 | handleLifecycleEvent(Event.ON_RESUME) 97 | } 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_goods.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 17 | 18 | 23 | 24 | 33 | 34 | 47 | 48 | 60 | 61 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/multitype/adapter/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.multitype.adapter.sample 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.View 7 | import android.widget.Toast 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.databinding.DataBindingUtil 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import com.google.android.material.snackbar.Snackbar 12 | import com.multitype.adapter.MultiTypeAdapter 13 | import com.multitype.adapter.binder.MultiTypeBinder 14 | import com.multitype.adapter.callback.OnViewClickListener 15 | import com.multitype.adapter.createMultiTypeAdapter 16 | import com.multitype.adapter.sample.binder.* 17 | import com.multitype.adapter.sample.databinding.ActivityMainBinding 18 | 19 | class MainActivity : AppCompatActivity(), OnViewClickListener { 20 | 21 | private lateinit var mAdapter: MultiTypeAdapter 22 | private lateinit var binding: ActivityMainBinding 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | binding = DataBindingUtil.setContentView(this, R.layout.activity_main) 27 | binding.setVariable(BR.data, this) 28 | binding.lifecycleOwner = this 29 | binding.executePendingBindings() 30 | mAdapter = createMultiTypeAdapter(binding.multiTypeRecycler, LinearLayoutManager(this)) 31 | setRecyclerViewContent() 32 | } 33 | 34 | private fun setRecyclerViewContent() { 35 | mAdapter.notifyAdapterChanged(mutableListOf>().apply { 36 | add(TopBannerBinder().apply { 37 | setOnClickListener(this@MainActivity::onClick) 38 | }) 39 | add(CategoryContainerBinder(listOf("男装", "女装", "鞋靴", "内衣内饰", "箱包", "美妆护肤", "洗护", "腕表珠宝", "手机", "数码").map { 40 | CategoryItemBinder(it).apply { 41 | setOnClickListener(this@MainActivity::onClick) 42 | } 43 | })) 44 | add(RecommendContainerBinder((1..8).map { RecommendGoodsBinder().apply { 45 | setOnClickListener(this@MainActivity::onClick) 46 | } })) 47 | add(HorizontalScrollBinder((0..11).map { HorizontalItemBinder("$it").apply { 48 | setOnClickListener(this@MainActivity::onClick) 49 | } })) 50 | add(GoodsGridContainerBinder((1..20).map { GoodsBinder(it).apply { 51 | setOnClickListener(this@MainActivity::onClick) 52 | } })) 53 | }) 54 | } 55 | 56 | override fun onClick(view: View, any: Any?) { 57 | when(view.id) { 58 | R.id.top_banner -> { 59 | any as TopBannerBinder 60 | toast(view, "点击Banner") 61 | } 62 | R.id.category_tab -> { 63 | any as CategoryItemBinder 64 | toast(view,"点击分类+${any.title}") 65 | } 66 | R.id.recommend_goods -> { 67 | any as RecommendGoodsBinder 68 | toast(view, "点击精选会场Item") 69 | } 70 | R.id.theme_index -> { 71 | any as HorizontalItemBinder 72 | toast(view, "点击主题会场${any.index}") 73 | } 74 | R.id.goods_container -> { 75 | any as GoodsBinder 76 | toast(view, "点击商品${any.index}") 77 | } 78 | } 79 | } 80 | } 81 | 82 | fun toast(view: View, message: String) { 83 | Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show() 84 | } 85 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /app/src/main/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 前言 4 | 在RecyclerView实现多种Item类型列表时,有很多种实现方式,这里结合 **AsyncListDiffer+DataBinding+Lifecycles** 实现一种简单,方便,快捷并以数据驱动UI变化的MultiTypeAdapter 5 | 6 | - AsyncListDiffer 一个在后台线程中使用DiffUtil计算两组新旧数据之间差异性的辅助类。 7 | - [DataBinding](https://developer.android.google.cn/topic/libraries/data-binding) 以声明方式将可观察的数据绑定到界面元素。 8 | - [Lifecycles](https://developer.android.google.cn/topic/libraries/architecture/lifecycle) 管理您的 Activity 和 Fragment 生命周期。 9 | >Tip: 对DataBinding和Lifecycles不熟悉的小伙伴可点击查看官方介绍。 10 | 11 | --- 12 | 13 | ## 效果图 14 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200521155222456.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L21qYjAwMDAw,size_16,color_FFFFFF,t_70#pic_center) 15 | 16 | ## 1. 定义一个基类MultiTypeBinder方便统一实现与管理 17 | 18 | > ***MultiTypeBinder中部分函数说明:*** 19 | >- layoutId():初始化xml。 20 | >- areContentsTheSame():该方法用于数据内容比较,比较两次内容是否一致,刷新UI时用到。 21 | >- onBindViewHolder(binding: V):与RecyclerView.Adapter中的onBindViewHolder方法功能一致,在该方法中做一些数据绑定与处理,不过这里推荐使用DataBinding去绑定数据,以数据去驱动UI。 22 | >- onUnBindViewHolder():该方法处理一些需要释放的资源。 23 | 24 | 继承MultiTypeBinder后进行Layout初始化和数据绑定及解绑处理 25 | 26 | ```kotlin 27 | abstract class MultiTypeBinder : ClickBinder() { 28 | 29 | /** 30 | * BR.data 31 | */ 32 | protected open val variableId = BR.data 33 | 34 | /** 35 | * 被绑定的ViewDataBinding 36 | */ 37 | open var binding: V? = null 38 | 39 | /** 40 | * 给绑定的View设置tag 41 | */ 42 | private var bindingViewVersion = (0L until Long.MAX_VALUE).random() 43 | 44 | /** 45 | * 返回LayoutId,供Adapter使用 46 | */ 47 | @LayoutRes 48 | abstract fun layoutId(): Int 49 | 50 | /** 51 | * 两次更新的Binder内容是否相同 52 | */ 53 | abstract fun areContentsTheSame(other: Any): Boolean 54 | 55 | /** 56 | * 绑定ViewDataBinding 57 | */ 58 | fun bindViewDataBinding(binding: V) { 59 | // 如果此次绑定与已绑定的一至,则不做绑定 60 | if (this.binding === binding && binding.root.getTag(R.id.bindingVersion) == bindingViewVersion) return 61 | binding.root.setTag(R.id.bindingVersion, ++bindingViewVersion) 62 | onUnBindViewHolder() 63 | this.binding = binding 64 | binding.setVariable(variableId, this) 65 | // 给 binding 绑定生命周期,方便观察LiveData的值,进而更新UI。如果不绑定,LiveData的值改变时,UI不会更新 66 | if (binding.root.context is LifecycleOwner) { 67 | binding.lifecycleOwner = binding.root.context as LifecycleOwner 68 | } else { 69 | binding.lifecycleOwner = AlwaysActiveLifecycleOwner() 70 | } 71 | onBindViewHolder(binding) 72 | // 及时更新绑定数据的View 73 | binding.executePendingBindings() 74 | } 75 | 76 | /** 77 | * 解绑ViewDataBinding 78 | */ 79 | fun unbindDataBinding() { 80 | if (this.binding != null) { 81 | onUnBindViewHolder() 82 | this.binding = null 83 | } 84 | } 85 | 86 | /** 87 | * 绑定后对View的一些操作,如:赋值,修改属性 88 | */ 89 | protected open fun onBindViewHolder(binding: V) { 90 | 91 | } 92 | 93 | /** 94 | * 解绑操作 95 | */ 96 | protected open fun onUnBindViewHolder() { 97 | 98 | } 99 | 100 | /** 101 | * 为 Binder 绑定生命周期,在 {@link Lifecycle.Event#ON_RESUME} 时响应 102 | */ 103 | internal class AlwaysActiveLifecycleOwner : LifecycleOwner { 104 | 105 | override fun getLifecycle(): Lifecycle = object : LifecycleRegistry(this) { 106 | init { 107 | handleLifecycleEvent(Event.ON_RESUME) 108 | } 109 | } 110 | } 111 | } 112 | ``` 113 | 在values中定义一个ids.xml文件,给 ViewDataBinding 中的 root View设置Tag 114 | ```xml 115 | 116 | 117 | 118 | ``` 119 | ## 2.处理MultiTypeBinder中View的点击事件 120 | 在ClickBinder中提供了两种事件点击方式 onClick 和 onLongClick,分别提供了携带参数和未带参数方法 121 | ```kotlin 122 | open class ClickBinder: OnViewClickListener { 123 | 124 | protected open var mOnClickListener: ((view: View, any: Any?) -> Unit)? = null 125 | 126 | protected open var mOnLongClickListener: ((view: View, any: Any?) -> Unit)? = null 127 | 128 | /** 129 | * 设置View点击事件 130 | */ 131 | open fun setOnClickListener(listener: (view: View, any: Any?) -> Unit): ClickBinder { 132 | this.mOnClickListener = listener 133 | return this 134 | } 135 | 136 | /** 137 | * 设置View长按点击事件 138 | */ 139 | open fun setOnLongClickListener(listener: (view: View, any: Any?) -> Unit): ClickBinder { 140 | this.mOnLongClickListener = listener 141 | return this 142 | } 143 | 144 | /** 145 | * 触发View点击事件时回调,携带参数 146 | */ 147 | override fun onClick(view: View) { 148 | onClick(view, this) 149 | } 150 | 151 | override fun onClick(view: View, any: Any?) { 152 | if (mOnClickListener != null) { 153 | mOnClickListener?.invoke(view, any) 154 | } else { 155 | if (BuildConfig.DEBUG) throw NullPointerException("OnClick事件未绑定!") 156 | } 157 | } 158 | 159 | /** 160 | * 触发View长按事件时回调,携带参数 161 | */ 162 | override fun onLongClick(view: View) { 163 | onLongClick(view, this) 164 | } 165 | 166 | override fun onLongClick(view: View, any: Any?){ 167 | if (mOnLongClickListener != null) { 168 | mOnLongClickListener?.invoke(view, any) 169 | } else { 170 | throw NullPointerException("OnLongClick事件未绑定!") 171 | } 172 | } 173 | } 174 | ``` 175 | ## 3.定义MultiTypeViewHolder 176 | MultiTypeViewHolder继承自RecyclerView.ViewHolder,传入一个ViewDataBinding对象,在这里对MultiTypeBinder中的ViewDataBinding对象进行解绑和绑定操作。 177 | ```kotlin 178 | class MultiTypeViewHolder(private val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root), AutoCloseable { 179 | 180 | private var mAlreadyBinding: MultiTypeBinder? = null 181 | 182 | /** 183 | * 绑定Binder 184 | */ 185 | fun onBindViewHolder(items: MultiTypeBinder) { 186 | // 如果两次绑定的 Binder 不一致,则直接销毁 187 | if (mAlreadyBinding != null && items !== mAlreadyBinding) close() 188 | // 开始绑定 189 | items.bindViewDataBinding(binding) 190 | // 保存绑定的 Binder 191 | mAlreadyBinding = items 192 | } 193 | 194 | /** 195 | * 销毁绑定的Binder 196 | */ 197 | override fun close() { 198 | mAlreadyBinding?.unbindDataBinding() 199 | mAlreadyBinding = null 200 | } 201 | } 202 | ``` 203 | ## 4.使用DiffUtil.ItemCallback进行差异性计算 204 | 在刷新列表时这里使用了DiffUtil.ItemCallback来做差异性计算,方法说明: 205 | > - areItemsTheSame(oldItem: T, newItem: T):比较两次MultiTypeBinder是否时同一个Binder 206 | > - areContentsTheSame(oldItem: T, newItem: T):比较两次MultiTypeBinder的类容是否一致。 207 | ```kotlin 208 | class DiffItemCallback> : DiffUtil.ItemCallback() { 209 | override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = oldItem.layoutId() == newItem.layoutId() 210 | override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = oldItem.hashCode() == newItem.hashCode() && oldItem.areContentsTheSame(newItem) 211 | } 212 | ``` 213 | ## 5.定义MultiTypeAdapter 214 | 在MultiTypeAdapter中的逻辑实现思路如下: 215 | >- 使用 LinkedHashMap 来存储每个 Binder 和 Binder 对应的 Type 值,确保顺序。 216 | >- 在 getItemViewType(position: Int) 函数中添加 Binder 类型 217 | >- 在 onCreateViewHolder(parent: ViewGroup, viewType: Int) 方法中对 Binder 的 Layout 进行初始化,其中 inflateDataBinding 为 Kotlin 扩展,主要是将 Layout 转换为一个 ViewDataBinding 的对象。 218 | >- 在 onBindViewHolder(holder: MultiTypeViewHolder, position: Int) 方法中调用 Binder 中的绑定方法,用以绑定数据。 219 | >- 使用 AsyncListDiffer 工具返回当前列表数据和刷新列表,具体用法下文说明 220 | ```kotlin 221 | // 这里将LayoutManager向外扩展,方便操作RecyclerView滚动平移等操作 222 | class MultiTypeAdapter constructor(val layoutManager: RecyclerView.LayoutManager): RecyclerView.Adapter() { 223 | 224 | // 使用后台线程通过差异性计算来更新列表 225 | private val mAsyncListChange by lazy { AsyncListDiffer(this, DiffItemCallback>()) } 226 | 227 | // 存储 MultiTypeBinder 和 MultiTypeViewHolder Type 228 | private var mHashCodeViewType = LinkedHashMap>() 229 | 230 | init { 231 | setHasStableIds(true) 232 | } 233 | 234 | fun notifyAdapterChanged(binders: List>) { 235 | mHashCodeViewType = LinkedHashMap() 236 | binders.forEach { 237 | mHashCodeViewType[it.hashCode()] = it 238 | } 239 | mAsyncListChange.submitList(mHashCodeViewType.map { it.value }) 240 | } 241 | 242 | fun notifyAdapterChanged(binder: MultiTypeBinder<*>) { 243 | mHashCodeViewType = LinkedHashMap() 244 | mHashCodeViewType[binder.hashCode()] = binder 245 | mAsyncListChange.submitList(mHashCodeViewType.map { it.value }) 246 | } 247 | 248 | override fun getItemViewType(position: Int): Int { 249 | val mItemBinder = mAsyncListChange.currentList[position] 250 | val mHasCode = mItemBinder.hashCode() 251 | // 如果Map中不存在当前Binder的hasCode,则向Map中添加当前类型的Binder 252 | if (!mHashCodeViewType.containsKey(mHasCode)) { 253 | mHashCodeViewType[mHasCode] = mItemBinder 254 | } 255 | return mHasCode 256 | } 257 | 258 | override fun getItemId(position: Int): Long = position.toLong() 259 | 260 | override fun getItemCount(): Int = mAsyncListChange.currentList.size 261 | 262 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MultiTypeViewHolder { 263 | try { 264 | return MultiTypeViewHolder(parent.inflateDataBinding(mHashCodeViewType[viewType]?.layoutId()!!)) 265 | }catch (e: Exception){ 266 | throw NullPointerException("不存在${mHashCodeViewType[viewType]}类型的ViewHolder!") 267 | } 268 | } 269 | 270 | @Suppress("UNCHECKED_CAST") 271 | override fun onBindViewHolder(holder: MultiTypeViewHolder, position: Int) { 272 | val mCurrentBinder = mAsyncListChange.currentList[position] as MultiTypeBinder 273 | holder.itemView.tag = mCurrentBinder.layoutId() 274 | holder.onBindViewHolder(mCurrentBinder) 275 | } 276 | } 277 | ``` 278 | ## 6.定义扩展Adapters文件 279 | ```kotlin 280 | /** 281 | * 创建一个MultiTypeAdapter 282 | */ 283 | fun createMultiTypeAdapter(recyclerView: RecyclerView, layoutManager: RecyclerView.LayoutManager): MultiTypeAdapter { 284 | recyclerView.layoutManager = layoutManager 285 | val mMultiTypeAdapter = MultiTypeAdapter(layoutManager) 286 | recyclerView.adapter = mMultiTypeAdapter 287 | // 处理RecyclerView的触发回调 288 | recyclerView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { 289 | override fun onViewDetachedFromWindow(v: View?) { 290 | mMultiTypeAdapter.onDetachedFromRecyclerView(recyclerView) 291 | } 292 | override fun onViewAttachedToWindow(v: View?) { } 293 | }) 294 | return mMultiTypeAdapter 295 | } 296 | 297 | /** 298 | * MultiTypeAdapter扩展函数,重载MultiTypeAdapter类,使用invoke操作符调用MultiTypeAdapter内部函数。 299 | */ 300 | inline operator fun MultiTypeAdapter.invoke(block: MultiTypeAdapter.() -> Unit): MultiTypeAdapter { 301 | this.block() 302 | return this 303 | } 304 | 305 | /** 306 | * 将Layout转换成ViewDataBinding 307 | */ 308 | fun ViewGroup.inflateDataBinding(layoutId: Int): T = DataBindingUtil.inflate(LayoutInflater.from(context), layoutId, this, false)!! 309 | 310 | 311 | /** 312 | * RecyclerView方向注解 313 | */ 314 | @IntDef( 315 | Orientation.VERTICAL, 316 | Orientation.HORIZONTAL 317 | ) 318 | @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) 319 | @Retention(AnnotationRetention.SOURCE) 320 | annotation class Orientation{ 321 | 322 | companion object{ 323 | const val VERTICAL = RecyclerView.VERTICAL 324 | const val HORIZONTAL = RecyclerView.HORIZONTAL 325 | } 326 | } 327 | ``` 328 | ## 7.MultiTypeAdapter使用 329 | - 创建 MultiTypeAdapter 330 | ```kotlin 331 | private val mAdapter by lazy { createMultiTypeAdapter(binding.recyclerView, LinearLayoutManager(this)) } 332 | ``` 333 | - 将 Binder 添加到 Adapter 中 334 | ```kotlin 335 | mAdapter.notifyAdapterChanged(mutableListOf>().apply { 336 | add(TopBannerBinder().apply { 337 | setOnClickListener(this@MainActivity::onClick) 338 | }) 339 | add(CategoryContainerBinder(listOf("男装", "女装", "鞋靴", "内衣内饰", "箱包", "美妆护肤", "洗护", "腕表珠宝", "手机", "数码").map { 340 | CategoryItemBinder(it).apply { 341 | setOnClickListener(this@MainActivity::onClick) 342 | } 343 | })) 344 | add(RecommendContainerBinder((1..8).map { RecommendGoodsBinder().apply { 345 | setOnClickListener(this@MainActivity::onClick) 346 | } })) 347 | add(HorizontalScrollBinder((0..11).map { HorizontalItemBinder("$it").apply { 348 | setOnClickListener(this@MainActivity::onClick) 349 | } })) 350 | add(GoodsGridContainerBinder((1..20).map { GoodsBinder(it).apply { 351 | setOnClickListener(this@MainActivity::onClick) 352 | } })) 353 | }) 354 | ``` 355 | - 点击事件处理,在Activity或Fragment中实现 OnViewClickListener 接口,重写 onClick 方法 356 | ```kotlin 357 | override fun onClick(view: View, any: Any?) { 358 | when(view.id) { 359 | R.id.top_banner -> { 360 | any as TopBannerBinder 361 | toast(view, "点击Banner") 362 | } 363 | R.id.category_tab -> { 364 | any as CategoryItemBinder 365 | toast(view,"点击分类+${any.title}") 366 | } 367 | R.id.recommend_goods -> { 368 | any as RecommendGoodsBinder 369 | toast(view, "点击精选会场Item") 370 | } 371 | R.id.theme_index -> { 372 | any as HorizontalItemBinder 373 | toast(view, "点击主题会场${any.index}") 374 | } 375 | R.id.goods_container -> { 376 | any as GoodsBinder 377 | toast(view, "点击商品${any.index}") 378 | } 379 | } 380 | } 381 | ``` 382 | ## 8.AsyncListDiffer 383 | 一个在后台线程中使用DiffUtil计算两个列表之间的差异的辅助类。AsyncListDiffer 的计算主要submitList 方法中。 384 | 385 | > Tip: 调用submitList()方法传递数据时,需要创建一个新的集合。 386 | 387 | 388 | 389 | ```java 390 | public class AsyncListDiffer { 391 | 392 | // 省略其它代码...... 393 | 394 | @SuppressWarnings("WeakerAccess") 395 | public void submitList(@Nullable final List newList, @Nullable final Runnable commitCallback) { 396 | // 定义变量 runGeneration 递增生成,用于缓存当前预执行线程的次数的最大值 397 | final int runGeneration = ++mMaxScheduledGeneration; 398 | // 首先判断 newList 与 AsyncListDiffer 中缓存的数据集 mList 是否为同一个对象,如果是的话,直接返回。也就是说,调用 submitList() 方法所传递数据集时,需要new一个新的List。 399 | if (newList == mList) { 400 | // nothing to do (Note - still had to inc generation, since may have ongoing work) 401 | if (commitCallback != null) { 402 | commitCallback.run(); 403 | } 404 | return; 405 | } 406 | 407 | final List previousList = mReadOnlyList; 408 | 409 | // 判断 newList 是否为null。若 newList 为 null,将移除所有 Item 的操作并分发给 ListUpdateCallback,mList 置为 null,同时将只读List - mReadOnlyList 清空 410 | if (newList == null) { 411 | //noinspection ConstantConditions 412 | int countRemoved = mList.size(); 413 | mList = null; 414 | mReadOnlyList = Collections.emptyList(); 415 | // notify last, after list is updated 416 | mUpdateCallback.onRemoved(0, countRemoved); 417 | onCurrentListChanged(previousList, commitCallback); 418 | return; 419 | } 420 | 421 | // 判断 mList 是否为null。若 mList 为null,表示这是第一次向 Adapter 添加数据集,此时将添加最新数据集操的作分发给 ListUpdateCallback,将 mList 设置为 newList, 同时将 newList 赋值给 mReadOnlyList 422 | if (mList == null) { 423 | mList = newList; 424 | mReadOnlyList = Collections.unmodifiableList(newList); 425 | // notify last, after list is updated 426 | mUpdateCallback.onInserted(0, newList.size()); 427 | onCurrentListChanged(previousList, commitCallback); 428 | return; 429 | } 430 | 431 | final List oldList = mList; 432 | // 通过AsyncDifferConfig获取到一个后台线程,在后台线程中使用DiffUtil对两个List进行差异性比较 433 | mConfig.getBackgroundThreadExecutor().execute(new Runnable() { 434 | @Override 435 | public void run() { 436 | final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { 437 | @Override 438 | public int getOldListSize() { 439 | return oldList.size(); 440 | } 441 | 442 | @Override 443 | public int getNewListSize() { 444 | return newList.size(); 445 | } 446 | 447 | @Override 448 | public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { 449 | T oldItem = oldList.get(oldItemPosition); 450 | T newItem = newList.get(newItemPosition); 451 | if (oldItem != null && newItem != null) { 452 | return mConfig.getDiffCallback().areItemsTheSame(oldItem, newItem); 453 | } 454 | // If both items are null we consider them the same. 455 | return oldItem == null && newItem == null; 456 | } 457 | 458 | @Override 459 | public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 460 | T oldItem = oldList.get(oldItemPosition); 461 | T newItem = newList.get(newItemPosition); 462 | if (oldItem != null && newItem != null) { 463 | return mConfig.getDiffCallback().areContentsTheSame(oldItem, newItem); 464 | } 465 | if (oldItem == null && newItem == null) { 466 | return true; 467 | } 468 | throw new AssertionError(); 469 | } 470 | 471 | @Nullable 472 | @Override 473 | public Object getChangePayload(int oldItemPosition, int newItemPosition) { 474 | T oldItem = oldList.get(oldItemPosition); 475 | T newItem = newList.get(newItemPosition); 476 | if (oldItem != null && newItem != null) { 477 | return mConfig.getDiffCallback().getChangePayload(oldItem, newItem); 478 | } 479 | throw new AssertionError(); 480 | } 481 | }); 482 | // 使用AsyncDifferConfig中的主线程更新UI,先判断递增生成的 runGeneration 变量是否与 AsyncListDiffer 中当前与执行线程的次数的最大值是否相等,如果相等,将 newList 赋值给 mList ,将 newList添加到只读集合 mReadOnlyList 中,然后通知列表更新。 483 | mMainThreadExecutor.execute(new Runnable() { 484 | @Override 485 | public void run() { 486 | if (mMaxScheduledGeneration == runGeneration) { 487 | latchList(newList, result, commitCallback); 488 | } 489 | } 490 | }); 491 | } 492 | }); 493 | } 494 | 495 | @SuppressWarnings("WeakerAccess") /* synthetic access */ 496 | void latchList(@NonNull List newList, @NonNull DiffUtil.DiffResult diffResult, @Nullable Runnable commitCallback) { 497 | final List previousList = mReadOnlyList; 498 | mList = newList; 499 | // 将 newList 添加到 mReadOnlyList 中 500 | mReadOnlyList = Collections.unmodifiableList(newList); 501 | // 通知列表更新 502 | diffResult.dispatchUpdatesTo(mUpdateCallback); 503 | onCurrentListChanged(previousList, commitCallback); 504 | } 505 | 506 | // 省略其它代码...... 507 | } 508 | ``` 509 | 510 | ## 9.ListUpdateCallback 511 | 操作列表更新的接口,此类可与DiffUtil一起使用,以检测两个列表之间的变化。至于ListUpdateCallback接口具体做了那些事儿,切看以下函数: 512 | 513 | onInserted 在指定位置插入Item时调用,position 指定位置, count 插入Item的数量 514 | ```java 515 | void onInserted(int position, int count); 516 | ``` 517 | onRemoved 在删除指定位置上的Item时调用,position 指定位置, count 删除的Item的数量 518 | ```java 519 | void onRemoved(int position, int count); 520 | ``` 521 | onMoved 当Item更改其在列表中的位置时调用, fromPosition 当前Item在移动之前的位置,toPosition 当前Item在移动之后的位置 522 | ```java 523 | void onMoved(int fromPosition, int toPosition); 524 | ``` 525 | onChanged 在指定位置更新Item时调用,position 指定位置,count 要更新的Item个数,payload 可选参数,值为null时表示全部更新,否则表示局部更新。 526 | ```java 527 | void onChanged(int position, int count, @Nullable Object payload); 528 | ``` 529 | ## 10.ListUpdateCallback的实现类AdapterListUpdateCallback 530 | AdapterListUpdateCallback的作用是将更新事件调度回调给Adapter,如下: 531 | ```java 532 | public final class AdapterListUpdateCallback implements ListUpdateCallback { 533 | 534 | @NonNull 535 | private final RecyclerView.Adapter mAdapter; 536 | 537 | public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) { 538 | mAdapter = adapter; 539 | } 540 | 541 | @Override 542 | public void onInserted(int position, int count) { 543 | mAdapter.notifyItemRangeInserted(position, count); 544 | } 545 | 546 | @Override 547 | public void onRemoved(int position, int count) { 548 | mAdapter.notifyItemRangeRemoved(position, count); 549 | } 550 | 551 | @Override 552 | public void onMoved(int fromPosition, int toPosition) { 553 | mAdapter.notifyItemMoved(fromPosition, toPosition); 554 | } 555 | 556 | @Override 557 | public void onChanged(int position, int count, Object payload) { 558 | mAdapter.notifyItemRangeChanged(position, count, payload); 559 | } 560 | } 561 | ``` 562 | ## 11.AsyncDifferConfig 563 | AsyncDifferConfig的角色很简单,是一个DiffUtil.ItemCallback的配置类,其内部创建了一个固定大小的线程池,提供了两种线程,即后台线程和主线程,主要用于差异性计算和更新UI。AsyncDifferConfig核心代码如下: 564 | 565 | ```java 566 | public final class AsyncDifferConfig { 567 | 568 | // 省略其他代码...... 569 | 570 | @SuppressWarnings("WeakerAccess") 571 | @RestrictTo(RestrictTo.Scope.LIBRARY) 572 | @Nullable 573 | public Executor getMainThreadExecutor() { 574 | return mMainThreadExecutor; 575 | } 576 | 577 | @SuppressWarnings("WeakerAccess") 578 | @NonNull 579 | public Executor getBackgroundThreadExecutor() { 580 | return mBackgroundThreadExecutor; 581 | } 582 | 583 | @SuppressWarnings("WeakerAccess") 584 | @NonNull 585 | public DiffUtil.ItemCallback getDiffCallback() { 586 | return mDiffCallback; 587 | } 588 | 589 | public static final class Builder { 590 | 591 | // 省略其他代码...... 592 | 593 | @NonNull 594 | public AsyncDifferConfig build() { 595 | if (mBackgroundThreadExecutor == null) { 596 | synchronized (sExecutorLock) { 597 | if (sDiffExecutor == null) { 598 | // 创建一个固定大小的线程池 599 | sDiffExecutor = Executors.newFixedThreadPool(2); 600 | } 601 | } 602 | mBackgroundThreadExecutor = sDiffExecutor; 603 | } 604 | return new AsyncDifferConfig<>( 605 | mMainThreadExecutor, 606 | mBackgroundThreadExecutor, 607 | mDiffCallback); 608 | } 609 | // 省略其他代码...... 610 | } 611 | } 612 | ``` 613 | 最近github访问特别慢,项目已经上传至码云,有兴趣的小伙伴可以前往下载看看,记得点赞哦~~~ 614 | ##### [码云Gitee](https://gitee.com/mengjingbo/multitype-adapter-sample) 615 | --------------------------------------------------------------------------------