diff --git a/README.md b/README.md index a20dde9..0b14893 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,17 @@ fun `when 2 of 4 registered students are attendees then student items correctly viewModel.adapterItems.subscribe(adapter) ``` +## Known issues +There is an issue in androidx.recyclerview version <= 1.1.0. If you use such version then you have to override +`LayoutManager.supportsPredictiveItemAnimations` and return false to avoid crash that may happen during list animation. +```kotlin +layoutManager = object : LinearLayoutManager(context, RecyclerView.VERTICAL, false) { + override fun supportsPredictiveItemAnimations(): Boolean { + return false + } + } +} +``` ### More diff --git a/app/build.gradle b/app/build.gradle index 649ab60..746b066 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 @@ -24,7 +25,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation "androidx.cardview:cardview:1.0.0" implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.recyclerview:recyclerview:1.2.0-beta02' implementation 'com.google.android.material:material:1.2.1' implementation project(":universal-adapter") diff --git a/app/src/main/java/com/jacekmarchwicki/changesdetector/example/MainActivity.kt b/app/src/main/java/com/jacekmarchwicki/changesdetector/example/MainActivity.kt index 2a23c25..9023612 100644 --- a/app/src/main/java/com/jacekmarchwicki/changesdetector/example/MainActivity.kt +++ b/app/src/main/java/com/jacekmarchwicki/changesdetector/example/MainActivity.kt @@ -17,64 +17,96 @@ package com.jacekmarchwicki.changesdetector.example import android.os.Bundle import android.view.View -import android.widget.TextView +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import androidx.cardview.widget.CardView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.jacekmarchwicki.universaladapter.DefaultAdapterItem -import com.jacekmarchwicki.universaladapter.LayoutViewHolderManager -import com.jacekmarchwicki.universaladapter.UniversalAdapter -import com.jacekmarchwicki.universaladapter.ViewHolderManager.BaseViewHolder +import com.jacekmarchwicki.universaladapter.* +import kotlinx.android.synthetic.main.item_header.view.* +import kotlinx.android.synthetic.main.item_song.view.* +import kotlinx.android.synthetic.main.main_activity.* -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), MainPresenter.MainView { - private val presenter = MainPresenter() + private val presenter = MainPresenter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) - val recyclerView = (findViewById(R.id.main_activity_recycler) as RecyclerView).apply { - layoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.VERTICAL, false).apply { + val recyclerView = main_activity_recycler.apply { + layoutManager = object : LinearLayoutManager(this@MainActivity, RecyclerView.VERTICAL, false) { + // If you use androidx.recyclerview version <= 1.1.0 then you have to override this method + // as there is a bug that may cause a crash when the list animates + /* override fun supportsPredictiveItemAnimations(): Boolean { + return false + }*/ + }.apply { recycleChildrenOnDetach = true } + } - val adapter = UniversalAdapter(listOf(DataViewHolder(), HeaderViewHolder())) + val adapter = UniversalAdapter(listOf( + SongViewHolder(ImageLoader()), HeaderViewHolder(), FooterViewHolder()) + ) recyclerView.adapter = adapter - findViewById(R.id.main_activity_fab) - .setOnClickListener { adapter.submitList(presenter.getItems()) } + main_activity_fab.setOnClickListener { adapter.submitList(presenter.getItems()) } } -} -data class DataItem(val id: Long, val name: String, val color: Int) : DefaultAdapterItem() { - override val itemId: Any = id + override fun onSongClicked(id: String) { + Toast.makeText(this, "Song $id clicked", Toast.LENGTH_SHORT).show() + } } -class DataViewHolder : LayoutViewHolderManager( - R.layout.data_item, DataItem::class, { ViewHolder(it) } +// Data models +data class HeaderItem(val text: String, val songsCount: Int, override val itemId: Any = text) : DefaultAdapterItem() + +data class SongItem( + val id: String, + val title: String, + val imageUrl: String, + override val itemId: Any = id, + val onSongClick: (id: String) -> Unit +) : DefaultAdapterItem() + +data class FooterItem(override val itemId: Any = BaseAdapterItem.NO_ID) : DefaultAdapterItem() + +// View Holders +class HeaderViewHolder : LayoutViewHolderManager( + R.layout.item_header, HeaderItem::class, { HeaderViewHolder(it) } ) { - class ViewHolder(itemView: View) : BaseViewHolder(itemView) { - override fun bind(item: DataItem) { - itemView.findViewById(R.id.data_item_text).apply { - text = item.name - setOnClickListener { println("Click: $item") } - } - itemView.findViewById(R.id.data_item_cardview) - .setCardBackgroundColor(item.color) + class HeaderViewHolder(itemView: View) : ViewHolderManager.BaseViewHolder(itemView) { + override fun bind(item: HeaderItem) { + itemView.item_header_tv.text = "${item.text} (songs count: ${item.songsCount})" } } } -data class HeaderItem(val text: String, override val itemId: Any = text) : DefaultAdapterItem() - -class HeaderViewHolder : LayoutViewHolderManager( - R.layout.item_header, HeaderItem::class, { HeaderViewHolder(it) } +class SongViewHolder(private val imageLoader: ImageLoader) : LayoutViewHolderManager( + R.layout.item_song, SongItem::class, { ViewHolder(it, imageLoader) } ) { - class HeaderViewHolder(itemView: View) : BaseViewHolder(itemView) { - override fun bind(item: HeaderItem) { - itemView.findViewById(R.id.item_header_tv).text = item.text + class ViewHolder(itemView: View, private val imageLoader: ImageLoader) : ViewHolderManager.BaseViewHolder(itemView) { + override fun bind(item: SongItem) { + itemView.item_song_title.text = item.title + itemView.setOnClickListener { item.onSongClick(item.id) } + imageLoader.load(item.imageUrl).into(itemView.item_song_card) } } +} + +class FooterViewHolder : LayoutViewHolderManager( + R.layout.item_footer, FooterItem::class, { ViewHolder(it) } +) { + class ViewHolder(itemView: View) : ViewHolderManager.BaseViewHolder(itemView) { + override fun bind(item: FooterItem) {} + } +} + +class ImageLoader { + fun load(url: String): ImageLoader { + return this + } + + fun into(view: View) {} } \ No newline at end of file diff --git a/app/src/main/java/com/jacekmarchwicki/changesdetector/example/MainPresenter.kt b/app/src/main/java/com/jacekmarchwicki/changesdetector/example/MainPresenter.kt index 2fe5644..0539d66 100644 --- a/app/src/main/java/com/jacekmarchwicki/changesdetector/example/MainPresenter.kt +++ b/app/src/main/java/com/jacekmarchwicki/changesdetector/example/MainPresenter.kt @@ -16,46 +16,38 @@ package com.jacekmarchwicki.changesdetector.example -import android.graphics.Color import com.jacekmarchwicki.universaladapter.BaseAdapterItem -import java.util.* -import kotlin.math.min +import kotlin.random.Random -class MainPresenter { +class MainPresenter(private val view: MainView) { + + interface MainView { + fun onSongClicked(id: String) + } fun getItems(): List { - val random = Random() - var hue = 0f - val itemsCount = randomBetween(random, 10, 30) - val items = mutableListOf() - for (i in 0 until itemsCount) { - hue += 0.12345f - if (!randomWithGaussianBoolean(random, 2.0)) { - continue + val albumsCount = Random.nextInt(2, 4) + return mutableListOf().apply { + repeat(albumsCount) { index -> + val songs = getSongsForAlbum() + add(HeaderItem("Album $index", songsCount = songs.size, itemId = index)) + addAll(songs) } - val realHue = hue % 1.0f - val color = Color.HSVToColor(255, floatArrayOf(realHue * 360, 1f, 0.5f)) - val name = if (randomWithGaussianBoolean(random, 2.0)) "item$i" else "item$i - changed" - items.add(DataItem(i.toLong(), name, color)) - } - val swapStart = randomBetween(random, 0, items.size - 2) - val swapEnd = min(items.size - 1, swapStart + 2) - Collections.swap(items, swapStart, swapEnd) - - items.add(0, HeaderItem("Header 0")) - val secondPosition = itemsCount / 3 - items.add(secondPosition, HeaderItem("Header $secondPosition")) - val thirdPosition = itemsCount / 2 - items.add(thirdPosition, HeaderItem("Header $thirdPosition")) - - return items.toList() + add(FooterItem()) + }.toList() } - private fun randomWithGaussianBoolean(random: Random, proablity: Double): Boolean { - return Math.abs(random.nextGaussian()) < proablity + private fun getSongsForAlbum(): List { + val songsCount = Random.nextInt(1, 4) + return mutableListOf().apply { + repeat(songsCount) { index -> + add(SongItem(index.toString(), "Song $index", + itemId = index, onSongClick = ::onSongClick, imageUrl = "url")) + } + }.toList() } - private fun randomBetween(random: Random, start: Int, end: Int): Int { - return random.nextInt(end - start) + start + private fun onSongClick(id: String) { + view.onSongClicked(id) } } \ No newline at end of file diff --git a/app/src/main/res/layout/item_footer.xml b/app/src/main/res/layout/item_footer.xml new file mode 100644 index 0000000..1b7dd6f --- /dev/null +++ b/app/src/main/res/layout/item_footer.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/data_item.xml b/app/src/main/res/layout/item_song.xml similarity index 77% rename from app/src/main/res/layout/data_item.xml rename to app/src/main/res/layout/item_song.xml index c919821..8eaabb6 100644 --- a/app/src/main/res/layout/data_item.xml +++ b/app/src/main/res/layout/item_song.xml @@ -4,28 +4,25 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="2dp"> + android:background="?selectableItemBackground"> - - - + tools:text="ASdfasdf asdfasdf aklsdfj "/> \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index 917accf..e02605a 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -22,6 +22,7 @@ android:layout_height="wrap_content" android:layout_gravity="end|bottom" android:src="@drawable/ic_switch_arrows_white_24dp" + app:useCompatPadding="true" app:elevation="4dp" app:fabSize="normal" /> diff --git a/data/screencast.gif b/data/screencast.gif index 8832f0d..f8f1682 100644 Binary files a/data/screencast.gif and b/data/screencast.gif differ