├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .idea └── codeStyles │ └── Project.xml ├── README.md ├── app ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── kronos │ │ └── sample │ │ └── ExampleInstrumentedTest.java │ ├── debug │ └── java │ │ └── com │ │ └── kronos │ │ └── sample │ │ └── Test.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── kronos │ │ │ └── sample │ │ │ ├── BRAVHAdapterActivity.kt │ │ │ ├── ConcatAdapterActivity.kt │ │ │ ├── DateHelper.kt │ │ │ ├── MainActivity.kt │ │ │ ├── SimpleAdapterActivity.kt │ │ │ ├── StringAdapterActivity.kt │ │ │ ├── adapter │ │ │ ├── BRAVHAdapter.kt │ │ │ ├── StringAdapter.kt │ │ │ └── TestAdapter.kt │ │ │ ├── concat │ │ │ └── ConcatAdapterDsl.kt │ │ │ ├── entity │ │ │ └── TestEntity.kt │ │ │ ├── glide │ │ │ ├── CustomGlideUrl.kt │ │ │ ├── KronosGlideModule.kt │ │ │ ├── KronosUrlLoader.kt │ │ │ └── OkHttpStreamFetcher.kt │ │ │ └── widget │ │ │ ├── HeaderAdapterCallBack.kt │ │ │ ├── HeaderBaseAdapter.kt │ │ │ └── SimpleViewHolder.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_recyclerview.xml │ │ ├── recycler_item_header.xml │ │ └── recycler_item_test.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 │ │ ├── colors.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── values.xml │ └── test │ └── java │ └── com │ └── kronos │ └── sample │ └── ExampleUnitTest.java ├── build.gradle ├── diflib ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── kronos │ │ └── diffutil │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── kronos │ │ │ └── diffutil │ │ │ ├── AbstractDiffHelper.kt │ │ │ ├── BaseDiffCallBack.kt │ │ │ ├── BaseDiffHelper.kt │ │ │ ├── IDifference.kt │ │ │ ├── IEqualsAdapter.kt │ │ │ ├── KotlinDataDiffHelper.kt │ │ │ ├── ParcelDiffHelper.kt │ │ │ ├── SimpleAdapterCallBack.kt │ │ │ ├── SimpleDiffHelper.kt │ │ │ ├── by │ │ │ └── KotlinDataAdapterDelegate.kt │ │ │ └── utils │ │ │ ├── DiffThreadFactory.kt │ │ │ ├── KotlinDataExtension.kt │ │ │ └── ReflectExt.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── kronos │ └── diffutil │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.bat text eol=crlf 4 | *.jar binary -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '.idea/**' 9 | - '.gitattributes' 10 | - '.github/**.json' 11 | - '.gitignore' 12 | - '.gitmodules' 13 | - '**.md' 14 | - 'LICENSE' 15 | - 'NOTICE' 16 | pull_request: 17 | paths-ignore: 18 | - '.idea/**' 19 | - '.gitattributes' 20 | - '.github/**.json' 21 | - '.gitignore' 22 | - '.gitmodules' 23 | - '**.md' 24 | - 'LICENSE' 25 | - 'NOTICE' 26 | 27 | jobs: 28 | check: 29 | name: Check 30 | runs-on: ubuntu-latest 31 | timeout-minutes: 10 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: actions/setup-java@v3 35 | with: 36 | distribution: 'zulu' 37 | java-version: 17 38 | - name: Validate Gradle Wrapper 39 | uses: gradle/wrapper-validation-action@v1 40 | 41 | build: 42 | name: Build 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v3 46 | - uses: actions/setup-java@v3 47 | with: 48 | distribution: 'zulu' 49 | java-version: 17 50 | - uses: gradle/gradle-build-action@v2 51 | with: 52 | arguments: app:assembleDebug 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | .DS_Store 5 | build 6 | captures 7 | local.properties 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 13 | 14 | 15 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | xmlns:android 24 | 25 | ^$ 26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 | xmlns:.* 35 | 36 | ^$ 37 | 38 | 39 | BY_NAME 40 | 41 |
42 |
43 | 44 | 45 | 46 | .*:id 47 | 48 | http://schemas.android.com/apk/res/android 49 | 50 | 51 | 52 |
53 |
54 | 55 | 56 | 57 | .*:name 58 | 59 | http://schemas.android.com/apk/res/android 60 | 61 | 62 | 63 |
64 |
65 | 66 | 67 | 68 | name 69 | 70 | ^$ 71 | 72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 | style 80 | 81 | ^$ 82 | 83 | 84 | 85 |
86 |
87 | 88 | 89 | 90 | .* 91 | 92 | ^$ 93 | 94 | 95 | BY_NAME 96 | 97 |
98 |
99 | 100 | 101 | 102 | .* 103 | 104 | http://schemas.android.com/apk/res/android 105 | 106 | 107 | ANDROID_ATTRIBUTE_ORDER 108 | 109 |
110 |
111 | 112 | 113 | 114 | .* 115 | 116 | .* 117 | 118 | 119 | BY_NAME 120 | 121 |
122 |
123 |
124 |
125 |
126 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DiffUtils 2 | 3 | ## 背景以及原理 4 | 5 | 谷歌在封装RecyclerView时候建议我们最好不要去调用`notifyDataSetChanged`,而是去调用其他的局部刷新的方法(增删改)。但是由于要计算游标等等所以感觉其实非常不好用,而且可能会导致一些奇奇怪怪的crash问题。 6 | 7 | 而DiffUtils的使用,首先要有两个List,一个代表旧数据,一个代表新数据,内部旧涉及到数据copy的问题,如果浅拷贝的话,其equals比较是没有任何意义的,所以项目使用了深拷贝的形式。 8 | 9 | 原理基于Parcel,用安卓原生的Parcelable进行数据模型拷贝。 10 | 11 | 当前项目简单的diffutil封装以及配合recyclerview adapter使用,可以实现数据动态增删移动等等操作同时配合,recyclerview adapter的局部数据调整刷新结合在一起。 12 | 13 | ## 使用 14 | 15 | 代码已经加入了线程池,以及主线程调度逻辑,所以可以直接子线程使用。不上传maven的原因是觉得可能还不够完善,可以直接考虑代码复制。 16 | 17 | 1. 对代码进行了重定义封装,当你需要使用深拷贝的时候,切记实现Parcelable接口,数据model最好实现IDifference接口,根据这个进行数据ID逻辑判断。如果要做内容比较的情况下实现IEqualsAdapter,同时使用插件生成hashcode,equals方法。 18 | 19 | 20 | ```kotlin 21 | data class TestEntity(var id: Int = 0, 22 | var displayTime: Long = 0, 23 | var text: String? = Random().nextInt(10000).toString()) : Parcelable, IDifference, IEqualsAdapter { 24 | 25 | override val uniqueId: String 26 | get() = id.toString() 27 | 28 | fun update() { 29 | displayTime = System.currentTimeMillis() 30 | text = "更新数据" 31 | } 32 | 33 | constructor(source: Parcel) : this( 34 | source.readInt(), 35 | source.readLong(), 36 | source.readString() 37 | ) 38 | 39 | override fun describeContents() = 0 40 | 41 | override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { 42 | writeInt(id) 43 | writeLong(displayTime) 44 | writeString(text) 45 | } 46 | 47 | companion object { 48 | @JvmField 49 | val CREATOR: Parcelable.Creator = object : Parcelable.Creator { 50 | override fun createFromParcel(source: Parcel): TestEntity = TestEntity(source) 51 | override fun newArray(size: Int): Array = arrayOfNulls(size) 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | 2. 初始化并传入数据,并设置数据刷新回掉,如果你有header或者别的话自己定义一个,提供了深拷贝和浅拷贝的两种实现。 58 | 59 | ```kotlin 60 | // 深拷贝 61 | val diffHelper: ParcelDiffHelper = ParcelDiffHelper() 62 | diffHelper.callBack = SimpleAdapterCallBack(this) 63 | // lifecyclerowner 64 | diffHelper.bindLifeCycle(this) 65 | diffHelper.setData(items) 66 | ``` 67 | 68 | ```kotlin 69 | //浅拷贝版本 70 | val diffHelper: SimpleDiffHelper = SimpleDiffHelper() 71 | diffHelper.callBack = SimpleAdapterCallBack(this) 72 | // lifecyclerowner 73 | diffHelper.bindLifeCycle(this) 74 | diffHelper.setData(items) 75 | ``` 76 | 77 | 3. 当items发生变化(任意变化增删改都行),调用数据刷新。 78 | 79 | ```kotlin 80 | diffHelper.notifyItemChanged() 81 | ``` 82 | 83 | ## 其他 84 | 85 | 代码可以结合任意的Adapter使用,包括BRVH等等。Demo中有简单的接入方式。 -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | id("kotlin-parcelize") 5 | } 6 | 7 | android { 8 | compileSdk 32 9 | 10 | defaultConfig { 11 | applicationId "com.kronos.sample" 12 | minSdk 21 13 | targetSdk 32 14 | versionCode 1 15 | versionName "1.0" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | buildFeatures { 25 | viewBinding = true 26 | } 27 | } 28 | configurations { 29 | all*.exclude group: 'com.google.protobuf', module: 'protobuf-lite' 30 | } 31 | 32 | 33 | dependencies { 34 | implementation fileTree(dir: 'libs', include: ['*.jar']) 35 | implementation project(":diflib") 36 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1" 37 | 38 | implementation 'androidx.appcompat:appcompat:1.4.1' 39 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3' 40 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 41 | // implementation "androidx.paging:paging-runtime-ktx:2.1.2" 42 | implementation 'jp.wasabeef:recyclerview-animators:4.0.2' 43 | implementation("com.squareup.okhttp3:okhttp:4.10.0") 44 | 45 | implementation('com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6') { 46 | transitive = false 47 | } 48 | 49 | kapt 'com.github.bumptech.glide:compiler:4.13.2' 50 | 51 | implementation 'com.github.bumptech.glide:glide:4.13.2' 52 | testImplementation 'junit:junit:4.13.2' 53 | androidTestImplementation 'androidx.test:runner:1.4.0' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 55 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/kronos/sample/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.kronos.sample; 2 | 3 | import android.content.Context; 4 | import androidx.test.InstrumentationRegistry; 5 | import androidx.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.kronos.sample", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/debug/java/com/kronos/sample/Test.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample 2 | 3 | /** 4 | * 5 | * @Author LiABao 6 | * @Since 2022/1/6 7 | * 8 | */ 9 | class Test { 10 | fun log() { 11 | 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/BRAVHAdapterActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.recyclerview.widget.LinearLayoutManager 6 | import by.kirich1409.viewbindingdelegate.viewBinding 7 | import com.kronos.diffutil.ParcelDiffHelper 8 | import com.kronos.sample.databinding.ActivityRecyclerviewBinding 9 | import com.kronos.sample.entity.TestEntity 10 | import jp.wasabeef.recyclerview.animators.SlideInRightAnimator 11 | import java.util.* 12 | 13 | class BRAVHAdapterActivity : AppCompatActivity() { 14 | private val items by lazy { 15 | mutableListOf() 16 | } 17 | private val parcelDiffHelper: ParcelDiffHelper by lazy { 18 | ParcelDiffHelper() 19 | } 20 | private val viewBind by viewBinding { 21 | ActivityRecyclerviewBinding.inflate(it.layoutInflater) 22 | } 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | setContentView(viewBind.root) 27 | mockEntity() 28 | viewBind.recyclerView.layoutManager = LinearLayoutManager(this) 29 | /* recyclerView.adapter = adapter 30 | adapter.addHeaderView( 31 | LayoutInflater.from(this@BRAVHAdapterActivity) 32 | .inflate(R.layout.recycler_item_header, recyclerView, false) 33 | )*/ 34 | // adapter.setNewInstance(items) 35 | viewBind.recyclerView.itemAnimator = SlideInRightAnimator() 36 | viewBind.addTv.setOnClickListener { 37 | mockEntity() 38 | parcelDiffHelper.notifyItemChanged() 39 | } 40 | viewBind.removeTv.setOnClickListener { 41 | items.removeAt(0) 42 | mockEntity(2) 43 | parcelDiffHelper.notifyItemChanged() 44 | } 45 | viewBind.swapTv.setOnClickListener { 46 | swap(items, 1, 4) 47 | parcelDiffHelper.notifyItemChanged() 48 | } 49 | viewBind.refreshTv.setOnClickListener { 50 | items.clear() 51 | mockEntity() 52 | parcelDiffHelper.notifyItemChanged() 53 | } 54 | 55 | } 56 | 57 | private fun mockEntity() { 58 | var count = 0 59 | val itemSize = if (items.isEmpty()) 0 else items[items.size - 1].id + 1 60 | while (count < 20) { 61 | val entity = TestEntity(itemSize.plus(count)) 62 | count++ 63 | items.add(entity) 64 | } 65 | } 66 | 67 | private fun mockEntity(index: Int) { 68 | var count = 0 69 | val itemSize = if (items.isEmpty()) 0 else items.size + 1 70 | while (count < 20) { 71 | val entity = TestEntity(itemSize.plus(count)) 72 | count++ 73 | items.add(index, entity) 74 | } 75 | } 76 | 77 | private fun swap(list: MutableList?, oldPosition: Int, newPosition: Int) { 78 | Collections.swap(list, oldPosition, newPosition) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/ConcatAdapterActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.recyclerview.widget.LinearLayoutManager 7 | import by.kirich1409.viewbindingdelegate.viewBinding 8 | import com.kronos.diffutil.ParcelDiffHelper 9 | import com.kronos.diffutil.SimpleDiffHelper 10 | import com.kronos.sample.adapter.StringAdapter 11 | import com.kronos.sample.adapter.TestAdapter 12 | import com.kronos.sample.concat.adapter 13 | import com.kronos.sample.concat.builderConcatAdapter 14 | import com.kronos.sample.concat.builderSlideInRightAnimationAdapter 15 | import com.kronos.sample.databinding.ActivityRecyclerviewBinding 16 | import com.kronos.sample.entity.TestEntity 17 | import jp.wasabeef.recyclerview.animators.SlideInRightAnimator 18 | import kotlinx.coroutines.GlobalScope 19 | import kotlinx.coroutines.delay 20 | import kotlinx.coroutines.launch 21 | import java.util.* 22 | 23 | /** 24 | * 25 | * @Author LiABao 26 | * @Since 2021/5/11 27 | * 28 | */ 29 | class ConcatAdapterActivity : AppCompatActivity() { 30 | private val items by lazy { 31 | mutableListOf() 32 | } 33 | 34 | private val stringParcelDiffHelper: SimpleDiffHelper = SimpleDiffHelper() 35 | 36 | private val parcelDiffHelper: ParcelDiffHelper = ParcelDiffHelper() 37 | private val viewBind by viewBinding { 38 | ActivityRecyclerviewBinding.inflate(it.layoutInflater) 39 | } 40 | 41 | override fun onCreate(savedInstanceState: Bundle?) { 42 | super.onCreate(savedInstanceState) 43 | setContentView(viewBind.root) 44 | GlobalScope.launch { 45 | delay(1000) 46 | mockEntity() 47 | parcelDiffHelper.setData(items) 48 | parcelDiffHelper.notifyItemChanged() 49 | delay(1000) 50 | stringParcelDiffHelper.setData(mutableListOf().apply { 51 | for (index in 0 until 20) { 52 | add(index.toString()) 53 | } 54 | }) 55 | } 56 | 57 | viewBind.recyclerView.layoutManager = LinearLayoutManager(this) 58 | val concatAdapter = builderConcatAdapter { 59 | adapter { 60 | builderSlideInRightAnimationAdapter { 61 | TestAdapter(parcelDiffHelper).apply { 62 | addHeaderView( 63 | LayoutInflater.from(this@ConcatAdapterActivity).inflate( 64 | R.layout.recycler_item_header, 65 | viewBind.recyclerView, false 66 | ) 67 | ) 68 | } 69 | } 70 | } 71 | adapter { 72 | builderSlideInRightAnimationAdapter { 73 | StringAdapter(stringParcelDiffHelper).apply { 74 | addHeaderView( 75 | LayoutInflater.from(this@ConcatAdapterActivity).inflate( 76 | R.layout.recycler_item_header, 77 | viewBind.recyclerView, false 78 | ) 79 | ) 80 | } 81 | } 82 | } 83 | } 84 | viewBind.recyclerView.adapter = concatAdapter 85 | parcelDiffHelper.bindLifeCycle(this) 86 | viewBind.recyclerView.itemAnimator = SlideInRightAnimator() 87 | viewBind.addTv.setOnClickListener { 88 | mockEntity() 89 | parcelDiffHelper.notifyItemChanged() 90 | } 91 | viewBind.removeTv.setOnClickListener { 92 | items.removeAt(0) 93 | mockEntity(2) 94 | parcelDiffHelper.notifyItemChanged() 95 | } 96 | viewBind.swapTv.setOnClickListener { 97 | swap(items, 1, 4) 98 | parcelDiffHelper.notifyItemChanged() 99 | } 100 | viewBind.refreshTv.setOnClickListener { 101 | items.clear() 102 | mockEntity() 103 | parcelDiffHelper.notifyItemChanged() 104 | } 105 | 106 | } 107 | 108 | private fun mockEntity() { 109 | var count = 0 110 | val itemSize = if (items.isEmpty()) 0 else items[items.size - 1].id + 1 111 | while (count < 20) { 112 | val entity = TestEntity(itemSize.plus(count)) 113 | count++ 114 | items.add(entity) 115 | } 116 | } 117 | 118 | private fun mockEntity(index: Int) { 119 | var count = 0 120 | val itemSize = if (items.isEmpty()) 0 else items.size + 1 121 | while (count < 20) { 122 | val entity = TestEntity(itemSize.plus(count)) 123 | count++ 124 | items.add(index, entity) 125 | } 126 | } 127 | 128 | private fun swap(list: MutableList?, oldPosition: Int, newPosition: Int) { 129 | Collections.swap(list, oldPosition, newPosition) 130 | } 131 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/DateHelper.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.Date 5 | import java.util.Locale 6 | 7 | fun Long.getTime(dateFormat: SimpleDateFormat): String { 8 | return dateFormat.format(Date(this)) 9 | } 10 | 11 | fun Long.getTime(format: String): String { 12 | val dateFormat = SimpleDateFormat(format, Locale.CHINA) 13 | return getTime(dateFormat) 14 | } 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample 2 | 3 | import android.content.Intent 4 | import androidx.appcompat.app.AppCompatActivity 5 | 6 | import android.os.Bundle 7 | import android.os.Handler 8 | import android.os.Looper 9 | import android.util.Log 10 | import by.kirich1409.viewbindingdelegate.viewBinding 11 | import com.kronos.sample.databinding.ActivityMainBinding 12 | 13 | 14 | class MainActivity : AppCompatActivity() { 15 | private val viewBind by viewBinding { 16 | ActivityMainBinding.inflate(it.layoutInflater) 17 | } 18 | 19 | private val message = "[bapi] branch target=feature/develop" 20 | private val regex = Regex("branch target=(\\D\\S*)").toPattern() 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContentView(viewBind.root) 25 | viewBind.simpleAdapterBtn.setOnClickListener { 26 | intent(SimpleAdapterActivity::class.java) 27 | } 28 | viewBind.bravhAdapterBtn.setOnClickListener { 29 | intent(ConcatAdapterActivity::class.java) 30 | } 31 | viewBind.stringAdapterBtn.setOnClickListener { 32 | intent(StringAdapterActivity::class.java) 33 | } 34 | val matcher = regex.matcher(message) 35 | if (matcher.find()) { 36 | val group = matcher.group(1) 37 | Log.i("regex", "group:$group") 38 | } 39 | //val concatAdapter 40 | } 41 | 42 | private fun intent(target: Class) { 43 | val intent = Intent(this, target) 44 | startActivity(intent) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/SimpleAdapterActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.recyclerview.widget.LinearLayoutManager 7 | import by.kirich1409.viewbindingdelegate.viewBinding 8 | import com.kronos.diffutil.KotlinDataDiffHelper 9 | import com.kronos.diffutil.by.dataDiff 10 | import com.kronos.sample.adapter.TestAdapter 11 | import com.kronos.sample.databinding.ActivityRecyclerviewBinding 12 | import com.kronos.sample.entity.TestEntity 13 | import jp.wasabeef.recyclerview.adapters.SlideInRightAnimationAdapter 14 | import jp.wasabeef.recyclerview.animators.SlideInRightAnimator 15 | import kotlinx.coroutines.* 16 | import java.util.* 17 | 18 | class SimpleAdapterActivity : AppCompatActivity() { 19 | 20 | private val items by lazy { 21 | mutableListOf() 22 | } 23 | private val dataDiffHelper: KotlinDataDiffHelper by dataDiff { 24 | it.copy() 25 | } 26 | private val viewBind by viewBinding { 27 | ActivityRecyclerviewBinding.inflate(it.layoutInflater) 28 | } 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | setContentView(R.layout.activity_recyclerview) 33 | GlobalScope.launch { 34 | delay(1000) 35 | mockEntity() 36 | dataDiffHelper.setData(items) 37 | dataDiffHelper.notifyItemChanged() 38 | } 39 | viewBind.recyclerView.layoutManager = LinearLayoutManager(this) 40 | viewBind.recyclerView.adapter = 41 | SlideInRightAnimationAdapter(TestAdapter(dataDiffHelper).apply { 42 | addHeaderView( 43 | LayoutInflater.from(this@SimpleAdapterActivity).inflate( 44 | R.layout.recycler_item_header, 45 | viewBind.recyclerView, false 46 | ) 47 | ) 48 | }) 49 | dataDiffHelper.bindLifeCycle(this) 50 | viewBind.recyclerView.itemAnimator = SlideInRightAnimator() 51 | viewBind.addTv.setOnClickListener { 52 | mockEntity() 53 | dataDiffHelper.notifyItemChanged() 54 | } 55 | viewBind.removeTv.setOnClickListener { 56 | items.removeAt(0) 57 | mockEntity(2) 58 | dataDiffHelper.notifyItemChanged() 59 | } 60 | viewBind.swapTv.setOnClickListener { 61 | swap(items, 1, 4) 62 | dataDiffHelper.notifyItemChanged() 63 | } 64 | viewBind.refreshTv.setOnClickListener { 65 | items.clear() 66 | mockEntity() 67 | dataDiffHelper.notifyItemChanged() 68 | } 69 | 70 | } 71 | 72 | private fun mockEntity() { 73 | var count = 0 74 | val itemSize = if (items.isEmpty()) 0 else items[items.size - 1].id + 1 75 | while (count < 20) { 76 | val entity = TestEntity(itemSize.plus(count)) 77 | count++ 78 | items.add(entity) 79 | } 80 | } 81 | 82 | private fun mockEntity(index: Int) { 83 | var count = 0 84 | val itemSize = if (items.isEmpty()) 0 else items.size + 1 85 | while (count < 20) { 86 | val entity = TestEntity(itemSize.plus(count)) 87 | count++ 88 | items.add(index, entity) 89 | } 90 | } 91 | 92 | private fun swap(list: MutableList?, oldPosition: Int, newPosition: Int) { 93 | Collections.swap(list, oldPosition, newPosition) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/StringAdapterActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.recyclerview.widget.LinearLayoutManager 7 | import by.kirich1409.viewbindingdelegate.viewBinding 8 | import com.kronos.diffutil.SimpleDiffHelper 9 | import com.kronos.sample.adapter.StringAdapter 10 | import com.kronos.sample.databinding.ActivityRecyclerviewBinding 11 | import jp.wasabeef.recyclerview.adapters.SlideInRightAnimationAdapter 12 | import jp.wasabeef.recyclerview.animators.SlideInRightAnimator 13 | import kotlinx.coroutines.GlobalScope 14 | import kotlinx.coroutines.delay 15 | import kotlinx.coroutines.launch 16 | import java.util.* 17 | 18 | class StringAdapterActivity : AppCompatActivity() { 19 | 20 | private val items by lazy { 21 | mutableListOf() 22 | } 23 | 24 | private val parcelDiffHelper: SimpleDiffHelper = SimpleDiffHelper() 25 | 26 | private val viewBind by viewBinding { 27 | ActivityRecyclerviewBinding.inflate(it.layoutInflater) 28 | } 29 | 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | setContentView(viewBind.root) 34 | GlobalScope.launch { 35 | delay(1000) 36 | mockEntity() 37 | parcelDiffHelper.setData(items) 38 | parcelDiffHelper.notifyItemChanged() 39 | } 40 | viewBind.recyclerView.layoutManager = LinearLayoutManager(this) 41 | viewBind.recyclerView.adapter = 42 | SlideInRightAnimationAdapter(StringAdapter(parcelDiffHelper).apply { 43 | addHeaderView( 44 | LayoutInflater.from(this@StringAdapterActivity).inflate( 45 | R.layout.recycler_item_header, 46 | viewBind.recyclerView, false 47 | ) 48 | ) 49 | }) 50 | viewBind.recyclerView.itemAnimator = SlideInRightAnimator() 51 | 52 | viewBind.addTv.setOnClickListener { 53 | mockEntity() 54 | parcelDiffHelper.notifyItemChanged() 55 | } 56 | viewBind.removeTv.setOnClickListener { 57 | items.removeAt(0) 58 | mockEntity(2) 59 | parcelDiffHelper.notifyItemChanged() 60 | } 61 | viewBind.swapTv.setOnClickListener { 62 | swap(items, 1, 4) 63 | parcelDiffHelper.notifyItemChanged() 64 | } 65 | viewBind.refreshTv.setOnClickListener { 66 | items.clear() 67 | mockEntity() 68 | parcelDiffHelper.notifyItemChanged() 69 | } 70 | 71 | } 72 | 73 | private fun mockEntity() { 74 | var count = 0 75 | val itemSize = if (items.isEmpty()) 0 else items.size + 1 76 | while (count < 20) { 77 | val entity = itemSize.plus(count).toString() 78 | count++ 79 | items.add(entity) 80 | } 81 | } 82 | 83 | private fun mockEntity(index: Int) { 84 | var count = 0 85 | val itemSize = if (items.isEmpty()) 0 else items.size + 1 86 | while (count < 20) { 87 | val entity = itemSize.plus(count).toString() 88 | count++ 89 | items.add(index, entity) 90 | } 91 | } 92 | 93 | private fun swap(list: MutableList?, oldPosition: Int, newPosition: Int) { 94 | Collections.swap(list, oldPosition, newPosition) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/adapter/BRAVHAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.adapter 2 | 3 | import com.kronos.diffutil.ParcelDiffHelper 4 | import com.kronos.sample.entity.TestEntity 5 | 6 | class BRAVHAdapter(private val parcelDiffHelper: ParcelDiffHelper) 7 | /*: BaseQuickAdapter(R.layout.recycler_item_test) { 8 | 9 | init { 10 | parcelDiffHelper.callBack = BRVHAdapterCallBack(this) 11 | } 12 | 13 | override fun setNewInstance(list: MutableList?) { 14 | super.setNewInstance(list) 15 | // super 内部已经调用过notifyDataChange 所以会有些问题 16 | parcelDiffHelper.setData(list, true) 17 | } 18 | 19 | override fun convert(holder: BaseViewHolder, item: TestEntity) { 20 | holder.itemView.titleTv.text = item.id.toString() + " ${item.text} " + 21 | item.displayTime.getTime("yyyy-MM-dd HH:mm") 22 | } 23 | 24 | override fun getDefItemCount(): Int { 25 | return parcelDiffHelper.getItemSize() 26 | } 27 | }*/ -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/adapter/StringAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.TextView 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.kronos.diffutil.SimpleDiffHelper 9 | import com.kronos.sample.R 10 | import com.kronos.sample.widget.HeaderAdapterCallBack 11 | import com.kronos.sample.widget.HeaderBaseAdapter 12 | 13 | class StringAdapter(private val parcelDiffHelper: SimpleDiffHelper) : 14 | HeaderBaseAdapter() { 15 | 16 | init { 17 | parcelDiffHelper.callBack = HeaderAdapterCallBack(this) 18 | } 19 | 20 | override fun listItemCount(): Int { 21 | return parcelDiffHelper.getItemSize() 22 | } 23 | 24 | override fun onCreateListViewHolder(parent: ViewGroup, viewType: Int): StringViewHolder { 25 | return StringViewHolder( 26 | LayoutInflater.from(parent.context).inflate( 27 | R.layout.recycler_item_test, 28 | parent, false 29 | ) 30 | ) 31 | } 32 | 33 | override fun onBindListViewHolder(holder: StringViewHolder, position: Int) { 34 | val entity = parcelDiffHelper.getEntity(position) 35 | holder.bindData(entity) 36 | } 37 | } 38 | 39 | class StringViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 40 | fun bindData(entity: String?) { 41 | entity?.apply { 42 | itemView.findViewById(R.id.titleTv).text = entity 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/adapter/TestAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import by.kirich1409.viewbindingdelegate.viewBinding 8 | import com.kronos.diffutil.BaseDiffHelper 9 | import com.kronos.sample.R 10 | import com.kronos.sample.databinding.RecyclerItemTestBinding 11 | import com.kronos.sample.entity.TestEntity 12 | import com.kronos.sample.getTime 13 | import com.kronos.sample.widget.HeaderAdapterCallBack 14 | import com.kronos.sample.widget.HeaderBaseAdapter 15 | 16 | class TestAdapter(private val parcelDiffHelper: BaseDiffHelper) : 17 | HeaderBaseAdapter() { 18 | 19 | init { 20 | parcelDiffHelper.callBack = HeaderAdapterCallBack(this) 21 | } 22 | 23 | override fun listItemCount(): Int { 24 | return parcelDiffHelper.getItemSize() 25 | } 26 | 27 | override fun onCreateListViewHolder(parent: ViewGroup, viewType: Int): VieHolder { 28 | return VieHolder( 29 | LayoutInflater.from(parent.context).inflate( 30 | R.layout.recycler_item_test, 31 | parent, false 32 | ) 33 | ) 34 | } 35 | 36 | override fun onBindListViewHolder(holder: VieHolder, position: Int) { 37 | val entity = parcelDiffHelper.getEntity(position) 38 | holder.bindData(entity) 39 | holder.itemView.setOnClickListener { 40 | entity?.update() 41 | parcelDiffHelper.notifyItemChanged() 42 | } 43 | } 44 | } 45 | 46 | class VieHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 47 | private val viewbind :RecyclerItemTestBinding by viewBinding { RecyclerItemTestBinding.bind(itemView) } 48 | fun bindData(entity: TestEntity?) { 49 | entity?.apply { 50 | viewbind.titleTv.text = entity.id.toString() + " ${entity.text} " + 51 | entity.displayTime.getTime("yyyy-MM-dd HH:mm") 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/concat/ConcatAdapterDsl.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.concat 2 | 3 | import androidx.recyclerview.widget.ConcatAdapter 4 | import androidx.recyclerview.widget.RecyclerView 5 | import jp.wasabeef.recyclerview.adapters.SlideInLeftAnimationAdapter 6 | 7 | /** 8 | * 9 | * @Author LiABao 10 | * @Since 2021/5/11 11 | * 12 | */ 13 | 14 | inline fun builderConcatAdapter(invoke: MutableList>.() -> Unit): 15 | ConcatAdapter { 16 | val list = mutableListOf>() 17 | invoke.invoke(list) 18 | return ConcatAdapter(list) 19 | } 20 | 21 | inline fun MutableList>.adapter(invoke: () -> RecyclerView.Adapter) { 22 | add(invoke.invoke()) 23 | } 24 | 25 | inline fun builderSlideInRightAnimationAdapter(invoke: () -> RecyclerView.Adapter): SlideInLeftAnimationAdapter { 26 | return SlideInLeftAnimationAdapter(invoke.invoke()) 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/entity/TestEntity.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.entity 2 | 3 | import android.os.Parcelable 4 | import com.kronos.diffutil.IDifference 5 | import com.kronos.diffutil.IEqualsAdapter 6 | import kotlinx.parcelize.Parcelize 7 | import java.util.Random 8 | 9 | @Parcelize 10 | data class TestEntity( 11 | var id: Int = 0, 12 | var displayTime: Long = 0, 13 | var text: String? = Random().nextInt(10000).toString() 14 | ) : Parcelable, IDifference, IEqualsAdapter { 15 | 16 | override val uniqueId: String 17 | get() = id.toString() 18 | 19 | fun update() { 20 | displayTime = System.currentTimeMillis() 21 | text = "更新数据" 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/glide/CustomGlideUrl.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.glide 2 | 3 | import android.net.Uri 4 | import android.text.TextUtils 5 | import com.bumptech.glide.load.model.GlideUrl 6 | import com.bumptech.glide.load.model.Headers 7 | import com.bumptech.glide.load.model.LazyHeaders 8 | 9 | /** 10 | * 11 | * @Author LiABao 12 | * @Since 2022/8/4 13 | * 14 | */ 15 | class CustomGlideUrl(glideUrl: GlideUrl) : 16 | GlideUrl(glideUrl.toStringUrl(), getHeadersUtils(glideUrl.headers)) { 17 | private val cacheKeyString: String by lazy { 18 | val url = Uri.parse(toStringUrl()) 19 | val cacheKeyBuilder = Uri.Builder() 20 | cacheKeyBuilder.scheme(url.scheme).authority(url.authority).path(url.path) 21 | for (key in url.queryParameterNames) { 22 | if (TextUtils.equals("Expires", key) || TextUtils.equals("Signature", key) 23 | || TextUtils.equals("OSSAccessKeyId", key) 24 | ) { 25 | continue 26 | } 27 | cacheKeyBuilder.appendQueryParameter(key, url.getQueryParameter(key)) 28 | } 29 | return@lazy cacheKeyBuilder.build().toString() 30 | } 31 | 32 | override fun getCacheKey(): String { 33 | return cacheKeyString 34 | } 35 | 36 | companion object { 37 | 38 | internal fun getHeadersUtils(head: Map): Headers { 39 | val builder = LazyHeaders.Builder() 40 | for ((key, value) in head) { 41 | builder.addHeader(key, value) 42 | } 43 | return builder.build() 44 | } 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/glide/KronosGlideModule.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.glide 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.bumptech.glide.Glide 6 | import com.bumptech.glide.GlideBuilder 7 | import com.bumptech.glide.Registry 8 | import com.bumptech.glide.annotation.GlideModule 9 | import com.bumptech.glide.load.DecodeFormat 10 | import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool 11 | import com.bumptech.glide.load.engine.cache.DiskCache 12 | import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory 13 | import com.bumptech.glide.load.engine.cache.LruResourceCache 14 | import com.bumptech.glide.load.engine.cache.MemorySizeCalculator 15 | import com.bumptech.glide.load.model.GlideUrl 16 | import com.bumptech.glide.module.AppGlideModule 17 | import com.bumptech.glide.request.RequestOptions 18 | import okhttp3.OkHttpClient 19 | import java.io.InputStream 20 | 21 | /** 22 | * 23 | * @Author LiABao 24 | * @Since 2022/8/9 25 | * 26 | */ 27 | @GlideModule 28 | open class KronosGlideModule : AppGlideModule() { 29 | override fun applyOptions(context: Context, builder: GlideBuilder) { 30 | val requestOptions = RequestOptions() 31 | requestOptions.format(DecodeFormat.PREFER_ARGB_8888) 32 | builder.setDefaultRequestOptions(requestOptions) 33 | val calculator = MemorySizeCalculator.Builder(context) 34 | .setMaxSizeMultiplier(0.5f).setMemoryCacheScreens(1f) 35 | .build() 36 | builder.setMemoryCache(LruResourceCache(calculator.memoryCacheSize.toLong())) 37 | .setBitmapPool(LruBitmapPool(calculator.bitmapPoolSize.toLong())) 38 | val diskCacheSizeBytes = 1024 * 1024 * 100L 39 | val newFileName = DiskCache.Factory.DEFAULT_DISK_CACHE_DIR 40 | builder.setDiskCache( 41 | InternalCacheDiskCacheFactory(context, newFileName, diskCacheSizeBytes) 42 | ) 43 | } 44 | 45 | override fun registerComponents(context: Context, glide: Glide, registry: Registry) { 46 | super.registerComponents(context, glide, registry) 47 | registry.replace( 48 | GlideUrl::class.java, 49 | InputStream::class.java, 50 | Factory(OkHttpClient()) 51 | ) 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/glide/KronosUrlLoader.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.glide 2 | 3 | import com.bumptech.glide.load.Options 4 | import com.bumptech.glide.load.model.GlideUrl 5 | import com.bumptech.glide.load.model.ModelLoader 6 | import com.bumptech.glide.load.model.ModelLoaderFactory 7 | import com.bumptech.glide.load.model.MultiModelLoaderFactory 8 | import okhttp3.Call 9 | import okhttp3.OkHttpClient 10 | import java.io.InputStream 11 | 12 | /** 13 | * 14 | * @Author LiABao 15 | * @Since 2022/8/9 16 | * 17 | */ 18 | class KronosUrlLoader(private val client: Call.Factory) : ModelLoader { 19 | 20 | override fun handles(url: GlideUrl): Boolean { 21 | return true 22 | } 23 | 24 | override fun buildLoadData( 25 | model: GlideUrl, 26 | width: Int, 27 | height: Int, 28 | options: Options 29 | ): ModelLoader.LoadData { 30 | val newModel: GlideUrl 31 | if (model is CustomGlideUrl) { 32 | newModel = model 33 | } else { 34 | newModel = CustomGlideUrl(model) 35 | } 36 | return ModelLoader.LoadData(newModel, OkHttpStreamFetcher(client, newModel)) 37 | } 38 | 39 | 40 | } 41 | 42 | class Factory constructor(private val client: Call.Factory = internalClient) : 43 | ModelLoaderFactory { 44 | 45 | override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { 46 | return KronosUrlLoader(client) 47 | } 48 | 49 | override fun teardown() {} 50 | 51 | companion object { 52 | 53 | val internalClient by lazy { 54 | OkHttpClient() 55 | } 56 | 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/glide/OkHttpStreamFetcher.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.glide 2 | 3 | import android.util.Log 4 | import com.bumptech.glide.Priority 5 | import com.bumptech.glide.load.DataSource 6 | import com.bumptech.glide.load.model.GlideUrl 7 | import com.bumptech.glide.load.data.DataFetcher 8 | import kotlin.jvm.Volatile 9 | import com.kronos.sample.glide.OkHttpStreamFetcher 10 | import com.bumptech.glide.util.ContentLengthInputStream 11 | import com.bumptech.glide.load.HttpException 12 | import com.bumptech.glide.util.Preconditions 13 | import okhttp3.* 14 | import java.io.IOException 15 | import java.io.InputStream 16 | 17 | class OkHttpStreamFetcher(client: Call.Factory, url: GlideUrl) : DataFetcher, 18 | Callback { 19 | private val client: Call.Factory 20 | private val url: GlideUrl 21 | private var stream: InputStream? = null 22 | private var responseBody: ResponseBody? = null 23 | private var callback: DataFetcher.DataCallback? = null 24 | 25 | // call may be accessed on the main thread while the object is in use on other threads. All other 26 | // accesses to variables may occur on different threads, but only one at a time. 27 | @Volatile 28 | private var call: Call? = null 29 | override fun loadData( 30 | priority: Priority, 31 | callback: DataFetcher.DataCallback 32 | ) { 33 | val requestBuilder: Request.Builder = Request.Builder().url(url.toStringUrl()) 34 | for ((key, value) in url.headers) { 35 | requestBuilder.addHeader(key, value) 36 | } 37 | val request: Request = requestBuilder.build() 38 | this.callback = callback 39 | call = client.newCall(request) 40 | call!!.enqueue(this) 41 | } 42 | 43 | override fun onFailure(call: Call, e: IOException) { 44 | if (Log.isLoggable(TAG, Log.DEBUG)) { 45 | Log.d(TAG, "OkHttp failed to obtain result", e) 46 | } 47 | callback!!.onLoadFailed(e) 48 | } 49 | 50 | override fun onResponse(call: Call, response: Response) { 51 | responseBody = response.body 52 | if (response.isSuccessful) { 53 | val contentLength = Preconditions.checkNotNull(responseBody).contentLength() 54 | stream = ContentLengthInputStream.obtain(responseBody!!.byteStream(), contentLength) 55 | callback!!.onDataReady(stream) 56 | } else { 57 | callback!!.onLoadFailed(HttpException(response.message, response.code)) 58 | } 59 | } 60 | 61 | override fun cleanup() { 62 | try { 63 | if (stream != null) { 64 | stream!!.close() 65 | } 66 | } catch (e: IOException) { 67 | // Ignored 68 | } 69 | if (responseBody != null) { 70 | responseBody!!.close() 71 | } 72 | callback = null 73 | } 74 | 75 | override fun cancel() { 76 | val local = call 77 | local?.cancel() 78 | } 79 | 80 | override fun getDataClass(): Class { 81 | return InputStream::class.java 82 | } 83 | 84 | override fun getDataSource(): DataSource { 85 | return DataSource.REMOTE 86 | } 87 | 88 | companion object { 89 | private const val TAG = "OkHttpFetcher" 90 | } 91 | 92 | // Public API. 93 | init { 94 | this.client = client 95 | this.url = url 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/widget/HeaderAdapterCallBack.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.widget 2 | 3 | import androidx.recyclerview.widget.ListUpdateCallback 4 | 5 | 6 | class HeaderAdapterCallBack(private val adapter: HeaderBaseAdapter<*>) : ListUpdateCallback { 7 | override fun onInserted(position: Int, count: Int) { 8 | adapter.notifyItemRangeInserted(adapter.headerLayoutCount() + position, count) 9 | } 10 | 11 | override fun onRemoved(position: Int, count: Int) { 12 | adapter.notifyItemRangeRemoved(adapter.headerLayoutCount() + position, count) 13 | } 14 | 15 | override fun onMoved(fromPosition: Int, toPosition: Int) { 16 | adapter.notifyItemMoved(adapter.headerLayoutCount() + fromPosition, adapter.headerLayoutCount() + toPosition) 17 | } 18 | 19 | override fun onChanged(position: Int, count: Int, payload: Any?) { 20 | adapter.notifyItemRangeChanged(adapter.headerLayoutCount() + position, count, payload) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/widget/HeaderBaseAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.widget 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import androidx.recyclerview.widget.RecyclerView.ViewHolder 7 | 8 | abstract class HeaderBaseAdapter : RecyclerView.Adapter() { 9 | 10 | private val headerViews = mutableListOf() 11 | private var viewCursor: View? = null 12 | 13 | final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 14 | if (viewType > HEAD_VIEW_TYPE) { 15 | return SimpleViewHolder(requireNotNull(viewCursor)) 16 | } 17 | return onCreateListViewHolder(parent, viewType) 18 | } 19 | 20 | final override fun onBindViewHolder(holder: ViewHolder, position: Int) { 21 | if (holder is SimpleViewHolder) { 22 | return 23 | } 24 | onBindListViewHolder(holder as VH, position - headerViews.size) 25 | } 26 | 27 | fun headerLayoutCount(): Int { 28 | return headerViews.size 29 | } 30 | 31 | fun addHeaderView(view: View?) { 32 | view?.apply { 33 | if (!headerViews.contains(this)) { 34 | headerViews.add(this) 35 | notifyItemInserted(headerViews.size - 1) 36 | } 37 | } 38 | } 39 | 40 | fun removeHeaderView(view: View?) { 41 | view?.apply { 42 | if (headerViews.contains(this)) { 43 | headerViews.remove(this) 44 | notifyDataSetChanged() 45 | } 46 | } 47 | } 48 | 49 | override fun getItemCount(): Int { 50 | return headerViews.size + listItemCount() 51 | } 52 | 53 | override fun getItemViewType(position: Int): Int { 54 | if (position < headerViews.size) { 55 | viewCursor = headerViews[position] 56 | return viewCursor.hashCode() % HEAD_VIEW_TYPE + HEAD_VIEW_TYPE 57 | } 58 | return getListViewType(position - (headerViews.size)) 59 | } 60 | 61 | abstract fun listItemCount(): Int 62 | 63 | abstract fun onCreateListViewHolder(parent: ViewGroup, viewType: Int): VH 64 | 65 | abstract fun onBindListViewHolder(holder: VH, position: Int) 66 | 67 | open fun getListViewType(position: Int): Int { 68 | return 1 69 | } 70 | 71 | companion object { 72 | const val HEAD_VIEW_TYPE = 10000 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kronos/sample/widget/SimpleViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.kronos.sample.widget 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import android.view.View 5 | 6 | class SimpleViewHolder(view: View) : RecyclerView.ViewHolder(view) 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 |