├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── com │ │ │ └── grzegorzojdana │ │ │ └── spacingitemdecorationapp │ │ │ ├── action │ │ │ ├── listcontrol │ │ │ │ ├── ListControlFragment.kt │ │ │ │ └── ListControlViewModel.kt │ │ │ ├── listpreview │ │ │ │ ├── ItemSizeProvider.kt │ │ │ │ ├── ItemViewAdjuster.kt │ │ │ │ ├── ListAdjuster.kt │ │ │ │ ├── ListPreviewFragment.kt │ │ │ │ ├── ListPreviewItemAdapter.kt │ │ │ │ ├── ListPreviewViewModel.kt │ │ │ │ └── RandomSpanSizeLookup.kt │ │ │ ├── main │ │ │ │ └── MainActivity.kt │ │ │ └── spacingconfig │ │ │ │ ├── SpacingConfigFragment.kt │ │ │ │ └── SpacingConfigViewModel.kt │ │ │ ├── extensions │ │ │ └── ResourcesExtensions.kt │ │ │ ├── model │ │ │ ├── DecorationConfig.kt │ │ │ ├── ListDataProvider.kt │ │ │ ├── ListDataRepository.kt │ │ │ └── ListLayoutConfig.kt │ │ │ └── util │ │ │ └── NullIgnoreObserver.kt │ └── res │ │ ├── drawable-nodpi │ │ └── ic_settings_white_24dp.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout-w520dp │ │ └── fragment_spacing_config.xml │ │ ├── layout │ │ ├── activity_main_bottom_sheet.xml │ │ ├── fragment_list_control.xml │ │ ├── fragment_list_preview.xml │ │ ├── fragment_spacing_config.xml │ │ └── layout_preview_list_item.xml │ │ ├── menu │ │ └── main_menu.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-h480dp │ │ └── dimens.xml │ │ ├── values-h640dp │ │ └── dimens.xml │ │ ├── values-sw600dp │ │ └── dimens.xml │ │ └── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── colors_md.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── kotlin │ └── com │ └── grzegorzojdana │ └── spacingitemdecorationapp │ └── action │ └── spacingconfig │ └── SpacingConfigViewModelTest.kt ├── art ├── grid-draw-with-legend.png ├── grid-draw.png ├── grid-spanning-draw.png ├── grid-spanning.png ├── grid.png ├── sample.gif ├── staggered-small.png └── staggered.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── spacingitemdecorationlib ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── main ├── AndroidManifest.xml ├── kotlin │ └── com │ │ └── grzegorzojdana │ │ └── spacingitemdecoration │ │ ├── ItemOffsetsCalculator.kt │ │ ├── ItemOffsetsRequestBuilder.kt │ │ ├── Spacing.kt │ │ └── SpacingItemDecoration.kt └── res │ └── values │ └── strings.xml └── test └── kotlin └── com └── grzegorzojdana └── spacingitemdecoration ├── ItemOffsetsCalculatorTestBase.kt ├── ItemOffsetsCalculatorTest_grid.kt ├── ItemOffsetsCalculatorTest_gridSpanSize.kt ├── ItemOffsetsCalculatorTest_singleColumn.kt ├── ItemOffsetsCalculatorTest_singleItem.kt ├── ItemOffsetsCalculatorTest_singleRow.kt ├── ItemOffsetsRequestBuilderTestBase.kt ├── ItemOffsetsRequestBuilderTest_gridLayout.kt ├── ItemOffsetsRequestBuilderTest_linearLayout.kt └── SpacingTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | .idea 37 | *.iml 38 | */*.iml 39 | 40 | # Signing data and keystore 41 | keystore.properties 42 | *.jks 43 | 44 | # External native build folder generated in Android Studio 2.2 and later 45 | .externalNativeBuild 46 | 47 | # Google Services (e.g. APIs or Firebase) 48 | google-services.json 49 | 50 | # Freeline 51 | freeline.py 52 | freeline/ 53 | freeline_project_description.json 54 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Grzegorz Ojdana 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpacingItemDecoration 2 | 3 | [![release](https://jitpack.io/v/grzegorzojdana/SpacingItemDecoration.svg)](https://jitpack.io/#grzegorzojdana/SpacingItemDecoration) 4 | 5 | ItemDecoration for RecyclerView that allows you to set spacing between and around list items in flexible way. 6 | 7 | 8 | 9 | ## How to install 10 | 11 | Add to your root `build.gradle`: 12 | ```groovy 13 | allprojects { 14 | repositories { 15 | maven { url "https://jitpack.io" } 16 | } 17 | } 18 | ``` 19 | 20 | Add the dependency to your project's `build.gradle`: 21 | ```groovy 22 | dependencies { 23 | implementation 'com.github.grzegorzojdana:SpacingItemDecoration:1.1.0' 24 | } 25 | ``` 26 | 27 | Since `1.1.0` version this library depends on `androidx` packages. If you have issues with manifest merger, you can stay with `1.0.1` version, which depends on old `com.android.support` packages. 28 | 29 | ## How to use 30 | 31 | Decoration with specified _Spacing_ can be created and added to RecyclerView like this: 32 | ```kotlin 33 | val spacingItemDecoration = SpacingItemDecoration(Spacing( 34 | // values in pixels are expected 35 | horizontal = resources.getDimensionPixelSize(R.dimen.spacing_horizontal), 36 | vertical = resources.dpToPx(16F).toInt(), 37 | edges = Rect(0, top, 0, 0) 38 | )) 39 | list.addItemDecoration(spacingItemDecoration) 40 | ``` 41 | 42 | There are several spacing modifiers: 43 | * `horizontal` and `vertical` are the gaps between each two items. 44 | * `item` rectangle defines inset for each item (similar to item padding). 45 | * `edges` rectangle means offsets from the parent (RecyclerView) edges (similar to RecyclerView padding). 46 | 47 | 48 | 49 | This library doesn't modify padding of any view, but calculates item offsets basing on given parameters. 50 | 51 | Spacing can be easily modified: 52 | ```kotlin 53 | spacingItemDecoration.spacing.apply { 54 | vertical = newVerticalSpacing 55 | edges.setEmpty() 56 | } 57 | // when modifying spacing properties, need to call invalidateSpacing() 58 | spacingItemDecoration.invalidateSpacing() 59 | // your's RecyclerView needs to know 60 | list.invalidateItemDecorations() 61 | ``` 62 | If you change Spacing instance, you don't need to call _invalidateSpacing_. 63 | ```kotlin 64 | val vItemSpacing = ... 65 | spacingItemDecoration.spacing = Spacing(item = Rect(0, vItemSpacing, 0, vItemSpacing)) 66 | list.invalidateItemDecorations() 67 | ``` 68 | 69 | To see how different spacing values impact list layout, run sample app from this repo and play with configuration controls. 70 | 71 | _SpacingItemDecoration_ can also draw determined spacing, which is useful for debugging. 72 | ```kotlin 73 | spacingItemDecoration.isSpacingDrawingEnabled = true 74 | // you can change default colors used to mark specific spacing 75 | spacingItemDecoration.drawingConfig = DrawingConfig(horizontalColor = Color.MAGENTA) 76 | // if your decoration is already in use (items have been laid out), invalidate decor 77 | list.invalidateItemDecorations() 78 | ``` 79 | 80 | ## Caveats 81 | 82 | From the fact how this library works (providing item offsets with desired layout spacing without changing number of list rows and columns), list items will in result be laid out smaller than without this decoration, because some spacing is brought uniformly from each item's width and height. 83 | 84 | This library works best if RecyclerView items have set one of the dimension to `MATCH_PARENT` (width if orientation is `VERTICAL`, and height if orientation is `HORIZONTAL`). Otherwise, you can see that when using GridLayoutManager the `item.bottom` or `item.right` spacing could not work. 85 | 86 | `StaggeredLayoutManager` is not currently fully supported. For `VERTICAL` orientation, `vertical` spacing won't work, and `edges.top` and `edges.bottom` spacings will behave like `item.top` and `item.bottom` spacings. Similar, for `HORIZONTAL` orientation, `horizontal` spacing, `edges.left` and `edges.right` are broken. However, it is planned to be fixed in some future release. 87 | 88 | 89 | ## Performance tips 90 | 91 | If you use GridLayoutManager with list of huge number of items (thousands), you might would like to try this tips: 92 | * Set `spacingItemDecoration.isGroupCountCacheEnabled` to `true`. This will make determined group count be held by decoration implementation, but you will need to call `invalidate()` method each time the number of items or the properties of layout manager changes (orientation, span count, span size lookup object or items span size). 93 | * If you use non-default implementation of _SpanSizeLookup_ but its `getSpanSize(position)` method always returns `1`, you might want to set `spacingItemDecoration.hintSpanSizeAlwaysOne` to `true`. You may also consider [enable span indices caching](https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.SpanSizeLookup.html#setSpanIndexCacheEnabled(boolean)). 94 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion globalCompileSdkVersion 7 | defaultConfig { 8 | applicationId "com.grzegorzojdana.spacingitemdecorationapp" 9 | minSdkVersion globalMinSdkVersion 10 | targetSdkVersion globalTargetSdkVersion 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | 16 | compileOptions { 17 | sourceCompatibility JavaVersion.VERSION_1_8 18 | targetCompatibility JavaVersion.VERSION_1_8 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled true 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | 28 | sourceSets { 29 | main.java.srcDirs += 'src/main/kotlin' 30 | test.java.srcDirs += 'src/test/kotlin' 31 | androidTest.java.srcDirs += 'src/androidTest/kotlin' 32 | } 33 | 34 | testOptions { 35 | unitTests { 36 | includeAndroidResources = true 37 | } 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(include: ['*.jar'], dir: 'libs') 43 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$globalKotlinVersion" 44 | implementation "androidx.appcompat:appcompat:$globalExtensionLibraryVersion" 45 | implementation "androidx.recyclerview:recyclerview:$globalExtensionLibraryVersion" 46 | implementation 'com.google.android.material:material:1.0.0' 47 | implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' 48 | implementation project(':spacingitemdecorationlib') 49 | 50 | // ViewModel and LiveData 51 | implementation "androidx.lifecycle:lifecycle-extensions:$globalLifecycleVersion" 52 | implementation "androidx.lifecycle:lifecycle-common-java8:$globalLifecycleVersion" 53 | 54 | testImplementation 'junit:junit:4.12' 55 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$globalKotlinVersion" 56 | testImplementation "org.robolectric:robolectric:$globalRobolectricVersion" 57 | testImplementation "androidx.lifecycle:lifecycle-extensions:$globalLifecycleVersion" 58 | testImplementation "androidx.lifecycle:lifecycle-common-java8:$globalLifecycleVersion" 59 | 60 | androidTestImplementation "androidx.test:runner:$globalTestRunnerVersion" 61 | androidTestImplementation "androidx.test.espresso:espresso-core:$globalEspressoVersion" 62 | } 63 | -------------------------------------------------------------------------------- /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/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/listcontrol/ListControlFragment.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.listcontrol 2 | 3 | import androidx.lifecycle.ViewModelProviders 4 | import android.content.Context 5 | import android.os.Bundle 6 | import androidx.fragment.app.Fragment 7 | import androidx.recyclerview.widget.OrientationHelper 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import android.widget.AdapterView 12 | import android.widget.ArrayAdapter 13 | import android.widget.SeekBar 14 | import com.grzegorzojdana.spacingitemdecorationapp.R 15 | import com.grzegorzojdana.spacingitemdecorationapp.model.DecorationConfig 16 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListLayoutConfig 17 | import com.grzegorzojdana.spacingitemdecorationapp.util.NullIgnoreObserver 18 | import kotlinx.android.synthetic.main.fragment_list_control.* 19 | 20 | 21 | class ListControlFragment: Fragment() { 22 | 23 | private lateinit var viewModel: ListControlViewModel 24 | 25 | override fun onCreateView(inflater: LayoutInflater, 26 | container: ViewGroup?, 27 | savedInstanceState: Bundle? 28 | ): View? { 29 | return inflater.inflate(R.layout.fragment_list_control, container, false) 30 | } 31 | 32 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 33 | super.onViewCreated(view, savedInstanceState) 34 | setupViews(view.context) 35 | 36 | viewModel = ViewModelProviders.of(this).get(ListControlViewModel::class.java).also { 37 | it.listLayoutConfig.observe(this, ListLayoutConfigObserver) 38 | it.decorationConfig.observe(this, DecorationConfigObserver) 39 | it.itemCount.observe(this, NullIgnoreObserver { seekBarItemCount.progress = it }) 40 | } 41 | } 42 | 43 | private fun setupViews(context: Context) { 44 | spinnerLayout.adapter = ArrayAdapter.createFromResource( 45 | context, R.array.layout_manager_types, android.R.layout.simple_spinner_item) 46 | 47 | spinnerLayout.onItemSelectedListener = object: AdapterView.OnItemSelectedListener { 48 | override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { 49 | viewModel.updateListLayoutConfig { it.copy(layoutType = position) } 50 | } 51 | 52 | override fun onNothingSelected(parent: AdapterView<*>?) {} 53 | } 54 | 55 | radioGroupOrientation.setOnCheckedChangeListener { _, checkedId -> 56 | viewModel.updateListLayoutConfig { 57 | it.copy(orientation = when (checkedId) { 58 | R.id.radioHorizontal -> OrientationHelper.HORIZONTAL 59 | R.id.radioVertical -> OrientationHelper.VERTICAL 60 | else -> it.orientation 61 | }) 62 | } 63 | } 64 | 65 | switchReversed.setOnCheckedChangeListener { _, isChecked -> 66 | viewModel.updateListLayoutConfig { it.copy(reversed = isChecked) } 67 | } 68 | 69 | seekBarItemCount.setOnSeekBarChangeListener(SeekBarProgressChangedListener { 70 | _, progress, fromUser -> 71 | if (fromUser) { 72 | viewModel.itemCount.value = progress 73 | } 74 | labelItemCountValue.text = progress.toString() 75 | }) 76 | 77 | seekBarSpan.setOnSeekBarChangeListener(SeekBarProgressChangedListener { 78 | _, progress, fromUser -> 79 | val newSpan = progress + 1 80 | if (fromUser) { 81 | viewModel.updateListLayoutConfig { it.copy(spanCount = newSpan) } 82 | } 83 | labelSpanValue.text = newSpan.toString() 84 | }) 85 | 86 | cbAllowItemSpan.setOnCheckedChangeListener { _, isChecked -> 87 | viewModel.updateListLayoutConfig { it.copy(allowItemSpan = isChecked) } 88 | } 89 | 90 | cbDrawSpacing.setOnCheckedChangeListener{ _, isChecked -> 91 | viewModel.updateDecorationConfig { it.copy(enableDrawSpacing = isChecked) } 92 | } 93 | } 94 | 95 | private val ListLayoutConfigObserver = NullIgnoreObserver { 96 | spinnerLayout.setSelection(it.layoutType) 97 | 98 | radioGroupOrientation.check(when(it.orientation) { 99 | OrientationHelper.HORIZONTAL -> R.id.radioHorizontal 100 | OrientationHelper.VERTICAL -> R.id.radioVertical 101 | else -> -1 102 | }) 103 | 104 | switchReversed.isChecked = it.reversed 105 | 106 | if (it.layoutType == ListLayoutConfig.LAYOUT_TYPE_LINEAR) { 107 | seekBarSpan.progress = 0 108 | seekBarSpan.isEnabled = false 109 | } else { 110 | seekBarSpan.progress = it.spanCount - 1 111 | seekBarSpan.isEnabled = true 112 | } 113 | 114 | val allowItemsSpanning = viewModel.allowItemsSpanning(it.layoutType) 115 | cbAllowItemSpan.isEnabled = allowItemsSpanning 116 | cbAllowItemSpan.isChecked = allowItemsSpanning && it.allowItemSpan 117 | } 118 | 119 | private val DecorationConfigObserver = NullIgnoreObserver { 120 | cbDrawSpacing.isChecked = it.enableDrawSpacing 121 | } 122 | } 123 | 124 | 125 | private class SeekBarProgressChangedListener( 126 | val callback: ((seekBar: SeekBar, progress: Int, fromUser: Boolean) -> Unit) 127 | ): SeekBar.OnSeekBarChangeListener { 128 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 129 | callback(seekBar, progress, fromUser) 130 | } 131 | 132 | override fun onStartTrackingTouch(seekBar: SeekBar?) {} 133 | override fun onStopTrackingTouch(seekBar: SeekBar?) {} 134 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/listcontrol/ListControlViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.listcontrol 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import com.grzegorzojdana.spacingitemdecorationapp.model.DecorationConfig 6 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListDataRepository 7 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListLayoutConfig 8 | 9 | /** 10 | * To modify list preview properties. 11 | */ 12 | class ListControlViewModel: ViewModel() { 13 | 14 | private val listDataRepository = ListDataRepository 15 | 16 | val listLayoutConfig: MutableLiveData 17 | get() = listDataRepository.listLayoutConfig 18 | 19 | val decorationConfig: MutableLiveData 20 | get() = listDataRepository.decorationConfig 21 | 22 | val itemCount: MutableLiveData get() = listDataRepository.itemCount 23 | 24 | fun allowItemsSpanning(layoutType: Int): Boolean = when(layoutType) { 25 | ListLayoutConfig.LAYOUT_TYPE_GRID, 26 | ListLayoutConfig.LAYOUT_TYPE_STAGGERED_GRID -> true 27 | else -> false 28 | } 29 | 30 | fun updateListLayoutConfig(block: (currentConfig: ListLayoutConfig) -> ListLayoutConfig) { 31 | listLayoutConfig.value?.let { 32 | listLayoutConfig.value = block(it) 33 | } 34 | } 35 | 36 | fun updateDecorationConfig(block: (currentConfig: DecorationConfig) -> DecorationConfig) { 37 | decorationConfig.value?.let { 38 | decorationConfig.value = block(it) 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/listpreview/ItemSizeProvider.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.listpreview 2 | 3 | import android.graphics.Point 4 | import androidx.recyclerview.widget.LinearLayoutManager 5 | import androidx.recyclerview.widget.OrientationHelper 6 | import androidx.recyclerview.widget.RecyclerView 7 | import androidx.recyclerview.widget.StaggeredGridLayoutManager 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import com.grzegorzojdana.spacingitemdecorationapp.R 11 | import com.grzegorzojdana.spacingitemdecorationapp.extensions.dimenPx 12 | import com.grzegorzojdana.spacingitemdecorationapp.extensions.dpToPxInt 13 | import java.util.* 14 | 15 | 16 | /** 17 | * Provides dimensions of list item. 18 | */ 19 | interface ItemSizeProvider { 20 | /** 21 | * Determine dimensions of item view and fill passed [size] with expected item's with and height. 22 | */ 23 | fun provideItemSize(size: Point, itemView: View, position: Int) 24 | } 25 | 26 | /** 27 | * Choose item dimensions basing on [layoutManager] properties. [seed] is used to randomize 28 | * values. 29 | */ 30 | class LayoutManagerDependentItemSizeProvider( 31 | var layoutManager: RecyclerView.LayoutManager, 32 | val seed: Long = System.currentTimeMillis() 33 | ) : ItemSizeProvider { 34 | 35 | private val random = Random() 36 | 37 | override fun provideItemSize(size: Point, itemView: View, position: Int) { 38 | val regularItemWidth = itemView.resources.dimenPx(R.dimen.list_preview_item_width) 39 | val regularItemHeight = itemView.resources.dimenPx(R.dimen.list_preview_item_height) 40 | 41 | val lm = layoutManager 42 | when (lm) { 43 | is LinearLayoutManager -> { 44 | // Linear or Grid 45 | if (lm.orientation == OrientationHelper.VERTICAL) { 46 | size.set(ViewGroup.LayoutParams.MATCH_PARENT, regularItemHeight) 47 | } else { 48 | size.set(regularItemWidth, ViewGroup.LayoutParams.MATCH_PARENT) 49 | } 50 | } 51 | is StaggeredGridLayoutManager -> { 52 | val length = determineLengthOfStaggeredGridItem(itemView, position) 53 | if (lm.orientation == OrientationHelper.VERTICAL) { 54 | size.set(ViewGroup.LayoutParams.MATCH_PARENT, length) 55 | } else { 56 | size.set(length, ViewGroup.LayoutParams.MATCH_PARENT) 57 | } 58 | } 59 | else -> { 60 | size.set(regularItemWidth, regularItemHeight) 61 | } 62 | } 63 | } 64 | 65 | private fun determineLengthOfStaggeredGridItem(itemView: View, position: Int): Int { 66 | // using bitmask instead of nextInt(16) for better performance and more interesting results, regardless of distribution 67 | val x = 8 + (generatePseudorandomFor(position) and 0x0F) 68 | return (x * itemView.resources.dpToPxInt(8)) 69 | } 70 | 71 | private fun generatePseudorandomFor(position: Int): Int { 72 | return random.apply { setSeed(seed + position) }.nextInt() 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/listpreview/ItemViewAdjuster.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.listpreview 2 | 3 | import androidx.recyclerview.widget.StaggeredGridLayoutManager 4 | import android.view.View 5 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListLayoutConfig 6 | import java.util.* 7 | 8 | /** 9 | * Allows to adjust list item view. 10 | */ 11 | interface ItemViewAdjuster { 12 | // fun adjustListItemViewOnCreate(itemView: View) 13 | fun adjustListItemViewOnBind(itemView: View, position: Int) 14 | } 15 | 16 | /** 17 | * Adjust list item view to be suitable for specific [listLayoutConfig]. [seed] is used to randomize 18 | * results. 19 | */ 20 | class ListConfigItemViewAdjuster( 21 | var listLayoutConfig: ListLayoutConfig? = null, 22 | val seed: Long = System.currentTimeMillis() 23 | ) : ItemViewAdjuster { 24 | 25 | private val random = Random() 26 | 27 | override fun adjustListItemViewOnBind(itemView: View, position: Int) { 28 | if (listLayoutConfig?.layoutType == ListLayoutConfig.LAYOUT_TYPE_STAGGERED_GRID) { 29 | val layoutParams = itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams 30 | val allowItemFullSpan = listLayoutConfig?.allowItemSpan ?: false 31 | layoutParams?.isFullSpan = allowItemFullSpan && isStaggeredGridItemFullSpan(position) 32 | } 33 | } 34 | 35 | private fun isStaggeredGridItemFullSpan(position: Int): Boolean { 36 | return (random.apply { setSeed(seed + position) }.nextInt() and 0x1F) == 0 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/listpreview/ListAdjuster.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.listpreview 2 | 3 | import android.content.Context 4 | import androidx.recyclerview.widget.GridLayoutManager 5 | import androidx.recyclerview.widget.LinearLayoutManager 6 | import androidx.recyclerview.widget.RecyclerView 7 | import androidx.recyclerview.widget.StaggeredGridLayoutManager 8 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListLayoutConfig 9 | 10 | /** 11 | * To adjust previewed list to [ListLayoutConfig] changes. 12 | */ 13 | class ListAdjuster { 14 | 15 | fun adjustListToConfig(list: RecyclerView, config: ListLayoutConfig) { 16 | if (config.layoutType != determineLayoutTypeOf(list.layoutManager)) { 17 | list.layoutManager = makeLayoutManagerFromConfig(list.context, config) 18 | } 19 | else list.layoutManager?.apply { 20 | when (this) { 21 | // check Grid before Linear because GridLayoutManager is subclass of LinearLayoutManager 22 | is GridLayoutManager -> { 23 | orientation = config.orientation 24 | reverseLayout = config.reversed 25 | spanCount = config.spanCount 26 | } 27 | is LinearLayoutManager -> { 28 | orientation = config.orientation 29 | reverseLayout = config.reversed 30 | } 31 | is StaggeredGridLayoutManager -> { 32 | orientation = config.orientation 33 | reverseLayout = config.reversed 34 | spanCount = config.spanCount 35 | } 36 | } 37 | } 38 | 39 | // regardless if layout manager instance have been changed above or not 40 | list.layoutManager?.apply { 41 | if (this is GridLayoutManager) { 42 | spanSizeLookup = adjustSpanSizeLookupFor(this, config) 43 | spanSizeLookup.invalidateSpanIndexCache() 44 | } 45 | } 46 | } 47 | 48 | private fun createSpanSizeLookupForGrid(spanCount: Int): GridLayoutManager.SpanSizeLookup { 49 | return RandomSpanSizeLookup(spanCount, preferSmallerSpans = true).apply { 50 | isSpanIndexCacheEnabled = true 51 | } 52 | } 53 | 54 | private fun adjustSpanSizeLookupFor(layoutManager: GridLayoutManager, 55 | config: ListLayoutConfig): GridLayoutManager.SpanSizeLookup { 56 | return layoutManager.spanSizeLookup.run { 57 | when (this) { 58 | is RandomSpanSizeLookup -> { 59 | if (config.allowItemSpan) this.apply { spanCount = config.spanCount } 60 | else GridLayoutManager.DefaultSpanSizeLookup() 61 | } 62 | else -> { 63 | if (config.allowItemSpan) createSpanSizeLookupForGrid(config.spanCount) 64 | else this 65 | } 66 | } 67 | } 68 | } 69 | 70 | private fun determineLayoutTypeOf(layoutManager: RecyclerView.LayoutManager?): Int { 71 | return when (layoutManager) { 72 | // check grid before linear - because grid is subclass of linear 73 | is GridLayoutManager -> ListLayoutConfig.LAYOUT_TYPE_GRID 74 | is LinearLayoutManager -> ListLayoutConfig.LAYOUT_TYPE_LINEAR 75 | is StaggeredGridLayoutManager -> ListLayoutConfig.LAYOUT_TYPE_STAGGERED_GRID 76 | else -> -1 77 | } 78 | } 79 | 80 | private fun makeLayoutManagerFromConfig(context: Context, 81 | config: ListLayoutConfig): RecyclerView.LayoutManager { 82 | return when (config.layoutType) { 83 | ListLayoutConfig.LAYOUT_TYPE_LINEAR -> { 84 | LinearLayoutManager(context, config.orientation, config.reversed) 85 | } 86 | ListLayoutConfig.LAYOUT_TYPE_GRID -> { 87 | GridLayoutManager(context, config.spanCount, config.orientation, config.reversed) 88 | } 89 | ListLayoutConfig.LAYOUT_TYPE_STAGGERED_GRID -> { 90 | StaggeredGridLayoutManager(config.spanCount, config.orientation) 91 | } 92 | else -> throw IllegalArgumentException("Unknown type of layout manager: ${config.layoutType}") 93 | } 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/listpreview/ListPreviewFragment.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.listpreview 2 | 3 | import androidx.lifecycle.ViewModelProviders 4 | import android.graphics.Rect 5 | import android.os.Bundle 6 | import androidx.fragment.app.Fragment 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import androidx.recyclerview.widget.RecyclerView 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.Toast 13 | import com.grzegorzojdana.spacingitemdecoration.Spacing 14 | import com.grzegorzojdana.spacingitemdecoration.SpacingItemDecoration 15 | import com.grzegorzojdana.spacingitemdecorationapp.R 16 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListLayoutConfig 17 | import com.grzegorzojdana.spacingitemdecorationapp.util.NullIgnoreObserver 18 | import kotlinx.android.synthetic.main.fragment_list_preview.* 19 | 20 | /** 21 | * List preview view. 22 | */ 23 | class ListPreviewFragment: Fragment() { 24 | 25 | private lateinit var viewModel: ListPreviewViewModel 26 | private val adapter: ListPreviewItemAdapter = ListPreviewItemAdapter(0) 27 | private val spacingItemDecoration = SpacingItemDecoration(Spacing()) 28 | private lateinit var itemSizeProvider: LayoutManagerDependentItemSizeProvider 29 | private val itemViewAdjuster = ListConfigItemViewAdjuster() 30 | 31 | override fun onCreateView(inflater: LayoutInflater, 32 | container: ViewGroup?, 33 | savedInstanceState: Bundle? 34 | ): View? { 35 | return inflater.inflate(R.layout.fragment_list_preview, container, false) 36 | } 37 | 38 | override fun onActivityCreated(savedInstanceState: Bundle?) { 39 | super.onActivityCreated(savedInstanceState) 40 | 41 | setupList() 42 | 43 | viewModel = ViewModelProviders.of(this).get(ListPreviewViewModel::class.java) 44 | viewModel.listLayoutConfig.observe(this, ListLayoutConfigObserver) 45 | 46 | viewModel.decorationConfig.observe(this, NullIgnoreObserver { 47 | spacingItemDecoration.isSpacingDrawingEnabled = it.enableDrawSpacing 48 | list?.invalidateItemDecorations() 49 | }) 50 | 51 | viewModel.spacing.observe(this, NullIgnoreObserver { 52 | spacingItemDecoration.spacing = it 53 | list?.invalidateItemDecorations() 54 | }) 55 | 56 | viewModel.itemCount.observe(this, NullIgnoreObserver { itemCount -> 57 | val delta = itemCount - adapter.numberCount 58 | adapter.numberCount = itemCount 59 | if (delta > 0) { 60 | adapter.notifyItemRangeInserted(itemCount - delta, delta) 61 | } else if (delta < 0) { 62 | adapter.notifyItemRangeRemoved(itemCount, -delta) 63 | } 64 | list?.invalidateItemDecorations() 65 | }) 66 | } 67 | 68 | private fun setupList() { 69 | adapter.itemClickListener = { vh -> 70 | val itemView = vh.itemView 71 | list?.layoutManager?.getDecorationRect(itemView)?.let { 72 | val msg = getString(R.string.preview_message_item_offsets, it.toString()) 73 | Toast.makeText(itemView.context, msg, Toast.LENGTH_SHORT).show() 74 | } 75 | } 76 | adapter.setHasStableIds(true) 77 | 78 | val layoutManager = LinearLayoutManager(list.context) 79 | list.setHasFixedSize(true) 80 | list.adapter = this.adapter 81 | list.layoutManager = layoutManager 82 | list.addItemDecoration(spacingItemDecoration) 83 | 84 | itemSizeProvider = LayoutManagerDependentItemSizeProvider(layoutManager) 85 | adapter.itemSizeProvider = itemSizeProvider 86 | adapter.itemViewAdjuster = itemViewAdjuster 87 | } 88 | 89 | private val ListLayoutConfigObserver = NullIgnoreObserver { 90 | val list = list ?: return@NullIgnoreObserver 91 | 92 | viewModel.listAdjuster.adjustListToConfig(list, it) 93 | 94 | itemSizeProvider.layoutManager = list.layoutManager!! 95 | itemViewAdjuster.listLayoutConfig = it 96 | 97 | // need to refresh item views sizing, which is done in onBindViewHolder() 98 | adapter.notifyDataSetChanged() 99 | list.invalidateItemDecorations() 100 | } 101 | } 102 | 103 | 104 | private fun RecyclerView.LayoutManager.getDecorationRect(itemView: View) = Rect( 105 | getLeftDecorationWidth(itemView), 106 | getTopDecorationHeight(itemView), 107 | getRightDecorationWidth(itemView), 108 | getBottomDecorationHeight(itemView) 109 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/listpreview/ListPreviewItemAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.listpreview 2 | 3 | import android.graphics.Point 4 | import androidx.recyclerview.widget.RecyclerView 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.TextView 9 | import com.grzegorzojdana.spacingitemdecorationapp.R 10 | 11 | /** 12 | * Simple adapter that will provide sequence of positive numbers as items of list. 13 | */ 14 | class ListPreviewItemAdapter( 15 | var numberCount: Int, 16 | var itemSizeProvider: ItemSizeProvider? = null, 17 | var itemViewAdjuster: ItemViewAdjuster? = null 18 | ): RecyclerView.Adapter() { 19 | 20 | private val itemSize: Point = Point() 21 | 22 | var itemClickListener: ListItemClickListener? = null 23 | 24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 25 | val itemView = LayoutInflater 26 | .from(parent.context) 27 | .inflate(R.layout.layout_preview_list_item, parent, false) 28 | 29 | val viewHolder = ViewHolder(itemView) 30 | itemView.setOnClickListener { itemClickListener?.invoke(viewHolder) } 31 | return viewHolder 32 | } 33 | 34 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 35 | val number = getNumberForPosition(position) 36 | holder.number = number 37 | 38 | itemSizeProvider?.let { 39 | it.provideItemSize(itemSize, holder.itemView, position) 40 | holder.itemView.layoutParams.width = itemSize.x 41 | holder.itemView.layoutParams.height = itemSize.y 42 | } 43 | 44 | itemViewAdjuster?.adjustListItemViewOnBind(holder.itemView, position) 45 | } 46 | 47 | override fun getItemCount(): Int = numberCount 48 | 49 | override fun getItemId(position: Int): Long = position.toLong() 50 | 51 | fun getNumberForPosition(position: Int): Int { 52 | // counting numbers from 1 to numberCount, inclusive 53 | return position + 1 54 | } 55 | 56 | 57 | class ViewHolder(view: View): RecyclerView.ViewHolder(view) { 58 | var number: Int = 0 59 | set(value) { field = value; updateModelViews() } 60 | 61 | fun updateModelViews() { 62 | itemView.findViewById(R.id.text)?.let { 63 | it.text = number.toString() 64 | } 65 | } 66 | } 67 | 68 | } 69 | 70 | typealias ListItemClickListener = (ListPreviewItemAdapter.ViewHolder) -> Unit -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/listpreview/ListPreviewViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.listpreview 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import com.grzegorzojdana.spacingitemdecoration.Spacing 6 | import com.grzegorzojdana.spacingitemdecorationapp.model.DecorationConfig 7 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListDataRepository 8 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListLayoutConfig 9 | 10 | /** 11 | * To pass model into list preview view. 12 | */ 13 | class ListPreviewViewModel: ViewModel() { 14 | 15 | private val listDataRepository = ListDataRepository 16 | 17 | // read-only properties since we not modify them, only need to setup preview list 18 | val listLayoutConfig: LiveData get() = listDataRepository.listLayoutConfig 19 | val decorationConfig: LiveData get() = listDataRepository.decorationConfig 20 | val spacing: LiveData get() = listDataRepository.spacing 21 | val itemCount: LiveData get() = listDataRepository.itemCount 22 | 23 | val listAdjuster: ListAdjuster = ListAdjuster() 24 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/listpreview/RandomSpanSizeLookup.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.listpreview 2 | 3 | import androidx.recyclerview.widget.GridLayoutManager 4 | import java.util.* 5 | 6 | /** 7 | * [SpanSizeLookup] implementation that provides pseudorandomly selected span size for each item. 8 | */ 9 | class RandomSpanSizeLookup( 10 | var spanCount: Int, 11 | val seed: Long = System.currentTimeMillis(), 12 | var preferSmallerSpans: Boolean = false 13 | ): GridLayoutManager.SpanSizeLookup() { 14 | 15 | private val random = Random() 16 | 17 | override fun getSpanSize(position: Int): Int { 18 | if (spanCount < 2) return 1 19 | random.setSeed(seed + position) 20 | if (preferSmallerSpans && random.nextBoolean()) { 21 | return 1 + random.nextInt(spanCount / 2) 22 | } 23 | return 1 + random.nextInt(spanCount) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.main 2 | 3 | import android.graphics.Rect 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.grzegorzojdana.spacingitemdecoration.Spacing 7 | import com.grzegorzojdana.spacingitemdecorationapp.R 8 | import com.grzegorzojdana.spacingitemdecorationapp.extensions.dpToPxInt 9 | import com.grzegorzojdana.spacingitemdecorationapp.model.DecorationConfig 10 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListDataProvider 11 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListDataRepository 12 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListLayoutConfig 13 | 14 | class MainActivity : AppCompatActivity() { 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | setContentView(R.layout.activity_main_bottom_sheet) 19 | 20 | setupDefaultConfigValues() 21 | 22 | if (savedInstanceState == null) { 23 | ListDataRepository.resetToDefaults() 24 | } 25 | } 26 | 27 | private fun setupDefaultConfigValues() { 28 | ListDataRepository.defaultListDataProvider = object : ListDataProvider { 29 | override val listLayoutConfig get() = ListLayoutConfig( 30 | layoutType = ListLayoutConfig.LAYOUT_TYPE_GRID, 31 | spanCount = 3) 32 | 33 | override val decorationConfig: DecorationConfig get() = DecorationConfig( 34 | enableDrawSpacing = false) 35 | 36 | override val spacing get() = Spacing( 37 | horizontal = resources.dpToPxInt(16), 38 | vertical = resources.dpToPxInt(8), 39 | edges = Rect( 40 | resources.dpToPxInt(16), 41 | resources.dpToPxInt(8), 42 | resources.dpToPxInt(16), 43 | resources.dpToPxInt(8))) 44 | 45 | override val itemCount get() = 11 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/spacingconfig/SpacingConfigFragment.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.spacingconfig 2 | 3 | import androidx.lifecycle.ViewModelProviders 4 | import android.os.Bundle 5 | import androidx.fragment.app.Fragment 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.NumberPicker 10 | import com.grzegorzojdana.spacingitemdecoration.Spacing 11 | import com.grzegorzojdana.spacingitemdecorationapp.R 12 | import com.grzegorzojdana.spacingitemdecorationapp.extensions.dpToPx 13 | import com.grzegorzojdana.spacingitemdecorationapp.extensions.pxToDp 14 | import com.grzegorzojdana.spacingitemdecorationapp.util.NullIgnoreObserver 15 | import kotlinx.android.synthetic.main.fragment_spacing_config.* 16 | 17 | /** 18 | * View where user can configure spacing. 19 | */ 20 | class SpacingConfigFragment: Fragment(), NumberPicker.OnValueChangeListener { 21 | 22 | private val SPACING_PICKER_STEP = 4F 23 | 24 | private lateinit var viewModel: SpacingConfigViewModel 25 | 26 | override fun onCreateView(inflater: LayoutInflater, 27 | container: ViewGroup?, 28 | savedInstanceState: Bundle? 29 | ): View? { 30 | return inflater.inflate(R.layout.fragment_spacing_config, container, false) 31 | } 32 | 33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 34 | super.onViewCreated(view, savedInstanceState) 35 | setupModelViews(view) 36 | viewModel = ViewModelProviders.of(this).get(SpacingConfigViewModel::class.java) 37 | viewModel.spacing.observe(this, NullIgnoreObserver { updateModelViews(it) }) 38 | } 39 | 40 | private fun setupModelViews(mainView: View) { 41 | btnSetZero.setOnClickListener { viewModel.setZeroSpacing() } 42 | btnSetDefault.setOnClickListener { viewModel.setDefaultSpacing() } 43 | 44 | val pickerMinValue = 0 45 | val pickerMaxValue = 16 46 | val displayValues = (pickerMinValue..pickerMaxValue) 47 | .map { SpacingPickerLabelFormatter.format(it) } 48 | .toTypedArray() 49 | 50 | (mainView as? ViewGroup)?.let { 51 | for (i in 0 until mainView.childCount) { 52 | (mainView.getChildAt(i) as? NumberPicker)?.apply { 53 | minValue = pickerMinValue 54 | maxValue = pickerMaxValue 55 | // using display values instead of setting Formatter to avoid problem with label rendering 56 | // setFormatter(PickerLabelFormatter) 57 | displayedValues = displayValues 58 | setOnValueChangedListener(this@SpacingConfigFragment) 59 | descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS 60 | } 61 | } 62 | } 63 | } 64 | 65 | private fun updateModelViews(spacing: Spacing) { 66 | fun spacingToPickerValue(spacingPx: Int): Int 67 | = resources.pxToDp(spacingPx.toFloat() / SPACING_PICKER_STEP).toInt() 68 | 69 | pickerEdgeLeft.value = spacingToPickerValue(spacing.edges.left) 70 | pickerEdgeTop.value = spacingToPickerValue(spacing.edges.top) 71 | pickerEdgeRight.value = spacingToPickerValue(spacing.edges.right) 72 | pickerEdgeBottom.value = spacingToPickerValue(spacing.edges.bottom) 73 | 74 | pickerItemLeft.value = spacingToPickerValue(spacing.item.left) 75 | pickerItemTop.value = spacingToPickerValue(spacing.item.top) 76 | pickerItemRight.value = spacingToPickerValue(spacing.item.right) 77 | pickerItemBottom.value = spacingToPickerValue(spacing.item.bottom) 78 | 79 | pickerHorizontal.value = spacingToPickerValue(spacing.horizontal) 80 | pickerVertical.value = spacingToPickerValue(spacing.vertical) 81 | } 82 | 83 | override fun onValueChange(picker: NumberPicker, oldVal: Int, newVal: Int) { 84 | val currentSpacing = viewModel.spacing.value ?: return 85 | val spacingNewValue = resources.dpToPx(newVal * SPACING_PICKER_STEP).toInt() 86 | viewModel.spacing.value = currentSpacing.apply { 87 | when (picker.id) { 88 | R.id.pickerEdgeLeft -> edges.left = spacingNewValue 89 | R.id.pickerEdgeTop -> edges.top = spacingNewValue 90 | R.id.pickerEdgeRight -> edges.right = spacingNewValue 91 | R.id.pickerEdgeBottom -> edges.bottom = spacingNewValue 92 | 93 | R.id.pickerItemLeft -> item.left = spacingNewValue 94 | R.id.pickerItemTop -> item.top = spacingNewValue 95 | R.id.pickerItemRight -> item.right = spacingNewValue 96 | R.id.pickerItemBottom -> item.bottom = spacingNewValue 97 | 98 | R.id.pickerHorizontal -> horizontal = spacingNewValue 99 | R.id.pickerVertical -> vertical = spacingNewValue 100 | } 101 | } 102 | } 103 | 104 | private val SpacingPickerLabelFormatter = NumberPicker.Formatter { value -> 105 | val valueAsDp = (value * SPACING_PICKER_STEP).toInt() 106 | getString(R.string.spacing_config_picker_formatter_dp, valueAsDp) 107 | } 108 | 109 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/spacingconfig/SpacingConfigViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.action.spacingconfig 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import com.grzegorzojdana.spacingitemdecoration.Spacing 6 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListDataRepository 7 | 8 | class SpacingConfigViewModel: ViewModel() { 9 | 10 | private val listDataRepository: ListDataRepository = ListDataRepository 11 | 12 | val spacing: MutableLiveData get() = listDataRepository.spacing 13 | 14 | fun setZeroSpacing() { 15 | spacing.value = Spacing() 16 | } 17 | 18 | fun setDefaultSpacing() { 19 | spacing.value = listDataRepository.defaultListDataProvider.spacing 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/extensions/ResourcesExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.extensions 2 | 3 | import android.content.res.Resources 4 | 5 | 6 | fun Resources.dpToPx(dp: Float): Float { 7 | return dp * displayMetrics.density 8 | } 9 | fun Resources.dpToPxInt(dp: Int): Int { 10 | return (dp * displayMetrics.density).toInt() 11 | } 12 | 13 | fun Resources.pxToDp(px: Float): Float { 14 | return px / displayMetrics.density 15 | } 16 | 17 | fun Resources.dimenPx(dimenResId: Int): Int = getDimensionPixelSize(dimenResId) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/model/DecorationConfig.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.model 2 | 3 | /** 4 | * Configuration od [SpacingItemDecoration]. 5 | */ 6 | data class DecorationConfig( 7 | val enableDrawSpacing: Boolean = false 8 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/model/ListDataProvider.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.model 2 | 3 | import com.grzegorzojdana.spacingitemdecoration.Spacing 4 | 5 | /** 6 | * To provide list data. 7 | */ 8 | interface ListDataProvider { 9 | val listLayoutConfig: ListLayoutConfig 10 | val decorationConfig: DecorationConfig 11 | val spacing: Spacing 12 | val itemCount: Int 13 | } 14 | 15 | /** 16 | * Provides objects created by its default constructors, and `0` item count. 17 | */ 18 | class EmptyListDataProvider : ListDataProvider { 19 | override val listLayoutConfig: ListLayoutConfig get() = ListLayoutConfig() 20 | override val decorationConfig: DecorationConfig get() = DecorationConfig() 21 | override val spacing: Spacing get() = Spacing() 22 | override val itemCount: Int get() = 0 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/model/ListDataRepository.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.model 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import com.grzegorzojdana.spacingitemdecoration.Spacing 5 | 6 | /** 7 | * Holds configuration of list and spacing. 8 | */ 9 | object ListDataRepository { 10 | 11 | val emptyListDataProvider = EmptyListDataProvider() 12 | 13 | var defaultListDataProvider: ListDataProvider = emptyListDataProvider 14 | 15 | val listLayoutConfig = MutableLiveData() 16 | val decorationConfig = MutableLiveData() 17 | val spacing = MutableLiveData() 18 | val itemCount = MutableLiveData() 19 | 20 | 21 | init { 22 | resetToDefaults() 23 | } 24 | 25 | fun resetToDefaults() = setWithProvider(defaultListDataProvider) 26 | 27 | private fun setWithProvider(listDataProvider: ListDataProvider) { 28 | listDataProvider.let { 29 | listLayoutConfig.value = it.listLayoutConfig 30 | decorationConfig.value = it.decorationConfig 31 | spacing.value = it.spacing 32 | itemCount.value = it.itemCount 33 | } 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/model/ListLayoutConfig.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.model 2 | 3 | import androidx.recyclerview.widget.OrientationHelper 4 | 5 | /** 6 | * Immutable model of RecyclerView LayoutManager configuration. 7 | */ 8 | data class ListLayoutConfig( 9 | val layoutType: Int = LAYOUT_TYPE_LINEAR, 10 | val spanCount: Int = 3, 11 | val orientation: Int = OrientationHelper.VERTICAL, 12 | val reversed: Boolean = false, 13 | val rtl: Boolean = false, 14 | /** Allow to item have span size greater than 1. */ 15 | val allowItemSpan: Boolean = false 16 | ) { 17 | companion object { 18 | const val LAYOUT_TYPE_LINEAR = 0 19 | const val LAYOUT_TYPE_GRID = 1 20 | const val LAYOUT_TYPE_STAGGERED_GRID = 2 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/grzegorzojdana/spacingitemdecorationapp/util/NullIgnoreObserver.kt: -------------------------------------------------------------------------------- 1 | package com.grzegorzojdana.spacingitemdecorationapp.util 2 | 3 | import androidx.lifecycle.Observer 4 | 5 | /** 6 | * [Observer] implementation that will ignore changes value to `null`. 7 | */ 8 | class NullIgnoreObserver( 9 | private val onChangedToNotNull: (value: T) -> Unit 10 | ) : Observer { 11 | override fun onChanged(value: T?) { 12 | if (value != null) onChangedToNotNull(value) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_settings_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /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-w520dp/fragment_spacing_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 30 | 31 | 40 | 41 | 50 | 51 | 59 | 60 | 61 | 72 | 73 | 84 | 85 | 93 | 94 | 103 | 104 | 112 | 113 | 114 | 122 | 123 | 132 | 133 | 142 | 143 | 152 | 153 | 154 |