├── .gitignore ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── scratchpad │ │ └── jetpack │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── scratchpad │ │ │ └── jetpack │ │ │ ├── ListAdapterActivity.kt │ │ │ ├── MainActivity.kt │ │ │ └── RecyclerViewSelectionActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_list_adapter.xml │ │ ├── activity_main.xml │ │ ├── activity_recycler_view_selection.xml │ │ └── card_item.xml │ │ ├── menu │ │ └── list_adapter.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── scratchpad │ └── jetpack │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | /build 3 | /*/build/ 4 | 5 | # Crashlytics configuations 6 | com_crashlytics_export_strings.xml 7 | 8 | # Local configuration file (sdk path, etc) 9 | local.properties 10 | 11 | # Gradle generated files 12 | .gradle/ 13 | 14 | # Signing files 15 | .signing/ 16 | 17 | # User-specific configurations 18 | .idea 19 | *.iml 20 | 21 | # OS-specific files 22 | .DS_Store 23 | .DS_Store? 24 | ._* 25 | .Spotlight-V100 26 | .Trashes 27 | ehthumbs.db 28 | Thumbs.db -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 'android-P' 9 | defaultConfig { 10 | applicationId "com.scratchpad.io2018" 11 | minSdkVersion 21 12 | targetSdkVersion 27 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" 28 | implementation "android.arch.lifecycle:extensions:$lifecycle_version" 29 | 30 | implementation 'com.android.support:appcompat-v7:28.0.0-alpha1' 31 | implementation 'com.android.support:recyclerview-v7:28.0.0-alpha1' 32 | implementation 'com.android.support:recyclerview-selection:28.0.0-alpha1' 33 | implementation 'com.android.support:cardview-v7:28.0.0-alpha1' 34 | implementation 'com.android.support.constraint:constraint-layout:1.1.0' 35 | testImplementation 'junit:junit:4.12' 36 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 37 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 38 | } 39 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/scratchpad/jetpack/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.scratchpad.jetpack 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.scratchpad.jetpack", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/scratchpad/jetpack/ListAdapterActivity.kt: -------------------------------------------------------------------------------- 1 | package com.scratchpad.jetpack 2 | 3 | import android.arch.lifecycle.MutableLiveData 4 | import android.arch.lifecycle.Observer 5 | import android.os.Bundle 6 | import android.support.v7.app.AppCompatActivity 7 | import android.support.v7.recyclerview.extensions.ListAdapter 8 | import android.support.v7.util.DiffUtil 9 | import android.support.v7.widget.RecyclerView 10 | import android.view.* 11 | import android.widget.TextView 12 | import kotlinx.android.synthetic.main.activity_list_adapter.* 13 | 14 | class ListAdapterActivity : AppCompatActivity() { 15 | 16 | private val numbers = MutableLiveData>() 17 | 18 | private val diffCallback = object : DiffUtil.ItemCallback() { 19 | override fun areItemsTheSame(oldItem: Int, newItem: Int) = 20 | oldItem == newItem // check uniqueness 21 | 22 | override fun areContentsTheSame(oldItem: Int, newItem: Int) = 23 | oldItem == newItem // check contents 24 | } 25 | 26 | private val adapter: ListAdapter = object : ListAdapter(diffCallback) { 27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 28 | NumberViewHolder(parent) 29 | 30 | override fun onBindViewHolder(holder: NumberViewHolder, position: Int) { 31 | holder.bindTo(getItem(position)) 32 | } 33 | } 34 | 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | setContentView(R.layout.activity_list_adapter) 38 | title = ListAdapterActivity::class.java.simpleName 39 | 40 | recyclerView.adapter = adapter.apply { 41 | registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { 42 | override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { 43 | recyclerView.scrollToPosition(0) 44 | } 45 | }) 46 | } 47 | 48 | numbers.apply { 49 | value = listOf(1, 4, 5, 6, 7, 8, 9, 10) 50 | observe(this@ListAdapterActivity, Observer> { 51 | adapter.submitList(it) 52 | }) 53 | } 54 | } 55 | 56 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 57 | menuInflater.inflate(R.menu.list_adapter, menu) 58 | return true 59 | } 60 | 61 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 62 | when (item!!.itemId) { 63 | R.id.add -> numbers.value = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) 64 | R.id.remove -> numbers.value = listOf(2, 4, 6, 8, 9) 65 | R.id.reorder -> numbers.value = numbers.value!!.shuffled() 66 | } 67 | return true 68 | } 69 | } 70 | 71 | private class NumberViewHolder(parentView: View) : RecyclerView.ViewHolder( 72 | LayoutInflater.from(parentView.context).inflate(R.layout.card_item, null, false)) { 73 | 74 | private val numberView = itemView.findViewById(R.id.number) 75 | 76 | fun bindTo(position: Int) { 77 | numberView.text = "# $position" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/scratchpad/jetpack/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.scratchpad.jetpack 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.support.v7.app.AppCompatActivity 6 | import kotlinx.android.synthetic.main.activity_main.* 7 | 8 | class MainActivity : AppCompatActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContentView(R.layout.activity_main) 13 | 14 | listAdapterButton.setOnClickListener { _ -> 15 | startActivity(Intent(MainActivity@this, ListAdapterActivity::class.java)) 16 | } 17 | recyclerViewSelectionButton.setOnClickListener { _ -> 18 | startActivity(Intent(MainActivity@this, RecyclerViewSelectionActivity::class.java)) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/scratchpad/jetpack/RecyclerViewSelectionActivity.kt: -------------------------------------------------------------------------------- 1 | package com.scratchpad.jetpack 2 | 3 | import android.arch.core.util.Function 4 | import android.graphics.Color 5 | import android.os.Bundle 6 | import android.support.v7.app.AppCompatActivity 7 | import android.support.v7.widget.RecyclerView 8 | import android.view.LayoutInflater 9 | import android.view.MotionEvent 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.TextView 13 | import android.widget.Toast 14 | import androidx.recyclerview.selection.ItemDetailsLookup 15 | import androidx.recyclerview.selection.SelectionTracker 16 | import androidx.recyclerview.selection.StableIdKeyProvider 17 | import androidx.recyclerview.selection.StorageStrategy 18 | import kotlinx.android.synthetic.main.activity_recycler_view_selection.* 19 | 20 | class RecyclerViewSelectionActivity : AppCompatActivity() { 21 | 22 | private val MAXIMUM_SELECTION = 5 23 | private lateinit var selectionTracker: SelectionTracker 24 | 25 | private val itemDetailsLookup = object : ItemDetailsLookup() { 26 | override fun getItemDetails(e: MotionEvent): ItemDetails? { 27 | val view = recyclerView.findChildViewUnder(e.x, e.y) 28 | if (view != null) { 29 | val holder = recyclerView.getChildViewHolder(view) 30 | (holder as? SelectableViewHolder).apply { 31 | return object : ItemDetails() { 32 | override fun getSelectionKey() = holder.itemId 33 | override fun getPosition() = holder.adapterPosition 34 | } 35 | } 36 | } 37 | return null 38 | } 39 | } 40 | 41 | private val selectionPredicate = object : SelectionTracker.SelectionPredicate() { 42 | override fun canSelectMultiple(): Boolean { 43 | return true 44 | } 45 | 46 | override fun canSetStateForKey(key: Long, nextState: Boolean): Boolean { 47 | return if (selectionTracker.selection.size() >= MAXIMUM_SELECTION && nextState) { 48 | Toast.makeText(this@RecyclerViewSelectionActivity, 49 | "You can only select $MAXIMUM_SELECTION items in the list.", Toast.LENGTH_SHORT).show() 50 | false 51 | } else { 52 | true 53 | } 54 | } 55 | 56 | override fun canSetStateAtPosition(position: Int, nextState: Boolean): Boolean { 57 | return true 58 | } 59 | } 60 | 61 | 62 | override fun onCreate(savedInstanceState: Bundle?) { 63 | super.onCreate(savedInstanceState) 64 | setContentView(R.layout.activity_recycler_view_selection) 65 | title = "recyclerview-selection" 66 | 67 | SelectableAdapter(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)).apply { 68 | selectionFun = Function { 69 | key -> selectionTracker.isSelected(key) 70 | } 71 | recyclerView.adapter = this 72 | } 73 | 74 | selectionTracker = SelectionTracker.Builder( 75 | "selection-demo", 76 | recyclerView, 77 | StableIdKeyProvider(recyclerView), 78 | itemDetailsLookup, 79 | StorageStrategy.createLongStorage()) 80 | .withSelectionPredicate(selectionPredicate) 81 | .build() 82 | 83 | selectionTracker.addObserver(object : SelectionTracker.SelectionObserver() { 84 | override fun onSelectionChanged() { 85 | title = if (selectionTracker.hasSelection()) { 86 | "${selectionTracker.selection.size()} / $MAXIMUM_SELECTION selected" 87 | } else { 88 | "recyclerview-selection" 89 | } 90 | } 91 | }) 92 | } 93 | 94 | override fun onSaveInstanceState(outState: Bundle?) { 95 | super.onSaveInstanceState(outState) 96 | selectionTracker.onSaveInstanceState(outState!!) 97 | } 98 | 99 | override fun onRestoreInstanceState(savedInstanceState: Bundle?) { 100 | super.onRestoreInstanceState(savedInstanceState) 101 | selectionTracker.onRestoreInstanceState(savedInstanceState) 102 | } 103 | } 104 | 105 | private class SelectableAdapter(private val items: List) : RecyclerView.Adapter() { 106 | 107 | lateinit var selectionFun: Function 108 | 109 | init { 110 | setHasStableIds(true) 111 | } 112 | 113 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectableViewHolder 114 | = SelectableViewHolder(parent) 115 | 116 | override fun getItemCount() 117 | = items.size 118 | 119 | override fun getItemId(position: Int) 120 | = items[position].toLong() 121 | 122 | override fun onBindViewHolder(holder: SelectableViewHolder, position: Int) { 123 | holder.bindTo(items[position], selectionFun.apply(getItemId(position))) 124 | } 125 | } 126 | 127 | private class SelectableViewHolder(parentView: View) : RecyclerView.ViewHolder( 128 | LayoutInflater.from(parentView.context).inflate(R.layout.card_item, null, false)) { 129 | 130 | private val selectionView = itemView.findViewById(R.id.selection) 131 | private val numberView = itemView.findViewById(R.id.number) 132 | 133 | fun bindTo(position: Int, isSelected: Boolean) { 134 | selectionView.setBackgroundColor( 135 | if (isSelected) { 136 | itemView.isActivated = true 137 | Color.parseColor("#ff4081") 138 | } else { 139 | itemView.isActivated = false 140 | Color.parseColor("#dedede") 141 | }) 142 | numberView.text = "# $position" 143 | } 144 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_list_adapter.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 |