Skip to content
This repository has been archived by the owner on Nov 14, 2018. It is now read-only.

Add adapterview listener setters #484

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
8 changes: 8 additions & 0 deletions api/current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,14 @@ package androidx.core.view {

package androidx.core.widget {

public final class AdapterViewKt {
ctor public AdapterViewKt();
method public static <T> void onItemClick(android.widget.AdapterView<?>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onItemClick);
method public static <T> void onItemLongClick(android.widget.AdapterView<?>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> onItemLongClick);
method public static void onItemSelected(android.widget.AdapterView<?>, kotlin.jvm.functions.Function1<? super android.widget.AdapterView<?>,kotlin.Unit> onNothingSelected = "{}", kotlin.jvm.functions.Function4<? super android.widget.AdapterView<?>,? super android.view.View,? super java.lang.Integer,? super java.lang.Long,kotlin.Unit> onItemSelected);
method public static <T> void onItemSelected(android.widget.AdapterView<?>, kotlin.jvm.functions.Function0<kotlin.Unit> onNothingSelected = "{}", kotlin.jvm.functions.Function1<? super T,kotlin.Unit> 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");
Expand Down
217 changes: 217 additions & 0 deletions src/androidTest/java/androidx/core/widget/AdapterViewTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* 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.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
import androidx.core.view.get
import androidx.testutils.assertThrows
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
import java.lang.reflect.InvocationTargetException

class AdapterViewTest {

private val context = InstrumentationRegistry.getContext()

private val data = listOf("KitKat", "Lollipop", "Marshmallow", "Nougat", "Oreo")
private val arrayAdapter: ArrayAdapter<String>
get() = ArrayAdapter<String>(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; choiceMode = CHOICE_MODE_SINGLE }
private val spinner: Spinner
get() = Spinner(context).apply { adapter = arrayAdapter }

private var testItem: Any? = null
private var testOnNothingSelectedTriggered = false

@Before
fun setup() {
testItem = null
testOnNothingSelectedTriggered = false
}

@Test
fun onItemClick() {
val adapterView = listView
adapterView.onItemClick { item: String -> testItem = item }
for (position in data.indices) {
assertTrue(
"listener not set",
adapterView.performItemClick(null, position, INVALID_ROW_ID)
)
assertEquals(data[position], testItem)
}
}

@Test(expected = ClassCastException::class)
fun onItemClickCastExceptionOnWrongClass() {
val adapterView = listView
adapterView.onItemClick { item: WrongClass -> testItem = item }
adapterView.performItemClick(null, 1, INVALID_ROW_ID)
}

@Test(expected = RuntimeException::class)
fun onItemClickRuntimeExceptionWithSpinner() {
spinner.onItemClick { _: Any? -> }
}

/**
* 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
adapterView.onItemLongClick { item: String -> testItem = item; true }
adapterView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT)
val position = 1
adapterView.showContextMenuForChild(adapterView[position])
assertEquals(data[position], testItem)
}

@Test(expected = ClassCastException::class)
fun onItemLongClickCastExceptionOnWrongClass() {
val adapterView = listView
adapterView.onItemLongClick { item: WrongClass -> testItem = item; true }
adapterView.layout(0, 0, LAYOUT_WIDTH, LAYOUT_HEIGHT)
adapterView.showContextMenuForChild(adapterView[1])
}

@Test
fun onItemSelected() {
listOf(listView, spinner).forEach { adapterView ->
adapterView.onItemSelected { parent, _, position, _ ->
testItem = parent.getItemAtPosition(position)
}
for (i in data.indices) checkSelectionForPosition(adapterView, i)
}
}

@Test
fun onItemSelectedWithCast() {
listOf(listView, spinner).forEach { adapterView ->
adapterView.onItemSelected { item: String -> testItem = item }
for (i in data.indices) checkSelectionForPosition(adapterView, i)
}
}

@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<WrongClass> { item -> testItem = item }
assertThrows<ClassCastException> {
for (i in data.indices) checkSelectionForPosition(adapterView, i)
}
}
}

@Test
fun onItemSelectedWithHandledOnNothingSelected() {
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() {
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)
}
}

private fun checkSelectionForPosition(adapterView: AdapterView<*>, position: Int) {
assertFalse(testOnNothingSelectedTriggered)
assertNull(testItem)
selectAndFireOnSelected(adapterView, position)
if (position < 0) {
assertTrue(testOnNothingSelectedTriggered)
assertNull(testItem)
} else {
assertFalse(testOnNothingSelectedTriggered)
assertEquals(data[position], testItem)
}
testOnNothingSelectedTriggered = false
testItem = null
}

companion object {
private const val LAYOUT_WIDTH = 200
private const val LAYOUT_HEIGHT = 200

class WrongClass

/**
* 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
}
}
}
}
113 changes: 113 additions & 0 deletions src/main/java/androidx/core/widget/AdapterView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* 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

/**
* Sets click listener with automatic item casting
* (ClassCastException will be thrown if adapter's item type does not match)
*/
inline fun <T> AdapterView<*>.onItemClick(crossinline onItemClick: (item: T) -> Unit) {
setOnItemClickListener { parent, _, position, _ ->
@Suppress("UNCHECKED_CAST")
onItemClick(parent.getItemAtPosition(position) as T)
}
}

/**
* Sets long click listener with automatic item casting
* (ClassCastException will be thrown if adapter's item type does not match)
*/
inline fun <T> AdapterView<*>.onItemLongClick(crossinline onItemLongClick: (item: T) -> Boolean) {
setOnItemLongClickListener { parent, _, position, _ ->
@Suppress("UNCHECKED_CAST")
onItemLongClick(parent.getItemAtPosition(position) as T)
}
}

/**
* Simple use case (empty `onNothingSelected` default provided):
* ```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: (
parent: AdapterView<*>,
view: View?,
position: Int,
id: Long
) -> Unit
) {
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>) = onNothingSelected(parent)

override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
onItemSelected(parent, view, position, id)
}
}
}

/**
* Sets selection listener with automatic item casting
* (ClassCastException will be thrown if adapter's item type does not match)
*
* Simple use case (empty `onNothingSelected` default provided):
* ```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 <T> AdapterView<*>.onItemSelected(
crossinline onNothingSelected: () -> 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 T)
}
}
}