blob: d8b26e2e68d85d2e72a27b1d5e156654b5ba8b0f [file] [log] [blame]
Matt Pietala635bd12020-02-03 13:36:18 -05001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.controls.ui
18
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070019import android.animation.Animator
20import android.animation.AnimatorListenerAdapter
21import android.animation.ValueAnimator
Matt Pietala635bd12020-02-03 13:36:18 -050022import android.content.Context
23import android.graphics.drawable.Drawable
24import android.graphics.drawable.LayerDrawable
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070025import android.os.Bundle
26import android.service.controls.Control
27import android.service.controls.actions.FloatAction
28import android.service.controls.templates.RangeTemplate
29import android.service.controls.templates.ToggleRangeTemplate
Matt Pietal307b1ef2020-02-10 07:27:01 -050030import android.util.Log
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070031import android.util.MathUtils
32import android.util.TypedValue
Matt Pietal307b1ef2020-02-10 07:27:01 -050033import android.view.GestureDetector
34import android.view.GestureDetector.SimpleOnGestureListener
Matt Pietala635bd12020-02-03 13:36:18 -050035import android.view.MotionEvent
36import android.view.View
Matt Pietal31ec9f22020-03-12 09:02:17 -040037import android.view.ViewGroup
38import android.view.accessibility.AccessibilityEvent
39import android.view.accessibility.AccessibilityNodeInfo
Matt Pietala635bd12020-02-03 13:36:18 -050040import android.widget.TextView
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070041import com.android.systemui.Interpolators
Matt Pietala635bd12020-02-03 13:36:18 -050042import com.android.systemui.R
Matt Pietal307b1ef2020-02-10 07:27:01 -050043import com.android.systemui.controls.ui.ControlActionCoordinator.MAX_LEVEL
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070044import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL
Matt Pietal307b1ef2020-02-10 07:27:01 -050045import java.util.IllegalFormatException
Matt Pietala635bd12020-02-03 13:36:18 -050046
47class ToggleRangeBehavior : Behavior {
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070048 private var rangeAnimator: ValueAnimator? = null
Matt Pietala635bd12020-02-03 13:36:18 -050049 lateinit var clipLayer: Drawable
50 lateinit var template: ToggleRangeTemplate
51 lateinit var control: Control
52 lateinit var cvh: ControlViewHolder
53 lateinit var rangeTemplate: RangeTemplate
Matt Pietala635bd12020-02-03 13:36:18 -050054 lateinit var status: TextView
55 lateinit var context: Context
Matt Pietal5f478c72020-04-01 15:53:54 -040056 var currentStatusText: CharSequence = ""
57 var currentRangeValue: String = ""
Matt Pietala635bd12020-02-03 13:36:18 -050058
Matt Pietal307b1ef2020-02-10 07:27:01 -050059 companion object {
60 private const val DEFAULT_FORMAT = "%.1f"
61 }
62
Matt Pietalb582b692020-02-14 19:37:57 -050063 override fun initialize(cvh: ControlViewHolder) {
Matt Pietala635bd12020-02-03 13:36:18 -050064 this.cvh = cvh
Matt Pietala635bd12020-02-03 13:36:18 -050065 status = cvh.status
Matt Pietala635bd12020-02-03 13:36:18 -050066 context = status.getContext()
67
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070068 cvh.applyRenderInfo(false /* enabled */, 0 /* offset */, false /* animated */)
Matt Pietalb582b692020-02-14 19:37:57 -050069
Matt Pietal307b1ef2020-02-10 07:27:01 -050070 val gestureListener = ToggleRangeGestureListener(cvh.layout)
71 val gestureDetector = GestureDetector(context, gestureListener)
Matt Pietal1cbf78d2020-03-26 11:16:42 -040072 cvh.layout.setOnTouchListener { v: View, e: MotionEvent ->
Matt Pietal307b1ef2020-02-10 07:27:01 -050073 if (gestureDetector.onTouchEvent(e)) {
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070074 // Don't return true to let the state list change to "pressed"
75 return@setOnTouchListener false
Matt Pietal307b1ef2020-02-10 07:27:01 -050076 }
77
78 if (e.getAction() == MotionEvent.ACTION_UP && gestureListener.isDragging) {
Matt Pietal1cbf78d2020-03-26 11:16:42 -040079 v.getParent().requestDisallowInterceptTouchEvent(false)
Matt Pietal307b1ef2020-02-10 07:27:01 -050080 gestureListener.isDragging = false
81 endUpdateRange()
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070082 return@setOnTouchListener false
Matt Pietal307b1ef2020-02-10 07:27:01 -050083 }
84
85 return@setOnTouchListener false
Fabian Kozynski6e51a7052020-02-19 12:54:31 -050086 }
Matt Pietalb582b692020-02-14 19:37:57 -050087 }
88
89 override fun bind(cws: ControlWithState) {
90 this.control = cws.control!!
91
Matt Pietal5f478c72020-04-01 15:53:54 -040092 currentStatusText = control.getStatusText()
93 status.setText(currentStatusText)
Matt Pietala635bd12020-02-03 13:36:18 -050094
Lucas Dupin2ee4ec92020-04-13 19:51:14 -070095 // ControlViewHolder sets a long click listener, but we want to handle touch in
96 // here instead, otherwise we'll have state conflicts.
97 cvh.layout.setOnLongClickListener(null)
98
Matt Pietala635bd12020-02-03 13:36:18 -050099 val ld = cvh.layout.getBackground() as LayerDrawable
100 clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
101
102 template = control.getControlTemplate() as ToggleRangeTemplate
103 rangeTemplate = template.getRange()
104
105 val checked = template.isChecked()
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700106 updateRange(rangeToLevelValue(rangeTemplate.currentValue), checked, /* isDragging */ false)
Matt Pietala635bd12020-02-03 13:36:18 -0500107
Matt Pietal53a8bbd2020-03-05 16:10:34 -0500108 cvh.applyRenderInfo(checked)
Matt Pietal31ec9f22020-03-12 09:02:17 -0400109
110 /*
111 * This is custom widget behavior, so add a new accessibility delegate to
112 * handle clicks and range events. Present as a seek bar control.
113 */
114 cvh.layout.setAccessibilityDelegate(object : View.AccessibilityDelegate() {
115 override fun onInitializeAccessibilityNodeInfo(
116 host: View,
117 info: AccessibilityNodeInfo
118 ) {
119 super.onInitializeAccessibilityNodeInfo(host, info)
120
121 val min = levelToRangeValue(MIN_LEVEL)
122 val current = levelToRangeValue(clipLayer.getLevel())
123 val max = levelToRangeValue(MAX_LEVEL)
124
125 val step = rangeTemplate.getStepValue().toDouble()
126 val type = if (step == Math.floor(step)) {
127 AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT
128 } else {
129 AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_FLOAT
130 }
131
132 val rangeInfo = AccessibilityNodeInfo.RangeInfo.obtain(type, min, max, current)
133 info.setRangeInfo(rangeInfo)
134 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS)
135 }
136
137 override fun performAccessibilityAction(
138 host: View,
139 action: Int,
140 arguments: Bundle?
141 ): Boolean {
142 val handled = when (action) {
143 AccessibilityNodeInfo.ACTION_CLICK -> {
144 ControlActionCoordinator.toggle(cvh, template.getTemplateId(),
145 template.isChecked())
146 true
147 }
148 AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.getId() -> {
149 if (arguments == null || !arguments.containsKey(
150 AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)) {
151 false
152 } else {
153 val value = arguments.getFloat(
154 AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700155 val level = rangeToLevelValue(value - rangeTemplate.getCurrentValue())
156 updateRange(level, template.isChecked(), /* isDragging */ false)
Matt Pietal31ec9f22020-03-12 09:02:17 -0400157 endUpdateRange()
158 true
159 }
160 }
161 else -> false
162 }
163
164 return handled || super.performAccessibilityAction(host, action, arguments)
165 }
166
167 override fun onRequestSendAccessibilityEvent(
168 host: ViewGroup,
169 child: View,
170 event: AccessibilityEvent
171 ): Boolean = false
172 })
Matt Pietala635bd12020-02-03 13:36:18 -0500173 }
174
Matt Pietala635bd12020-02-03 13:36:18 -0500175 fun beginUpdateRange() {
Matt Pietal5f478c72020-04-01 15:53:54 -0400176 status.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
Matt Pietala635bd12020-02-03 13:36:18 -0500177 .getDimensionPixelSize(R.dimen.control_status_expanded).toFloat())
178 }
179
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700180 fun updateRange(level: Int, checked: Boolean, isDragging: Boolean) {
181 val newLevel = if (checked) Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, level)) else MIN_LEVEL
182
183 rangeAnimator?.cancel()
184 if (isDragging) {
185 clipLayer.level = newLevel
186 } else {
187 rangeAnimator = ValueAnimator.ofInt(cvh.clipLayer.level, newLevel).apply {
188 addUpdateListener {
189 cvh.clipLayer.level = it.animatedValue as Int
190 }
191 addListener(object : AnimatorListenerAdapter() {
192 override fun onAnimationEnd(animation: Animator?) {
193 rangeAnimator = null
194 }
195 })
196 duration = ControlViewHolder.STATE_ANIMATION_DURATION
197 interpolator = Interpolators.CONTROL_STATE
198 start()
199 }
200 }
Matt Pietala635bd12020-02-03 13:36:18 -0500201
Matt Pietal307b1ef2020-02-10 07:27:01 -0500202 if (checked) {
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700203 val newValue = levelToRangeValue(newLevel)
Matt Pietal5f478c72020-04-01 15:53:54 -0400204 currentRangeValue = format(rangeTemplate.getFormatString().toString(),
Matt Pietal307b1ef2020-02-10 07:27:01 -0500205 DEFAULT_FORMAT, newValue)
Matt Pietal5f478c72020-04-01 15:53:54 -0400206 val text = if (isDragging) {
207 currentRangeValue
208 } else {
209 "$currentStatusText $currentRangeValue"
210 }
211 status.setText(text)
Matt Pietala635bd12020-02-03 13:36:18 -0500212 } else {
Matt Pietal5f478c72020-04-01 15:53:54 -0400213 status.setText(currentStatusText)
Matt Pietala635bd12020-02-03 13:36:18 -0500214 }
215 }
216
Matt Pietal307b1ef2020-02-10 07:27:01 -0500217 private fun format(primaryFormat: String, backupFormat: String, value: Float): String {
218 return try {
219 String.format(primaryFormat, value)
220 } catch (e: IllegalFormatException) {
221 Log.w(ControlsUiController.TAG, "Illegal format in range template", e)
222 if (backupFormat == "") {
223 ""
224 } else {
225 format(backupFormat, "", value)
226 }
227 }
228 }
Matt Pietala635bd12020-02-03 13:36:18 -0500229
Matt Pietal31ec9f22020-03-12 09:02:17 -0400230 private fun levelToRangeValue(i: Int): Float {
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700231 return MathUtils.constrainedMap(rangeTemplate.minValue, rangeTemplate.maxValue,
232 MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(), i.toFloat())
233 }
234
235 private fun rangeToLevelValue(i: Float): Int {
236 return MathUtils.constrainedMap(MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(),
237 rangeTemplate.minValue, rangeTemplate.maxValue, i).toInt()
Matt Pietal307b1ef2020-02-10 07:27:01 -0500238 }
Matt Pietala635bd12020-02-03 13:36:18 -0500239
Matt Pietal307b1ef2020-02-10 07:27:01 -0500240 fun endUpdateRange() {
Matt Pietal5f478c72020-04-01 15:53:54 -0400241 status.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
Matt Pietala635bd12020-02-03 13:36:18 -0500242 .getDimensionPixelSize(R.dimen.control_status_normal).toFloat())
Matt Pietal5f478c72020-04-01 15:53:54 -0400243 status.setText("$currentStatusText $currentRangeValue")
Matt Pietal31ec9f22020-03-12 09:02:17 -0400244 cvh.action(FloatAction(rangeTemplate.getTemplateId(),
245 findNearestStep(levelToRangeValue(clipLayer.getLevel()))))
Matt Pietala635bd12020-02-03 13:36:18 -0500246 }
247
248 fun findNearestStep(value: Float): Float {
249 var minDiff = 1000f
250
251 var f = rangeTemplate.getMinValue()
252 while (f <= rangeTemplate.getMaxValue()) {
253 val currentDiff = Math.abs(value - f)
254 if (currentDiff < minDiff) {
255 minDiff = currentDiff
256 } else {
257 return f - rangeTemplate.getStepValue()
258 }
259
260 f += rangeTemplate.getStepValue()
261 }
262
263 return rangeTemplate.getMaxValue()
264 }
265
Matt Pietal307b1ef2020-02-10 07:27:01 -0500266 inner class ToggleRangeGestureListener(
267 val v: View
268 ) : SimpleOnGestureListener() {
269 var isDragging: Boolean = false
Matt Pietala635bd12020-02-03 13:36:18 -0500270
Matt Pietal307b1ef2020-02-10 07:27:01 -0500271 override fun onDown(e: MotionEvent): Boolean {
Matt Pietala635bd12020-02-03 13:36:18 -0500272 return true
273 }
274
Matt Pietal307b1ef2020-02-10 07:27:01 -0500275 override fun onLongPress(e: MotionEvent) {
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700276 if (isDragging) {
277 return
278 }
Matt Pietal307b1ef2020-02-10 07:27:01 -0500279 ControlActionCoordinator.longPress(this@ToggleRangeBehavior.cvh)
Matt Pietala635bd12020-02-03 13:36:18 -0500280 }
281
Matt Pietal307b1ef2020-02-10 07:27:01 -0500282 override fun onScroll(
283 e1: MotionEvent,
284 e2: MotionEvent,
285 xDiff: Float,
286 yDiff: Float
287 ): Boolean {
Lucas Dupin3e18f222020-04-13 15:11:20 -0700288 if (!template.isChecked) {
289 return false
290 }
Matt Pietala635bd12020-02-03 13:36:18 -0500291 if (!isDragging) {
Matt Pietal1cbf78d2020-03-26 11:16:42 -0400292 v.getParent().requestDisallowInterceptTouchEvent(true)
Matt Pietal307b1ef2020-02-10 07:27:01 -0500293 this@ToggleRangeBehavior.beginUpdateRange()
294 isDragging = true
Matt Pietala635bd12020-02-03 13:36:18 -0500295 }
296
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700297 val ratioDiff = -xDiff / v.width
298 val changeAmount = ((MAX_LEVEL - MIN_LEVEL) * ratioDiff).toInt()
299 this@ToggleRangeBehavior.updateRange(clipLayer.level + changeAmount,
300 checked = true, isDragging = true)
Matt Pietal307b1ef2020-02-10 07:27:01 -0500301 return true
302 }
303
304 override fun onSingleTapUp(e: MotionEvent): Boolean {
305 val th = this@ToggleRangeBehavior
306 ControlActionCoordinator.toggle(th.cvh, th.template.getTemplateId(),
307 th.template.isChecked())
308 return true
Matt Pietala635bd12020-02-03 13:36:18 -0500309 }
310 }
311}