1 /* <lambda>null2 * 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 17 package com.android.systemui.controls.ui 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ValueAnimator 22 import android.content.Context 23 import android.graphics.drawable.Drawable 24 import android.graphics.drawable.LayerDrawable 25 import android.os.Bundle 26 import android.service.controls.Control 27 import android.service.controls.templates.ControlTemplate 28 import android.service.controls.templates.RangeTemplate 29 import android.service.controls.templates.TemperatureControlTemplate 30 import android.service.controls.templates.ToggleRangeTemplate 31 import android.util.Log 32 import android.util.MathUtils 33 import android.view.GestureDetector 34 import android.view.GestureDetector.SimpleOnGestureListener 35 import android.view.MotionEvent 36 import android.view.View 37 import android.view.ViewGroup 38 import android.view.accessibility.AccessibilityEvent 39 import android.view.accessibility.AccessibilityNodeInfo 40 import com.android.systemui.res.R 41 import com.android.app.animation.Interpolators 42 import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL 43 import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL 44 import java.util.IllegalFormatException 45 46 /** 47 * Supports [ToggleRangeTemplate] and [RangeTemplate], as well as when one of those templates is 48 * defined as the subtemplate in [TemperatureControlTemplate]. 49 */ 50 class ToggleRangeBehavior : Behavior { 51 private var rangeAnimator: ValueAnimator? = null 52 lateinit var clipLayer: Drawable 53 lateinit var templateId: String 54 lateinit var control: Control 55 lateinit var cvh: ControlViewHolder 56 lateinit var rangeTemplate: RangeTemplate 57 lateinit var context: Context 58 var currentStatusText: CharSequence = "" 59 var currentRangeValue: String = "" 60 var isChecked: Boolean = false 61 var isToggleable: Boolean = false 62 var colorOffset: Int = 0 63 64 companion object { 65 private const val DEFAULT_FORMAT = "%.1f" 66 } 67 68 override fun initialize(cvh: ControlViewHolder) { 69 this.cvh = cvh 70 context = cvh.context 71 72 val gestureListener = ToggleRangeGestureListener(cvh.layout) 73 val gestureDetector = GestureDetector(context, gestureListener) 74 cvh.layout.setOnTouchListener { v: View, e: MotionEvent -> 75 if (gestureDetector.onTouchEvent(e)) { 76 // Don't return true to let the state list change to "pressed" 77 return@setOnTouchListener false 78 } 79 80 if (e.getAction() == MotionEvent.ACTION_UP && gestureListener.isDragging) { 81 v.getParent().requestDisallowInterceptTouchEvent(false) 82 gestureListener.isDragging = false 83 endUpdateRange() 84 return@setOnTouchListener false 85 } 86 87 return@setOnTouchListener false 88 } 89 } 90 91 private fun setup(template: ToggleRangeTemplate) { 92 rangeTemplate = template.getRange() 93 isToggleable = true 94 isChecked = template.isChecked() 95 } 96 97 private fun setup(template: RangeTemplate) { 98 rangeTemplate = template 99 100 // only show disabled state when value is at the minimum 101 isChecked = rangeTemplate.currentValue != rangeTemplate.minValue 102 } 103 104 private fun setupTemplate(template: ControlTemplate): Boolean { 105 return when (template) { 106 is ToggleRangeTemplate -> { 107 setup(template) 108 true 109 } 110 is RangeTemplate -> { 111 setup(template) 112 true 113 } 114 is TemperatureControlTemplate -> setupTemplate(template.getTemplate()) 115 else -> { 116 Log.e(ControlsUiController.TAG, "Unsupported template type: $template") 117 false 118 } 119 } 120 } 121 122 override fun bind(cws: ControlWithState, colorOffset: Int) { 123 this.control = cws.control!! 124 this.colorOffset = colorOffset 125 126 currentStatusText = control.getStatusText() 127 128 // ControlViewHolder sets a long click listener, but we want to handle touch in 129 // here instead, otherwise we'll have state conflicts. 130 cvh.layout.setOnLongClickListener(null) 131 132 val ld = cvh.layout.getBackground() as LayerDrawable 133 clipLayer = ld.findDrawableByLayerId(R.id.clip_layer) 134 135 val template = control.getControlTemplate() 136 if (!setupTemplate(template)) return 137 templateId = template.getTemplateId() 138 139 updateRange(rangeToLevelValue(rangeTemplate.currentValue), isChecked, 140 /* isDragging */ false) 141 142 cvh.applyRenderInfo(isChecked, colorOffset) 143 144 /* 145 * This is custom widget behavior, so add a new accessibility delegate to 146 * handle clicks and range events. Present as a seek bar control. 147 */ 148 cvh.layout.setAccessibilityDelegate(object : View.AccessibilityDelegate() { 149 override fun onInitializeAccessibilityNodeInfo( 150 host: View, 151 info: AccessibilityNodeInfo 152 ) { 153 super.onInitializeAccessibilityNodeInfo(host, info) 154 155 val min = levelToRangeValue(MIN_LEVEL) 156 val current = levelToRangeValue(clipLayer.getLevel()) 157 val max = levelToRangeValue(MAX_LEVEL) 158 159 val step = rangeTemplate.getStepValue().toDouble() 160 val type = if (step == Math.floor(step)) { 161 AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT 162 } else { 163 AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_FLOAT 164 } 165 166 if (isChecked) { 167 val rangeInfo = AccessibilityNodeInfo.RangeInfo.obtain(type, min, max, current) 168 info.setRangeInfo(rangeInfo) 169 } 170 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS) 171 } 172 173 override fun performAccessibilityAction( 174 host: View, 175 action: Int, 176 arguments: Bundle? 177 ): Boolean { 178 val handled = when (action) { 179 AccessibilityNodeInfo.ACTION_CLICK -> { 180 if (!isToggleable) { 181 false 182 } else { 183 cvh.controlActionCoordinator.toggle(cvh, templateId, isChecked) 184 true 185 } 186 } 187 AccessibilityNodeInfo.ACTION_LONG_CLICK -> { 188 cvh.controlActionCoordinator.longPress(cvh) 189 true 190 } 191 AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.getId() -> { 192 if (arguments == null || !arguments.containsKey( 193 AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)) { 194 false 195 } else { 196 val value = arguments.getFloat( 197 AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE) 198 val level = rangeToLevelValue(value) 199 updateRange(level, isChecked, /* isDragging */ true) 200 endUpdateRange() 201 true 202 } 203 } 204 else -> false 205 } 206 207 return handled || super.performAccessibilityAction(host, action, arguments) 208 } 209 210 override fun onRequestSendAccessibilityEvent( 211 host: ViewGroup, 212 child: View, 213 event: AccessibilityEvent 214 ): Boolean = true 215 }) 216 } 217 218 fun beginUpdateRange() { 219 cvh.userInteractionInProgress = true 220 cvh.setStatusTextSize(context.getResources() 221 .getDimensionPixelSize(R.dimen.control_status_expanded).toFloat()) 222 } 223 224 fun updateRange(level: Int, checked: Boolean, isDragging: Boolean) { 225 val newLevel = Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, level)) 226 227 // If the current level is at the minimum and the user is dragging, set the control to 228 // the enabled state to indicate their intention to enable the device. This will update 229 // control colors to support dragging. 230 if (clipLayer.level == MIN_LEVEL && newLevel > MIN_LEVEL) { 231 cvh.applyRenderInfo(checked, colorOffset, false /* animated */) 232 } 233 234 rangeAnimator?.cancel() 235 if (isDragging) { 236 val isEdge = newLevel == MIN_LEVEL || newLevel == MAX_LEVEL 237 if (clipLayer.level != newLevel) { 238 cvh.controlActionCoordinator.drag(cvh, isEdge) 239 clipLayer.level = newLevel 240 } 241 } else if (newLevel != clipLayer.level) { 242 rangeAnimator = ValueAnimator.ofInt(cvh.clipLayer.level, newLevel).apply { 243 addUpdateListener { 244 cvh.clipLayer.level = it.animatedValue as Int 245 } 246 addListener(object : AnimatorListenerAdapter() { 247 override fun onAnimationEnd(animation: Animator) { 248 rangeAnimator = null 249 } 250 }) 251 duration = ControlViewHolder.STATE_ANIMATION_DURATION 252 interpolator = Interpolators.CONTROL_STATE 253 start() 254 } 255 } 256 257 if (checked) { 258 val newValue = levelToRangeValue(newLevel) 259 currentRangeValue = format(rangeTemplate.getFormatString().toString(), 260 DEFAULT_FORMAT, newValue) 261 if (isDragging) { 262 cvh.setStatusText(currentRangeValue, /* immediately */ true) 263 } else { 264 cvh.setStatusText("$currentStatusText $currentRangeValue") 265 } 266 } else { 267 cvh.setStatusText(currentStatusText) 268 } 269 } 270 271 private fun format(primaryFormat: String, backupFormat: String, value: Float): String { 272 return try { 273 String.format(primaryFormat, findNearestStep(value)) 274 } catch (e: IllegalFormatException) { 275 Log.w(ControlsUiController.TAG, "Illegal format in range template", e) 276 if (backupFormat == "") { 277 "" 278 } else { 279 format(backupFormat, "", value) 280 } 281 } 282 } 283 284 private fun levelToRangeValue(i: Int): Float { 285 return MathUtils.constrainedMap(rangeTemplate.minValue, rangeTemplate.maxValue, 286 MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(), i.toFloat()) 287 } 288 289 private fun rangeToLevelValue(i: Float): Int { 290 return MathUtils.constrainedMap(MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(), 291 rangeTemplate.minValue, rangeTemplate.maxValue, i).toInt() 292 } 293 294 fun endUpdateRange() { 295 cvh.setStatusTextSize(context.getResources() 296 .getDimensionPixelSize(R.dimen.control_status_normal).toFloat()) 297 cvh.setStatusText("$currentStatusText $currentRangeValue", /* immediately */ true) 298 cvh.controlActionCoordinator.setValue(cvh, rangeTemplate.getTemplateId(), 299 findNearestStep(levelToRangeValue(clipLayer.getLevel()))) 300 cvh.userInteractionInProgress = false 301 } 302 303 fun findNearestStep(value: Float): Float { 304 var minDiff = Float.MAX_VALUE 305 306 var f = rangeTemplate.getMinValue() 307 while (f <= rangeTemplate.getMaxValue()) { 308 val currentDiff = Math.abs(value - f) 309 if (currentDiff < minDiff) { 310 minDiff = currentDiff 311 } else { 312 return f - rangeTemplate.getStepValue() 313 } 314 315 f += rangeTemplate.getStepValue() 316 } 317 318 return rangeTemplate.getMaxValue() 319 } 320 321 inner class ToggleRangeGestureListener( 322 val v: View 323 ) : SimpleOnGestureListener() { 324 var isDragging: Boolean = false 325 326 override fun onDown(e: MotionEvent): Boolean { 327 return true 328 } 329 330 override fun onLongPress(e: MotionEvent) { 331 if (isDragging) { 332 return 333 } 334 cvh.controlActionCoordinator.longPress(cvh) 335 } 336 337 override fun onScroll( 338 e1: MotionEvent?, 339 e2: MotionEvent, 340 xDiff: Float, 341 yDiff: Float 342 ): Boolean { 343 if (!isDragging) { 344 v.getParent().requestDisallowInterceptTouchEvent(true) 345 beginUpdateRange() 346 isDragging = true 347 } 348 349 val ratioDiff = -xDiff / v.width 350 val changeAmount = ((MAX_LEVEL - MIN_LEVEL) * ratioDiff).toInt() 351 updateRange(clipLayer.level + changeAmount, checked = true, isDragging = true) 352 return true 353 } 354 355 override fun onSingleTapUp(e: MotionEvent): Boolean { 356 if (!isToggleable) return false 357 cvh.controlActionCoordinator.toggle(cvh, templateId, isChecked) 358 return true 359 } 360 } 361 } 362