├── .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 | [](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 |
163 |
164 |
173 |
174 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main_bottom_sheet.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
26 |
27 |
32 |
33 |
39 |
40 |
50 |
51 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_list_control.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
26 |
27 |
37 |
38 |
44 |
45 |
51 |
52 |
53 |
54 |
55 |
56 |
65 |
66 |
75 |
76 |
85 |
86 |
87 |
88 |
97 |
98 |
107 |
108 |
117 |
118 |
126 |
127 |
136 |
137 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_list_preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_spacing_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
21 |
22 |
31 |
32 |
41 |
42 |
51 |
52 |
60 |
61 |
62 |
73 |
74 |
85 |
86 |
95 |
96 |
103 |
104 |
112 |
113 |
114 |
126 |
127 |
136 |
137 |
149 |
150 |
159 |
160 |
161 |
172 |
173 |
182 |
183 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_preview_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/main_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-h480dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 240dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-h640dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 320dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-sw600dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 320dp
5 |
6 | 96dp
7 | 120dp
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | - @string/layout_manager_type_linear
7 | - @string/layout_manager_type_grid
8 | - @string/layout_manager_type_staggeredgrid
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | #009688
16 | #52c7b8
17 | #00675b
18 | #ff6e40
19 | #ffa06d
20 | #c53d13
21 | #000000
22 | #000000
23 |
24 | @color/md_indigo_400
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors_md.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | #ffecb3
6 | #ffe082
7 | #ffd54f
8 | #ffca28
9 | #fff8e1
10 | #ffc107
11 | #ffb300
12 | #ffa000
13 | #ff8f00
14 | #ff6f00
15 | #ffe57f
16 | #ffd740
17 | #ffc400
18 | #ffab00
19 | #000000
20 | #d0d9ff
21 | #afbfff
22 | #91a7ff
23 | #738ffe
24 | #e7e9fd
25 | #5677fc
26 | #4e6cef
27 | #455ede
28 | #3b50ce
29 | #2a36b1
30 | #a6baff
31 | #6889ff
32 | #4d73ff
33 | #4d69ff
34 | #cfd8dc
35 | #b0bec5
36 | #90a4ae
37 | #78909c
38 | #eceff1
39 | #607d8b
40 | #546e7a
41 | #455a64
42 | #37474f
43 | #263238
44 | #d7ccc8
45 | #bcaaa4
46 | #a1887f
47 | #8d6e63
48 | #efebe9
49 | #795548
50 | #6d4c41
51 | #5d4037
52 | #4e342e
53 | #3e2723
54 | #b2ebf2
55 | #80deea
56 | #4dd0e1
57 | #26c6da
58 | #e0f7fa
59 | #00bcd4
60 | #00acc1
61 | #0097a7
62 | #00838f
63 | #006064
64 | #84ffff
65 | #18ffff
66 | #00e5ff
67 | #00b8d4
68 | #ffccbc
69 | #ffab91
70 | #ff8a65
71 | #ff7043
72 | #fbe9e7
73 | #ff5722
74 | #f4511e
75 | #e64a19
76 | #d84315
77 | #bf360c
78 | #ff9e80
79 | #ff6e40
80 | #ff3d00
81 | #dd2c00
82 | #d1c4e9
83 | #b39ddb
84 | #9575cd
85 | #7e57c2
86 | #ede7f6
87 | #673ab7
88 | #5e35b1
89 | #512da8
90 | #4527a0
91 | #311b92
92 | #b388ff
93 | #7c4dff
94 | #651fff
95 | #6200ea
96 | #C8E6C9
97 | #A5D6A7
98 | #81C784
99 | #66BB6A
100 | #E8F5E9
101 | #4CAF50
102 | #43A047
103 | #388E3C
104 | #2E7D32
105 | #1B5E20
106 | #B9F6CA
107 | #69F0AE
108 | #00E676
109 | #00C853
110 | #f5f5f5
111 | #eeeeee
112 | #e0e0e0
113 | #bdbdbd
114 | #fafafa
115 | #9e9e9e
116 | #757575
117 | #616161
118 | #424242
119 | #212121
120 | #c5cae9
121 | #9fa8da
122 | #7986cb
123 | #5c6bc0
124 | #e8eaf6
125 | #3f51b5
126 | #3949ab
127 | #303f9f
128 | #283593
129 | #1a237e
130 | #8c9eff
131 | #536dfe
132 | #3d5afe
133 | #304ffe
134 | #b3e5fc
135 | #81d4fa
136 | #4fc3f7
137 | #29b6f6
138 | #e1f5fe
139 | #03a9f4
140 | #039be5
141 | #0288d1
142 | #0277bd
143 | #01579b
144 | #80d8ff
145 | #40c4ff
146 | #00b0ff
147 | #0091ea
148 | #dcedc8
149 | #c5e1a5
150 | #aed581
151 | #9ccc65
152 | #f1f8e9
153 | #8bc34a
154 | #7cb342
155 | #689f38
156 | #558b2f
157 | #33691e
158 | #ccff90
159 | #b2ff59
160 | #76ff03
161 | #64dd17
162 | #f0f4c3
163 | #e6ee9c
164 | #dce775
165 | #d4e157
166 | #f9fbe7
167 | #cddc39
168 | #c0ca33
169 | #afb42b
170 | #9e9d24
171 | #827717
172 | #f4ff81
173 | #eeff41
174 | #c6ff00
175 | #aeea00
176 | #ffe0b2
177 | #ffcc80
178 | #ffb74d
179 | #ffa726
180 | #fff3e0
181 | #ff9800
182 | #fb8c00
183 | #f57c00
184 | #ef6c00
185 | #e65100
186 | #ffd180
187 | #ffab40
188 | #ff9100
189 | #ff6d00
190 | #f8bbd0
191 | #f48fb1
192 | #f06292
193 | #ec407a
194 | #fce4ec
195 | #e91e63
196 | #d81b60
197 | #c2185b
198 | #ad1457
199 | #880e4f
200 | #ff80ab
201 | #ff4081
202 | #f50057
203 | #c51162
204 | #e1bee7
205 | #ce93d8
206 | #ba68c8
207 | #ab47bc
208 | #f3e5f5
209 | #9c27b0
210 | #8e24aa
211 | #7b1fa2
212 | #6a1b9a
213 | #4a148c
214 | #ea80fc
215 | #e040fb
216 | #d500f9
217 | #aa00ff
218 | #f9bdbb
219 | #f69988
220 | #f36c60
221 | #e84e40
222 | #fde0dc
223 | #e51c23
224 | #dd191d
225 | #d01716
226 | #c41411
227 | #b0120a
228 | #ff7997
229 | #ff5177
230 | #ff2d6f
231 | #e00032
232 | #b2dfdb
233 | #80cbc4
234 | #4db6ac
235 | #26a69a
236 | #e0f2f1
237 | #009688
238 | #00897b
239 | #00796b
240 | #00695c
241 | #004d40
242 | #a7ffeb
243 | #64ffda
244 | #1de9b6
245 | #00bfa5
246 | #ffffff
247 | #fff9c4
248 | #fff59d
249 | #fff176
250 | #ffee58
251 | #fffde7
252 | #ffeb3b
253 | #fdd835
254 | #fbc02d
255 | #f9a825
256 | #f57f17
257 | #ffff8d
258 | #ffff00
259 | #ffea00
260 | #ffd600
261 |
262 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 280dp
5 |
6 | 160dp
7 |
8 | 48dp
9 | 112dp
10 |
11 | 64dp
12 | 80dp
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | SpacingItemDecoration
4 |
5 | Layout manager
6 | Horizontal
7 | Vertical
8 | Reversed
9 | Item count
10 | Span count
11 | Allow items spanning
12 | Draw spacing
13 |
14 | Linear
15 | Grid
16 | Grid + spans
17 | Staggered Grid
18 |
19 | Spacing configuration
20 | Show configuration
21 |
22 | %d dp
23 | Edges
24 | Item
25 | Between items
26 | Horizontal
27 | Vertical
28 | Left
29 | Top
30 | Right
31 | Bottom
32 | Reset spacing
33 | No spacing
34 | Default
35 |
36 | Item offsets: %s
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/grzegorzojdana/spacingitemdecorationapp/action/spacingconfig/SpacingConfigViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecorationapp.action.spacingconfig
2 |
3 | import android.graphics.Rect
4 | import com.grzegorzojdana.spacingitemdecoration.Spacing
5 | import com.grzegorzojdana.spacingitemdecorationapp.model.DecorationConfig
6 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListDataProvider
7 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListDataRepository
8 | import com.grzegorzojdana.spacingitemdecorationapp.model.ListLayoutConfig
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 | import org.robolectric.RobolectricTestRunner
12 | import kotlin.test.assertEquals
13 | import kotlin.test.assertNotEquals
14 |
15 | @RunWith(RobolectricTestRunner::class)
16 | class SpacingConfigViewModelTest {
17 |
18 | @Test
19 | fun setDefaultSpacing() {
20 | val defaultSpacing = Spacing(
21 | horizontal = 12,
22 | vertical = 8,
23 | edges = Rect(0, 6, 0, 12))
24 | val defaultConfigProvider = object : ListDataProvider {
25 | override val listLayoutConfig: ListLayoutConfig get() = ListLayoutConfig()
26 | override val decorationConfig: DecorationConfig get() = DecorationConfig()
27 | override val spacing: Spacing get() = defaultSpacing.copy()
28 | override val itemCount: Int = 8
29 | }
30 |
31 | val listDataRepository = ListDataRepository
32 | listDataRepository.defaultListDataProvider = defaultConfigProvider
33 |
34 | val viewModel = SpacingConfigViewModel()
35 |
36 | viewModel.spacing.value?.apply {
37 | horizontal = 20
38 | vertical = 4
39 | item = Rect(12, 4, 6, 4)
40 | }
41 |
42 | assertNotEquals(defaultSpacing, viewModel.spacing.value)
43 |
44 | viewModel.setDefaultSpacing()
45 | assertEquals(defaultSpacing, viewModel.spacing.value)
46 | assertEquals(12, viewModel.spacing.value?.horizontal)
47 | }
48 |
49 | }
--------------------------------------------------------------------------------
/art/grid-draw-with-legend.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/art/grid-draw-with-legend.png
--------------------------------------------------------------------------------
/art/grid-draw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/art/grid-draw.png
--------------------------------------------------------------------------------
/art/grid-spanning-draw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/art/grid-spanning-draw.png
--------------------------------------------------------------------------------
/art/grid-spanning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/art/grid-spanning.png
--------------------------------------------------------------------------------
/art/grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/art/grid.png
--------------------------------------------------------------------------------
/art/sample.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/art/sample.gif
--------------------------------------------------------------------------------
/art/staggered-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/art/staggered-small.png
--------------------------------------------------------------------------------
/art/staggered.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/art/staggered.png
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 |
4 |
5 | buildscript {
6 | ext {
7 | globalMinSdkVersion = 14
8 | globalTargetSdkVersion = 28
9 | globalCompileSdkVersion = 28
10 | globalKotlinVersion = '1.3.0'
11 | globalExtensionLibraryVersion = '1.0.0'
12 | globalRobolectricVersion = '4.0-alpha-4-SNAPSHOT' // using snapshot version until 4.0 will came up to support android api 28
13 | globalLifecycleVersion = '2.0.0'
14 | globalTestRunnerVersion = '1.1.0-alpha4'
15 | globalEspressoVersion = '3.1.0-alpha4'
16 | }
17 |
18 | repositories {
19 | google()
20 | jcenter()
21 | }
22 | dependencies {
23 | classpath 'com.android.tools.build:gradle:3.2.1'
24 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$globalKotlinVersion"
25 |
26 | // NOTE: Do not place your application dependencies here; they belong
27 | // in the individual module build.gradle files
28 | }
29 | }
30 |
31 | allprojects {
32 | repositories {
33 | google()
34 | jcenter()
35 | maven { url "https://oss.sonatype.org/content/repositories/snapshots" } // robolectric snapshot
36 | }
37 | }
38 |
39 | task clean(type: Delete) {
40 | delete rootProject.buildDir
41 | }
42 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
19 | android.useAndroidX=true
20 | android.enableJetifier=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grzegorzojdana/SpacingItemDecoration/04b214cde5e8331413c4aaf1b95f16978a2c3004/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Sep 28 19:18:31 EEST 2018
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-4.6-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
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 Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':spacingitemdecorationlib'
2 |
--------------------------------------------------------------------------------
/spacingitemdecorationlib/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/spacingitemdecorationlib/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion globalCompileSdkVersion
6 |
7 | defaultConfig {
8 | minSdkVersion globalMinSdkVersion
9 | targetSdkVersion globalTargetSdkVersion
10 | versionCode 3
11 | versionName "1.1.0"
12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
13 | consumerProguardFiles 'proguard-rules.pro'
14 | }
15 |
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | }
20 | }
21 |
22 | sourceSets {
23 | main.java.srcDirs += 'src/main/kotlin'
24 | test.java.srcDirs += 'src/test/kotlin'
25 | androidTest.java.srcDirs += 'src/androidTest/kotlin'
26 | }
27 |
28 | testOptions {
29 | unitTests {
30 | includeAndroidResources = true
31 | }
32 | }
33 | }
34 |
35 | repositories {
36 | jcenter()
37 | }
38 |
39 |
40 | dependencies {
41 | implementation fileTree(dir: 'libs', include: ['*.jar'])
42 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$globalKotlinVersion"
43 | implementation "androidx.appcompat:appcompat:$globalExtensionLibraryVersion"
44 | implementation "androidx.recyclerview:recyclerview:$globalExtensionLibraryVersion"
45 |
46 | testImplementation 'junit:junit:4.12'
47 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$globalKotlinVersion"
48 | testImplementation "org.robolectric:robolectric:$globalRobolectricVersion"
49 |
50 | androidTestImplementation "androidx.test:runner:$globalTestRunnerVersion"
51 | androidTestImplementation "androidx.test.espresso:espresso-core:$globalEspressoVersion"
52 | androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$globalKotlinVersion"
53 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -keep class com.grzegorzojdana.spacingitemdecoration.** { *; }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/main/kotlin/com/grzegorzojdana/spacingitemdecoration/ItemOffsetsCalculator.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import android.graphics.Rect
4 |
5 | /**
6 | * Calculates RecyclerView item offsets depending on data prepared by [ItemOffsetsRequestBuilder].
7 | *
8 | * This class expects item position translated from RecyclerView LayoutManager coordinates to
9 | * grid table coordinates with first row on top and first column on left. Translation is done
10 | * by [ItemOffsetsRequestBuilder].
11 | */
12 | open class ItemOffsetsCalculator(
13 | spacing: Spacing
14 | ) {
15 |
16 | /**
17 | * Input data for [ItemOffsetsCalculator].
18 | */
19 | data class OffsetsRequest(
20 | // indices of item in the table
21 | var row: Int = 0,
22 | var col: Int = 0,
23 | // size of item
24 | var spanSizeH: Int = 1,
25 | var spanSizeV: Int = 1,
26 | // total number of rows and cols in the table
27 | var rows: Int = 0,
28 | var cols: Int = 0
29 | ) {
30 | val lastRow: Int get() = row + spanSizeV - 1
31 | val lastCol: Int get() = col + spanSizeH - 1
32 | }
33 |
34 | open var spacing: Spacing = spacing
35 | set(value) { field = value; invalidatePrecalculatedValues() }
36 |
37 | private var areCachedValuesInvalid = true
38 | // Data used to determine precalculated values; stored to compare inside getItemOffsets()
39 | // and determine if cached values are valid or not.
40 | private var cachedRows = 0
41 | private var cachedCols = 0
42 |
43 | // Precalculated (cached) values. Need to be calculated again when spacing, item count or
44 | // layout manager params change.
45 |
46 | private var startMargin: Int = 0
47 | private var topMargin: Int = 0
48 | private var itemDistanceH: Int = 0
49 | private var itemDistanceV: Int = 0
50 | private var itemDeltaH: Float = 0F
51 | private var itemDeltaV: Float = 0F
52 |
53 |
54 | /**
55 | * Calculate offsets for item specified by [OffsetsRequest] instance.
56 | * [offsetsRequest] `rows` and `cols` can't be `0`.
57 | */
58 | open fun getItemOffsets(outRect: Rect, offsetsRequest: OffsetsRequest) {
59 | updatePrecalculatedValuesValidityFor(offsetsRequest)
60 | if (areCachedValuesInvalid) {
61 | validatePrecalculatedValues(offsetsRequest)
62 | }
63 |
64 | outRect.apply {
65 | left = Math.round(startMargin + offsetsRequest.col * itemDeltaH)
66 | top = Math.round(topMargin + offsetsRequest.row * itemDeltaV)
67 | right = Math.round(itemDistanceH - left - offsetsRequest.spanSizeH * itemDeltaH)
68 | bottom = Math.round(itemDistanceV - top - offsetsRequest.spanSizeV * itemDeltaV)
69 | }
70 | }
71 |
72 | open fun invalidatePrecalculatedValues() {
73 | areCachedValuesInvalid = true
74 | }
75 |
76 | private fun updatePrecalculatedValuesValidityFor(offsetsRequest: OffsetsRequest) {
77 | if (offsetsRequest.rows != cachedRows || offsetsRequest.cols != cachedCols) {
78 | areCachedValuesInvalid = true
79 | }
80 | }
81 |
82 | private fun validatePrecalculatedValues(offsetsRequest: OffsetsRequest) {
83 | with (spacing) {
84 | startMargin = edges.left + item.left
85 | itemDistanceH = horizontal + item.left + item.right
86 | itemDeltaH = (horizontal - edges.left - edges.right) / offsetsRequest.cols.toFloat()
87 |
88 | topMargin = edges.top + item.top
89 | itemDistanceV = vertical + item.top + item.bottom
90 | itemDeltaV = (vertical - edges.top - edges.bottom) / offsetsRequest.rows.toFloat()
91 | }
92 |
93 | areCachedValuesInvalid = false
94 |
95 | cachedRows = offsetsRequest.rows
96 | cachedCols = offsetsRequest.cols
97 | }
98 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/main/kotlin/com/grzegorzojdana/spacingitemdecoration/ItemOffsetsRequestBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | /**
4 | * Determine params for [ItemOffsetsCalculator] basing on given RecyclerView layout manager specifics.
5 | */
6 | open class ItemOffsetsRequestBuilder {
7 |
8 | /**
9 | * Data that describes position of item that offsets are desired, and some of its list layout
10 | * manager params.
11 | */
12 | data class ItemOffsetsParams(
13 | /* Item data */
14 | var spanIndex: Int = 0,
15 | var groupIndex: Int = 0,
16 | var spanSize: Int = 1,
17 | /* Layout manager data */
18 | var spanCount: Int = 1,
19 | var groupCount: Int = 1,
20 | var isLayoutVertical: Boolean = true,
21 | var isLayoutReverse: Boolean = false
22 | )
23 |
24 | /**
25 | * Fill [offsetsRequest] basing on RecyclerView item and layout params passed by
26 | * [itemOffsetsParams]. All fields of [offsetsRequest] will be set.
27 | */
28 | open fun fillItemOffsetsRequest(itemOffsetsParams: ItemOffsetsParams,
29 | offsetsRequest: ItemOffsetsCalculator.OffsetsRequest) {
30 | val groupIndexAdjustedToReverse =
31 | if (!itemOffsetsParams.isLayoutReverse) itemOffsetsParams.groupIndex
32 | else itemOffsetsParams.groupCount - itemOffsetsParams.groupIndex - 1
33 |
34 | with (offsetsRequest) {
35 | if (itemOffsetsParams.isLayoutVertical) {
36 | row = groupIndexAdjustedToReverse
37 | col = itemOffsetsParams.spanIndex
38 |
39 | spanSizeH = itemOffsetsParams.spanSize
40 | spanSizeV = 1
41 |
42 | rows = itemOffsetsParams.groupCount
43 | cols = itemOffsetsParams.spanCount
44 | } else {
45 | row = itemOffsetsParams.spanIndex
46 | col = groupIndexAdjustedToReverse
47 |
48 | spanSizeH = 1
49 | spanSizeV = itemOffsetsParams.spanSize
50 |
51 | rows = itemOffsetsParams.spanCount
52 | cols = itemOffsetsParams.groupCount
53 | }
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/main/kotlin/com/grzegorzojdana/spacingitemdecoration/Spacing.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import android.graphics.Rect
4 |
5 | /**
6 | * Set of offsets available to define for [SpacingItemDecoration].
7 | */
8 | data class Spacing(
9 |
10 | /**
11 | * Horizontal offset between every two items, in pixels.
12 | */
13 | var horizontal: Int = 0,
14 |
15 | /**
16 | * Vertical offset between every two items, in pixels.
17 | */
18 | var vertical: Int = 0,
19 |
20 | /**
21 | * Set of offsets that can be used to move items from parent edges toward parent's center.
22 | * Think about it like a list view padding.
23 | * Values in pixels are expected.
24 | */
25 | var edges: Rect = Rect(),
26 |
27 | /**
28 | * Offsets added to each of item edges, in pixels.
29 | */
30 | var item: Rect = Rect()
31 | ) {
32 |
33 | /**
34 | * Set all spacing to `0`.
35 | */
36 | fun zero() {
37 | horizontal = 0
38 | vertical = 0
39 | edges.setEmpty()
40 | item.setEmpty()
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/main/kotlin/com/grzegorzojdana/spacingitemdecoration/SpacingItemDecoration.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Color
5 | import android.graphics.Paint
6 | import android.graphics.Rect
7 | import android.view.View
8 | import androidx.recyclerview.widget.GridLayoutManager
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import androidx.recyclerview.widget.OrientationHelper
11 | import androidx.recyclerview.widget.RecyclerView
12 | import androidx.recyclerview.widget.RecyclerView.ItemDecoration
13 | import androidx.recyclerview.widget.StaggeredGridLayoutManager
14 | import com.grzegorzojdana.spacingitemdecoration.ItemOffsetsRequestBuilder.ItemOffsetsParams
15 |
16 | /**
17 | * [ItemDecoration] implementation that adds specified spacing to [RecyclerView]s items elements.
18 | * For flexibility, there are several offsets to define. Each of them can be used separately, or
19 | * together, depending on desired effect.
20 | */
21 | class SpacingItemDecoration(
22 | /**
23 | * Desired offsets of RecyclerView items. See [Spacing].
24 | */
25 | spacing: Spacing
26 | ): ItemDecoration() {
27 |
28 | var itemOffsetsCalculator = ItemOffsetsCalculator(spacing)
29 | var itemOffsetsRequestBuilder = ItemOffsetsRequestBuilder()
30 |
31 | private val itemOffsetsParams = ItemOffsetsParams()
32 | private val offsetsRequest = ItemOffsetsCalculator.OffsetsRequest()
33 |
34 | private var cachedGroupCount: Int = -1
35 |
36 | private val drawing: Drawing by lazy(LazyThreadSafetyMode.NONE) { Drawing() }
37 |
38 |
39 | /**
40 | * Desired offsets of RecyclerView items. See [Spacing].
41 | * Returned object can be modified, but [invalidateSpacing] need to be called to apply changes.
42 | */
43 | var spacing: Spacing
44 | get() = itemOffsetsCalculator.spacing
45 | set(value) { itemOffsetsCalculator.spacing = value }
46 |
47 | /**
48 | * If you use `GridLayoutManager` with non-default `SpanSizeLookup` but its
49 | * `getSpanSize(position)` method always returns `1`, you can set this property to `true`
50 | * to improve performance.
51 | */
52 | var hintSpanSizeAlwaysOne: Boolean = false
53 |
54 | /**
55 | * Enable to improve performance for GridLayoutManager with relatively big number of list items
56 | * (thousands). This will make once determined items group count held to use for items offsets
57 | * calculations. If items count or layout manager specification (orientation, span size lookup,
58 | * spans) are changed, group count will also change, so if you set [cachedGroupCount] to `true`,
59 | * remember to call [invalidate] in that case.
60 | */
61 | var isGroupCountCacheEnabled: Boolean = false
62 |
63 | /**
64 | * Set to enable drawing applied spacing by this decoration. Useful for debugging.
65 | * @see [drawingConfig]
66 | */
67 | var isSpacingDrawingEnabled: Boolean = false
68 |
69 | /**
70 | * Colors used to mark spacing if [isSpacingDrawingEnabled] is set to `true`.
71 | */
72 | var drawingConfig: DrawingConfig = DrawingConfig()
73 |
74 |
75 | override fun getItemOffsets(outRect: Rect,
76 | view: View,
77 | parent: RecyclerView,
78 | state: RecyclerView.State) {
79 | if (parent.getChildAdapterPosition(view) < 0) {
80 | outRect.setEmpty()
81 | return
82 | }
83 |
84 | determineItemOffsetsParams(view, parent, state, itemOffsetsParams)
85 | itemOffsetsRequestBuilder.fillItemOffsetsRequest(itemOffsetsParams, offsetsRequest)
86 | itemOffsetsCalculator.getItemOffsets(outRect, offsetsRequest)
87 | }
88 |
89 | private fun determineItemOffsetsParams(view: View,
90 | parent: RecyclerView,
91 | state: RecyclerView.State,
92 | itemOffsetsParams: ItemOffsetsParams) {
93 | val layoutManager = parent.layoutManager
94 | val itemPosition = parent.getChildAdapterPosition(view)
95 | val itemCount = state.itemCount
96 |
97 | when (layoutManager) {
98 | null -> throw IllegalArgumentException("RecyclerView without layout manager")
99 | is GridLayoutManager -> {
100 | val layoutParams = view.layoutParams as GridLayoutManager.LayoutParams
101 | val clampedSpanCount = Math.max(layoutManager.spanCount, 1)
102 | val spanGroupIndex = layoutManager.spanSizeLookup.getSpanGroupIndex(
103 | itemPosition, clampedSpanCount)
104 |
105 | itemOffsetsParams.apply {
106 | spanIndex = layoutParams.spanIndex
107 | groupIndex = spanGroupIndex
108 | spanSize = layoutParams.spanSize
109 | spanCount = clampedSpanCount
110 | groupCount = getGridGroupCount(itemCount, layoutManager)
111 | isLayoutVertical = (layoutManager.orientation == OrientationHelper.VERTICAL)
112 | isLayoutReverse = layoutManager.reverseLayout
113 | }
114 | }
115 | is StaggeredGridLayoutManager -> {
116 | val layoutParams = view.layoutParams as StaggeredGridLayoutManager.LayoutParams
117 |
118 | // could write some logic to determine and cache group index for each item
119 | // could access to Span object in item layout params through reflection
120 |
121 | itemOffsetsParams.apply {
122 | spanIndex = layoutParams.spanIndex
123 | groupIndex = 0
124 | spanSize = if (layoutParams.isFullSpan) layoutManager.spanCount else 1
125 | spanCount = layoutManager.spanCount
126 | groupCount = 1
127 | isLayoutVertical = (layoutManager.orientation == OrientationHelper.VERTICAL)
128 | isLayoutReverse = layoutManager.reverseLayout
129 | }
130 | }
131 | is LinearLayoutManager -> {
132 | itemOffsetsParams.apply {
133 | spanIndex = 0
134 | groupIndex = itemPosition
135 | spanSize = 1
136 | spanCount = 1
137 | groupCount = itemCount
138 | isLayoutVertical = (layoutManager.orientation == OrientationHelper.VERTICAL)
139 | isLayoutReverse = layoutManager.reverseLayout
140 | }
141 | }
142 | else -> throw IllegalArgumentException(
143 | "Unsupported layout manager: ${layoutManager::class.java.simpleName}")
144 | }
145 | }
146 |
147 | override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
148 | if (!isSpacingDrawingEnabled) return
149 | drawItemDependentSpacing(canvas, parent)
150 | drawEdges(canvas, parent, state)
151 | }
152 |
153 | private fun drawItemDependentSpacing(canvas: Canvas, parent: RecyclerView) {
154 | with (drawing) {
155 | parent.getChildrenSequence().forEach { itemView ->
156 | // drawing horizontal and vertical spacing in one loop with item spacing
157 | // for better performance
158 |
159 | // item spacing
160 | // drawing as one rect under the item
161 |
162 | drawingRect.set(
163 | itemView.left - spacing.item.left,
164 | itemView.top - spacing.item.top,
165 | itemView.right + spacing.item.right,
166 | itemView.bottom + spacing.item.bottom)
167 | drawRect(canvas, drawingConfig.itemColor)
168 |
169 | // horizontal spacing after item
170 | // no checking if item is most right item, we will draw edges spacing anyway
171 |
172 | drawingRect.left = itemView.right + spacing.item.right
173 | drawingRect.right = drawingRect.left + spacing.horizontal
174 | drawRect(canvas, drawingConfig.horizontalColor)
175 |
176 | // vertical spacing after item
177 |
178 | drawingRect.set(
179 | itemView.left - spacing.item.left,
180 | itemView.bottom + spacing.item.bottom,
181 | itemView.right + spacing.item.right,
182 | itemView.bottom + spacing.item.bottom + spacing.vertical)
183 | drawRect(canvas, drawingConfig.verticalColor)
184 | }
185 | }
186 | }
187 |
188 | private fun drawEdges(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
189 | if (parent.childCount == 0) return
190 | if (spacing.edges.isAllZeros()) return
191 |
192 | val extremeItems = parent.getExtremeChildren()
193 | var internalEdge: Int
194 |
195 | with (drawing) {
196 | paint.color = drawingConfig.edgeColor
197 | parent.getDrawingRect(visibleRect)
198 |
199 | determineItemOffsetsParams(extremeItems[0], parent, state, itemOffsetsParams)
200 | itemOffsetsRequestBuilder.fillItemOffsetsRequest(itemOffsetsParams, offsetsRequest)
201 | if (offsetsRequest.col == 0) {
202 | internalEdge = Math.min(extremeItems[0].left - spacing.item.left,
203 | spacing.edges.left)
204 | drawingRect.set(0, 0, internalEdge, visibleRect.bottom)
205 | drawRect(canvas)
206 | }
207 |
208 | determineItemOffsetsParams(extremeItems[1], parent, state, itemOffsetsParams)
209 | itemOffsetsRequestBuilder.fillItemOffsetsRequest(itemOffsetsParams, offsetsRequest)
210 | if (offsetsRequest.row == 0) {
211 | internalEdge = Math.min(extremeItems[1].top - spacing.item.top,
212 | spacing.edges.top)
213 | drawingRect.set(0, 0, visibleRect.right, internalEdge)
214 | drawRect(canvas)
215 | }
216 |
217 | determineItemOffsetsParams(extremeItems[2], parent, state, itemOffsetsParams)
218 | itemOffsetsRequestBuilder.fillItemOffsetsRequest(itemOffsetsParams, offsetsRequest)
219 | if (offsetsRequest.lastCol == offsetsRequest.cols - 1) {
220 | internalEdge = Math.max(extremeItems[2].right + spacing.item.right,
221 | visibleRect.right - spacing.edges.right)
222 | drawingRect.set(internalEdge, 0, visibleRect.right, visibleRect.bottom)
223 | drawRect(canvas)
224 | }
225 |
226 | determineItemOffsetsParams(extremeItems[3], parent, state, itemOffsetsParams)
227 | itemOffsetsRequestBuilder.fillItemOffsetsRequest(itemOffsetsParams, offsetsRequest)
228 | if (offsetsRequest.lastRow == offsetsRequest.rows - 1) {
229 | internalEdge = Math.max(extremeItems[3].bottom + spacing.item.bottom,
230 | visibleRect.bottom - spacing.edges.bottom)
231 | drawingRect.set(0, internalEdge, visibleRect.right, visibleRect.bottom)
232 | drawRect(canvas)
233 | }
234 | }
235 | }
236 |
237 | /**
238 | * Invalidate caches [spacing]. Should be called if [spacing] properties was modified without
239 | * setting [spacing] field.
240 | */
241 | fun invalidateSpacing() {
242 | itemOffsetsCalculator.invalidatePrecalculatedValues()
243 | }
244 |
245 | /**
246 | * Invalidate cached values.
247 | */
248 | fun invalidate() {
249 | invalidateSpacing()
250 | cachedGroupCount = -1
251 | }
252 |
253 | private fun getGridGroupCount(itemCount: Int, layoutManager: GridLayoutManager): Int {
254 | if (isGroupCountCacheEnabled && cachedGroupCount > 0)
255 | return cachedGroupCount
256 |
257 | val spanSizeLookup = layoutManager.spanSizeLookup
258 | val clampedSpanCount = Math.max(layoutManager.spanCount, 1)
259 |
260 | val groupCount: Int = when {
261 | hintSpanSizeAlwaysOne || spanSizeLookup is GridLayoutManager.DefaultSpanSizeLookup -> {
262 | Math.ceil(itemCount / clampedSpanCount.toDouble()).toInt()
263 | }
264 | else -> {
265 | val lastItemIndex = if (layoutManager.reverseLayout) 0 else itemCount - 1
266 | spanSizeLookup.getSpanGroupIndex(lastItemIndex, clampedSpanCount) + 1
267 | }
268 | }
269 |
270 | if (isGroupCountCacheEnabled)
271 | cachedGroupCount = groupCount
272 |
273 | return groupCount
274 | }
275 |
276 |
277 | data class DrawingConfig(
278 | var edgeColor: Int = Color.parseColor("#F44336"), // Red 500
279 | var itemColor: Int = Color.parseColor("#FFEB3B"), // Yellow 500
280 | var horizontalColor: Int = Color.parseColor("#00BCD4"), // Cyan 500
281 | var verticalColor: Int = Color.parseColor("#76FF03") // Light Green A400
282 | )
283 |
284 |
285 | /**
286 | * Stuff used to draw spacings.
287 | */
288 | private class Drawing(
289 | val drawingRect: Rect = Rect(),
290 | val visibleRect: Rect = Rect(),
291 | val paint: Paint = Paint().apply {
292 | style = Paint.Style.FILL
293 | }
294 | ) {
295 | fun drawRect(canvas: Canvas, color: Int) {
296 | paint.color = color
297 | canvas.drawRect(drawingRect, paint)
298 | }
299 |
300 | fun drawRect(canvas: Canvas) = canvas.drawRect(drawingRect, paint)
301 | }
302 |
303 | }
304 |
305 |
306 | /**
307 | * Find most left, top, right and bottom children and return array with 4 items.
308 | * If list doesn't have children, empty array is returned.
309 | */
310 | private fun RecyclerView.getExtremeChildren(): Array {
311 | if (childCount == 0) return emptyArray()
312 |
313 | val firstChild = getChildAt(0)
314 | val extremeChildren = Array(4, { firstChild })
315 |
316 | for (i in 1 until childCount) {
317 | val child = getChildAt(i)
318 | if (child.left < extremeChildren[0].left) extremeChildren[0] = child
319 | if (child.top < extremeChildren[1].top) extremeChildren[1] = child
320 | if (child.right > extremeChildren[2].right) extremeChildren[2] = child
321 | if (child.bottom > extremeChildren[3].bottom) extremeChildren[3] = child
322 | }
323 |
324 | return extremeChildren
325 | }
326 |
327 | /**
328 | * Check if all Rect edges are `0`.
329 | */
330 | private fun Rect.isAllZeros(): Boolean {
331 | return (left == 0 && top == 0 && right == 0 && bottom == 0)
332 | }
333 |
334 | private fun RecyclerView.getChildrenSequence(): Sequence {
335 | return (0 until childCount).asSequence().map { getChildAt(it) }
336 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SpacingItemDecoration
3 |
4 |
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/test/kotlin/com/grzegorzojdana/spacingitemdecoration/ItemOffsetsCalculatorTestBase.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import android.graphics.Rect
4 | import org.junit.runner.RunWith
5 | import org.robolectric.RobolectricTestRunner
6 | import kotlin.test.assertEquals
7 |
8 | @RunWith(RobolectricTestRunner::class)
9 | abstract class ItemOffsetsCalculatorTestBase {
10 |
11 | protected fun onCalcWithSpacing(spacing: Spacing, block: ItemOffsetsCalculator.() -> Unit) {
12 | ItemOffsetsCalculator(spacing).block()
13 | }
14 |
15 | protected fun ItemOffsetsCalculator.table(
16 | rows: Int,
17 | cols: Int,
18 | block: TableContext.() -> Unit
19 | ) {
20 | val request = ItemOffsetsCalculator.OffsetsRequest(rows = rows, cols = cols)
21 | TableContext(calc = this, request = request).block()
22 | }
23 |
24 | protected class TableContext(val calc: ItemOffsetsCalculator,
25 | val request: ItemOffsetsCalculator.OffsetsRequest) {
26 | fun cell(row: Int,
27 | col: Int,
28 | transform: (ItemOffsetsCalculator.OffsetsRequest.() -> Unit)? = null
29 | ): TableContext {
30 | request.row = row
31 | request.col = col
32 | transform?.let { request.it() }
33 | return this
34 | }
35 |
36 | fun forEachCell(predicate: TableContext.(row: Int, col: Int) -> Unit) {
37 | for (row in 0 until request.rows) {
38 | request.row = row
39 | for (col in 0 until request.cols) {
40 | request.col = col
41 | this.predicate(row, col)
42 | }
43 | }
44 | }
45 |
46 | infix fun expects(expectedOffsets: Rect) {
47 | val result = Rect()
48 | calc.getItemOffsets(result, request)
49 | assertEquals(expectedOffsets, result)
50 | }
51 |
52 | fun eachCellExpects(expected: Rect) {
53 | forEachCell { _, _ -> expects(expected) }
54 | }
55 | }
56 |
57 | protected fun offsets(left: Int, top: Int, right: Int, bottom: Int): Rect {
58 | return Rect(left, top, right, bottom)
59 | }
60 |
61 | protected fun noOffsets(): Rect = Rect()
62 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/test/kotlin/com/grzegorzojdana/spacingitemdecoration/ItemOffsetsCalculatorTest_grid.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import android.graphics.Rect
4 | import org.junit.Test
5 |
6 | class ItemOffsetsCalculatorTest_grid : ItemOffsetsCalculatorTestBase() {
7 |
8 | @Test
9 | fun noSpacing() {
10 | onCalcWithSpacing(Spacing()) {
11 | table(rows = 2, cols = 2) {
12 | eachCellExpects(noOffsets())
13 | }
14 | }
15 | }
16 |
17 | @Test
18 | fun edges() {
19 | val spacing = Spacing(
20 | edges = Rect(6, 4, 12, 10))
21 | // sum of horizontal spacing: 18; need to take 18/3 = 6 px from each item's width
22 | // sum of vertical spacing: 14; need to take 14/2 = 7 px from each item's height
23 | onCalcWithSpacing(spacing) {
24 | table(rows = 2, cols = 3) {
25 | cell(0, 0) expects offsets(6, 4, 0, 3)
26 | cell(0, 1) expects offsets(0, 4, 6, 3)
27 | cell(0, 2) expects offsets(-6, 4, 12, 3)
28 | cell(1, 0) expects offsets(6, -3, 0, 10)
29 | cell(1, 1) expects offsets(0, -3, 6, 10)
30 | cell(1, 2) expects offsets(-6, -3, 12, 10)
31 | }
32 | }
33 | }
34 |
35 | @Test
36 | fun item() {
37 | val spacing = Spacing(
38 | item = Rect(3, 2, 1, 4))
39 | // for only item offsets, each item have the same offset, which is equal to specified spacing
40 | val expectedOffsets = offsets(3, 2, 1, 4)
41 | onCalcWithSpacing(spacing) {
42 | table(rows = 2, cols = 3) {
43 | eachCellExpects(expectedOffsets)
44 | }
45 | }
46 | }
47 |
48 | @Test
49 | fun horizontalAndVertical() {
50 | val spacing = Spacing(horizontal = 3, vertical = 6)
51 | // sum of horizontal spacing: 2*3 = 6; need to take 6/3 = 2 px from each item's width
52 | // sum of vertical spacing: 2*6 = 12; need to take 12/3 = 4 px from each item's height
53 | onCalcWithSpacing(spacing) {
54 | table(rows = 3, cols = 3) {
55 | cell(0,0) expects offsets(0, 0, 2, 4)
56 | cell(0,1) expects offsets(1, 0, 1, 4)
57 | cell(0,2) expects offsets(2, 0, 0, 4)
58 | cell(1,0) expects offsets(0, 2, 2, 2)
59 | cell(1,1) expects offsets(1, 2, 1, 2)
60 | cell(1,2) expects offsets(2, 2, 0, 2)
61 | cell(2,0) expects offsets(0, 4, 2, 0)
62 | cell(2,1) expects offsets(1, 4, 1, 0)
63 | cell(2,2) expects offsets(2, 4, 0, 0)
64 | }
65 | }
66 | }
67 |
68 | @Test
69 | fun allSpacingProperties() {
70 | val spacing = Spacing(
71 | edges = Rect(4, 3, 2, 4),
72 | item = Rect(1, 2, 1, 2),
73 | horizontal = 8,
74 | vertical = 4)
75 | // horizontal spacing: edges: 6, items: 2*2 = 4; horizontal: 1*8 = 8; sum: 18; need to take 18/2 = 9 px from each item's width
76 | // vertical spacing: edges: 7, items: 3*4 = 12; vertical: 2*4 = 8; sum: 21; need to take 27/3 = 9 px from each item's height
77 | onCalcWithSpacing(spacing) {
78 | table(rows = 3, cols = 2) {
79 | cell(0, 0) expects offsets(5, 5, 4, 4)
80 | cell(0, 1) expects offsets(6, 5, 3, 4)
81 | cell(1, 0) expects offsets(5, 4, 4, 5)
82 | cell(1, 1) expects offsets(6, 4, 3, 5)
83 | cell(2, 0) expects offsets(5, 3, 4, 6)
84 | cell(2, 1) expects offsets(6, 3, 3, 6)
85 | }
86 | }
87 | }
88 |
89 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/test/kotlin/com/grzegorzojdana/spacingitemdecoration/ItemOffsetsCalculatorTest_gridSpanSize.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import android.graphics.Rect
4 | import org.junit.Test
5 |
6 | class ItemOffsetsCalculatorTest_gridSpanSize : ItemOffsetsCalculatorTestBase() {
7 |
8 | @Test
9 | fun horizontalSpan() {
10 | val spacing = Spacing(
11 | edges = Rect(5, 3, 3, 4),
12 | item = Rect(2, 2, 0, 2),
13 | horizontal = 4,
14 | vertical = 4)
15 | // horizontal spacing: edges: 8, items: 4*2 = 8; horizontal: 3*4 = 12; sum: 28; need to take 28/4 = 7 px from each item's width
16 | // vertical spacing: edges: 7, items: 3*4 = 12; vertical: 2*4 = 8; sum: 21; need to take 27/3 = 9 px from each item's height
17 | // Table layout:
18 | // 0123
19 | // +----+
20 | // 0|AAB |
21 | // 1|CCCD|
22 | // 2| EFF|
23 | // +----+
24 | onCalcWithSpacing(spacing) {
25 | table(rows = 3, cols = 4) {
26 | cell(0, 0) { spanSizeH = 2 } expects offsets(7, 5, 1, 4)
27 | cell(0, 2) { spanSizeH = 1 } expects offsets(5, 5, 2, 4)
28 | cell(1, 0) { spanSizeH = 3 } expects offsets(7, 4, 2, 5)
29 | cell(1, 3) { spanSizeH = 1 } expects offsets(4, 4, 3, 5)
30 | cell(2, 1) { spanSizeH = 1 } expects offsets(6, 3, 1, 6)
31 | cell(2, 2) { spanSizeH = 2 } expects offsets(5, 3, 3, 6)
32 | }
33 | }
34 | }
35 |
36 | @Test
37 | fun verticalSpan() {
38 | val spacing = Spacing(
39 | edges = Rect(5, 4, 3, 4),
40 | item = Rect(2, 3, 0, 1),
41 | horizontal = 4,
42 | vertical = 4)
43 | // horizontal spacing: edges: 8, items: 4*2 = 8; horizontal: 3*4 = 12; sum: 28; need to take 28/4 = 7 px from each item's width
44 | // vertical spacing: edges: 8, items: 4*4 = 16; vertical: 3*4 = 12; sum: 36; need to take 36/4 = 9 px from each item's height
45 | // Table layout:
46 | // 0123
47 | // +----+
48 | // 0|ACD |
49 | // 1|ACE |
50 | // 2|BCEF|
51 | // 3| C F|
52 | // +----+
53 | onCalcWithSpacing(spacing) {
54 | table(rows = 4, cols = 4) {
55 | cell(0, 0) { spanSizeV = 2 } expects offsets(7, 7, 0, 3)
56 | cell(2, 0) { spanSizeV = 1 } expects offsets(7, 5, 0, 4)
57 | cell(0, 1) { spanSizeV = 4 } expects offsets(6, 7, 1, 5)
58 | cell(0, 2) { spanSizeV = 1 } expects offsets(5, 7, 2, 2)
59 | cell(1, 2) { spanSizeV = 2 } expects offsets(5, 6, 2, 4)
60 | cell(2, 3) { spanSizeV = 2 } expects offsets(4, 5, 3, 5)
61 | }
62 | }
63 | }
64 |
65 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/test/kotlin/com/grzegorzojdana/spacingitemdecoration/ItemOffsetsCalculatorTest_singleColumn.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import android.graphics.Rect
4 | import org.junit.Test
5 |
6 | class ItemOffsetsCalculatorTest_singleColumn : ItemOffsetsCalculatorTestBase() {
7 |
8 | @Test
9 | fun noSpacing() {
10 | onCalcWithSpacing(Spacing()) {
11 | table(rows = 3, cols = 1) {
12 | eachCellExpects(noOffsets())
13 | }
14 | }
15 | }
16 |
17 | @Test
18 | fun edges() {
19 | val spacing = Spacing(
20 | edges = Rect(8, 5, 6, 10))
21 | // sum of vertical spacing: 15; need to take 15/3 = 5 px from each item's height
22 | onCalcWithSpacing(spacing) {
23 | table(rows = 3, cols = 1) {
24 | cell(0, 0) expects offsets(8, 5, 6, 0)
25 | cell(1, 0) expects offsets(8, 0, 6, 5)
26 | cell(2, 0) expects offsets(8, -5, 6, 10)
27 | }
28 | }
29 | }
30 |
31 | @Test
32 | fun item() {
33 | val spacing = Spacing(
34 | item = Rect(2, 2, 3, 4))
35 | // sum of vertical spacing: 3*(2+4) = 18; need to take 18/3 = 6 px from each item's height
36 | onCalcWithSpacing(spacing) {
37 | table(rows = 3, cols = 1) {
38 | eachCellExpects(offsets(2, 2, 3, 4))
39 | }
40 | }
41 | }
42 |
43 | @Test
44 | fun horizontalAndVertical() {
45 | val spacing = Spacing(
46 | horizontal = 4,
47 | vertical = 6)
48 | // sum of vertical spacing: 2*6 = 12; need to take 12/3 = 4 px from each item's height
49 | onCalcWithSpacing(spacing) {
50 | table(rows = 3, cols = 1) {
51 | cell(0, 0) expects offsets(0, 0, 0, 4)
52 | cell(1, 0) expects offsets(0, 2, 0, 2)
53 | cell(2, 0) expects offsets(0, 4, 0, 0)
54 | }
55 | }
56 | }
57 |
58 | @Test
59 | fun allSpacingProperties() {
60 | val spacing = Spacing(
61 | edges = Rect(2, 3, 2, 4),
62 | item = Rect(1, 1, 1, 1),
63 | horizontal = 4,
64 | vertical = 4)
65 | // vertical spacing: edges: 7, items: 3*2 = 6; vertical: 2*4 = 8; sum: 21; need to take 21/3 = 7 px from each item's height
66 | onCalcWithSpacing(spacing) {
67 | table(rows = 3, cols = 1) {
68 | cell(0, 0) expects offsets(3, 4, 3, 3)
69 | cell(1, 0) expects offsets(3, 3, 3, 4)
70 | cell(2, 0) expects offsets(3, 2, 3, 5)
71 | }
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/test/kotlin/com/grzegorzojdana/spacingitemdecoration/ItemOffsetsCalculatorTest_singleItem.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import android.graphics.Rect
4 | import org.junit.Test
5 |
6 | class ItemOffsetsCalculatorTest_singleItem : ItemOffsetsCalculatorTestBase() {
7 |
8 | @Test
9 | fun noSpacing() {
10 | onCalcWithSpacing(Spacing()) {
11 | table(rows = 1, cols = 1) {
12 | cell(0, 0) expects noOffsets()
13 | }
14 | }
15 | }
16 |
17 | @Test
18 | fun edges() {
19 | val spacing = Spacing(
20 | edges = Rect(8, 5, 6, 10))
21 | onCalcWithSpacing(spacing) {
22 | table(rows = 1, cols = 1) {
23 | cell(0, 0) expects offsets(8, 5, 6, 10)
24 | }
25 | }
26 | }
27 |
28 | @Test
29 | fun item() {
30 | val spacing = Spacing(
31 | item = Rect(2, 2, 3, 4))
32 | onCalcWithSpacing(spacing) {
33 | table(rows = 1, cols = 1) {
34 | cell(0, 0) expects offsets(2, 2, 3, 4)
35 | }
36 | }
37 | }
38 |
39 | @Test
40 | fun horizontalAndVertical() {
41 | val spacing = Spacing(
42 | horizontal = 4,
43 | vertical = 6)
44 | onCalcWithSpacing(spacing) {
45 | table(rows = 1, cols = 1) {
46 | cell(0, 0) expects noOffsets()
47 | }
48 | }
49 | }
50 |
51 | @Test
52 | fun allSpacingProperties() {
53 | val spacing = Spacing(
54 | edges = Rect(2, 3, 2, 4),
55 | item = Rect(0, 1, 1, 1),
56 | horizontal = 5,
57 | vertical = 5)
58 | onCalcWithSpacing(spacing) {
59 | table(rows = 1, cols = 1) {
60 | cell(0, 0) expects offsets(2, 4, 3, 5)
61 | }
62 | }
63 | }
64 |
65 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/test/kotlin/com/grzegorzojdana/spacingitemdecoration/ItemOffsetsCalculatorTest_singleRow.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import android.graphics.Rect
4 | import org.junit.Test
5 |
6 | class ItemOffsetsCalculatorTest_singleRow : ItemOffsetsCalculatorTestBase() {
7 |
8 | @Test
9 | fun noSpacing() {
10 | onCalcWithSpacing(Spacing()) {
11 | table(rows = 1, cols = 3) {
12 | eachCellExpects(noOffsets())
13 | }
14 | }
15 | }
16 |
17 | @Test
18 | fun edges() {
19 | val spacing = Spacing(
20 | edges = Rect(6, 4, 12, 10))
21 | // sum of horizontal spacing: 18; need to take 18/3 = 6 px from each item's width
22 | onCalcWithSpacing(spacing) {
23 | table(rows = 1, cols = 3) {
24 | cell(0, 0) expects offsets(6, 4, 0, 10)
25 | cell(0, 1) expects offsets(0, 4, 6, 10)
26 | cell(0, 2) expects offsets(-6, 4, 12, 10)
27 | }
28 | }
29 | }
30 |
31 | @Test
32 | fun item() {
33 | val spacing = Spacing(
34 | item = Rect(3, 2, 1, 4))
35 | val offsetsRequest = ItemOffsetsCalculator.OffsetsRequest(rows = 1, cols = 3)
36 | // sum of horizontal spacing: 3*(3+1) = 12; need to take 12/3 = 4 px from each item's width
37 | onCalcWithSpacing(spacing) {
38 | table(rows = 1, cols = 3) {
39 | eachCellExpects(offsets(3, 2, 1, 4))
40 | }
41 | }
42 | }
43 |
44 | @Test
45 | fun horizontalAndVertical() {
46 | val spacing = Spacing(
47 | horizontal = 4,
48 | vertical = 6)
49 | // sum of horizontal spacing: 3*4 = 12; need to take 12/4 = 3 px from each item's width
50 | onCalcWithSpacing(spacing) {
51 | table(rows = 1, cols = 4) {
52 | cell(0, 0) expects offsets(0, 0, 3, 0)
53 | cell(0, 1) expects offsets(1, 0, 2, 0)
54 | cell(0, 2) expects offsets(2, 0, 1, 0)
55 | cell(0, 3) expects offsets(3, 0, 0, 0)
56 | }
57 | }
58 | }
59 |
60 | @Test
61 | fun allSpacingProperties() {
62 | val spacing = Spacing(
63 | edges = Rect(5, 3, 2, 4),
64 | item = Rect(1, 1, 1, 1),
65 | horizontal = 4,
66 | vertical = 4)
67 | // horizontal spacing: edges: 7, items: 3*2 = 6; horizontal: 2*4 = 8; sum: 18; need to take 21/3 = 7 px from each item's width
68 | onCalcWithSpacing(spacing) {
69 | table(rows = 1, cols = 3) {
70 | cell(0, 0) expects offsets(6, 4, 1, 5)
71 | cell(0, 1) expects offsets(5, 4, 2, 5)
72 | cell(0, 2) expects offsets(4, 4, 3, 5)
73 | }
74 | }
75 | }
76 |
77 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/test/kotlin/com/grzegorzojdana/spacingitemdecoration/ItemOffsetsRequestBuilderTestBase.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import com.grzegorzojdana.spacingitemdecoration.ItemOffsetsCalculator.OffsetsRequest
4 | import com.grzegorzojdana.spacingitemdecoration.ItemOffsetsRequestBuilder.ItemOffsetsParams
5 | import org.junit.runner.RunWith
6 | import org.robolectric.RobolectricTestRunner
7 | import kotlin.test.assertEquals
8 |
9 | @RunWith(RobolectricTestRunner::class)
10 | abstract class ItemOffsetsRequestBuilderTestBase(
11 | protected val offsetsRequestBuilder: ItemOffsetsRequestBuilder = ItemOffsetsRequestBuilder()
12 | ) {
13 |
14 | protected fun givenParams(params: ItemOffsetsParams,
15 | contextBlock: ParamsContext.() -> Unit): ParamsContext {
16 | return ParamsContext(
17 | offsetsParams = params,
18 | offsetsRequestBuilder = offsetsRequestBuilder).apply(contextBlock)
19 | }
20 |
21 | protected class ParamsContext(val offsetsParams: ItemOffsetsParams,
22 | val offsetsRequestBuilder: ItemOffsetsRequestBuilder) {
23 | fun on(paramsConditions: ItemOffsetsParams.() -> Unit): ParamsContext {
24 | offsetsParams.paramsConditions()
25 | return this
26 | }
27 |
28 | infix fun expects(expected: OffsetsRequest) {
29 | val result = OffsetsRequest()
30 | offsetsRequestBuilder.fillItemOffsetsRequest(offsetsParams, result)
31 | assertEquals(expected, result)
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/test/kotlin/com/grzegorzojdana/spacingitemdecoration/ItemOffsetsRequestBuilderTest_gridLayout.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import com.grzegorzojdana.spacingitemdecoration.ItemOffsetsCalculator.OffsetsRequest
4 | import com.grzegorzojdana.spacingitemdecoration.ItemOffsetsRequestBuilder.ItemOffsetsParams
5 | import org.junit.Test
6 |
7 | class ItemOffsetsRequestBuilderTest_gridLayout: ItemOffsetsRequestBuilderTestBase() {
8 |
9 | @Test
10 | fun vertical() {
11 | val params = ItemOffsetsParams(
12 | spanSize = 1, spanCount = 3, groupCount = 2,
13 | isLayoutVertical = true, isLayoutReverse = false)
14 | val expectedResult = OffsetsRequest(
15 | spanSizeH = 1, spanSizeV = 1, rows = 2, cols = 3)
16 |
17 | givenParams(params) {
18 | on { groupIndex = 0; spanIndex = 0 } expects expectedResult.apply { row = 0; col = 0 }
19 | on { groupIndex = 0; spanIndex = 1 } expects expectedResult.apply { row = 0; col = 1 }
20 | on { groupIndex = 0; spanIndex = 2 } expects expectedResult.apply { row = 0; col = 2 }
21 | on { groupIndex = 1; spanIndex = 0 } expects expectedResult.apply { row = 1; col = 0 }
22 | on { groupIndex = 1; spanIndex = 1 } expects expectedResult.apply { row = 1; col = 1 }
23 | on { groupIndex = 1; spanIndex = 2 } expects expectedResult.apply { row = 1; col = 2 }
24 | }
25 | }
26 |
27 | @Test
28 | fun horizontal() {
29 | val params = ItemOffsetsParams(
30 | spanSize = 1, spanCount = 3, groupCount = 2,
31 | isLayoutVertical = false, isLayoutReverse = false)
32 | val expectedResult = OffsetsRequest(
33 | spanSizeH = 1, spanSizeV = 1, rows = 3, cols = 2)
34 |
35 | givenParams(params) {
36 | on { groupIndex = 0; spanIndex = 0 } expects expectedResult.apply { row = 0; col = 0 }
37 | on { groupIndex = 0; spanIndex = 1 } expects expectedResult.apply { row = 1; col = 0 }
38 | on { groupIndex = 0; spanIndex = 2 } expects expectedResult.apply { row = 2; col = 0 }
39 | on { groupIndex = 1; spanIndex = 0 } expects expectedResult.apply { row = 0; col = 1 }
40 | on { groupIndex = 1; spanIndex = 1 } expects expectedResult.apply { row = 1; col = 1 }
41 | on { groupIndex = 1; spanIndex = 2 } expects expectedResult.apply { row = 2; col = 1 }
42 | }
43 | }
44 |
45 | @Test
46 | fun verticalReverse() {
47 | val params = ItemOffsetsParams(
48 | spanSize = 1, spanCount = 3, groupCount = 2,
49 | isLayoutVertical = true, isLayoutReverse = true)
50 | val expectedResult = OffsetsRequest(
51 | spanSizeH = 1, spanSizeV = 1, rows = 2, cols = 3)
52 |
53 | givenParams(params) {
54 | on { groupIndex = 0; spanIndex = 0 } expects expectedResult.apply { row = 1; col = 0 }
55 | on { groupIndex = 0; spanIndex = 1 } expects expectedResult.apply { row = 1; col = 1 }
56 | on { groupIndex = 0; spanIndex = 2 } expects expectedResult.apply { row = 1; col = 2 }
57 | on { groupIndex = 1; spanIndex = 0 } expects expectedResult.apply { row = 0; col = 0 }
58 | on { groupIndex = 1; spanIndex = 1 } expects expectedResult.apply { row = 0; col = 1 }
59 | on { groupIndex = 1; spanIndex = 2 } expects expectedResult.apply { row = 0; col = 2 }
60 | }
61 | }
62 |
63 | @Test
64 | fun horizontalReverse() {
65 | val params = ItemOffsetsParams(
66 | spanSize = 1, spanCount = 3, groupCount = 2,
67 | isLayoutVertical = false, isLayoutReverse = true)
68 | val expectedResult = OffsetsRequest(
69 | spanSizeH = 1, spanSizeV = 1, rows = 3, cols = 2)
70 |
71 | givenParams(params) {
72 | on { groupIndex = 0; spanIndex = 0 } expects expectedResult.apply { row = 0; col = 1 }
73 | on { groupIndex = 0; spanIndex = 1 } expects expectedResult.apply { row = 1; col = 1 }
74 | on { groupIndex = 0; spanIndex = 2 } expects expectedResult.apply { row = 2; col = 1 }
75 | on { groupIndex = 1; spanIndex = 0 } expects expectedResult.apply { row = 0; col = 0 }
76 | on { groupIndex = 1; spanIndex = 1 } expects expectedResult.apply { row = 1; col = 0 }
77 | on { groupIndex = 1; spanIndex = 2 } expects expectedResult.apply { row = 2; col = 0 }
78 | }
79 | }
80 |
81 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/test/kotlin/com/grzegorzojdana/spacingitemdecoration/ItemOffsetsRequestBuilderTest_linearLayout.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import com.grzegorzojdana.spacingitemdecoration.ItemOffsetsCalculator.OffsetsRequest
4 | import com.grzegorzojdana.spacingitemdecoration.ItemOffsetsRequestBuilder.ItemOffsetsParams
5 | import org.junit.Test
6 |
7 | class ItemOffsetsRequestBuilderTest_linearLayout: ItemOffsetsRequestBuilderTestBase() {
8 |
9 | @Test
10 | fun vertical() {
11 | val params = ItemOffsetsParams(
12 | spanIndex = 0, spanSize = 1,
13 | spanCount = 1, groupCount = 3,
14 | isLayoutVertical = true, isLayoutReverse = false)
15 | val expectedResult = OffsetsRequest(
16 | spanSizeH = 1, spanSizeV = 1, rows = 3, cols = 1)
17 |
18 | givenParams(params) {
19 | on { groupIndex = 0 } expects expectedResult.apply { row = 0; col = 0 }
20 | on { groupIndex = 1 } expects expectedResult.apply { row = 1; col = 0 }
21 | on { groupIndex = 2 } expects expectedResult.apply { row = 2; col = 0 }
22 | }
23 | }
24 |
25 | @Test
26 | fun horizontal() {
27 | val params = ItemOffsetsParams(
28 | spanIndex = 0, spanSize = 1,
29 | spanCount = 1, groupCount = 3,
30 | isLayoutVertical = false, isLayoutReverse = false)
31 | val expectedResult = OffsetsRequest(
32 | spanSizeH = 1, spanSizeV = 1, rows = 1, cols = 3)
33 |
34 | givenParams(params) {
35 | on { groupIndex = 0 } expects expectedResult.apply { row = 0; col = 0 }
36 | on { groupIndex = 1 } expects expectedResult.apply { row = 0; col = 1 }
37 | on { groupIndex = 2 } expects expectedResult.apply { row = 0; col = 2 }
38 | }
39 | }
40 |
41 | @Test
42 | fun verticalReverse() {
43 | val params = ItemOffsetsParams(
44 | spanIndex = 0, spanSize = 1,
45 | spanCount = 1, groupCount = 3,
46 | isLayoutVertical = true, isLayoutReverse = true)
47 | val expectedResult = OffsetsRequest(
48 | spanSizeH = 1, spanSizeV = 1, rows = 3, cols = 1)
49 |
50 | givenParams(params) {
51 | on { groupIndex = 0 } expects expectedResult.apply { row = 2; col = 0 }
52 | on { groupIndex = 1 } expects expectedResult.apply { row = 1; col = 0 }
53 | on { groupIndex = 2 } expects expectedResult.apply { row = 0; col = 0 }
54 | }
55 | }
56 |
57 | @Test
58 | fun horizontalReverse() {
59 | val params = ItemOffsetsParams(
60 | spanIndex = 0, spanSize = 1,
61 | spanCount = 1, groupCount = 3,
62 | isLayoutVertical = false, isLayoutReverse = true)
63 | val expectedResult = OffsetsRequest(
64 | spanSizeH = 1, spanSizeV = 1, rows = 1, cols = 3)
65 |
66 | givenParams(params) {
67 | on { groupIndex = 0 } expects expectedResult.apply { row = 0; col = 2 }
68 | on { groupIndex = 1 } expects expectedResult.apply { row = 0; col = 1 }
69 | on { groupIndex = 2 } expects expectedResult.apply { row = 0; col = 0 }
70 | }
71 | }
72 |
73 | }
--------------------------------------------------------------------------------
/spacingitemdecorationlib/src/test/kotlin/com/grzegorzojdana/spacingitemdecoration/SpacingTest.kt:
--------------------------------------------------------------------------------
1 | package com.grzegorzojdana.spacingitemdecoration
2 |
3 | import android.graphics.Rect
4 | import org.junit.Test
5 | import org.junit.runner.RunWith
6 | import org.robolectric.RobolectricTestRunner
7 | import kotlin.test.assertEquals
8 |
9 | /**
10 | *
11 | *
12 | * @author Grzegorz Ojdana (grzegorz@nativetap.io)
13 | * Created 2018-02-27
14 | */
15 | @RunWith(RobolectricTestRunner::class)
16 | class SpacingTest {
17 |
18 | @Test
19 | fun zero() {
20 | val spacing = Spacing(
21 | horizontal = 12,
22 | vertical = 24,
23 | edges = Rect(12, 0, 54, 40),
24 | item = Rect(10, 5, 10, 5)
25 | )
26 | spacing.zero()
27 | assertEquals(spacing, Spacing())
28 | }
29 |
30 | }
--------------------------------------------------------------------------------