diff options
| author | 2023-08-28 16:27:17 +0000 | |
|---|---|---|
| committer | 2023-08-28 16:27:17 +0000 | |
| commit | 90db903ff22eba476dd898a82aaa9b10a9eb773f (patch) | |
| tree | 252279bc79cf9cd0eae3a3119235642a83e58215 | |
| parent | b6a6a939161d6cd1844c7d2486db00c4ee59d55a (diff) | |
| parent | 75e5a76d93b007d474ec442b8affc179ae187944 (diff) | |
Merge "Introducing API to produce slider events." into udc-qpr-dev
5 files changed, 288 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt new file mode 100644 index 000000000000..629b361064a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 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 com.android.systemui.haptics.slider + +import android.widget.SeekBar +import android.widget.SeekBar.OnSeekBarChangeListener +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +/** An event producer for a Seekable element such as the Android [SeekBar] */ +class SeekableSliderEventProducer : SliderEventProducer, OnSeekBarChangeListener { + + /** The current event reported by a SeekBar */ + private val _currentEvent = MutableStateFlow(SliderEvent(SliderEventType.NOTHING, 0f)) + + override fun produceEvents(): Flow<SliderEvent> = _currentEvent.asStateFlow() + + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + val eventType = + if (fromUser) SliderEventType.PROGRESS_CHANGE_BY_USER + else SliderEventType.PROGRESS_CHANGE_BY_PROGRAM + + _currentEvent.value = SliderEvent(eventType, normalizeProgress(seekBar, progress)) + } + + /** + * Normalize the integer progress of a SeekBar to the range from 0F to 1F. + * + * @param[seekBar] The SeekBar that reports a progress. + * @param[progress] The integer progress of the SeekBar within its min and max values. + * @return The progress in the range from 0F to 1F. + */ + private fun normalizeProgress(seekBar: SeekBar, progress: Int): Float { + if (seekBar.max == seekBar.min) { + return 1.0f + } + val range = seekBar.max - seekBar.min + return (progress - seekBar.min) / range.toFloat() + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + _currentEvent.update { previousEvent -> + SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, previousEvent.currentProgress) + } + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + _currentEvent.update { previousEvent -> + SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, previousEvent.currentProgress) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEvent.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEvent.kt new file mode 100644 index 000000000000..1377b29d8d21 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEvent.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 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 com.android.systemui.haptics.slider + +import androidx.annotation.FloatRange + +/** + * An event arising from a slider. + * + * @property[type] The type of event. Must be one of [SliderEventType]. + * @property[currentProgress] The current progress of the slider normalized to the range between 0F + * and 1F (inclusive). + */ +data class SliderEvent( + val type: SliderEventType, + @FloatRange(from = 0.0, to = 1.0) val currentProgress: Float +) diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventProducer.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventProducer.kt new file mode 100644 index 000000000000..8b17e86e7cc8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventProducer.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 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 com.android.systemui.haptics.slider + +import kotlinx.coroutines.flow.Flow + +/** Defines a producer of [SliderEvent] to be consumed as a [Flow] */ +interface SliderEventProducer { + + /** + * Produce a stream of [SliderEvent] + * + * @return A [Flow] of [SliderEvent] produced from the operation of a slider. + */ + fun produceEvents(): Flow<SliderEvent> +} diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt new file mode 100644 index 000000000000..413e27763ba8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 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 com.android.systemui.haptics.slider + +/** The type of a [SliderEvent]. */ +enum class SliderEventType { + /* No event. */ + NOTHING, + /* The slider has captured a touch input and is tracking touch events. */ + STARTED_TRACKING_TOUCH, + /* The slider progress is changing due to user touch input. */ + PROGRESS_CHANGE_BY_USER, + /* The slider progress is changing programmatically. */ + PROGRESS_CHANGE_BY_PROGRAM, + /* The slider has stopped tracking touch events. */ + STOPPED_TRACKING_TOUCH, + /* The external (not touch) stimulus that was modifying the slider progress has stopped. */ + EXTERNAL_STIMULUS_RELEASE, +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt new file mode 100644 index 000000000000..71a56cd8588c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2023 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 com.android.systemui.haptics.slider + +import android.widget.SeekBar +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SeekableSliderEventProducerTest : SysuiTestCase() { + + private val seekBar = SeekBar(mContext) + private val eventProducer = SeekableSliderEventProducer() + private val eventFlow = eventProducer.produceEvents() + + @Test + fun onStartTrackingTouch_noProgress_trackingTouchEventProduced() = runTest { + val latest by collectLastValue(eventFlow) + + eventProducer.onStartTrackingTouch(seekBar) + + assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0F), latest) + } + + @Test + fun onStopTrackingTouch_noProgress_StoppedTrackingTouchEventProduced() = runTest { + val latest by collectLastValue(eventFlow) + + eventProducer.onStopTrackingTouch(seekBar) + + assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0F), latest) + } + + @Test + fun onProgressChangeByUser_changeByUserEventProduced_withNormalizedProgress() = runTest { + val progress = 50 + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(seekBar, progress, true) + + assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5F), latest) + } + + @Test + fun onProgressChangeByUser_zeroWidthSlider_changeByUserEventProduced_withMaxProgress() = + runTest { + // No-width slider where the min and max values are the same + seekBar.min = 100 + seekBar.max = 100 + val progress = 50 + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(seekBar, progress, true) + + assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 1.0F), latest) + } + + @Test + fun onProgressChangeByProgram_changeByProgramEventProduced_withNormalizedProgress() = runTest { + val progress = 50 + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(seekBar, progress, false) + + assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 0.5F), latest) + } + + @Test + fun onProgressChangeByProgram_zeroWidthSlider_changeByProgramEventProduced_withMaxProgress() = + runTest { + // No-width slider where the min and max values are the same + seekBar.min = 100 + seekBar.max = 100 + val progress = 50 + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(seekBar, progress, false) + + assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 1.0F), latest) + } + + @Test + fun onStartTrackingTouch_afterProgress_trackingTouchEventProduced_withNormalizedProgress() = + runTest { + val progress = 50 + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(seekBar, progress, true) + eventProducer.onStartTrackingTouch(seekBar) + + assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0.5F), latest) + } + + @Test + fun onStopTrackingTouch_afterProgress_stopTrackingTouchEventProduced_withNormalizedProgress() = + runTest { + val progress = 50 + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(seekBar, progress, true) + eventProducer.onStopTrackingTouch(seekBar) + + assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5F), latest) + } +} |