Skip to content
This repository was archived by the owner on Nov 14, 2018. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
}