From 34f36d56f9d47c795d545aaaf3d04d2cbb8bb454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Czaplicki?= Date: Tue, 3 Apr 2018 13:22:58 +0200 Subject: [PATCH 1/7] add adapterview listener setters extensions for onItemClick, onItemLongClick and onItemSelected --- api/current.txt | 8 ++ .../androidx/core/widget/AdapterViewTest.kt | 129 ++++++++++++++++++ .../java/androidx/core/widget/AdapterView.kt | 70 ++++++++++ 3 files changed, 207 insertions(+) create mode 100644 src/androidTest/java/androidx/core/widget/AdapterViewTest.kt create mode 100644 src/main/java/androidx/core/widget/AdapterView.kt diff --git a/api/current.txt b/api/current.txt index 4c56ac98..fe471b3e 100644 --- a/api/current.txt +++ b/api/current.txt @@ -654,5 +654,13 @@ package androidx.core.widget { method public static android.widget.Toast toast(android.content.Context, @StringRes int resId, int duration = "Toast.LENGTH_SHORT"); } + public final class AdapterViewKt { + ctor public AdapterViewKt(); + method public static void onItemClick(android.widget.AdapterView,kotlin.jvm.functions.Function1); + method public static void onItemLongClick(android.widget.AdapterView,kotlin.jvm.functions.Function1); + method public static void onItemSelected(android.widget.AdapterView,kotlin.jvm.functions.Function0 = "{}",kotlin.jvm.functions.Function4,? super android.view.View,? super java.lang.Integer,? super java.lang.Long,kotlin.Unit>); + method public static void onItemSelected(android.widget.AdapterView,kotlin.jvm.functions.Function0 = "{}",kotlin.jvm.functions.Function1); + } + } diff --git a/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt b/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt new file mode 100644 index 00000000..0070ce90 --- /dev/null +++ b/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.core.widget + +import android.support.test.InstrumentationRegistry +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.ListView +import androidx.core.view.get +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.lang.ClassCastException + +class AdapterViewTest { + + private val context = InstrumentationRegistry.getContext() + private lateinit var adapterView: AdapterView<*> + private lateinit var adapter: ArrayAdapter + private var itemUnderTest: String? = null + + @Before + fun setup() { + adapterView = ListView(context) + adapter = ArrayAdapter( + context, + android.R.layout.simple_list_item_1, + listOf(K, L, M, N, O) + ) + adapterView.adapter = adapter + itemUnderTest = null + } + + /** + * test that ext call is equal to call + * adapterView.setOnItemClickListener { _, _, position, _ -> + * itemUnderTest = adapterView.getItemAtPosition(position) as String + * } + */ + @Test + fun onItemClick() { + adapterView.onItemClick { item: String -> itemUnderTest = item } + + assertTrue("listener not set", adapterView.performItemClick(null, 1, -1)) + assertEquals(L, itemUnderTest) + } + + @Test(expected = ClassCastException::class) + fun onItemClickCastExceptionOnWrongClass() { + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + var any: Any? = null + adapterView.onItemClick { item: WrongClass -> any = item } + adapterView.performItemClick(null, 1, -1) + } + + /** + * test that ext call is equal to call + * adapterView.setOnItemLongClickListener { _, _, position, _ -> + * itemUnderTest = adapterView.getItemAtPosition(position) as String + * true + * } + */ + @Test + fun onItemLongClick() { + adapterView.onItemLongClick { item: String -> itemUnderTest = item; true } + adapterView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT) + adapterView.showContextMenuForChild(adapterView[1]) + assertEquals(L, itemUnderTest) + } + + @Test(expected = ClassCastException::class) + fun onItemLongClickCastExceptionOnWrongClass() { + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + var any: Any? = null + adapterView.onItemLongClick { item: WrongClass -> any = item; true } + adapterView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT) + adapterView.showContextMenuForChild(adapterView[1]) + } + + @Test() + fun onItemSelected() { + TODO() + } + + @Test() + fun onItemSelectedWithCast() { + TODO() + } + + @Test() + fun onItemSelectedCastExceptionOnWrongClass() { + TODO() + } + + class WrongClass + + data class OnSelectedActionMock( + val parent: AdapterView<*>?, + val view: View?, + val position: Int, + val id: Long + ) + + companion object { + private const val K = "KitKat" + private const val L = "Lollipop" + private const val M = "Marshmallow" + private const val N = "Nougat" + private const val O = "Oreo" + private const val LAYOUT_WIDTH = 200 + private const val LAYOUT_HEIGHT = 200 + } +} \ No newline at end of file diff --git a/src/main/java/androidx/core/widget/AdapterView.kt b/src/main/java/androidx/core/widget/AdapterView.kt new file mode 100644 index 00000000..94e77310 --- /dev/null +++ b/src/main/java/androidx/core/widget/AdapterView.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.core.widget + +import android.view.View +import android.widget.AdapterView + +inline fun AdapterView<*>.onItemClick( + crossinline onItemClick: (item: ITEM) -> Unit +) { + setOnItemClickListener { _, _, position, _ -> + @Suppress("UNCHECKED_CAST") + onItemClick(getItemAtPosition(position) as ITEM) + } +} + +inline fun AdapterView<*>.onItemLongClick( + crossinline onItemLongClick: (item: ITEM) -> Boolean +) { + setOnItemLongClickListener { _, _, position, _ -> + @Suppress("UNCHECKED_CAST") + onItemLongClick(getItemAtPosition(position) as ITEM) + } +} + +inline fun AdapterView<*>.onItemSelected( + crossinline onNothingSelected: () -> Unit = {}, + crossinline onItemSelected: ( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) -> Unit +) { + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) = onNothingSelected() + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + onItemSelected(parent, view, position, id) + } + } +} + +inline fun AdapterView<*>.onItemSelected( + crossinline onNothingSelected: () -> Unit = {}, + crossinline onItemSelected: (item: ITEM) -> Unit +) { + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) = onNothingSelected() + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + @Suppress("UNCHECKED_CAST") + onItemSelected(getItemAtPosition(position) as ITEM) + } + } +} \ No newline at end of file From 23f855c708abc06c2d7410229063332b4589045d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Czaplicki?= Date: Wed, 25 Apr 2018 09:38:39 +0200 Subject: [PATCH 2/7] remove nullability from the parent, use parent to get item at position --- .../java/androidx/core/widget/AdapterView.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/androidx/core/widget/AdapterView.kt b/src/main/java/androidx/core/widget/AdapterView.kt index 94e77310..aa1a4b27 100644 --- a/src/main/java/androidx/core/widget/AdapterView.kt +++ b/src/main/java/androidx/core/widget/AdapterView.kt @@ -22,34 +22,34 @@ import android.widget.AdapterView inline fun AdapterView<*>.onItemClick( crossinline onItemClick: (item: ITEM) -> Unit ) { - setOnItemClickListener { _, _, position, _ -> + setOnItemClickListener { parent, _, position, _ -> @Suppress("UNCHECKED_CAST") - onItemClick(getItemAtPosition(position) as ITEM) + onItemClick(parent.getItemAtPosition(position) as ITEM) } } inline fun AdapterView<*>.onItemLongClick( crossinline onItemLongClick: (item: ITEM) -> Boolean ) { - setOnItemLongClickListener { _, _, position, _ -> + setOnItemLongClickListener { parent, _, position, _ -> @Suppress("UNCHECKED_CAST") - onItemLongClick(getItemAtPosition(position) as ITEM) + onItemLongClick(parent.getItemAtPosition(position) as ITEM) } } inline fun AdapterView<*>.onItemSelected( - crossinline onNothingSelected: () -> Unit = {}, + crossinline onNothingSelected: (parent: AdapterView<*>) -> Unit = {}, crossinline onItemSelected: ( - parent: AdapterView<*>?, + parent: AdapterView<*>, view: View?, position: Int, id: Long ) -> Unit ) { onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onNothingSelected(parent: AdapterView<*>?) = onNothingSelected() + override fun onNothingSelected(parent: AdapterView<*>) = onNothingSelected(parent) - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { onItemSelected(parent, view, position, id) } } @@ -60,11 +60,11 @@ inline fun AdapterView<*>.onItemSelected( crossinline onItemSelected: (item: ITEM) -> Unit ) { onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onNothingSelected(parent: AdapterView<*>?) = onNothingSelected() + override fun onNothingSelected(parent: AdapterView<*>) = onNothingSelected() - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { @Suppress("UNCHECKED_CAST") - onItemSelected(getItemAtPosition(position) as ITEM) + onItemSelected(parent.getItemAtPosition(position) as ITEM) } } } \ No newline at end of file From c885c5c23646e928a390bc9c3089d0e61175be81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Czaplicki?= Date: Wed, 25 Apr 2018 10:52:43 +0200 Subject: [PATCH 3/7] update api --- api/current.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/api/current.txt b/api/current.txt index 10444444..226aca4a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -653,19 +653,19 @@ package androidx.core.view { package androidx.core.widget { + public final class AdapterViewKt { + ctor public AdapterViewKt(); + method public static void onItemClick(android.widget.AdapterView, kotlin.jvm.functions.Function1 onItemClick); + method public static void onItemLongClick(android.widget.AdapterView, kotlin.jvm.functions.Function1 onItemLongClick); + method public static void onItemSelected(android.widget.AdapterView, kotlin.jvm.functions.Function1,kotlin.Unit> onNothingSelected = "{}", kotlin.jvm.functions.Function4,? super android.view.View,? super java.lang.Integer,? super java.lang.Long,kotlin.Unit> onItemSelected); + method public static void onItemSelected(android.widget.AdapterView, kotlin.jvm.functions.Function0 onNothingSelected = "{}", kotlin.jvm.functions.Function1 onItemSelected); + } + public final class ToastKt { ctor public ToastKt(); method public static android.widget.Toast toast(android.content.Context, CharSequence text, int duration = "Toast.LENGTH_SHORT"); method public static android.widget.Toast toast(android.content.Context, @StringRes int resId, int duration = "Toast.LENGTH_SHORT"); } - public final class AdapterViewKt { - ctor public AdapterViewKt(); - method public static void onItemClick(android.widget.AdapterView,kotlin.jvm.functions.Function1); - method public static void onItemLongClick(android.widget.AdapterView,kotlin.jvm.functions.Function1); - method public static void onItemSelected(android.widget.AdapterView,kotlin.jvm.functions.Function0 = "{}",kotlin.jvm.functions.Function4,? super android.view.View,? super java.lang.Integer,? super java.lang.Long,kotlin.Unit>); - method public static void onItemSelected(android.widget.AdapterView,kotlin.jvm.functions.Function0 = "{}",kotlin.jvm.functions.Function1); - } - } From fafa216015ee7ffee8697b41678d945b96fe9cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Czaplicki?= Date: Wed, 25 Apr 2018 16:57:29 +0200 Subject: [PATCH 4/7] add tests --- .../androidx/core/widget/AdapterViewTest.kt | 185 ++++++++++++------ 1 file changed, 125 insertions(+), 60 deletions(-) diff --git a/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt b/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt index 0070ce90..c0c3835f 100644 --- a/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt +++ b/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt @@ -17,112 +17,177 @@ package androidx.core.widget import android.support.test.InstrumentationRegistry -import android.view.View import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.ListView +import android.widget.Spinner import androidx.core.view.get -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue +import androidx.testutils.assertThrows +import org.junit.Assert.* import org.junit.Before import org.junit.Test import java.lang.ClassCastException +import java.lang.reflect.InvocationTargetException class AdapterViewTest { private val context = InstrumentationRegistry.getContext() - private lateinit var adapterView: AdapterView<*> - private lateinit var adapter: ArrayAdapter - private var itemUnderTest: String? = null + + private val data = listOf("KitKat", "Lollipop", "Marshmallow", "Nougat", "Oreo") + private val arrayAdapter: ArrayAdapter + get() = ArrayAdapter(context, android.R.layout.simple_list_item_1, data) + .apply { setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } + + private val listView: ListView + get() = ListView(context).apply { adapter = arrayAdapter } + private val spinner: Spinner + get() = Spinner(context).apply { adapter = arrayAdapter } + + private var testItem: Any? = null + private var testOnNothingSelectedFired = false @Before fun setup() { - adapterView = ListView(context) - adapter = ArrayAdapter( - context, - android.R.layout.simple_list_item_1, - listOf(K, L, M, N, O) - ) - adapterView.adapter = adapter - itemUnderTest = null + testItem = null + testOnNothingSelectedFired = false } - /** - * test that ext call is equal to call - * adapterView.setOnItemClickListener { _, _, position, _ -> - * itemUnderTest = adapterView.getItemAtPosition(position) as String - * } - */ @Test fun onItemClick() { - adapterView.onItemClick { item: String -> itemUnderTest = item } - - assertTrue("listener not set", adapterView.performItemClick(null, 1, -1)) - assertEquals(L, itemUnderTest) + val adapterView = listView + adapterView.onItemClick { item: String -> testItem = item } + for (position in data.indices) { + assertTrue( + "listener not set", + adapterView.performItemClick(null, position, AdapterView.INVALID_ROW_ID) + ) + assertEquals(data[position], testItem) + } } - @Test(expected = ClassCastException::class) + @Test fun onItemClickCastExceptionOnWrongClass() { - @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") - var any: Any? = null - adapterView.onItemClick { item: WrongClass -> any = item } - adapterView.performItemClick(null, 1, -1) + val adapterView = listView + adapterView.onItemClick { item: WrongClass -> testItem = item } + assertThrows { + adapterView.performItemClick(null, 1, AdapterView.INVALID_ROW_ID) + } } - /** - * test that ext call is equal to call - * adapterView.setOnItemLongClickListener { _, _, position, _ -> - * itemUnderTest = adapterView.getItemAtPosition(position) as String - * true - * } - */ + + @Test(expected = RuntimeException::class) + fun onItemClickRuntimeExceptionWithSpinner() { + spinner.onItemClick { _: Any? -> } + } + + // borrowed from https://android.googlesource.com/platform/cts/+/master/tests/tests/widget/src/android/widget/cts/AdapterViewTest.java#255 @Test fun onItemLongClick() { - adapterView.onItemLongClick { item: String -> itemUnderTest = item; true } + val adapterView = listView + adapterView.onItemLongClick { item: String -> testItem = item; true } adapterView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT) - adapterView.showContextMenuForChild(adapterView[1]) - assertEquals(L, itemUnderTest) + val position = 1 + adapterView.showContextMenuForChild(adapterView[position]) + assertEquals(data[position], testItem) } @Test(expected = ClassCastException::class) fun onItemLongClickCastExceptionOnWrongClass() { - @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") - var any: Any? = null - adapterView.onItemLongClick { item: WrongClass -> any = item; true } + val adapterView = listView + adapterView.onItemLongClick { item: WrongClass -> testItem = item; true } adapterView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT) adapterView.showContextMenuForChild(adapterView[1]) } - @Test() + @Test fun onItemSelected() { - TODO() + listOf(listView, spinner).forEach { adapterView -> + adapterView.onItemSelected { parent, _, position, _ -> + testItem = parent.getItemAtPosition(position) + } + for (i in data.indices) checkSelectionForPosition(adapterView, i) + } } - @Test() + @Test fun onItemSelectedWithCast() { - TODO() + listOf(listView, spinner).forEach { adapterView -> + adapterView.onItemSelected { item: String -> testItem = item } + for (i in data.indices) checkSelectionForPosition(adapterView, i) + } + } + + @Test + fun onItemSelectedWithCastExceptionOnWrongClass() { + listOf(listView, spinner).forEach { adapterView -> + adapterView.onItemSelected { item -> testItem = item } + assertThrows { + checkSelectionForPosition(adapterView, 1) + } + } } - @Test() - fun onItemSelectedCastExceptionOnWrongClass() { - TODO() + @Test + fun onItemSelectedWithHandledOnNothingSelected() { + val adapterView = spinner + adapterView.onItemSelected( + onNothingSelected = { _: AdapterView<*> -> testOnNothingSelectedFired = true }, + onItemSelected = { parent, _, position, _ -> + testItem = parent.getItemAtPosition(position) + }) + checkSelectionForPosition(adapterView, AdapterView.INVALID_POSITION) + for (i in data.indices) checkSelectionForPosition(adapterView, i) + checkSelectionForPosition(adapterView, AdapterView.INVALID_POSITION) + } + + @Test + fun onItemSelectedWithCastWithHandledOnNothingSelected() { + val adapterView = spinner + adapterView.onItemSelected( + onNothingSelected = { testOnNothingSelectedFired = true }, + onItemSelected = { item: String -> testItem = item } + ) + checkSelectionForPosition(adapterView, AdapterView.INVALID_POSITION) + for (i in data.indices) checkSelectionForPosition(adapterView, i) + checkSelectionForPosition(adapterView, AdapterView.INVALID_POSITION) } class WrongClass - data class OnSelectedActionMock( - val parent: AdapterView<*>?, - val view: View?, - val position: Int, - val id: Long - ) + private fun checkSelectionForPosition(adapterView: AdapterView<*>, position: Int) { + assertFalse(testOnNothingSelectedFired) + assertNull(testItem) + adapterView.setSelection(position) + fireOnSelected(adapterView) + if (position < 0) { + assertTrue(testOnNothingSelectedFired) + assertNull(testItem) + } else { + assertEquals(data[position], testItem) + assertFalse(testOnNothingSelectedFired) + } + testItem = null + testOnNothingSelectedFired = false + } + + /** + * reflection used to test to trigger selection + * + * workaround for using ActivityRule like here: https://android.googlesource.com/platform/cts/+/master/tests/tests/widget/src/android/widget/cts/AdapterViewTest.java#286 + */ + private fun fireOnSelected(adapterView: AdapterView<*>) { + try { + AdapterView::class.java.getDeclaredMethod("fireOnSelected") + .apply { + isAccessible = true + invoke(adapterView) + } + } catch (e: InvocationTargetException) { + throw e.targetException + } + } companion object { - private const val K = "KitKat" - private const val L = "Lollipop" - private const val M = "Marshmallow" - private const val N = "Nougat" - private const val O = "Oreo" private const val LAYOUT_WIDTH = 200 private const val LAYOUT_HEIGHT = 200 } From d79900efead51950d5eff556010c140cbc54876b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Czaplicki?= Date: Wed, 25 Apr 2018 23:03:18 +0200 Subject: [PATCH 5/7] clean up code, add documentation and comments --- api/current.txt | 6 +- .../androidx/core/widget/AdapterViewTest.kt | 126 ++++++++++-------- .../java/androidx/core/widget/AdapterView.kt | 65 +++++++-- 3 files changed, 131 insertions(+), 66 deletions(-) diff --git a/api/current.txt b/api/current.txt index 226aca4a..33fa9918 100644 --- a/api/current.txt +++ b/api/current.txt @@ -655,10 +655,10 @@ package androidx.core.widget { public final class AdapterViewKt { ctor public AdapterViewKt(); - method public static void onItemClick(android.widget.AdapterView, kotlin.jvm.functions.Function1 onItemClick); - method public static void onItemLongClick(android.widget.AdapterView, kotlin.jvm.functions.Function1 onItemLongClick); + method public static void onItemClick(android.widget.AdapterView, kotlin.jvm.functions.Function1 onItemClick); + method public static void onItemLongClick(android.widget.AdapterView, kotlin.jvm.functions.Function1 onItemLongClick); method public static void onItemSelected(android.widget.AdapterView, kotlin.jvm.functions.Function1,kotlin.Unit> onNothingSelected = "{}", kotlin.jvm.functions.Function4,? super android.view.View,? super java.lang.Integer,? super java.lang.Long,kotlin.Unit> onItemSelected); - method public static void onItemSelected(android.widget.AdapterView, kotlin.jvm.functions.Function0 onNothingSelected = "{}", kotlin.jvm.functions.Function1 onItemSelected); + method public static void onItemSelected(android.widget.AdapterView, kotlin.jvm.functions.Function0 onNothingSelected = "{}", kotlin.jvm.functions.Function1 onItemSelected); } public final class ToastKt { diff --git a/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt b/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt index c0c3835f..20f1b997 100644 --- a/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt +++ b/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt @@ -17,7 +17,10 @@ package androidx.core.widget import android.support.test.InstrumentationRegistry +import android.widget.AbsListView.CHOICE_MODE_SINGLE import android.widget.AdapterView +import android.widget.AdapterView.INVALID_POSITION +import android.widget.AdapterView.INVALID_ROW_ID import android.widget.ArrayAdapter import android.widget.ListView import android.widget.Spinner @@ -39,17 +42,17 @@ class AdapterViewTest { .apply { setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } private val listView: ListView - get() = ListView(context).apply { adapter = arrayAdapter } + get() = ListView(context).apply { adapter = arrayAdapter; choiceMode = CHOICE_MODE_SINGLE } private val spinner: Spinner get() = Spinner(context).apply { adapter = arrayAdapter } private var testItem: Any? = null - private var testOnNothingSelectedFired = false + private var testOnNothingSelectedTriggered = false @Before fun setup() { testItem = null - testOnNothingSelectedFired = false + testOnNothingSelectedTriggered = false } @Test @@ -59,19 +62,17 @@ class AdapterViewTest { for (position in data.indices) { assertTrue( "listener not set", - adapterView.performItemClick(null, position, AdapterView.INVALID_ROW_ID) + adapterView.performItemClick(null, position, INVALID_ROW_ID) ) assertEquals(data[position], testItem) } } - @Test + @Test(expected = ClassCastException::class) fun onItemClickCastExceptionOnWrongClass() { val adapterView = listView adapterView.onItemClick { item: WrongClass -> testItem = item } - assertThrows { - adapterView.performItemClick(null, 1, AdapterView.INVALID_ROW_ID) - } + adapterView.performItemClick(null, 1, INVALID_ROW_ID) } @@ -80,7 +81,9 @@ class AdapterViewTest { spinner.onItemClick { _: Any? -> } } - // borrowed from https://android.googlesource.com/platform/cts/+/master/tests/tests/widget/src/android/widget/cts/AdapterViewTest.java#255 + /** + * borrowed from [AdapterViewTest line:279](https://android.googlesource.com/platform/cts/+/42fbcbb2518ea10cc729c44614a93b182bf58696/tests/tests/widget/src/android/widget/cts/AdapterViewTest.java#279) + */ @Test fun onItemLongClick() { val adapterView = listView @@ -117,78 +120,97 @@ class AdapterViewTest { } } + @Test + fun onItemSelectedWithCastIgnoresOnNothingSelectedActions() { + listOf(listView, spinner).forEach { adapterView -> + adapterView.onItemSelected { item: String -> testItem = item } + assertFalse(testOnNothingSelectedTriggered) + assertNull(testItem) + selectAndFireOnSelected(adapterView, INVALID_POSITION) + assertFalse(testOnNothingSelectedTriggered) + assertNull(testItem) + } + } + @Test fun onItemSelectedWithCastExceptionOnWrongClass() { listOf(listView, spinner).forEach { adapterView -> adapterView.onItemSelected { item -> testItem = item } assertThrows { - checkSelectionForPosition(adapterView, 1) + for (i in data.indices) checkSelectionForPosition(adapterView, i) } } } @Test fun onItemSelectedWithHandledOnNothingSelected() { - val adapterView = spinner - adapterView.onItemSelected( - onNothingSelected = { _: AdapterView<*> -> testOnNothingSelectedFired = true }, - onItemSelected = { parent, _, position, _ -> - testItem = parent.getItemAtPosition(position) - }) - checkSelectionForPosition(adapterView, AdapterView.INVALID_POSITION) - for (i in data.indices) checkSelectionForPosition(adapterView, i) - checkSelectionForPosition(adapterView, AdapterView.INVALID_POSITION) + listOf(listView, spinner).forEach { adapterView -> + adapterView.onItemSelected( + onNothingSelected = { _: AdapterView<*> -> testOnNothingSelectedTriggered = true }, + onItemSelected = { parent, _, position, _ -> + testItem = parent.getItemAtPosition(position) + }) + checkSelectionForPosition(adapterView, INVALID_POSITION) + for (i in data.indices) checkSelectionForPosition(adapterView, i) + checkSelectionForPosition(adapterView, INVALID_POSITION) + } } @Test fun onItemSelectedWithCastWithHandledOnNothingSelected() { - val adapterView = spinner - adapterView.onItemSelected( - onNothingSelected = { testOnNothingSelectedFired = true }, - onItemSelected = { item: String -> testItem = item } - ) - checkSelectionForPosition(adapterView, AdapterView.INVALID_POSITION) - for (i in data.indices) checkSelectionForPosition(adapterView, i) - checkSelectionForPosition(adapterView, AdapterView.INVALID_POSITION) + listOf(listView, spinner).forEach { adapterView -> + adapterView.onItemSelected( + onNothingSelected = { testOnNothingSelectedTriggered = true }, + onItemSelected = { item: String -> testItem = item } + ) + checkSelectionForPosition(adapterView, INVALID_POSITION) + for (i in data.indices) checkSelectionForPosition(adapterView, i) + checkSelectionForPosition(adapterView, INVALID_POSITION) + } } - class WrongClass - private fun checkSelectionForPosition(adapterView: AdapterView<*>, position: Int) { - assertFalse(testOnNothingSelectedFired) + assertFalse(testOnNothingSelectedTriggered) assertNull(testItem) - adapterView.setSelection(position) - fireOnSelected(adapterView) + selectAndFireOnSelected(adapterView, position) if (position < 0) { - assertTrue(testOnNothingSelectedFired) + assertTrue(testOnNothingSelectedTriggered) assertNull(testItem) } else { + assertFalse(testOnNothingSelectedTriggered) assertEquals(data[position], testItem) - assertFalse(testOnNothingSelectedFired) } + testOnNothingSelectedTriggered = false testItem = null - testOnNothingSelectedFired = false - } - - /** - * reflection used to test to trigger selection - * - * workaround for using ActivityRule like here: https://android.googlesource.com/platform/cts/+/master/tests/tests/widget/src/android/widget/cts/AdapterViewTest.java#286 - */ - private fun fireOnSelected(adapterView: AdapterView<*>) { - try { - AdapterView::class.java.getDeclaredMethod("fireOnSelected") - .apply { - isAccessible = true - invoke(adapterView) - } - } catch (e: InvocationTargetException) { - throw e.targetException - } } companion object { private const val LAYOUT_WIDTH = 200 private const val LAYOUT_HEIGHT = 200 + + /** + * Reflection used to shortcut trigger selection via AdapterView#fireOnSelected() + * + * More comprehensive test would involve ActivityRule like in [AdapterViewTest line:286](https://android.googlesource.com/platform/cts/+/42fbcbb2518ea10cc729c44614a93b182bf58696/tests/tests/widget/src/android/widget/cts/AdapterViewTest.java#286) + * + * @see android.widget.AdapterView + */ + private fun selectAndFireOnSelected(adapterView: AdapterView<*>, position: Int) { + try { + AdapterView::class.java + .getDeclaredMethod("setNextSelectedPositionInt", Int::class.java) + .apply { isAccessible = true } + .invoke(adapterView, position) + AdapterView::class.java + .getDeclaredMethod("fireOnSelected") + .apply { isAccessible = true } + .invoke(adapterView) + } catch (e: InvocationTargetException) { + throw e.targetException + } + } + + class WrongClass + } } \ No newline at end of file diff --git a/src/main/java/androidx/core/widget/AdapterView.kt b/src/main/java/androidx/core/widget/AdapterView.kt index aa1a4b27..d1214b6c 100644 --- a/src/main/java/androidx/core/widget/AdapterView.kt +++ b/src/main/java/androidx/core/widget/AdapterView.kt @@ -19,24 +19,47 @@ package androidx.core.widget import android.view.View import android.widget.AdapterView -inline fun AdapterView<*>.onItemClick( - crossinline onItemClick: (item: ITEM) -> Unit -) { +/** + * Sets click listener with automatic item casting + * (ClassCastException will be thrown if adapter's item type does not match) + */ +inline fun AdapterView<*>.onItemClick(crossinline onItemClick: (item: T) -> Unit) { setOnItemClickListener { parent, _, position, _ -> @Suppress("UNCHECKED_CAST") - onItemClick(parent.getItemAtPosition(position) as ITEM) + onItemClick(parent.getItemAtPosition(position) as T) } } -inline fun AdapterView<*>.onItemLongClick( - crossinline onItemLongClick: (item: ITEM) -> Boolean -) { +/** + * Sets long click listener with automatic item casting + * (ClassCastException will be thrown if adapter's item type does not match) + */ +inline fun AdapterView<*>.onItemLongClick(crossinline onItemLongClick: (item: T) -> Boolean) { setOnItemLongClickListener { parent, _, position, _ -> @Suppress("UNCHECKED_CAST") - onItemLongClick(parent.getItemAtPosition(position) as ITEM) + onItemLongClick(parent.getItemAtPosition(position) as T) } } +/** + * Simple use case (default empty `onNothingSelected` set): + * ```kotlin + * spinner.onItemSelected { parent, _, position, _ -> + * val item = parent.getItemAtPosition(position) + * ??? + * } + * ``` + * Use case with `onNothingSelected` handling: + * ```kotlin + * spinner.onItemSelected( + * onNothingSelected = { _: AdapterView<*> -> ??? }, + * onItemSelected = { parent, _, position, _ -> + * val item = parent.getItemAtPosition(position) + * ??? + * }) + * ``` + * @see android.widget.AdapterView.OnItemSelectedListener + */ inline fun AdapterView<*>.onItemSelected( crossinline onNothingSelected: (parent: AdapterView<*>) -> Unit = {}, crossinline onItemSelected: ( @@ -55,16 +78,36 @@ inline fun AdapterView<*>.onItemSelected( } } -inline fun AdapterView<*>.onItemSelected( +/** + * Sets selection listener with automatic item casting + * (ClassCastException will be thrown if adapter's item type does not match) + * + * Simple use case (default empty `onNothingSelected` set): + * ```kotlin + * spinner.onItemSelected { item: T -> ??? } + * ``` + * Use case with `onNothingSelected` handling: + * ```kotlin + * + * spinner.onItemSelected( + * onNothingSelected = { ??? }, + * onItemSelected = { item: String -> ??? } + * ) + * ``` + * @param onNothingSelected optional action, default `{}` + * @param onItemSelected action with casted item passed + * @see android.widget.AdapterView.OnItemSelectedListener + */ +inline fun AdapterView<*>.onItemSelected( crossinline onNothingSelected: () -> Unit = {}, - crossinline onItemSelected: (item: ITEM) -> Unit + crossinline onItemSelected: (item: T) -> Unit ) { onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onNothingSelected(parent: AdapterView<*>) = onNothingSelected() override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { @Suppress("UNCHECKED_CAST") - onItemSelected(parent.getItemAtPosition(position) as ITEM) + onItemSelected(parent.getItemAtPosition(position) as T) } } } \ No newline at end of file From 122f76c7ea6b8525ba780827a93255ee55d60bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Czaplicki?= Date: Wed, 25 Apr 2018 23:11:54 +0200 Subject: [PATCH 6/7] fix style issues --- .../java/androidx/core/widget/AdapterViewTest.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt b/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt index 20f1b997..51b683d3 100644 --- a/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt +++ b/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt @@ -26,7 +26,10 @@ import android.widget.ListView import android.widget.Spinner import androidx.core.view.get import androidx.testutils.assertThrows -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.lang.ClassCastException @@ -75,7 +78,6 @@ class AdapterViewTest { adapterView.performItemClick(null, 1, INVALID_ROW_ID) } - @Test(expected = RuntimeException::class) fun onItemClickRuntimeExceptionWithSpinner() { spinner.onItemClick { _: Any? -> } @@ -188,6 +190,8 @@ class AdapterViewTest { private const val LAYOUT_WIDTH = 200 private const val LAYOUT_HEIGHT = 200 + class WrongClass + /** * Reflection used to shortcut trigger selection via AdapterView#fireOnSelected() * @@ -209,8 +213,5 @@ class AdapterViewTest { throw e.targetException } } - - class WrongClass - } } \ No newline at end of file From fd55e68bace553be64415ee1ef9dc79a34c06bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Czaplicki?= Date: Wed, 25 Apr 2018 23:31:03 +0200 Subject: [PATCH 7/7] add newlines at the end of files (mostly for pipeline retrigger) --- .../java/androidx/core/widget/AdapterViewTest.kt | 2 +- src/main/java/androidx/core/widget/AdapterView.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt b/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt index 51b683d3..b815d2d0 100644 --- a/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt +++ b/src/androidTest/java/androidx/core/widget/AdapterViewTest.kt @@ -214,4 +214,4 @@ class AdapterViewTest { } } } -} \ No newline at end of file +} diff --git a/src/main/java/androidx/core/widget/AdapterView.kt b/src/main/java/androidx/core/widget/AdapterView.kt index d1214b6c..7c02db4c 100644 --- a/src/main/java/androidx/core/widget/AdapterView.kt +++ b/src/main/java/androidx/core/widget/AdapterView.kt @@ -42,7 +42,7 @@ inline fun AdapterView<*>.onItemLongClick(crossinline onItemLongClick: (item } /** - * Simple use case (default empty `onNothingSelected` set): + * Simple use case (empty `onNothingSelected` default provided): * ```kotlin * spinner.onItemSelected { parent, _, position, _ -> * val item = parent.getItemAtPosition(position) @@ -82,7 +82,7 @@ inline fun AdapterView<*>.onItemSelected( * Sets selection listener with automatic item casting * (ClassCastException will be thrown if adapter's item type does not match) * - * Simple use case (default empty `onNothingSelected` set): + * Simple use case (empty `onNothingSelected` default provided): * ```kotlin * spinner.onItemSelected { item: T -> ??? } * ``` @@ -110,4 +110,4 @@ inline fun AdapterView<*>.onItemSelected( onItemSelected(parent.getItemAtPosition(position) as T) } } -} \ No newline at end of file +}