├── .gitignore ├── .idea └── codeStyles │ └── Project.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── osome │ │ └── stickydecorator │ │ ├── ItemAdapter.kt │ │ ├── MainActivity.kt │ │ ├── SectionItemAdapter.kt │ │ ├── decor │ │ ├── SectionDecor.kt │ │ ├── SectionDecorReverse.kt │ │ ├── StickyDecorReverse.kt │ │ └── StickySectionDecor.kt │ │ ├── model.kt │ │ └── util.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_main.xml │ ├── list_item.xml │ └── section.xml │ ├── menu │ └── main.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 ├── build.gradle ├── dependencies.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── sticky_header.gif ├── sticky_header_reverse.gif └── sticky_view_holder.gif ├── settings.gradle └── sticky ├── .gitignore ├── bintray.gradle ├── build.gradle ├── consumer-rules.pro ├── gradle.properties ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml └── java └── com └── osome └── stickydecorator ├── ConditionItemDecorator.java ├── SimpleTextDrawable.java ├── Util.java ├── VerticalDrawableSectionDecor.java ├── VerticalSectionDecor.java ├── VerticalStickyDecor.java ├── VerticalStickyDrawableDecor.java └── ViewHolderStickyDecoration.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /.idea/caches 3 | /.idea/libraries 4 | /.idea/modules.xml 5 | /.idea/workspace.xml 6 | /.idea/navEditor.xml 7 | /.idea/assetWizardSettings.xml 8 | /.idea/vcs.xml 9 | /.idea/runConfigurations.xml 10 | /.idea/gradle.xml 11 | /.idea/encodings.xml 12 | /.idea/misc.xml 13 | /.idea/compiler.xml 14 | .gradle 15 | /local.properties 16 | .DS_Store 17 | /build 18 | /captures 19 | .externalNativeBuild 20 | .cxx -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Osome Pte Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### StickyDecorator 2 | [![Download](https://api.bintray.com/packages/osome/decoration/sticky/images/download.svg)](https://bintray.com/osome/decoration/sticky/_latestVersion) 3 | 4 | 5 | **Lightweight library to easy implement sectioning and sticky header. 6 | Some decoration use drawable, which gives great performance compared to view.** 7 | 8 | ![Sticky header](https://github.com/OsomePteLtd/StickyDecorator/blob/master/images/sticky_header.gif) 9 | ![Sticky header reverse layout](https://github.com/OsomePteLtd/StickyDecorator/blob/master/images/sticky_header_reverse.gif) 10 | ![Sticky view holder](https://github.com/OsomePteLtd/StickyDecorator/blob/master/images/sticky_view_holder.gif) 11 | 12 | **Important!**: this Drawable decorations tested only with LinearLayoutManager, with other it can work incorrectly 13 | 14 | ### Install 15 | add repository in to root `build.gradle` 16 | ``` 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | 22 | // this repository 23 | maven { 24 | url "https://dl.bintray.com/osome/decoration" 25 | } 26 | } 27 | } 28 | ``` 29 | 30 | and add dependency 31 | ``` 32 | implementation "com.osome.decoration:sticky:" 33 | ``` 34 | 35 | ### Usage 36 | #### ViewHolderStickyDecoration 37 | Easiest way to integrate sticky header is view holder based decoration [ViewHolderStickyDecoration](https://github.com/OsomePteLtd/StickyDecorator/blob/master/sticky/src/main/java/com/osome/stickydecorator/ViewHolderStickyDecoration.java) 38 | This decoration also support **vertical** GridLayoutManager(see example application) 39 | Firstly, you should make implement yours Adapter this interface `ViewHolderStickyDecoration.Condition` like this: 40 | ``` 41 | class SectionItemAdapter : RecyclerView.Adapter(), ViewHolderStickyDecoration.Condition { 42 | 43 | override fun isHeader(position: Int): Boolean { 44 | return getItem(position) is SectionItem 45 | } 46 | // other adapter's methods 47 | } 48 | ``` 49 | and after it add decoration in to RecyclerView 50 | ``` 51 | recyclerView.addItemDecoration(ViewHolderStickyDecoration(recyclerView, adapter)) 52 | ``` 53 | If you are use reverse layout 54 | ``` 55 | recyclerView.addItemDecoration(ViewHolderStickyDecoration(recyclerView, adapter, true)) 56 | ``` 57 | 58 | For GridLayoutManager you should set up SpanSizeLookup, for example 59 | ``` 60 | val spanCount = 3 61 | val lm = GridLayoutManager(recycler.context, spanCount) 62 | 63 | lm.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { 64 | override fun getSpanSize(position: Int): Int { 65 | // here we detect header and make it full width by set up max span 66 | if (adapter.getItemViewType(position) == SectionItemAdapter.TYPE_HEADER) 67 | return spanCount 68 | return 1 // or other value which are you want 69 | } 70 | } 71 | ``` 72 | [Full example here](https://github.com/OsomePteLtd/StickyDecorator/blob/master/app/src/main/java/com/osome/stickydecorator/SectionItemAdapter.kt) 73 | 74 | 75 | #### VerticalDrawableSectionDecor 76 | This decoration allows implement sectioning with drawable. 77 | 78 | **First step:** 79 | You should extends abstract class VerticalDrawableSectionDecor and implements two methods 80 | ``` 81 | class SectionDecor : VerticalDrawableSectionDecor() { 82 | override fun getDrawable(position: Int, sectionBounds: Rect, child: View): Drawable { 83 | // here you return drawable which will be drawn 84 | } 85 | 86 | override fun getSectionHeight(position: Int): Int { 87 | // here you return section height. 88 | // this value uses for calculate drawable bounds for draw 89 | } 90 | } 91 | ``` 92 | **Second step:** 93 | Implement interface ConditionItemDecorator.Condition with single method to determine for which adapter item will be drawn section 94 | ``` 95 | // here the first element and each 10th element will be split with section 96 | val condition = ConditionItemDecorator.Condition { position -> 97 | (itemProvider.get(position).value + 1) % 10 == 0 || position == 0 98 | } 99 | ``` 100 | **And finally** 101 | ``` 102 | val decorator = ConditionItemDecorator(condition, SectionDecor()) 103 | recyclerView.addItemDecoration(decorator) 104 | ``` 105 | 106 | also you can add top and bottom margins for section, just override two methods 107 | ``` 108 | override fun getSectionMarginTop(): Int { 109 | // return value for top space 110 | } 111 | 112 | override fun getSectionMarginBottom(): Int { 113 | // return value for bottom space 114 | } 115 | ``` 116 | 117 | In this case drawable will be drawn above item(for reverse layout too) 118 | Full examples [here](https://github.com/OsomePteLtd/StickyDecorator/blob/master/app/src/main/java/com/osome/stickydecorator/decor/SectionDecor.kt) and [here](https://github.com/OsomePteLtd/StickyDecorator/blob/master/app/src/main/java/com/osome/stickydecorator/decor/SectionDecorReverse.kt) 119 | 120 | #### VerticalStickyDrawableDecor 121 | This decoration allows implement sectioning and sticky header with drawable. 122 | 123 | **First step:** 124 | You should extends abstract class VerticalStickyDrawableDecor and implement three methods 125 | ``` 126 | class StickySectionDecor : VerticalStickyDrawableDecor() { 127 | 128 | override fun getSectionHeight(position: Int): Int { 129 | // here you return section height. 130 | // this value uses for calculate drawable bounds for draw 131 | // and by default will be used for header height 132 | } 133 | 134 | override fun getHeaderDrawable(position: Int, headerBounds: Rect): Drawable { 135 | // here you return drawable which will be drawn for header 136 | } 137 | 138 | override fun getSectionDrawable(position: Int, sectionBounds: Rect, child: View): Drawable { 139 | // here you return drawable which will be drawn for section 140 | } 141 | } 142 | ``` 143 | 144 | or for reverse layout 145 | 146 | ``` 147 | class StickySectionDecor : VerticalStickyDrawableDecor(true) { <==== here set in to constructor flag for reverse layout 148 | 149 | override fun getSectionHeight(position: Int): Int { 150 | // here you return section height. 151 | // this value uses for calculate drawable bounds for draw 152 | // and by default will be used for header height 153 | } 154 | 155 | override fun getHeaderDrawable(position: Int, headerBounds: Rect): Drawable { 156 | // here you return drawable which will be drawn for header 157 | } 158 | 159 | override fun getSectionDrawable(position: Int, sectionBounds: Rect, child: View): Drawable { 160 | // here you return drawable which will be drawn for section 161 | } 162 | } 163 | ``` 164 | 165 | **Second step:** 166 | Implement interface ConditionItemDecorator.Condition with single method to determine for which adapter item will be drawn section 167 | ``` 168 | // here the first element and each 10th element will be split with section 169 | val condition = ConditionItemDecorator.Condition { position -> 170 | (itemProvider.get(position).value + 1) % 10 == 0 || position == 0 171 | } 172 | ``` 173 | **And finally** 174 | ``` 175 | val decorator = ConditionItemDecorator(condition, StickySectionDecor()) 176 | recyclerView.addItemDecoration(decorator) 177 | ``` 178 | 179 | also you can add top and bottom margins for header, just override two methods 180 | ``` 181 | override fun getHeaderMarginTop(): Int { 182 | // return header top space 183 | } 184 | 185 | override fun getHeaderMarginBottom(): Int { 186 | // return header bottom space 187 | } 188 | ``` 189 | 190 | #### SimpleTextDrawable 191 | To simplify usage drawable decorator library contains also SimpleTextDrawable class. 192 | This class allow build styled drawable 193 | ``` 194 | SimpleTextDrawable.Builder() 195 | .setPaddingSymmetricDp(12, 4) 196 | .setBackgroundColor(Color.LTGRAY) 197 | .setTextGravity(SimpleTextDrawable.SimpleGravity.LEFT) 198 | .setTextColor(Color.BLACK) 199 | .setTextSizeDp(14) 200 | .setTypeface(customTypeFace) 201 | .build() 202 | ``` 203 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-android-extensions' 5 | id 'kotlin-kapt' 6 | } 7 | 8 | android { 9 | compileSdkVersion versions.compileSdk 10 | buildToolsVersion versions.buildTools 11 | 12 | defaultConfig { 13 | applicationId "com.osome.stickydecorator" 14 | minSdkVersion versions.minSdk 15 | targetSdkVersion versions.targetSdk 16 | versionCode 1 17 | versionName "1.0" 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | implementation libraries.kotlinStdlib 31 | implementation project(':sticky') 32 | //implementation "com.osome.decoration:sticky:1.0.1" 33 | implementation "androidx.appcompat:appcompat:$versions.compat" 34 | implementation "androidx.recyclerview:recyclerview:$versions.recycler" 35 | implementation "com.google.android.material:material:$versions.support" 36 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 37 | testImplementation 'junit:junit:4.13' 38 | } 39 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/osome/stickydecorator/ItemAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import androidx.recyclerview.widget.AsyncListDiffer 9 | import androidx.recyclerview.widget.DiffUtil 10 | import androidx.recyclerview.widget.RecyclerView 11 | 12 | class ItemAdapter(items: List) : RecyclerView.Adapter(), ItemProvider { 13 | 14 | private val differ: AsyncListDiffer = AsyncListDiffer(this, DiffItem()) 15 | 16 | init { 17 | setList(items) 18 | } 19 | 20 | override fun getItemCount(): Int = differ.currentList.size 21 | 22 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { 23 | val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false) 24 | return ItemHolder(view) 25 | } 26 | 27 | override fun onBindViewHolder(holder: ItemHolder, position: Int) { 28 | holder.bind(getItem(position)) 29 | } 30 | 31 | private fun getItem(position: Int): Item { 32 | return get(position) 33 | } 34 | 35 | fun setList(list: List) { 36 | differ.submitList(list.toList()) 37 | } 38 | 39 | fun getList() = differ.currentList.toList() 40 | 41 | class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 42 | private val tvItem = itemView.findViewById(R.id.tvItem) 43 | 44 | @SuppressLint("SetTextI18n") 45 | fun bind(item: Item) { 46 | tvItem.text = "Item with number ${item.value}" 47 | } 48 | } 49 | 50 | override fun get(position: Int): Item { 51 | return differ.currentList[position] 52 | } 53 | 54 | override fun size(): Int = itemCount 55 | } 56 | 57 | class DiffItem : DiffUtil.ItemCallback() { 58 | override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean { 59 | return oldItem.value == newItem.value 60 | } 61 | 62 | override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { 63 | return oldItem.value == newItem.value 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/osome/stickydecorator/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator 2 | 3 | import android.os.Bundle 4 | import android.view.Menu 5 | import android.view.MenuItem 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.recyclerview.widget.GridLayoutManager 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import com.osome.stickydecorator.decor.SectionDecor 10 | import com.osome.stickydecorator.decor.SectionDecorReverse 11 | import com.osome.stickydecorator.decor.StickyDecorReverse 12 | import com.osome.stickydecorator.decor.StickySectionDecor 13 | import kotlinx.android.synthetic.main.activity_main.* 14 | 15 | 16 | class MainActivity : AppCompatActivity() { 17 | 18 | private val adapter = ItemAdapter(generateItem(32)) 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_main) 23 | title = "Select options here =======>" 24 | 25 | setUpStickyRecycler() 26 | } 27 | 28 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 29 | menuInflater.inflate(R.menu.main, menu) 30 | return true 31 | } 32 | 33 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 34 | title = item.title 35 | when (item.itemId) { 36 | R.id.sticky -> setUpStickyRecycler() 37 | R.id.stickyReverse -> setUpStickyReverseRecycler() 38 | R.id.section -> setUpSectionRecycler() 39 | R.id.sectionReverse -> setUpSectionReverseRecycler() 40 | R.id.viewHolder -> setUpViewHolderSection() 41 | R.id.viewHolderReverse -> setUpViewHolderSectionReverse() 42 | R.id.viewHolderGrid -> setUpViewHolderSectionGrid() 43 | R.id.viewHolderGridReverse -> setUpViewHolderSectionGridReverse() 44 | } 45 | return true 46 | } 47 | 48 | private fun setUpStickyRecycler() { 49 | clearDecoration() 50 | recycler.adapter = adapter 51 | recycler.layoutManager = LinearLayoutManager(recycler.context) 52 | val itemProvider: ItemProvider = adapter 53 | recycler.addItemDecoration(ConditionItemDecorator( 54 | { position -> (itemProvider.get(position).value + 1) % 10 == 0 || position == 0 }, 55 | StickySectionDecor(adapter))) 56 | } 57 | 58 | private fun setUpStickyReverseRecycler() { 59 | clearDecoration() 60 | recycler.adapter = adapter 61 | val itemProvider: ItemProvider = adapter 62 | recycler.layoutManager = LinearLayoutManager(recycler.context, LinearLayoutManager.VERTICAL, true) 63 | recycler.addItemDecoration( 64 | ConditionItemDecorator({ position -> 65 | position != 0 && ((itemProvider.get(position)).value + 1) % 10 == 0 66 | }, StickyDecorReverse(adapter))) 67 | } 68 | 69 | private fun setUpSectionRecycler() { 70 | clearDecoration() 71 | recycler.adapter = adapter 72 | val itemProvider: ItemProvider = adapter 73 | recycler.layoutManager = LinearLayoutManager(recycler.context) 74 | recycler.addItemDecoration( 75 | ConditionItemDecorator( 76 | { position -> (itemProvider.get(position).value + 1) % 10 == 0 || position == 0 }, 77 | SectionDecor(adapter))) 78 | } 79 | 80 | private fun setUpSectionReverseRecycler() { 81 | clearDecoration() 82 | recycler.adapter = adapter 83 | val itemProvider: ItemProvider = adapter 84 | recycler.layoutManager = LinearLayoutManager(recycler.context, LinearLayoutManager.VERTICAL, true) 85 | recycler.addItemDecoration( 86 | ConditionItemDecorator({ position -> 87 | position != 0 && ((itemProvider.get(position)).value + 1) % 10 == 0 || position == itemProvider.size() - 1 88 | }, SectionDecorReverse(adapter))) 89 | } 90 | 91 | private fun setUpViewHolderSection() { 92 | clearDecoration() 93 | val adapter = SectionItemAdapter(generateItemWithSection(34)) 94 | recycler.adapter = adapter 95 | recycler.layoutManager = LinearLayoutManager(recycler.context) 96 | recycler.addItemDecoration(ViewHolderStickyDecoration(recycler, adapter)) 97 | } 98 | 99 | private fun setUpViewHolderSectionReverse() { 100 | clearDecoration() 101 | val adapter = SectionItemAdapter(generateItemWithSectionReverse(34)) 102 | recycler.adapter = adapter 103 | recycler.layoutManager = LinearLayoutManager(recycler.context, LinearLayoutManager.VERTICAL, true) 104 | recycler.addItemDecoration(ViewHolderStickyDecoration(recycler, adapter, true)) 105 | } 106 | 107 | private fun setUpViewHolderSectionGrid() { 108 | clearDecoration() 109 | val adapter = SectionItemAdapter(generateItemWithSection(34)) 110 | recycler.adapter = adapter 111 | 112 | val spanCount = 3 113 | val lm = GridLayoutManager(recycler.context, spanCount) 114 | 115 | lm.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { 116 | override fun getSpanSize(position: Int): Int { 117 | if (adapter.getItemViewType(position) == SectionItemAdapter.TYPE_HEADER) 118 | return spanCount 119 | return 1 120 | } 121 | } 122 | recycler.layoutManager = lm 123 | recycler.addItemDecoration(ViewHolderStickyDecoration(recycler, adapter)) 124 | } 125 | 126 | private fun setUpViewHolderSectionGridReverse() { 127 | clearDecoration() 128 | val adapter = SectionItemAdapter(generateItemWithSectionReverse(34)) 129 | recycler.adapter = adapter 130 | 131 | val spanCount = 3 132 | val lm = GridLayoutManager(recycler.context, spanCount, GridLayoutManager.VERTICAL, true) 133 | 134 | lm.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { 135 | override fun getSpanSize(position: Int): Int { 136 | if (adapter.getItemViewType(position) == SectionItemAdapter.TYPE_HEADER) 137 | return spanCount 138 | return 1 139 | } 140 | } 141 | recycler.layoutManager = lm 142 | recycler.addItemDecoration(ViewHolderStickyDecoration(recycler, adapter, true)) 143 | } 144 | 145 | private fun clearDecoration() { 146 | for (i in 0 until recycler.itemDecorationCount) { 147 | recycler.removeItemDecorationAt(i) 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /app/src/main/java/com/osome/stickydecorator/SectionItemAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import androidx.recyclerview.widget.RecyclerView 9 | 10 | class SectionItemAdapter(private val items: List) : RecyclerView.Adapter(), ViewHolderStickyDecoration.Condition { 11 | companion object { 12 | const val TYPE_HEADER = R.layout.section 13 | const val TYPE_ITEM = R.layout.list_item 14 | } 15 | 16 | override fun getItemCount(): Int = items.size 17 | 18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 19 | if (viewType == R.layout.list_item) { 20 | val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) 21 | return ItemHolder(view) 22 | } 23 | 24 | val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) 25 | return SectionHolder(view) 26 | } 27 | 28 | override fun getItemViewType(position: Int): Int { 29 | if (isHeader(position)) 30 | return TYPE_HEADER 31 | return TYPE_ITEM 32 | } 33 | 34 | override fun isHeader(position: Int): Boolean { 35 | return getItem(position) is SectionItem 36 | } 37 | 38 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 39 | if (holder.itemViewType == TYPE_ITEM) { 40 | (holder as ItemHolder).bind(getItem(position)) 41 | } else if (holder.itemViewType == TYPE_HEADER) { 42 | (holder as SectionHolder).bind(getItem(position) as SectionItem) 43 | } 44 | } 45 | 46 | private fun getItem(position: Int): Item { 47 | return items[position] 48 | } 49 | 50 | class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 51 | private val tvItem = itemView.findViewById(R.id.tvItem) 52 | 53 | @SuppressLint("SetTextI18n") 54 | fun bind(item: Item) { 55 | tvItem.text = "Item with number ${item.value}" 56 | } 57 | } 58 | 59 | class SectionHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 60 | private val tv = itemView as TextView 61 | 62 | fun bind(item: SectionItem) { 63 | tv.text = "${item.value}th" 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/osome/stickydecorator/decor/SectionDecor.kt: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator.decor 2 | 3 | import android.graphics.Color 4 | import android.graphics.Rect 5 | import android.graphics.drawable.Drawable 6 | import android.view.View 7 | import com.osome.stickydecorator.Item 8 | import com.osome.stickydecorator.ItemProvider 9 | import com.osome.stickydecorator.SimpleTextDrawable 10 | import com.osome.stickydecorator.VerticalDrawableSectionDecor 11 | 12 | class SectionDecor(private val itemProvider: ItemProvider) : VerticalDrawableSectionDecor() { 13 | 14 | private val section = buildTextDrawable() 15 | 16 | private fun buildTextDrawable(): SimpleTextDrawable { 17 | return SimpleTextDrawable.Builder() 18 | .setPaddingSymmetricDp(12, 4) 19 | .setBackgroundColor(Color.LTGRAY) 20 | .setTextGravity(SimpleTextDrawable.SimpleGravity.LEFT) 21 | .setTextColor(Color.BLACK) 22 | .setTextSizeDp(14) 23 | .build() 24 | } 25 | 26 | override fun getSectionHeight(position: Int): Int { 27 | return section.height 28 | } 29 | 30 | override fun getSectionMarginTop(): Int { 31 | return section.height 32 | } 33 | 34 | override fun getDrawable(position: Int, sectionBounds: Rect, child: View): Drawable { 35 | val item = itemProvider.get(position) 36 | val value: Int = (((item.value + 1) / 10) * 10) 37 | section.text = "${value}th" 38 | return section 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/osome/stickydecorator/decor/SectionDecorReverse.kt: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator.decor 2 | 3 | import android.graphics.Color 4 | import android.graphics.Rect 5 | import android.graphics.drawable.Drawable 6 | import android.view.View 7 | import com.osome.stickydecorator.Item 8 | import com.osome.stickydecorator.ItemProvider 9 | import com.osome.stickydecorator.SimpleTextDrawable 10 | import com.osome.stickydecorator.VerticalDrawableSectionDecor 11 | 12 | class SectionDecorReverse(private val itemProvider: ItemProvider) : VerticalDrawableSectionDecor() { 13 | 14 | private val section = buildTextDrawable() 15 | 16 | private fun buildTextDrawable(): SimpleTextDrawable { 17 | return SimpleTextDrawable.Builder() 18 | .setPaddingSymmetricDp(12, 4) 19 | .setBackgroundColor(Color.LTGRAY) 20 | .setTextGravity(SimpleTextDrawable.SimpleGravity.RIGHT) 21 | .build() 22 | } 23 | 24 | override fun getSectionHeight(position: Int): Int { 25 | return section.height 26 | } 27 | 28 | override fun getSectionMarginTop(): Int { 29 | return section.height 30 | } 31 | 32 | override fun getDrawable(position: Int, sectionBounds: Rect, child: View): Drawable { 33 | val item = itemProvider.get(position) 34 | val value: Int = (((item.value) / 10) * 10) 35 | section.text = "${value}th" 36 | return section 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/osome/stickydecorator/decor/StickyDecorReverse.kt: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator.decor 2 | 3 | import android.graphics.Color 4 | import android.graphics.Rect 5 | import android.graphics.drawable.Drawable 6 | import android.view.View 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.osome.stickydecorator.Item 9 | import com.osome.stickydecorator.ItemProvider 10 | import com.osome.stickydecorator.SimpleTextDrawable 11 | import com.osome.stickydecorator.VerticalStickyDrawableDecor 12 | 13 | 14 | class StickyDecorReverse(private val itemProvider: ItemProvider) : VerticalStickyDrawableDecor(true) { 15 | 16 | private val section = buildTextDrawable() 17 | private val header = buildTextDrawable() 18 | 19 | private fun buildTextDrawable(): SimpleTextDrawable { 20 | return SimpleTextDrawable.Builder() 21 | .setPaddingSymmetricDp(12, 4) 22 | .setBackgroundCornerRadiusDp(14) 23 | .setBackgroundColor(Color.LTGRAY) 24 | .build() 25 | } 26 | 27 | override fun getSectionDrawable(position: Int, sectionBounds: Rect, child: View): Drawable { 28 | val item = itemProvider.get(position) 29 | val value: Int = (((item.value) / 10) * 10) 30 | section.text = "${value}th" 31 | return section 32 | } 33 | 34 | override fun getHeaderDrawable(position: Int, headerBounds: Rect): Drawable { 35 | val value = getHeaderValue(position) 36 | header.text = value 37 | return header 38 | } 39 | 40 | override fun getSectionBounds(parent: RecyclerView, position: Int, viewBounds: Rect, decoratedBounds: Rect): Rect { 41 | section.text = getHeaderValue(position) 42 | val temp = super.getSectionBounds(parent, position, viewBounds, decoratedBounds) 43 | section.setTopCenter(temp.exactCenterX(), temp.top.toFloat()) 44 | return section.bounds 45 | } 46 | 47 | 48 | override fun getHeaderBounds(parent: RecyclerView, headerBottom: Int, itemPosition: Int, viewBounds: Rect, decoratedBounds: Rect): Rect { 49 | val temp = super.getHeaderBounds(parent, headerBottom, itemPosition, viewBounds, decoratedBounds) 50 | header.text = getHeaderValue(itemPosition) 51 | header.setTopCenter(temp.exactCenterX(), temp.top.toFloat()) 52 | return header.bounds 53 | } 54 | 55 | private fun getHeaderValue(position: Int): String { 56 | val item = itemProvider.get(position) 57 | return "${(((item.value) / 10) * 10)}th" 58 | } 59 | 60 | override fun getHeaderHeight(): Int { 61 | return header.height 62 | } 63 | 64 | override fun getSectionMarginTop(): Int { 65 | return section.height 66 | } 67 | 68 | override fun getSectionMarginBottom(): Int { 69 | return section.height 70 | } 71 | 72 | override fun getHeaderMarginTop(): Int { 73 | return header.height / 3 74 | } 75 | 76 | override fun getHeaderMarginBottom(): Int { 77 | return header.height / 3 78 | } 79 | 80 | override fun getSectionHeight(position: Int): Int { 81 | return section.height 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/osome/stickydecorator/decor/StickySectionDecor.kt: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator.decor 2 | 3 | import android.graphics.Color 4 | import android.graphics.Rect 5 | import android.graphics.drawable.Drawable 6 | import android.view.View 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.osome.stickydecorator.Item 9 | import com.osome.stickydecorator.ItemProvider 10 | import com.osome.stickydecorator.SimpleTextDrawable 11 | import com.osome.stickydecorator.VerticalStickyDrawableDecor 12 | 13 | class StickySectionDecor(private val itemProvider: ItemProvider) : VerticalStickyDrawableDecor() { 14 | 15 | private val section = buildTextDrawable() 16 | private val header = buildTextDrawable() 17 | 18 | private fun buildTextDrawable(): SimpleTextDrawable { 19 | return SimpleTextDrawable.Builder() 20 | .setPaddingSymmetricDp(12, 4) 21 | .setBackgroundCornerRadiusDp(14) 22 | .setBackgroundColor(Color.LTGRAY) 23 | .build() 24 | } 25 | 26 | override fun getSectionDrawable(position: Int, sectionBounds: Rect, child: View): Drawable { 27 | section.text = getHeaderValue(position) 28 | return section 29 | } 30 | 31 | override fun getHeaderDrawable(position: Int, headerBounds: Rect): Drawable { 32 | header.text = getHeaderValue(position) 33 | return header 34 | } 35 | 36 | override fun getHeaderBounds(parent: RecyclerView, headerBottom: Int, itemPosition: Int, viewBounds: Rect, decoratedBounds: Rect): Rect { 37 | header.text = getHeaderValue(itemPosition) 38 | val temp = super.getHeaderBounds(parent, headerBottom, itemPosition, viewBounds, decoratedBounds) 39 | header.setTopCenter(temp.exactCenterX(), temp.top.toFloat()) 40 | return header.bounds 41 | } 42 | 43 | private fun getHeaderValue(position: Int): String { 44 | val item = itemProvider.get(position) 45 | return "${(((item.value + 1) / 10) * 10)}th" 46 | } 47 | 48 | override fun getSectionHeight(position: Int): Int { 49 | return section.height 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/osome/stickydecorator/model.kt: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator 2 | 3 | open class Item(val value: Int) 4 | class SectionItem(value: Int) : Item(value) 5 | 6 | fun generateItem(count: Int): List { 7 | return (0 until count).map { Item(it + 1) } 8 | } 9 | 10 | fun generateItemWithSection(count: Int): List { 11 | val list = mutableListOf() 12 | (0 until count).forEach { value -> 13 | if ((value + 1) % 10 == 0 || value == 0) 14 | list += SectionItem(((value + 1) / 10) * 10) 15 | list += Item(value + 1) 16 | } 17 | return list 18 | } 19 | 20 | fun generateItemWithSectionReverse(count: Int): List { 21 | val list = mutableListOf() 22 | (0 until count).forEach { value -> 23 | if (value != 0 && ((value + 1) % 10 == 0) && value != count - 1) 24 | list += SectionItem((((value) / 10) * 10)) 25 | list += Item(value + 1) 26 | 27 | if (value == count - 1) 28 | list += SectionItem((((value) / 10) * 10)) 29 | } 30 | return list 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/osome/stickydecorator/util.kt: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator 2 | 3 | import android.content.res.Resources 4 | 5 | val Int.px2dpF: Float 6 | get() = (this * Resources.getSystem().displayMetrics.density) 7 | 8 | val Int.px2dp: Int 9 | get() = (this * Resources.getSystem().displayMetrics.density).toInt() 10 | 11 | interface ItemProvider { 12 | fun get(position: Int): T 13 | 14 | fun size(): Int 15 | } -------------------------------------------------------------------------------- /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 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/section.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | 11 | 14 | 15 | 18 | 19 | 22 | 23 | 26 | 27 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | StickyDecorator 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | apply from: 'dependencies.gradle' 5 | repositories { 6 | google() 7 | jcenter() 8 | 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.6.3' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" 13 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' 14 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 15 | 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | jcenter() 23 | maven { 24 | url "https://dl.bintray.com/osome/decoration" 25 | } 26 | } 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext.versions = [ 2 | minSdk : 21, 3 | targetSdk : 29, 4 | compileSdk: 29, 5 | buildTools: '29.0.3', 6 | 7 | support : '1.1.0', 8 | recycler : '1.1.0', 9 | compat : '1.1.0', 10 | kotlin : '1.3.72', 11 | ] 12 | 13 | ext.libraries = [ 14 | junit : "junit:junit:$versions.junit", 15 | mockitoCore : "org.mockito:mockito-core:$versions.mockito", 16 | kotlinStdlib: "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin", 17 | ] -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Sep 03 11:41:08 YEKT 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/sticky_header.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/images/sticky_header.gif -------------------------------------------------------------------------------- /images/sticky_header_reverse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/images/sticky_header_reverse.gif -------------------------------------------------------------------------------- /images/sticky_view_holder.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/images/sticky_view_holder.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':sticky' 2 | rootProject.name='StickyDecorator' 3 | -------------------------------------------------------------------------------- /sticky/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | local.properties -------------------------------------------------------------------------------- /sticky/bintray.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.jfrog.bintray' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | apply plugin: 'maven-publish' 4 | 5 | group = project.property('group') 6 | 7 | task androidSourcesJar(type: Jar) { 8 | classifier = 'sources' 9 | from android.sourceSets.main.java.sourceFiles 10 | } 11 | 12 | task sourcesJar(type: Jar) { 13 | from android.sourceSets.main.java.srcDirs 14 | classifier = 'sources' 15 | } 16 | artifacts { 17 | archives sourcesJar 18 | } 19 | 20 | artifacts { 21 | archives sourcesJar 22 | } 23 | 24 | Properties properties = new Properties() 25 | properties.load(project.file('local.properties').newDataInputStream()) 26 | bintray { 27 | user = properties.getProperty("bintrayUser") 28 | key = properties.getProperty("bintrayApiKey") 29 | configurations = ['archives'] 30 | publish = true 31 | override = true 32 | 33 | pkg { 34 | repo = project.property("repo") 35 | name = project.property('name') 36 | userOrg = project.property("organization") 37 | 38 | websiteUrl = project.property('siteUrl') 39 | vcsUrl = project.property('gitUrl') 40 | licenses = ['MIT'] 41 | 42 | publicDownloadNumbers = true 43 | version { 44 | name = project.property("version") 45 | desc = project.property("desc") 46 | vcsTag = project.version 47 | attributes = ['gradle-plugin': 'com.use.less:com.use.less.gradle:gradle-useless-plugin'] 48 | } 49 | } 50 | } 51 | install { 52 | repositories.mavenInstaller { 53 | pom { 54 | project { 55 | packaging 'aar' 56 | url project.property('siteUrl') 57 | licenses { 58 | license { 59 | name 'The Apache Software License, Version 2.0' 60 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 61 | } 62 | } 63 | developers { 64 | developer { 65 | id properties.getProperty("bintrayUser") 66 | name project.property("user") 67 | email project.property('email') 68 | } 69 | } 70 | scm { 71 | connection project.property('gitUrl') 72 | developerConnection project.property('gitUrl') 73 | url project.property('siteUrl') 74 | 75 | } 76 | 77 | issueManagement { 78 | system 'github' 79 | url project.property('siteUrl') + '/issues' 80 | } 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /sticky/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | 4 | android { 5 | compileSdkVersion versions.compileSdk 6 | buildToolsVersion versions.buildTools 7 | 8 | defaultConfig { 9 | minSdkVersion versions.minSdk 10 | targetSdkVersion versions.targetSdk 11 | versionCode 1 12 | versionName project.property("version") 13 | 14 | consumerProguardFiles 'consumer-rules.pro' 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | 23 | debug { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | 29 | } 30 | 31 | dependencies { 32 | implementation "androidx.recyclerview:recyclerview:$versions.support" 33 | } 34 | 35 | apply from: 'bintray.gradle' -------------------------------------------------------------------------------- /sticky/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsomePteLtd/StickyDecorator/c3151a10f86c67576d3d28095fd48841d5d22261/sticky/consumer-rules.pro -------------------------------------------------------------------------------- /sticky/gradle.properties: -------------------------------------------------------------------------------- 1 | user=Max Rovkin 2 | email=rovkinmax@gmail.com 3 | version=1.0.9 4 | organization=osome 5 | group=com.osome.decoration 6 | name=sticky 7 | repo=decoration 8 | desc=Lightweight library for sticky header and simple sectioning in RecyclerView 9 | gitUrl=https://github.com/OsomePteLtd/StickyDecorator.git 10 | siteUrl=https://github.com/OsomePteLtd -------------------------------------------------------------------------------- /sticky/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 | -------------------------------------------------------------------------------- /sticky/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sticky/src/main/java/com/osome/stickydecorator/ConditionItemDecorator.java: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Rect; 5 | import android.view.View; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | /** 11 | * An ConditionItemDecorator allows the application to add a special 12 | * drawing and layout offset to specific item views from the adapter's data set. 13 | * To determine which item should decorate you can use {@link ConditionItemDecorator.Condition} 14 | * This can be useful for drawing sections or header 15 | * between items, highlights, visual grouping boundaries and more. 16 | */ 17 | public class ConditionItemDecorator extends RecyclerView.ItemDecoration { 18 | 19 | private final Condition condition; 20 | private final Decor decor; 21 | 22 | public ConditionItemDecorator(@NonNull Condition condition, @NonNull Decor decor) { 23 | this.condition = condition; 24 | this.decor = decor; 25 | } 26 | 27 | @Override 28 | public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 29 | int position = parent.getChildAdapterPosition(view); 30 | if (position != RecyclerView.NO_POSITION) { 31 | if (condition.isForDrawOver(position)) { 32 | decor.getConditionItemOffsets(parent, outRect, view, position); 33 | } 34 | decor.getItemOffsets(parent, outRect, view, position, state); 35 | } 36 | } 37 | 38 | @Override 39 | public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 40 | if (state.getItemCount() == 0) { 41 | return; 42 | } 43 | 44 | decor.prepareDrawOver(c, parent, state); 45 | for (int i = 0; i < parent.getChildCount(); i++) { 46 | View child = parent.getChildAt(i); 47 | int index = parent.getChildAdapterPosition(child); 48 | if (condition.isForDrawOver(index)) { 49 | decor.onDrawOver(c, parent, child, index, state); 50 | } 51 | } 52 | decor.onPostDrawOver(c, parent, state); 53 | } 54 | 55 | public interface Condition { 56 | /** 57 | * Determines for which item should apply decoration 58 | * 59 | * @param position RecyclerView's adapter position 60 | * @return true if for position should apply decoration, false otherwise 61 | */ 62 | boolean isForDrawOver(int position); 63 | } 64 | 65 | public interface Decor { 66 | 67 | /** 68 | * Called every time when called {@link RecyclerView.ItemDecoration#onDrawOver(Canvas, RecyclerView, RecyclerView.State)}. 69 | * Called once before {@link ConditionItemDecorator.Decor#onDrawOver(Canvas, RecyclerView, View, int, RecyclerView.State)} 70 | * 71 | * @param c Canvas to draw into 72 | * @param parent RecyclerView this Decor is drawing into 73 | * @param state The current state for RecyclerView 74 | */ 75 | void prepareDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state); 76 | 77 | /** 78 | * Draw any appropriate decorations into the Canvas supplied to the RecyclerView. 79 | * Any content drawn by this method will be drawn after the item views are drawn 80 | * and will thus appear over the views. This method call for each view in RecyclerView 81 | * 82 | * @param c Canvas to draw into 83 | * @param parent RecyclerView this Decor is drawing into 84 | * @param child RecyclerView's child view to to determine place to drawing 85 | * @param position RecyclerView's adapter position of view 86 | * @param state The current state for RecyclerView 87 | */ 88 | void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull View child, int position, @NonNull RecyclerView.State state); 89 | 90 | /** 91 | * Called every time when called {@link RecyclerView.ItemDecoration#onDrawOver(Canvas, RecyclerView, RecyclerView.State)}. 92 | * Called once after {@link ConditionItemDecorator.Decor#onDrawOver(Canvas, RecyclerView, View, int, RecyclerView.State)} 93 | * 94 | * @param c Canvas to draw into 95 | * @param parent RecyclerView this Decor is drawing into 96 | * @param state The current state for RecyclerView 97 | */ 98 | void onPostDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state); 99 | 100 | /** 101 | * Retrieve any offsets for the given item which satisfies condition {@link ConditionItemDecorator.Condition#isForDrawOver(int)}. 102 | * Each field of rect specifies 103 | * the number of pixels that the item view should be inset by, similar to padding or margin. 104 | * The default implementation sets the bounds of outRect to 0 and returns. 105 | * 106 | *

107 | * If this Decor does not affect the positioning of item views, it should set 108 | * all four fields of rect (left, top, right, bottom) to zero 109 | * before returning. 110 | * 111 | * @param parent RecyclerView this ItemDecoration is decorating 112 | * @param rect Rect to receive the output. 113 | * @param view The child view to decorate 114 | * @param position The child view adapter position 115 | */ 116 | void getConditionItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position); 117 | 118 | 119 | /** 120 | * Retrieve any offsets for the given item. Each field of outRect specifies 121 | * the number of pixels that the item view should be inset by, similar to padding or margin. 122 | * The default implementation sets the bounds of outRect to 0 and returns. 123 | * 124 | *

125 | * If this Decor does not affect the positioning of item views, it should set 126 | * all four fields of rect (left, top, right, bottom) to zero 127 | * before returning. 128 | * 129 | * @param parent RecyclerView this ItemDecoration is decorating 130 | * @param rect Rect to receive the output. 131 | * @param view The child view to decorate 132 | * @param position The child view adapter position 133 | * @param state The current state of RecyclerView. 134 | */ 135 | void getItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position, @NonNull RecyclerView.State state); 136 | } 137 | 138 | public static abstract class SimpleDecor implements Decor { 139 | 140 | @Override 141 | public void prepareDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 142 | 143 | } 144 | 145 | @Override 146 | public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull View child, int position, @NonNull RecyclerView.State state) { 147 | 148 | } 149 | 150 | @Override 151 | public void onPostDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 152 | 153 | } 154 | 155 | @Override 156 | public void getConditionItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position) { 157 | 158 | } 159 | 160 | @Override 161 | public void getItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position, @NonNull RecyclerView.State state) { 162 | 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /sticky/src/main/java/com/osome/stickydecorator/SimpleTextDrawable.java: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Color; 5 | import android.graphics.ColorFilter; 6 | import android.graphics.Paint; 7 | import android.graphics.PixelFormat; 8 | import android.graphics.Rect; 9 | import android.graphics.RectF; 10 | import android.graphics.Typeface; 11 | import android.graphics.drawable.Drawable; 12 | 13 | import androidx.annotation.NonNull; 14 | import androidx.annotation.Nullable; 15 | 16 | public class SimpleTextDrawable extends Drawable { 17 | 18 | private final int paddingLeft; 19 | private final int paddingTop; 20 | private final int paddingRight; 21 | private final int paddingBottom; 22 | private final int bgRadius; 23 | private final Paint paintText = new Paint(Paint.ANTI_ALIAS_FLAG); 24 | private final Paint paintBack = new Paint(Paint.ANTI_ALIAS_FLAG); 25 | 26 | private final Rect textBounds = new Rect(); 27 | private final RectF bgBounds = new RectF(); 28 | private String textForDraw; 29 | private int textHeight; 30 | private int textWidth; 31 | private float textX = 0; 32 | private float textY = 0; 33 | private SimpleGravity textGravity; 34 | 35 | protected SimpleTextDrawable(float textSize, 36 | int colorText, 37 | SimpleGravity gravity, 38 | int colorBackground, 39 | int bgRadius, 40 | int paddingLeft, 41 | int paddingTop, 42 | int paddingRight, 43 | int paddingBottom, 44 | Typeface typeface) { 45 | this.paddingLeft = paddingLeft; 46 | this.paddingTop = paddingTop; 47 | this.paddingRight = paddingRight; 48 | this.paddingBottom = paddingBottom; 49 | this.bgRadius = bgRadius; 50 | 51 | textGravity = gravity; 52 | paintText.setColor(colorText); 53 | if (typeface != null) { 54 | paintText.setTypeface(typeface); 55 | } 56 | paintText.setTextSize(textSize); 57 | 58 | paintBack.setColor(colorBackground); 59 | } 60 | 61 | @Override 62 | public void setAlpha(int alpha) { 63 | paintText.setAlpha(alpha); 64 | paintBack.setAlpha(alpha); 65 | } 66 | 67 | @Override 68 | public void setColorFilter(@Nullable ColorFilter colorFilter) { 69 | paintText.setColorFilter(colorFilter); 70 | paintBack.setColorFilter(colorFilter); 71 | } 72 | 73 | @Override 74 | public int getOpacity() { 75 | return PixelFormat.TRANSLUCENT; 76 | } 77 | 78 | 79 | @Override 80 | public void draw(@NonNull Canvas canvas) { 81 | if (paintBack.getColor() != Color.TRANSPARENT || paintBack.getAlpha() > 0) { 82 | canvas.drawRoundRect(bgBounds, bgRadius, bgRadius, paintBack); 83 | } 84 | 85 | canvas.drawText(textForDraw, 0, textForDraw.length(), textX, textY, paintText); 86 | } 87 | 88 | /** 89 | * Update text for this drawable and recalculate size 90 | * 91 | * @param text - New text value 92 | */ 93 | public void setText(@NonNull String text) { 94 | if (!text.equals(this.textForDraw)) { 95 | this.textForDraw = text; 96 | paintText.getTextBounds(text, 0, text.length(), textBounds); 97 | textHeight = textBounds.height(); 98 | textWidth = textBounds.width(); 99 | } 100 | } 101 | 102 | /** 103 | * Return current text 104 | * 105 | * @return current text 106 | */ 107 | public String getText() { 108 | return textForDraw; 109 | } 110 | 111 | /** 112 | * Set top left position for drawable. 113 | *

114 | * This method calculate bounds based on text size and text padding 115 | *

116 | * DON'T use this method if your section or header has full width 117 | * 118 | * @param x X coordinate of top left corner 119 | * @param y Y coordinate of top left corner 120 | */ 121 | public void setTopLeft(float x, float y) { 122 | setBounds( 123 | (int) x, 124 | (int) y, 125 | (int) (x + textWidth + paddingLeft + paddingRight), 126 | (int) (y + textHeight + paddingTop + paddingBottom) 127 | ); 128 | } 129 | 130 | /** 131 | * Set top left position for drawable. 132 | *

133 | * This method calculate bounds based on text size and text padding 134 | *

135 | * DON'T use this method if your section or header has full width 136 | * 137 | * @param x X coordinate of top left corner 138 | * @param y Y coordinate of top left corner 139 | */ 140 | public void setTopLeft(int x, int y) { 141 | setTopLeft((float) x, (float) y); 142 | } 143 | 144 | /** 145 | * Set top center position for drawable 146 | *

147 | * This method calculate bounds based on text size and text padding 148 | *

149 | * DON'T use this method if your section or header has full width 150 | * 151 | * @param x X coordinate of top center 152 | * @param y Y coordinate of top center 153 | */ 154 | public void setTopCenter(float x, float y) { 155 | setBounds( 156 | (int) (x - (textWidth / 2) - paddingLeft), 157 | (int) y, 158 | (int) (x + (textWidth / 2) + paddingRight), 159 | (int) (y + textHeight + paddingTop + paddingBottom) 160 | ); 161 | } 162 | 163 | /** 164 | * Set top center position for drawable 165 | *

166 | * This method calculate bounds based on text size and text padding 167 | *

168 | * DON'T use this method if your section or header has full width 169 | * 170 | * @param x X coordinate of top center 171 | * @param y Y coordinate of top center 172 | */ 173 | public void setTopCenter(int x, int y) { 174 | setTopCenter((float) x, (float) y); 175 | } 176 | 177 | @Override 178 | public void setBounds(int left, int top, int right, int bottom) { 179 | super.setBounds(left, top, right, bottom); 180 | Rect bounds = getBounds(); 181 | bgBounds.set(bounds); 182 | textX = calculateTextX(bounds); 183 | textY = bounds.exactCenterY() - (paintText.descent() + paintText.ascent()) / 2f; 184 | } 185 | 186 | private float calculateTextX(Rect bounds) { 187 | switch (textGravity) { 188 | case LEFT: 189 | return bounds.left + paddingLeft; 190 | case RIGHT: 191 | return bounds.right - paddingRight - textWidth; 192 | default: 193 | return bounds.exactCenterX() - textWidth / 2f; 194 | } 195 | } 196 | 197 | /** 198 | * Text width with padding 199 | * 200 | * @return text width with padding 201 | */ 202 | public int getTextWidth() { 203 | return textWidth + paddingLeft + paddingRight; 204 | } 205 | 206 | /** 207 | * This is drawable bounds width 208 | *

209 | * {@link #getWidth()} >= {@link #getTextWidth()} 210 | * 211 | * @return drawable bounds width 212 | */ 213 | public int getWidth() { 214 | return getBounds().width(); 215 | } 216 | 217 | /** 218 | * Text height with padding 219 | * 220 | * @return text height with padding 221 | */ 222 | public int getHeight() { 223 | return textHeight + paddingTop + paddingBottom; 224 | } 225 | 226 | public enum SimpleGravity { 227 | LEFT, RIGHT, CENTER 228 | } 229 | 230 | public static class Builder { 231 | private float textSize = Util.dpToPxF(12); 232 | private int colorText = Color.BLACK; 233 | private int colorBackground = Color.TRANSPARENT; 234 | private int bgRadius = 0; 235 | private int paddingLeft = 0; 236 | private int paddingTop = 0; 237 | private int paddingRight = 0; 238 | private int paddingBottom = 0; 239 | private SimpleGravity gravity = SimpleGravity.CENTER; 240 | private Typeface typeface; 241 | 242 | /** 243 | * Set up text size in pixels 244 | * 245 | * @param textSize test size in pixels 246 | * @return updated builder instance 247 | */ 248 | public Builder setTextSize(float textSize) { 249 | this.textSize = textSize; 250 | return this; 251 | } 252 | 253 | /** 254 | * Set up text size in dip and convert in to pixels 255 | * 256 | * @param textSize text size in dix 257 | * @return updated builder instance 258 | */ 259 | public Builder setTextSizeDp(int textSize) { 260 | this.textSize = Util.dpToPxF(textSize); 261 | return this; 262 | } 263 | 264 | /** 265 | * Set up text color 266 | * 267 | * @param colorText text color 268 | * @return updated builder instance 269 | */ 270 | public Builder setTextColor(int colorText) { 271 | this.colorText = colorText; 272 | return this; 273 | } 274 | 275 | /** 276 | * Set up background color 277 | * 278 | * @param colorBackground background color 279 | * @return updated builder instance 280 | */ 281 | public Builder setBackgroundColor(int colorBackground) { 282 | this.colorBackground = colorBackground; 283 | return this; 284 | } 285 | 286 | /** 287 | * Set up background corners radius in pixels 288 | * 289 | * @param bgRadius background corners radius in pixels 290 | * @return updated builder instance 291 | */ 292 | public Builder setBackgroundCornerRadius(int bgRadius) { 293 | this.bgRadius = bgRadius; 294 | return this; 295 | } 296 | 297 | /** 298 | * Set up background corners radius in dip and convert in to pixels 299 | * 300 | * @param bgRadius background corners radius in dip 301 | * @return updated builder instance 302 | */ 303 | public Builder setBackgroundCornerRadiusDp(int bgRadius) { 304 | return setBackgroundCornerRadius(Util.dp2px(bgRadius)); 305 | } 306 | 307 | /** 308 | * Set up text left padding in pixels 309 | * 310 | * @param paddingLeft text left padding in pixels 311 | * @return updated builder instance 312 | */ 313 | public Builder setPaddingLeft(int paddingLeft) { 314 | this.paddingLeft = paddingLeft; 315 | return this; 316 | } 317 | 318 | /** 319 | * Set up text left padding in dip and covert in to pixels 320 | * 321 | * @param paddingLeft left padding in dip 322 | * @return updated builder instance 323 | */ 324 | public Builder setPaddingLeftDp(int paddingLeft) { 325 | this.paddingLeft = Util.dp2px(paddingLeft); 326 | return this; 327 | } 328 | 329 | /** 330 | * Set up text top padding in in pixels 331 | * 332 | * @param paddingTop top padding in pixels 333 | * @return updated builder instance 334 | */ 335 | public Builder setPaddingTop(int paddingTop) { 336 | this.paddingTop = paddingTop; 337 | return this; 338 | } 339 | 340 | /** 341 | * Set up text top padding in dip and covert in to pixels 342 | * 343 | * @param paddingTop top padding in dip 344 | * @return updated builder instance 345 | */ 346 | public Builder setPaddingTopDp(int paddingTop) { 347 | this.paddingTop = Util.dp2px(paddingTop); 348 | return this; 349 | } 350 | 351 | /** 352 | * Set up text right padding in in pixels 353 | * 354 | * @param paddingRight right padding in pixels 355 | * @return updated builder instance 356 | */ 357 | public Builder setPaddingRight(int paddingRight) { 358 | this.paddingRight = paddingRight; 359 | return this; 360 | } 361 | 362 | /** 363 | * Set up text right padding in dip and covert in to pixels 364 | * 365 | * @param paddingRight right padding in dip 366 | * @return updated builder instance 367 | */ 368 | public Builder setPaddingRightDp(int paddingRight) { 369 | this.paddingRight = Util.dp2px(paddingRight); 370 | return this; 371 | } 372 | 373 | /** 374 | * Set up text bottom padding in in pixels 375 | * 376 | * @param paddingBottom right padding in pixels 377 | * @return updated builder instance 378 | */ 379 | public Builder setPaddingBottom(int paddingBottom) { 380 | this.paddingBottom = paddingBottom; 381 | return this; 382 | } 383 | 384 | /** 385 | * Set up text bottom padding in dip and covert in to pixels 386 | * 387 | * @param paddingBottom bottom padding in dip 388 | * @return updated builder instance 389 | */ 390 | public Builder setPaddingBottomDp(int paddingBottom) { 391 | this.paddingBottom = Util.dp2px(paddingBottom); 392 | return this; 393 | } 394 | 395 | 396 | /** 397 | * Set up symmetric padding in pixels 398 | * 399 | * @param horizontal horizontal padding in pixels 400 | * @param vertical vertical padding in pixels 401 | * @return updated builder instance 402 | */ 403 | public Builder setPaddingSymmetric(int horizontal, int vertical) { 404 | paddingLeft = horizontal; 405 | paddingTop = vertical; 406 | paddingRight = horizontal; 407 | paddingBottom = vertical; 408 | return this; 409 | } 410 | 411 | 412 | /** 413 | * Set up symmetric padding in dip and convert in to pixels 414 | * 415 | * @param horizontal horizontal padding in dip 416 | * @param vertical vertical padding in dip 417 | * @return updated builder instance 418 | */ 419 | public Builder setPaddingSymmetricDp(int horizontal, int vertical) { 420 | return setPaddingSymmetric(Util.dp2px(horizontal), Util.dp2px(vertical)); 421 | } 422 | 423 | /** 424 | * Set up text typeface 425 | * 426 | * @param typeface text typeface 427 | * @return updated builder instance 428 | */ 429 | public Builder setTypeface(@Nullable Typeface typeface) { 430 | this.typeface = typeface; 431 | return this; 432 | } 433 | 434 | /** 435 | * Set up text gravity. 436 | *

437 | * Support only horizontal gravity: left,right and center 438 | *

439 | * Center by default 440 | * 441 | * @param gravity value of {@link SimpleGravity} 442 | * @return updated builder instance 443 | */ 444 | public Builder setTextGravity(@NonNull SimpleGravity gravity) { 445 | this.gravity = gravity; 446 | return this; 447 | } 448 | 449 | @NonNull 450 | public SimpleTextDrawable build() { 451 | SimpleTextDrawable drawable = new SimpleTextDrawable(textSize, 452 | colorText, 453 | gravity, 454 | colorBackground, 455 | bgRadius, 456 | paddingLeft, 457 | paddingTop, 458 | paddingRight, 459 | paddingBottom, 460 | typeface); 461 | 462 | drawable.setText("0"); // init drawable height 463 | return drawable; 464 | } 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /sticky/src/main/java/com/osome/stickydecorator/Util.java: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator; 2 | 3 | import android.content.res.Resources; 4 | 5 | abstract class Util { 6 | 7 | static int dp2px(int px) { 8 | return (int) dpToPxF(px); 9 | } 10 | 11 | static float dpToPxF(int px) { 12 | return Resources.getSystem().getDisplayMetrics().density * px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sticky/src/main/java/com/osome/stickydecorator/VerticalDrawableSectionDecor.java: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Rect; 5 | import android.graphics.drawable.Drawable; 6 | import android.view.View; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | /** 11 | * Uses drawable to draw section above item 12 | */ 13 | public abstract class VerticalDrawableSectionDecor extends VerticalSectionDecor { 14 | 15 | @Override 16 | protected void onDrawSection(@NonNull Canvas c, int position, @NonNull Rect sectionBounds, @NonNull View child) { 17 | Drawable drawable = getDrawable(position, sectionBounds, child); 18 | onDrawDrawable(c, position, drawable, sectionBounds); 19 | } 20 | 21 | protected void onDrawDrawable(@NonNull Canvas c, int position, @NonNull Drawable drawable, Rect bounds) { 22 | drawable.setBounds(bounds); 23 | drawable.draw(c); 24 | } 25 | 26 | /** 27 | * Return drawable which will be drawn 28 | * 29 | * @param position Adapter item position 30 | * @param sectionBounds Section bounds 31 | * @param child RecyclerView's child view 32 | */ 33 | @NonNull 34 | protected abstract Drawable getDrawable(int position, @NonNull Rect sectionBounds, @NonNull View child); 35 | } 36 | -------------------------------------------------------------------------------- /sticky/src/main/java/com/osome/stickydecorator/VerticalSectionDecor.java: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Rect; 5 | import android.view.View; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.recyclerview.widget.GridLayoutManager; 9 | import androidx.recyclerview.widget.RecyclerView; 10 | 11 | /** 12 | * Draws sections decoration 13 | * By default decor draw full width and height allocated in {@link ConditionItemDecorator.Decor#getConditionItemOffsets(RecyclerView, Rect, View, int)} 14 | */ 15 | public abstract class VerticalSectionDecor implements ConditionItemDecorator.Decor { 16 | protected GridLayoutManager.SpanSizeLookup spanSizeLookup = new GridLayoutManager.DefaultSpanSizeLookup(); 17 | 18 | protected final Rect decoratedBounds = new Rect(); 19 | protected final Rect viewBounds = new Rect(); 20 | private Rect sectionBounds = new Rect(); 21 | 22 | @Override 23 | public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull View child, int position, @NonNull RecyclerView.State state) { 24 | Rect decoratedBounds = getDecoratedViewBounds(parent, child); 25 | Rect bounds = getViewBounds(parent, child); 26 | onDrawSectionInternal(c, position, getSectionBounds(parent, position, bounds, decoratedBounds), child); 27 | } 28 | 29 | /** 30 | * Returns the bounds of the view including its decoration and margins. 31 | * This bounds also include view translations 32 | * 33 | * @param child The view element to check 34 | * @return The bounds of the view including its decoration and margins. 35 | */ 36 | protected Rect getDecoratedViewBounds(@NonNull RecyclerView parent, @NonNull View child) { 37 | parent.getDecoratedBoundsWithMargins(child, decoratedBounds); 38 | addTranslationOffsetToViewBounds(child, decoratedBounds); 39 | return decoratedBounds; 40 | } 41 | 42 | void onDrawSectionInternal(@NonNull Canvas c, int position, @NonNull Rect sectionBounds, @NonNull View child) { 43 | onDrawSection(c, position, sectionBounds, child); 44 | } 45 | 46 | protected abstract void onDrawSection(@NonNull Canvas c, int position, @NonNull Rect sectionBounds, @NonNull View child); 47 | 48 | /** 49 | * Get view bounds without decoration offsets 50 | * 51 | * @param parent - current recycler view 52 | * @param child - itemView from recycler view 53 | * @return real view bounds without decoration offsets 54 | */ 55 | protected Rect getViewBounds(@NonNull RecyclerView parent, @NonNull View child) { 56 | child.getDrawingRect(viewBounds); 57 | parent.offsetDescendantRectToMyCoords(child, viewBounds); 58 | addTranslationOffsetToViewBounds(child, viewBounds); 59 | return viewBounds; 60 | } 61 | 62 | /** 63 | * Add view translation to bound to support {@link androidx.recyclerview.widget.DefaultItemAnimator} 64 | * 65 | * @param child - itemView from recycler 66 | * @param viewBounds - current itemView bounds without decoration offsets 67 | */ 68 | protected void addTranslationOffsetToViewBounds(@NonNull View child, @NonNull Rect viewBounds) { 69 | viewBounds.offset((int) child.getTranslationX(), (int) child.getTranslationY()); 70 | } 71 | 72 | @NonNull 73 | protected Rect getSectionBounds(@NonNull RecyclerView parent, int position, @NonNull Rect viewBounds, @NonNull Rect decoratedBounds) { 74 | int left = viewBounds.left; 75 | int top = decoratedBounds.top + getSectionMarginTop(); 76 | int right = viewBounds.right; 77 | int bottom = viewBounds.top - getSectionMarginBottom(); 78 | sectionBounds.set(left, top, right, bottom); 79 | return sectionBounds; 80 | } 81 | 82 | @Override 83 | public void prepareDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 84 | 85 | } 86 | 87 | @Override 88 | public void onPostDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 89 | 90 | } 91 | 92 | @Override 93 | public void getItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position, @NonNull RecyclerView.State state) { 94 | 95 | } 96 | 97 | 98 | @Override 99 | public void getConditionItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position) { 100 | rect.top += getSectionHeight(position) + getSectionMarginTop() + getSectionMarginBottom(); 101 | } 102 | 103 | /** 104 | * Get section height 105 | * 106 | * @param position item adapter position 107 | * @return section height 108 | */ 109 | protected abstract int getSectionHeight(int position); 110 | 111 | /** 112 | * Get top margin for section 113 | * 114 | * @return vertical margin 115 | */ 116 | protected int getSectionMarginTop() { 117 | return 0; 118 | } 119 | 120 | /** 121 | * Get bottom margin for section 122 | * 123 | * @return vertical margin 124 | */ 125 | protected int getSectionMarginBottom() { 126 | return 0; 127 | } 128 | 129 | public void setSpanSizeLookup(GridLayoutManager.SpanSizeLookup spanSizeLookup) { 130 | this.spanSizeLookup = spanSizeLookup; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /sticky/src/main/java/com/osome/stickydecorator/VerticalStickyDecor.java: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Rect; 5 | import android.view.View; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | /** 11 | * Draws section decoration and sticky header on the top of RecyclerView. 12 | * Now tested only with {@link androidx.recyclerview.widget.LinearLayoutManager} and vertical orientation. 13 | * Support reversed layout. 14 | */ 15 | public abstract class VerticalStickyDecor extends VerticalSectionDecor { 16 | public static int HEADER_HEIGHT_UNDEFINED = -1; 17 | 18 | private int contactPosition = RecyclerView.NO_POSITION; 19 | private Section contactSection = new Section(-1, RecyclerView.NO_POSITION); 20 | private int lastHeaderHeight = 0; 21 | private Rect headerBounds = new Rect(); 22 | private boolean reverseLayout; 23 | 24 | public VerticalStickyDecor() { 25 | this(false); 26 | } 27 | 28 | public VerticalStickyDecor(boolean reverseLayout) { 29 | this.reverseLayout = reverseLayout; 30 | } 31 | 32 | @Override 33 | public void getItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position, @NonNull RecyclerView.State state) { 34 | super.getItemOffsets(parent, rect, view, position, state); 35 | int count = 0; 36 | RecyclerView.LayoutManager layout = parent.getLayoutManager(); 37 | if (layout != null) { 38 | count = layout.getItemCount(); 39 | } 40 | if (reverseLayout && position == count - 1) { 41 | rect.top += getHeaderHeight() + getHeaderMarginTop() + getHeaderMarginBottom(); 42 | } 43 | } 44 | 45 | @Override 46 | public void prepareDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 47 | super.prepareDrawOver(c, parent, state); 48 | 49 | // clear last header state 50 | contactPosition = RecyclerView.NO_POSITION; 51 | contactSection.isValid = false; 52 | } 53 | 54 | @Override 55 | void onDrawSectionInternal(@NonNull Canvas c, int position, @NonNull Rect sectionBounds, @NonNull View child) { 56 | if (sectionBounds.top > getHeaderMarginTop()) { 57 | // don't draw section above header 58 | super.onDrawSectionInternal(c, position, sectionBounds, child); 59 | } 60 | 61 | lastHeaderHeight = sectionBounds.height(); 62 | int contactPoint = getHeaderHeightInternal() + getHeaderMarginTop() + getHeaderMarginBottom(); 63 | if ((contactPoint >= sectionBounds.top && contactPoint < sectionBounds.bottom + getHeaderMarginBottom())) { 64 | contactSection.isValid = true; 65 | contactSection.position = position; 66 | contactSection.top = sectionBounds.top; 67 | } 68 | } 69 | 70 | @Override 71 | public void onPostDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 72 | super.onPostDrawOver(c, parent, state); 73 | for (int i = 0; i < parent.getChildCount(); i++) { 74 | View child = parent.getChildAt(i); 75 | int position = parent.getChildAdapterPosition(child); 76 | parent.getDecoratedBoundsWithMargins(child, decoratedBounds); 77 | getViewBounds(parent, child); 78 | int viewTop = decoratedBounds.top; 79 | int contactPoint = getHeaderHeightInternal() + getHeaderMarginTop(); 80 | if (viewTop <= contactPoint) { 81 | contactPosition = position; 82 | } 83 | } 84 | 85 | drawHeaderInternal(c, parent, state); 86 | } 87 | 88 | private void drawHeaderInternal(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 89 | if (contactSection.isValid) { 90 | int itemPosition = getItemPosition(state); 91 | int headerBottom = contactSection.top - getHeaderMarginBottom(); 92 | Rect headerBounds = getHeaderBounds(parent, headerBottom, itemPosition, viewBounds, decoratedBounds); 93 | if (!reverseLayout && contactSection.position == 0 && headerBounds.top - getHeaderMarginTop() < 0) { 94 | return; 95 | } 96 | 97 | onDrawHeader(c, itemPosition, headerBounds); 98 | return; 99 | } 100 | 101 | if (contactPosition != RecyclerView.NO_POSITION) { 102 | int itemPosition = contactPosition; 103 | int headerBottom = getHeaderMarginTop() + getHeaderHeightInternal(); 104 | Rect headerBounds = getHeaderBounds(parent, headerBottom, itemPosition, viewBounds, decoratedBounds); 105 | onDrawHeader(c, itemPosition, headerBounds); 106 | } 107 | } 108 | 109 | private int getItemPosition(@NonNull RecyclerView.State state) { 110 | return reverseLayout ? 111 | Math.min(contactSection.position + 1, state.getItemCount() - 1) : 112 | Math.max(contactSection.position - 1, 0); 113 | } 114 | 115 | /** 116 | * Draw header drawable on canvas 117 | * 118 | * @param c RecyclerView canvas 119 | * @param position item position from 0 to {@link RecyclerView.Adapter#getItemCount()} 120 | * @param headerBounds header bounds 121 | */ 122 | protected abstract void onDrawHeader(@NonNull Canvas c, int position, @NonNull Rect headerBounds); 123 | 124 | @NonNull 125 | protected Rect getHeaderBounds(@NonNull RecyclerView parent, int headerBottom, int itemPosition, @NonNull Rect viewBounds, @NonNull Rect decoratedBounds) { 126 | int left = decoratedBounds.left; 127 | int top = headerBottom - getHeaderHeightInternal(); 128 | int right = decoratedBounds.right; 129 | headerBounds.set(left, top, right, headerBottom); 130 | return headerBounds; 131 | } 132 | 133 | protected int getHeaderMarginTop() { 134 | return getSectionMarginTop(); 135 | } 136 | 137 | protected int getHeaderMarginBottom() { 138 | return getSectionMarginBottom(); 139 | } 140 | 141 | /** 142 | * Return header height. As default return -1. It means that header height takes from section size 143 | * 144 | * @return header height 145 | */ 146 | protected int getHeaderHeight() { 147 | return HEADER_HEIGHT_UNDEFINED; 148 | } 149 | 150 | private int getHeaderHeightInternal() { 151 | int userHeight = getHeaderHeight(); 152 | if (userHeight != HEADER_HEIGHT_UNDEFINED) { 153 | return userHeight; 154 | } 155 | return lastHeaderHeight; 156 | } 157 | 158 | private static class Section { 159 | int top; 160 | int position; 161 | boolean isValid = false; 162 | 163 | Section(int top, int position) { 164 | this.top = top; 165 | this.position = position; 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /sticky/src/main/java/com/osome/stickydecorator/VerticalStickyDrawableDecor.java: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Rect; 5 | import android.graphics.drawable.Drawable; 6 | import android.view.View; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | public abstract class VerticalStickyDrawableDecor extends VerticalStickyDecor { 11 | 12 | public VerticalStickyDrawableDecor() { 13 | } 14 | 15 | public VerticalStickyDrawableDecor(boolean reverseLayout) { 16 | super(reverseLayout); 17 | } 18 | 19 | @Override 20 | protected void onDrawHeader(@NonNull Canvas c, int position, @NonNull Rect headerBounds) { 21 | Drawable drawable = getHeaderDrawable(position, headerBounds); 22 | onDrawHeaderDrawable(c, position, headerBounds, drawable); 23 | } 24 | 25 | protected void onDrawHeaderDrawable(@NonNull Canvas c, int position, @NonNull Rect headerBounds, @NonNull Drawable drawable) { 26 | drawable.setBounds(headerBounds); 27 | drawable.draw(c); 28 | } 29 | 30 | @Override 31 | protected void onDrawSection(@NonNull Canvas c, int position, @NonNull Rect sectionBounds, @NonNull View child) { 32 | Drawable drawable = getSectionDrawable(position, sectionBounds, child); 33 | onDrawSectionDrawable(c, position, drawable, sectionBounds); 34 | } 35 | 36 | protected void onDrawSectionDrawable(@NonNull Canvas c, int position, @NonNull Drawable drawable, Rect bounds) { 37 | drawable.setBounds(bounds); 38 | drawable.draw(c); 39 | } 40 | 41 | /** 42 | * Return drawable which will be drawn for section 43 | * 44 | * @param position Adapter item position 45 | * @param sectionBounds Section bounds 46 | * @param child RecyclerView's child view 47 | */ 48 | @NonNull 49 | protected abstract Drawable getSectionDrawable(int position, @NonNull Rect sectionBounds, @NonNull View child); 50 | 51 | /** 52 | * Return drawable which will be drawn for header 53 | * 54 | * @param position Adapter item position near by header 55 | * @param headerBounds Header bounds 56 | */ 57 | @NonNull 58 | protected abstract Drawable getHeaderDrawable(int position, @NonNull Rect headerBounds); 59 | } 60 | -------------------------------------------------------------------------------- /sticky/src/main/java/com/osome/stickydecorator/ViewHolderStickyDecoration.java: -------------------------------------------------------------------------------- 1 | package com.osome.stickydecorator; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Rect; 5 | import android.util.Pair; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.recyclerview.widget.RecyclerView; 12 | 13 | public class ViewHolderStickyDecoration extends RecyclerView.ItemDecoration { 14 | 15 | private Rect bounds = new Rect(); 16 | private Pair currentHeader; 17 | private final Condition condition; 18 | private final boolean reverseLayout; 19 | 20 | public ViewHolderStickyDecoration(@NonNull RecyclerView parent, @NonNull Condition condition) { 21 | this(parent, condition, false); 22 | } 23 | 24 | public ViewHolderStickyDecoration(@NonNull RecyclerView parent, @NonNull Condition condition, boolean reverseLayout) { 25 | this.condition = condition; 26 | this.reverseLayout = reverseLayout; 27 | init(parent); 28 | } 29 | 30 | private void init(@NonNull RecyclerView parent) { 31 | RecyclerView.Adapter adapter = parent.getAdapter(); 32 | if (adapter == null) { 33 | throw new IllegalArgumentException("Firstly set adapter"); 34 | } 35 | 36 | adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { 37 | @Override 38 | public void onChanged() { 39 | clearHeader(); 40 | } 41 | }); 42 | 43 | parent.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 44 | @Override 45 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 46 | clearHeader(); 47 | } 48 | }); 49 | } 50 | 51 | private void clearHeader() { 52 | currentHeader = null; 53 | } 54 | 55 | @Override 56 | public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 57 | if (state.getItemCount() == 0) { 58 | return; 59 | } 60 | 61 | View topChild = getTopChild(parent); 62 | if (topChild == null) { 63 | return; 64 | } 65 | 66 | int topPosition = parent.getChildAdapterPosition(topChild); 67 | View header = getHeaderViewForItem(topPosition, parent); 68 | if (header == null) { 69 | return; 70 | } 71 | 72 | int contactPoint = header.getBottom(); 73 | View contactChild = getChildInContact(parent, contactPoint); 74 | if (contactChild == null) { 75 | return; 76 | } 77 | 78 | if (condition.isHeader(parent.getChildAdapterPosition(contactChild))) { 79 | moveHeader(c, header, contactChild); 80 | return; 81 | } 82 | 83 | drawHeader(c, header); 84 | } 85 | 86 | @Nullable 87 | private View getTopChild(@NonNull RecyclerView parent) { 88 | if (reverseLayout) { 89 | return parent.findChildViewUnder(0, 0); 90 | } 91 | return parent.getChildAt(0); 92 | } 93 | 94 | @SuppressWarnings("unchecked") 95 | @Nullable 96 | private View getHeaderViewForItem(int position, RecyclerView parent) { 97 | RecyclerView.Adapter adapter = parent.getAdapter(); 98 | if (adapter == null) { 99 | return null; 100 | } 101 | int headerPosition = reverseLayout ? 102 | getHeaderPositionForItemRevense(adapter.getItemCount(), position) : 103 | getHeaderPositionForItem(position); 104 | 105 | if (headerPosition == RecyclerView.NO_POSITION) { 106 | return null; 107 | } 108 | 109 | int viewType = adapter.getItemViewType(headerPosition); 110 | if (currentHeader != null && headerPosition == currentHeader.first && currentHeader.second.getItemViewType() == viewType) { 111 | return currentHeader.second.itemView; 112 | } 113 | 114 | RecyclerView.ViewHolder holder = adapter.createViewHolder(parent, viewType); 115 | adapter.onBindViewHolder(holder, headerPosition); 116 | fixViewSize(parent, holder.itemView); 117 | currentHeader = new Pair<>(headerPosition, holder); 118 | return holder.itemView; 119 | } 120 | 121 | private int getHeaderPositionForItem(int position) { 122 | int headerPosition = RecyclerView.NO_POSITION; 123 | int currentPosition = position; 124 | do { 125 | if (condition.isHeader(currentPosition)) { 126 | headerPosition = currentPosition; 127 | break; 128 | } 129 | currentPosition -= 1; 130 | } while (currentPosition >= 0); 131 | 132 | return headerPosition; 133 | } 134 | 135 | private int getHeaderPositionForItemRevense(int totalCount, int position) { 136 | int headerPosition = RecyclerView.NO_POSITION; 137 | int currentPosition = position; 138 | do { 139 | if (condition.isHeader(currentPosition)) { 140 | headerPosition = currentPosition; 141 | break; 142 | } 143 | currentPosition += 1; 144 | } while (currentPosition < totalCount); 145 | 146 | return headerPosition; 147 | } 148 | 149 | private void fixViewSize(ViewGroup parent, View view) { 150 | // Specs for parent (RecyclerView) 151 | int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); 152 | int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); 153 | 154 | // Specs for children (headers) 155 | int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width); 156 | int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height); 157 | 158 | view.measure(childWidthSpec, childHeightSpec); 159 | view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); 160 | } 161 | 162 | @Nullable 163 | private View getChildInContact(RecyclerView parent, int contactPoint) { 164 | for (int i = 0; i < parent.getChildCount(); i++) { 165 | View child = parent.getChildAt(i); 166 | parent.getDecoratedBoundsWithMargins(child, bounds); 167 | if (bounds.bottom > contactPoint && bounds.top <= contactPoint) { 168 | return child; 169 | } 170 | } 171 | 172 | return null; 173 | } 174 | 175 | private void moveHeader(Canvas c, View header, View nextHeader) { 176 | c.save(); 177 | c.translate(0, (nextHeader.getTop() - header.getHeight())); 178 | header.draw(c); 179 | c.restore(); 180 | } 181 | 182 | private void drawHeader(Canvas c, View header) { 183 | c.save(); 184 | c.translate(0, 0); 185 | header.draw(c); 186 | c.restore(); 187 | } 188 | 189 | public interface Condition { 190 | boolean isHeader(int position); 191 | } 192 | } 193 | --------------------------------------------------------------------------------