blob: 3dc0ff36339ae1d9b3050c7bfad3bcb9f18da985 [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 Pietal677981d2020-04-24 14:38:32 -040043import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL
44import com.android.systemui.controls.ui.ControlViewHolder.Companion.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 -> {
Matt Pietal677981d2020-04-24 14:38:32 -0400144 cvh.controlActionCoordinator.toggle(cvh, template.getTemplateId(),
Matt Pietal31ec9f22020-03-12 09:02:17 -0400145 template.isChecked())
146 true
147 }
Matt Pietalb2c7a442020-05-01 10:48:44 -0400148 AccessibilityNodeInfo.ACTION_LONG_CLICK -> {
149 cvh.controlActionCoordinator.longPress(cvh)
150 true
151 }
Matt Pietal31ec9f22020-03-12 09:02:17 -0400152 AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.getId() -> {
153 if (arguments == null || !arguments.containsKey(
154 AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)) {
155 false
156 } else {
157 val value = arguments.getFloat(
158 AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)
Matt Pietalb2c7a442020-05-01 10:48:44 -0400159 val level = rangeToLevelValue(value)
160 updateRange(level, template.isChecked(), /* isDragging */ true)
Matt Pietal31ec9f22020-03-12 09:02:17 -0400161 endUpdateRange()
162 true
163 }
164 }
165 else -> false
166 }
167
168 return handled || super.performAccessibilityAction(host, action, arguments)
169 }
170
171 override fun onRequestSendAccessibilityEvent(
172 host: ViewGroup,
173 child: View,
174 event: AccessibilityEvent
Matt Pietalb2c7a442020-05-01 10:48:44 -0400175 ): Boolean = true
Matt Pietal31ec9f22020-03-12 09:02:17 -0400176 })
Matt Pietala635bd12020-02-03 13:36:18 -0500177 }
178
Matt Pietala635bd12020-02-03 13:36:18 -0500179 fun beginUpdateRange() {
Matt Pietal5f478c72020-04-01 15:53:54 -0400180 status.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
Matt Pietala635bd12020-02-03 13:36:18 -0500181 .getDimensionPixelSize(R.dimen.control_status_expanded).toFloat())
182 }
183
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700184 fun updateRange(level: Int, checked: Boolean, isDragging: Boolean) {
185 val newLevel = if (checked) Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, level)) else MIN_LEVEL
186
187 rangeAnimator?.cancel()
188 if (isDragging) {
189 clipLayer.level = newLevel
Matt Pietalb5080842020-04-23 14:02:05 -0400190 val isEdge = newLevel == MIN_LEVEL || newLevel == MAX_LEVEL
Matt Pietal677981d2020-04-24 14:38:32 -0400191 cvh.controlActionCoordinator.drag(isEdge)
Matt Pietalb2c7a442020-05-01 10:48:44 -0400192 } else if (newLevel != clipLayer.level) {
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700193 rangeAnimator = ValueAnimator.ofInt(cvh.clipLayer.level, newLevel).apply {
194 addUpdateListener {
195 cvh.clipLayer.level = it.animatedValue as Int
196 }
197 addListener(object : AnimatorListenerAdapter() {
198 override fun onAnimationEnd(animation: Animator?) {
199 rangeAnimator = null
200 }
201 })
202 duration = ControlViewHolder.STATE_ANIMATION_DURATION
203 interpolator = Interpolators.CONTROL_STATE
204 start()
205 }
206 }
Matt Pietala635bd12020-02-03 13:36:18 -0500207
Matt Pietal307b1ef2020-02-10 07:27:01 -0500208 if (checked) {
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700209 val newValue = levelToRangeValue(newLevel)
Matt Pietal5f478c72020-04-01 15:53:54 -0400210 currentRangeValue = format(rangeTemplate.getFormatString().toString(),
Matt Pietal307b1ef2020-02-10 07:27:01 -0500211 DEFAULT_FORMAT, newValue)
Matt Pietal5f478c72020-04-01 15:53:54 -0400212 val text = if (isDragging) {
213 currentRangeValue
214 } else {
215 "$currentStatusText $currentRangeValue"
216 }
217 status.setText(text)
Matt Pietala635bd12020-02-03 13:36:18 -0500218 } else {
Matt Pietal5f478c72020-04-01 15:53:54 -0400219 status.setText(currentStatusText)
Matt Pietala635bd12020-02-03 13:36:18 -0500220 }
221 }
222
Matt Pietal307b1ef2020-02-10 07:27:01 -0500223 private fun format(primaryFormat: String, backupFormat: String, value: Float): String {
224 return try {
225 String.format(primaryFormat, value)
226 } catch (e: IllegalFormatException) {
227 Log.w(ControlsUiController.TAG, "Illegal format in range template", e)
228 if (backupFormat == "") {
229 ""
230 } else {
231 format(backupFormat, "", value)
232 }
233 }
234 }
Matt Pietala635bd12020-02-03 13:36:18 -0500235
Matt Pietal31ec9f22020-03-12 09:02:17 -0400236 private fun levelToRangeValue(i: Int): Float {
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700237 return MathUtils.constrainedMap(rangeTemplate.minValue, rangeTemplate.maxValue,
238 MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(), i.toFloat())
239 }
240
241 private fun rangeToLevelValue(i: Float): Int {
242 return MathUtils.constrainedMap(MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(),
243 rangeTemplate.minValue, rangeTemplate.maxValue, i).toInt()
Matt Pietal307b1ef2020-02-10 07:27:01 -0500244 }
Matt Pietala635bd12020-02-03 13:36:18 -0500245
Matt Pietal307b1ef2020-02-10 07:27:01 -0500246 fun endUpdateRange() {
Matt Pietal5f478c72020-04-01 15:53:54 -0400247 status.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
Matt Pietala635bd12020-02-03 13:36:18 -0500248 .getDimensionPixelSize(R.dimen.control_status_normal).toFloat())
Matt Pietal5f478c72020-04-01 15:53:54 -0400249 status.setText("$currentStatusText $currentRangeValue")
Matt Pietal31ec9f22020-03-12 09:02:17 -0400250 cvh.action(FloatAction(rangeTemplate.getTemplateId(),
251 findNearestStep(levelToRangeValue(clipLayer.getLevel()))))
Matt Pietala635bd12020-02-03 13:36:18 -0500252 }
253
254 fun findNearestStep(value: Float): Float {
255 var minDiff = 1000f
256
257 var f = rangeTemplate.getMinValue()
258 while (f <= rangeTemplate.getMaxValue()) {
259 val currentDiff = Math.abs(value - f)
260 if (currentDiff < minDiff) {
261 minDiff = currentDiff
262 } else {
263 return f - rangeTemplate.getStepValue()
264 }
265
266 f += rangeTemplate.getStepValue()
267 }
268
269 return rangeTemplate.getMaxValue()
270 }
271
Matt Pietal307b1ef2020-02-10 07:27:01 -0500272 inner class ToggleRangeGestureListener(
273 val v: View
274 ) : SimpleOnGestureListener() {
275 var isDragging: Boolean = false
Matt Pietala635bd12020-02-03 13:36:18 -0500276
Matt Pietal307b1ef2020-02-10 07:27:01 -0500277 override fun onDown(e: MotionEvent): Boolean {
Matt Pietala635bd12020-02-03 13:36:18 -0500278 return true
279 }
280
Matt Pietal307b1ef2020-02-10 07:27:01 -0500281 override fun onLongPress(e: MotionEvent) {
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700282 if (isDragging) {
283 return
284 }
Matt Pietal677981d2020-04-24 14:38:32 -0400285 cvh.controlActionCoordinator.longPress(this@ToggleRangeBehavior.cvh)
Matt Pietala635bd12020-02-03 13:36:18 -0500286 }
287
Matt Pietal307b1ef2020-02-10 07:27:01 -0500288 override fun onScroll(
289 e1: MotionEvent,
290 e2: MotionEvent,
291 xDiff: Float,
292 yDiff: Float
293 ): Boolean {
Lucas Dupin3e18f222020-04-13 15:11:20 -0700294 if (!template.isChecked) {
295 return false
296 }
Matt Pietala635bd12020-02-03 13:36:18 -0500297 if (!isDragging) {
Matt Pietal1cbf78d2020-03-26 11:16:42 -0400298 v.getParent().requestDisallowInterceptTouchEvent(true)
Matt Pietal307b1ef2020-02-10 07:27:01 -0500299 this@ToggleRangeBehavior.beginUpdateRange()
300 isDragging = true
Matt Pietala635bd12020-02-03 13:36:18 -0500301 }
302
Lucas Dupin2ee4ec92020-04-13 19:51:14 -0700303 val ratioDiff = -xDiff / v.width
304 val changeAmount = ((MAX_LEVEL - MIN_LEVEL) * ratioDiff).toInt()
305 this@ToggleRangeBehavior.updateRange(clipLayer.level + changeAmount,
306 checked = true, isDragging = true)
Matt Pietal307b1ef2020-02-10 07:27:01 -0500307 return true
308 }
309
310 override fun onSingleTapUp(e: MotionEvent): Boolean {
311 val th = this@ToggleRangeBehavior
Matt Pietal677981d2020-04-24 14:38:32 -0400312 cvh.controlActionCoordinator.toggle(th.cvh, th.template.getTemplateId(),
Matt Pietal307b1ef2020-02-10 07:27:01 -0500313 th.template.isChecked())
314 return true
Matt Pietala635bd12020-02-03 13:36:18 -0500315 }
316 }
317}